mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
merge
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "codex-protocol"
|
||||
version = { workspace = true }
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_protocol"
|
||||
@@ -12,18 +13,17 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-git = { workspace = true }
|
||||
|
||||
base64 = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-image = { workspace = true }
|
||||
icu_decimal = { workspace = true }
|
||||
icu_locale_core = { workspace = true }
|
||||
icu_provider = { workspace = true, features = ["sync"] }
|
||||
mcp-types = { workspace = true }
|
||||
mime_guess = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true, features = ["macros", "base64"] }
|
||||
schemars = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
sys-locale = { workspace = true }
|
||||
@@ -37,6 +37,7 @@ uuid = { workspace = true, features = ["serde", "v7", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||
|
||||
use crate::parse_command::ParsedCommand;
|
||||
use crate::protocol::FileChange;
|
||||
use mcp_types::RequestId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -12,27 +13,31 @@ fn is_false(value: &bool) -> bool {
|
||||
!*value
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SandboxRiskLevel {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// Proposed execpolicy change to allow commands starting with this prefix.
|
||||
///
|
||||
/// The `command` tokens form the prefix that would be added as an execpolicy
|
||||
/// `prefix_rule(..., decision="allow")`, letting the agent bypass approval for
|
||||
/// commands that start with this token sequence.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct SandboxCommandAssessment {
|
||||
pub description: String,
|
||||
pub risk_level: SandboxRiskLevel,
|
||||
#[serde(transparent)]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub struct ExecPolicyAmendment {
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
impl SandboxRiskLevel {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
}
|
||||
impl ExecPolicyAmendment {
|
||||
pub fn new(command: Vec<String>) -> Self {
|
||||
Self { command }
|
||||
}
|
||||
|
||||
pub fn command(&self) -> &[String] {
|
||||
&self.command
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for ExecPolicyAmendment {
|
||||
fn from(command: Vec<String>) -> Self {
|
||||
Self { command }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +45,10 @@ impl SandboxRiskLevel {
|
||||
pub struct ExecApprovalRequestEvent {
|
||||
/// Identifier for the associated exec call, if available.
|
||||
pub call_id: String,
|
||||
/// Turn ID that this command belongs to.
|
||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
/// The command to be executed.
|
||||
pub command: Vec<String>,
|
||||
/// The command's working directory.
|
||||
@@ -47,18 +56,41 @@ pub struct ExecApprovalRequestEvent {
|
||||
/// Optional human-readable reason for the approval (e.g. retry without sandbox).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
/// Optional model-provided risk assessment describing the blocked command.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub risk: Option<SandboxCommandAssessment>,
|
||||
/// Proposed execpolicy amendment that can be applied to allow future runs.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub network_preflight_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ElicitationRequestEvent {
|
||||
pub server_name: String,
|
||||
pub id: RequestId,
|
||||
pub message: String,
|
||||
// TODO: MCP servers can request we fill out a schema for the elicitation. We don't support
|
||||
// this yet.
|
||||
// pub requested_schema: ElicitRequestParamsRequestedSchema,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ElicitationAction {
|
||||
Accept,
|
||||
Decline,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ApplyPatchApprovalRequestEvent {
|
||||
/// Responses API call id for the associated patch apply call, if available.
|
||||
pub call_id: String,
|
||||
/// Turn ID that this patch belongs to.
|
||||
/// Uses `#[serde(default)]` for backwards compatibility with older senders.
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
pub changes: HashMap<PathBuf, FileChange>,
|
||||
/// Optional explanatory reason (e.g. request for extra write access).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -2,36 +2,8 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum_macros::Display;
|
||||
use strum_macros::EnumIter;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
#[derive(
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Display,
|
||||
JsonSchema,
|
||||
TS,
|
||||
EnumIter,
|
||||
Hash,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum ReasoningEffort {
|
||||
None,
|
||||
Minimal,
|
||||
Low,
|
||||
#[default]
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// A summary of the reasoning performed by the model. This can be useful for
|
||||
/// debugging and understanding the model's reasoning process.
|
||||
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries
|
||||
@@ -98,3 +70,13 @@ pub enum ForcedLoginMethod {
|
||||
Chatgpt,
|
||||
Api,
|
||||
}
|
||||
|
||||
/// Represents the trust level for a project directory.
|
||||
/// This determines the approval policy and sandbox mode applied.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum TrustLevel {
|
||||
Trusted,
|
||||
Untrusted,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum TurnItem {
|
||||
UserMessage(UserMessageItem),
|
||||
AgentMessage(AgentMessageItem),
|
||||
@@ -25,6 +27,8 @@ pub struct UserMessageItem {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum AgentMessageContent {
|
||||
Text { text: String },
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod items;
|
||||
pub mod message_history;
|
||||
pub mod models;
|
||||
pub mod num_format;
|
||||
pub mod openai_models;
|
||||
pub mod parse_command;
|
||||
pub mod plan_tool;
|
||||
pub mod protocol;
|
||||
|
||||
+211
-45
@@ -1,6 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use base64::Engine;
|
||||
use codex_utils_image::load_and_resize_to_fit;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::ContentBlock;
|
||||
@@ -15,6 +14,25 @@ use codex_git::GhostCommit;
|
||||
use codex_utils_image::error::ImageProcessingError;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
/// Controls whether a command should use the session sandbox or bypass it.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SandboxPermissions {
|
||||
/// Run with the configured sandbox
|
||||
#[default]
|
||||
UseDefault,
|
||||
/// Request to run outside the sandbox
|
||||
RequireEscalated,
|
||||
}
|
||||
|
||||
impl SandboxPermissions {
|
||||
pub fn requires_escalated_permissions(self) -> bool {
|
||||
matches!(self, SandboxPermissions::RequireEscalated)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ResponseInputItem {
|
||||
@@ -132,6 +150,10 @@ pub enum ResponseItem {
|
||||
GhostSnapshot {
|
||||
ghost_commit: GhostCommit,
|
||||
},
|
||||
#[serde(alias = "compaction_summary")]
|
||||
Compaction {
|
||||
encrypted_content: String,
|
||||
},
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
@@ -158,6 +180,29 @@ fn local_image_error_placeholder(
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_image_error_placeholder(
|
||||
path: &std::path::Path,
|
||||
error: impl std::fmt::Display,
|
||||
) -> ContentItem {
|
||||
ContentItem::InputText {
|
||||
text: format!(
|
||||
"Image located at `{}` is invalid: {}",
|
||||
path.display(),
|
||||
error
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_image_error_placeholder(path: &std::path::Path, mime: &str) -> ContentItem {
|
||||
ContentItem::InputText {
|
||||
text: format!(
|
||||
"Codex cannot attach image at `{}`: unsupported image format `{}`.",
|
||||
path.display(),
|
||||
mime
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseInputItem> for ResponseItem {
|
||||
fn from(item: ResponseInputItem) -> Self {
|
||||
match item {
|
||||
@@ -214,8 +259,24 @@ pub struct LocalShellExecAction {
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum WebSearchAction {
|
||||
Search {
|
||||
query: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
query: Option<String>,
|
||||
},
|
||||
OpenPage {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
url: Option<String>,
|
||||
},
|
||||
FindInPage {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
url: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pattern: Option<String>,
|
||||
},
|
||||
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
@@ -239,52 +300,37 @@ impl From<Vec<UserInput>> for ResponseInputItem {
|
||||
role: "user".to_string(),
|
||||
content: items
|
||||
.into_iter()
|
||||
.map(|c| match c {
|
||||
UserInput::Text { text } => ContentItem::InputText { text },
|
||||
UserInput::Image { image_url } => ContentItem::InputImage { image_url },
|
||||
.filter_map(|c| match c {
|
||||
UserInput::Text { text } => Some(ContentItem::InputText { text }),
|
||||
UserInput::Image { image_url } => Some(ContentItem::InputImage { image_url }),
|
||||
UserInput::LocalImage { path } => match load_and_resize_to_fit(&path) {
|
||||
Ok(image) => ContentItem::InputImage {
|
||||
Ok(image) => Some(ContentItem::InputImage {
|
||||
image_url: image.into_data_url(),
|
||||
},
|
||||
}),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to resize image {}: {}", path.display(), err);
|
||||
if matches!(&err, ImageProcessingError::Read { .. }) {
|
||||
local_image_error_placeholder(&path, &err)
|
||||
Some(local_image_error_placeholder(&path, &err))
|
||||
} else if err.is_invalid_image() {
|
||||
Some(invalid_image_error_placeholder(&path, &err))
|
||||
} else {
|
||||
match std::fs::read(&path) {
|
||||
Ok(bytes) => {
|
||||
let Some(mime_guess) = mime_guess::from_path(&path).first()
|
||||
else {
|
||||
return local_image_error_placeholder(
|
||||
&path,
|
||||
"unsupported MIME type (unknown)",
|
||||
);
|
||||
};
|
||||
let mime = mime_guess.essence_str().to_owned();
|
||||
if !mime.starts_with("image/") {
|
||||
return local_image_error_placeholder(
|
||||
&path,
|
||||
format!("unsupported MIME type `{mime}`"),
|
||||
);
|
||||
}
|
||||
let encoded =
|
||||
base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
ContentItem::InputImage {
|
||||
image_url: format!("data:{mime};base64,{encoded}"),
|
||||
}
|
||||
}
|
||||
Err(read_err) => {
|
||||
tracing::warn!(
|
||||
"Skipping image {} – could not read file: {}",
|
||||
path.display(),
|
||||
read_err
|
||||
);
|
||||
local_image_error_placeholder(&path, &read_err)
|
||||
}
|
||||
let Some(mime_guess) = mime_guess::from_path(&path).first() else {
|
||||
return Some(local_image_error_placeholder(
|
||||
&path,
|
||||
"unsupported MIME type (unknown)",
|
||||
));
|
||||
};
|
||||
let mime = mime_guess.essence_str().to_owned();
|
||||
if !mime.starts_with("image/") {
|
||||
return Some(local_image_error_placeholder(
|
||||
&path,
|
||||
format!("unsupported MIME type `{mime}`"),
|
||||
));
|
||||
}
|
||||
Some(unsupported_image_error_placeholder(&path, &mime))
|
||||
}
|
||||
}
|
||||
},
|
||||
UserInput::Skill { .. } => None, // Skill bodies are injected later in core
|
||||
})
|
||||
.collect::<Vec<ContentItem>>(),
|
||||
}
|
||||
@@ -301,8 +347,9 @@ pub struct ShellToolCallParams {
|
||||
/// This is the maximum time in milliseconds that the command is allowed to run.
|
||||
#[serde(alias = "timeout")]
|
||||
pub timeout_ms: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub with_escalated_permissions: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub sandbox_permissions: Option<SandboxPermissions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub justification: Option<String>,
|
||||
}
|
||||
@@ -314,11 +361,15 @@ pub struct ShellCommandToolCallParams {
|
||||
pub command: String,
|
||||
pub workdir: Option<String>,
|
||||
|
||||
/// Whether to run the shell with login shell semantics
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub login: Option<bool>,
|
||||
/// This is the maximum time in milliseconds that the command is allowed to run.
|
||||
#[serde(alias = "timeout")]
|
||||
pub timeout_ms: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub with_escalated_permissions: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub sandbox_permissions: Option<SandboxPermissions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub justification: Option<String>,
|
||||
}
|
||||
@@ -365,6 +416,7 @@ impl Serialize for FunctionCallOutputPayload {
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
tracing::debug!("Function call output payload: {:?}", self);
|
||||
if let Some(items) = &self.content_items {
|
||||
items.serialize(serializer)
|
||||
} else {
|
||||
@@ -452,7 +504,7 @@ fn convert_content_blocks_to_items(
|
||||
) -> Option<Vec<FunctionCallOutputContentItem>> {
|
||||
let mut saw_image = false;
|
||||
let mut items = Vec::with_capacity(blocks.len());
|
||||
|
||||
tracing::warn!("Blocks: {:?}", blocks);
|
||||
for block in blocks {
|
||||
match block {
|
||||
ContentBlock::TextContent(text) => {
|
||||
@@ -503,6 +555,7 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use mcp_types::ImageContent;
|
||||
use mcp_types::TextContent;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
@@ -616,6 +669,87 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_compaction_alias() -> Result<()> {
|
||||
let json = r#"{"type":"compaction_summary","encrypted_content":"abc"}"#;
|
||||
|
||||
let item: ResponseItem = serde_json::from_str(json)?;
|
||||
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::Compaction {
|
||||
encrypted_content: "abc".into(),
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrips_web_search_call_actions() -> Result<()> {
|
||||
let cases = vec![
|
||||
(
|
||||
r#"{
|
||||
"type": "web_search_call",
|
||||
"status": "completed",
|
||||
"action": {
|
||||
"type": "search",
|
||||
"query": "weather seattle"
|
||||
}
|
||||
}"#,
|
||||
WebSearchAction::Search {
|
||||
query: Some("weather seattle".into()),
|
||||
},
|
||||
Some("completed".into()),
|
||||
),
|
||||
(
|
||||
r#"{
|
||||
"type": "web_search_call",
|
||||
"status": "open",
|
||||
"action": {
|
||||
"type": "open_page",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
}"#,
|
||||
WebSearchAction::OpenPage {
|
||||
url: Some("https://example.com".into()),
|
||||
},
|
||||
Some("open".into()),
|
||||
),
|
||||
(
|
||||
r#"{
|
||||
"type": "web_search_call",
|
||||
"status": "in_progress",
|
||||
"action": {
|
||||
"type": "find_in_page",
|
||||
"url": "https://example.com/docs",
|
||||
"pattern": "installation"
|
||||
}
|
||||
}"#,
|
||||
WebSearchAction::FindInPage {
|
||||
url: Some("https://example.com/docs".into()),
|
||||
pattern: Some("installation".into()),
|
||||
},
|
||||
Some("in_progress".into()),
|
||||
),
|
||||
];
|
||||
|
||||
for (json_literal, expected_action, expected_status) in cases {
|
||||
let parsed: ResponseItem = serde_json::from_str(json_literal)?;
|
||||
let expected = ResponseItem::WebSearchCall {
|
||||
id: None,
|
||||
status: expected_status.clone(),
|
||||
action: expected_action.clone(),
|
||||
};
|
||||
assert_eq!(parsed, expected);
|
||||
|
||||
let serialized = serde_json::to_value(&parsed)?;
|
||||
let original_value: serde_json::Value = serde_json::from_str(json_literal)?;
|
||||
assert_eq!(serialized, original_value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_shell_tool_call_params() -> Result<()> {
|
||||
let json = r#"{
|
||||
@@ -630,7 +764,7 @@ mod tests {
|
||||
command: vec!["ls".to_string(), "-l".to_string()],
|
||||
workdir: Some("/tmp".to_string()),
|
||||
timeout_ms: Some(1000),
|
||||
with_escalated_permissions: None,
|
||||
sandbox_permissions: None,
|
||||
justification: None,
|
||||
},
|
||||
params
|
||||
@@ -703,4 +837,36 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_image_unsupported_image_format_adds_placeholder() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let svg_path = dir.path().join("example.svg");
|
||||
std::fs::write(
|
||||
&svg_path,
|
||||
br#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>"#,
|
||||
)?;
|
||||
|
||||
let item = ResponseInputItem::from(vec![UserInput::LocalImage {
|
||||
path: svg_path.clone(),
|
||||
}]);
|
||||
|
||||
match item {
|
||||
ResponseInputItem::Message { content, .. } => {
|
||||
assert_eq!(content.len(), 1);
|
||||
let expected = format!(
|
||||
"Codex cannot attach image at `{}`: unsupported image format `image/svg+xml`.",
|
||||
svg_path.display()
|
||||
);
|
||||
match &content[0] {
|
||||
ContentItem::InputText { text } => assert_eq!(text, &expected),
|
||||
other => panic!("expected placeholder text but found {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("expected message response but got {other:?}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::Display;
|
||||
use strum_macros::EnumIter;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::config_types::Verbosity;
|
||||
|
||||
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
#[derive(
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Display,
|
||||
JsonSchema,
|
||||
TS,
|
||||
EnumIter,
|
||||
Hash,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum ReasoningEffort {
|
||||
None,
|
||||
Minimal,
|
||||
Low,
|
||||
#[default]
|
||||
Medium,
|
||||
High,
|
||||
XHigh,
|
||||
}
|
||||
|
||||
/// A reasoning effort option that can be surfaced for a model.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ReasoningEffortPreset {
|
||||
/// Effort level that the model supports.
|
||||
pub effort: ReasoningEffort,
|
||||
/// Short human description shown next to the effort in UIs.
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
|
||||
pub struct ModelUpgrade {
|
||||
pub id: String,
|
||||
pub reasoning_effort_mapping: Option<HashMap<ReasoningEffort, ReasoningEffort>>,
|
||||
pub migration_config_key: String,
|
||||
}
|
||||
|
||||
/// Metadata describing a Codex-supported model.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
|
||||
pub struct ModelPreset {
|
||||
/// Stable identifier for the preset.
|
||||
pub id: String,
|
||||
/// Model slug (e.g., "gpt-5").
|
||||
pub model: String,
|
||||
/// Display name shown in UIs.
|
||||
pub display_name: String,
|
||||
/// Short human description shown in UIs.
|
||||
pub description: String,
|
||||
/// Reasoning effort applied when none is explicitly chosen.
|
||||
pub default_reasoning_effort: ReasoningEffort,
|
||||
/// Supported reasoning effort options.
|
||||
pub supported_reasoning_efforts: Vec<ReasoningEffortPreset>,
|
||||
/// Whether this is the default model for new users.
|
||||
pub is_default: bool,
|
||||
/// recommended upgrade model
|
||||
pub upgrade: Option<ModelUpgrade>,
|
||||
/// Whether this preset should appear in the picker UI.
|
||||
pub show_in_picker: bool,
|
||||
}
|
||||
|
||||
/// Visibility of a model in the picker or APIs.
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema, EnumIter, Display,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum ModelVisibility {
|
||||
List,
|
||||
Hide,
|
||||
None,
|
||||
}
|
||||
|
||||
/// Shell execution capability for a model.
|
||||
#[derive(
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
TS,
|
||||
JsonSchema,
|
||||
EnumIter,
|
||||
Display,
|
||||
Hash,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ConfigShellToolType {
|
||||
Default,
|
||||
Local,
|
||||
UnifiedExec,
|
||||
Disabled,
|
||||
ShellCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ApplyPatchToolType {
|
||||
Freeform,
|
||||
Function,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default, Hash, TS, JsonSchema, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReasoningSummaryFormat {
|
||||
#[default]
|
||||
None,
|
||||
Experimental,
|
||||
}
|
||||
|
||||
/// Server-provided truncation policy metadata for a model.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TruncationMode {
|
||||
Bytes,
|
||||
Tokens,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct TruncationPolicyConfig {
|
||||
pub mode: TruncationMode,
|
||||
pub limit: i64,
|
||||
}
|
||||
|
||||
impl TruncationPolicyConfig {
|
||||
pub const fn bytes(limit: i64) -> Self {
|
||||
Self {
|
||||
mode: TruncationMode::Bytes,
|
||||
limit,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn tokens(limit: i64) -> Self {
|
||||
Self {
|
||||
mode: TruncationMode::Tokens,
|
||||
limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Semantic version triple encoded as an array in JSON (e.g. [0, 62, 0]).
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct ClientVersion(pub i32, pub i32, pub i32);
|
||||
|
||||
/// Model metadata returned by the Codex backend `/models` endpoint.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct ModelInfo {
|
||||
pub slug: String,
|
||||
pub display_name: String,
|
||||
pub description: Option<String>,
|
||||
pub default_reasoning_level: ReasoningEffort,
|
||||
pub supported_reasoning_levels: Vec<ReasoningEffortPreset>,
|
||||
pub shell_type: ConfigShellToolType,
|
||||
pub visibility: ModelVisibility,
|
||||
pub minimal_client_version: ClientVersion,
|
||||
pub supported_in_api: bool,
|
||||
pub priority: i32,
|
||||
pub upgrade: Option<String>,
|
||||
pub base_instructions: Option<String>,
|
||||
pub supports_reasoning_summaries: bool,
|
||||
pub support_verbosity: bool,
|
||||
pub default_verbosity: Option<Verbosity>,
|
||||
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
||||
pub truncation_policy: TruncationPolicyConfig,
|
||||
pub supports_parallel_tool_calls: bool,
|
||||
pub context_window: Option<i64>,
|
||||
pub reasoning_summary_format: ReasoningSummaryFormat,
|
||||
pub experimental_supported_tools: Vec<String>,
|
||||
}
|
||||
|
||||
/// Response wrapper for `/models`.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema, Default)]
|
||||
pub struct ModelsResponse {
|
||||
pub models: Vec<ModelInfo>,
|
||||
#[serde(default)]
|
||||
pub etag: String,
|
||||
}
|
||||
|
||||
// convert ModelInfo to ModelPreset
|
||||
impl From<ModelInfo> for ModelPreset {
|
||||
fn from(info: ModelInfo) -> Self {
|
||||
ModelPreset {
|
||||
id: info.slug.clone(),
|
||||
model: info.slug.clone(),
|
||||
display_name: info.display_name,
|
||||
description: info.description.unwrap_or_default(),
|
||||
default_reasoning_effort: info.default_reasoning_level,
|
||||
supported_reasoning_efforts: info.supported_reasoning_levels.clone(),
|
||||
is_default: false, // default is the highest priority available model
|
||||
upgrade: info.upgrade.as_ref().map(|upgrade_slug| ModelUpgrade {
|
||||
id: upgrade_slug.clone(),
|
||||
reasoning_effort_mapping: reasoning_effort_mapping_from_presets(
|
||||
&info.supported_reasoning_levels,
|
||||
),
|
||||
migration_config_key: info.slug.clone(),
|
||||
}),
|
||||
show_in_picker: info.visibility == ModelVisibility::List,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_effort_mapping_from_presets(
|
||||
presets: &[ReasoningEffortPreset],
|
||||
) -> Option<HashMap<ReasoningEffort, ReasoningEffort>> {
|
||||
if presets.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Map every canonical effort to the closest supported effort for the new model.
|
||||
let supported: Vec<ReasoningEffort> = presets.iter().map(|p| p.effort).collect();
|
||||
let mut map = HashMap::new();
|
||||
for effort in ReasoningEffort::iter() {
|
||||
let nearest = nearest_effort(effort, &supported);
|
||||
map.insert(effort, nearest);
|
||||
}
|
||||
Some(map)
|
||||
}
|
||||
|
||||
fn effort_rank(effort: ReasoningEffort) -> i32 {
|
||||
match effort {
|
||||
ReasoningEffort::None => 0,
|
||||
ReasoningEffort::Minimal => 1,
|
||||
ReasoningEffort::Low => 2,
|
||||
ReasoningEffort::Medium => 3,
|
||||
ReasoningEffort::High => 4,
|
||||
ReasoningEffort::XHigh => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
|
||||
let target_rank = effort_rank(target);
|
||||
supported
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs())
|
||||
.unwrap_or(target)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::ConversationId;
|
||||
use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crate::approvals::ElicitationRequestEvent;
|
||||
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use crate::custom_prompts::CustomPrompt;
|
||||
use crate::items::TurnItem;
|
||||
@@ -19,10 +19,13 @@ use crate::message_history::HistoryEntry;
|
||||
use crate::models::ContentItem;
|
||||
use crate::models::ResponseItem;
|
||||
use crate::num_format::format_with_separators;
|
||||
use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crate::parse_command::ParsedCommand;
|
||||
use crate::plan_tool::UpdatePlanArgs;
|
||||
use crate::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::RequestId;
|
||||
use mcp_types::Resource as McpResource;
|
||||
use mcp_types::ResourceTemplate as McpResourceTemplate;
|
||||
use mcp_types::Tool as McpTool;
|
||||
@@ -32,12 +35,13 @@ use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use serde_with::serde_as;
|
||||
use strum_macros::Display;
|
||||
use tracing::error;
|
||||
use ts_rs::TS;
|
||||
|
||||
pub use crate::approvals::ApplyPatchApprovalRequestEvent;
|
||||
pub use crate::approvals::ElicitationAction;
|
||||
pub use crate::approvals::ExecApprovalRequestEvent;
|
||||
pub use crate::approvals::SandboxCommandAssessment;
|
||||
pub use crate::approvals::SandboxRiskLevel;
|
||||
pub use crate::approvals::ExecPolicyAmendment;
|
||||
|
||||
/// Open/close tags for special user-input blocks. Used across crates to avoid
|
||||
/// duplicated hardcoded strings.
|
||||
@@ -153,6 +157,16 @@ pub enum Op {
|
||||
decision: ReviewDecision,
|
||||
},
|
||||
|
||||
/// Resolve an MCP elicitation request.
|
||||
ResolveElicitation {
|
||||
/// Name of the MCP server that issued the request.
|
||||
server_name: String,
|
||||
/// Request identifier from the MCP server.
|
||||
request_id: RequestId,
|
||||
/// User's decision for the request.
|
||||
decision: ElicitationAction,
|
||||
},
|
||||
|
||||
/// Cache a network approval decision for this session.
|
||||
NetworkApprovalCache {
|
||||
/// The host that was approved.
|
||||
@@ -180,6 +194,19 @@ pub enum Op {
|
||||
/// Request the list of available custom prompts.
|
||||
ListCustomPrompts,
|
||||
|
||||
/// Request the list of skills for the provided `cwd` values or the session default.
|
||||
ListSkills {
|
||||
/// Working directories to scope repo skills discovery.
|
||||
///
|
||||
/// When empty, the session default working directory is used.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
cwds: Vec<PathBuf>,
|
||||
|
||||
/// When true, recompute skills even if a cached result exists.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
force_reload: bool,
|
||||
},
|
||||
|
||||
/// Request the agent to summarize the current conversation context.
|
||||
/// The agent will use its existing context (either conversation history or previous response id)
|
||||
/// to generate a summary which will be returned as an AgentMessage event.
|
||||
@@ -203,6 +230,9 @@ pub enum Op {
|
||||
/// The raw command string after '!'
|
||||
command: String,
|
||||
},
|
||||
|
||||
/// Request the list of available models.
|
||||
ListModels,
|
||||
}
|
||||
|
||||
/// Determines the conditions under which the user is consulted to approve
|
||||
@@ -249,7 +279,7 @@ pub enum AskForApproval {
|
||||
/// Determines execution restrictions for model shell commands.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
#[serde(tag = "mode", rename_all = "kebab-case")]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum SandboxPolicy {
|
||||
/// No restrictions whatsoever. Use with caution.
|
||||
#[serde(rename = "danger-full-access")]
|
||||
@@ -266,7 +296,7 @@ pub enum SandboxPolicy {
|
||||
/// Additional folders (beyond cwd and possibly TMPDIR) that should be
|
||||
/// writable from within the sandbox.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
writable_roots: Vec<PathBuf>,
|
||||
writable_roots: Vec<AbsolutePathBuf>,
|
||||
|
||||
/// When set to `true`, outbound network access is allowed. `false` by
|
||||
/// default.
|
||||
@@ -288,15 +318,15 @@ pub enum SandboxPolicy {
|
||||
|
||||
/// A writable root path accompanied by a list of subpaths that should remain
|
||||
/// read‑only even when the root is writable. This is primarily used to ensure
|
||||
/// top‑level VCS metadata directories (e.g. `.git`) under a writable root are
|
||||
/// not modified by the agent.
|
||||
/// that folders containing files that could be modified to escalate the
|
||||
/// privileges of the agent (e.g. `.codex`, `.git`, notably `.git/hooks`) under
|
||||
/// a writable root are not modified by the agent.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||
pub struct WritableRoot {
|
||||
/// Absolute path, by construction.
|
||||
pub root: PathBuf,
|
||||
pub root: AbsolutePathBuf,
|
||||
|
||||
/// Also absolute paths, by construction.
|
||||
pub read_only_subpaths: Vec<PathBuf>,
|
||||
/// By construction, these subpaths are all under `root`.
|
||||
pub read_only_subpaths: Vec<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
impl WritableRoot {
|
||||
@@ -378,16 +408,30 @@ impl SandboxPolicy {
|
||||
network_access: _,
|
||||
} => {
|
||||
// Start from explicitly configured writable roots.
|
||||
let mut roots: Vec<PathBuf> = writable_roots.clone();
|
||||
let mut roots: Vec<AbsolutePathBuf> = writable_roots.clone();
|
||||
|
||||
// Always include defaults: cwd, /tmp (if present on Unix), and
|
||||
// on macOS, the per-user TMPDIR unless explicitly excluded.
|
||||
roots.push(cwd.to_path_buf());
|
||||
// TODO(mbolin): cwd param should be AbsolutePathBuf.
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd);
|
||||
match cwd_absolute {
|
||||
Ok(cwd) => {
|
||||
roots.push(cwd);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Ignoring invalid cwd {:?} for sandbox writable root: {}",
|
||||
cwd, e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Include /tmp on Unix unless explicitly excluded.
|
||||
if cfg!(unix) && !exclude_slash_tmp {
|
||||
let slash_tmp = PathBuf::from("/tmp");
|
||||
if slash_tmp.is_dir() {
|
||||
#[allow(clippy::expect_used)]
|
||||
let slash_tmp =
|
||||
AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
|
||||
if slash_tmp.as_path().is_dir() {
|
||||
roots.push(slash_tmp);
|
||||
}
|
||||
}
|
||||
@@ -404,7 +448,16 @@ impl SandboxPolicy {
|
||||
&& let Some(tmpdir) = std::env::var_os("TMPDIR")
|
||||
&& !tmpdir.is_empty()
|
||||
{
|
||||
roots.push(PathBuf::from(tmpdir));
|
||||
match AbsolutePathBuf::from_absolute_path(PathBuf::from(&tmpdir)) {
|
||||
Ok(tmpdir_path) => {
|
||||
roots.push(tmpdir_path);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Ignoring invalid TMPDIR value {tmpdir:?} for sandbox writable root: {e}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each root, compute subpaths that should remain read-only.
|
||||
@@ -412,10 +465,20 @@ impl SandboxPolicy {
|
||||
.into_iter()
|
||||
.map(|writable_root| {
|
||||
let mut subpaths = Vec::new();
|
||||
let top_level_git = writable_root.join(".git");
|
||||
if top_level_git.is_dir() {
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_git = writable_root
|
||||
.join(".git")
|
||||
.expect(".git is a valid relative path");
|
||||
if top_level_git.as_path().is_dir() {
|
||||
subpaths.push(top_level_git);
|
||||
}
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_codex = writable_root
|
||||
.join(".codex")
|
||||
.expect(".codex is a valid relative path");
|
||||
if top_level_codex.as_path().is_dir() {
|
||||
subpaths.push(top_level_codex);
|
||||
}
|
||||
WritableRoot {
|
||||
root: writable_root,
|
||||
read_only_subpaths: subpaths,
|
||||
@@ -440,6 +503,7 @@ pub struct Event {
|
||||
/// NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum EventMsg {
|
||||
/// Error while executing a submission
|
||||
@@ -449,6 +513,9 @@ pub enum EventMsg {
|
||||
/// indicates the task continued but the user should still be notified.
|
||||
Warning(WarningEvent),
|
||||
|
||||
/// Conversation history was compacted (either automatically or manually).
|
||||
ContextCompacted(ContextCompactedEvent),
|
||||
|
||||
/// Agent has started a task
|
||||
TaskStarted(TaskStartedEvent),
|
||||
|
||||
@@ -485,6 +552,12 @@ pub enum EventMsg {
|
||||
/// Ack the client's configure message.
|
||||
SessionConfigured(SessionConfiguredEvent),
|
||||
|
||||
/// Incremental MCP startup progress updates.
|
||||
McpStartupUpdate(McpStartupUpdateEvent),
|
||||
|
||||
/// Aggregate MCP startup completion summary.
|
||||
McpStartupComplete(McpStartupCompleteEvent),
|
||||
|
||||
McpToolCallBegin(McpToolCallBeginEvent),
|
||||
|
||||
McpToolCallEnd(McpToolCallEndEvent),
|
||||
@@ -499,6 +572,9 @@ pub enum EventMsg {
|
||||
/// Incremental chunk of output from a running command.
|
||||
ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
|
||||
|
||||
/// Terminal interaction for an in-progress command (stdin sent and stdout observed).
|
||||
TerminalInteraction(TerminalInteractionEvent),
|
||||
|
||||
ExecCommandEnd(ExecCommandEndEvent),
|
||||
|
||||
/// Notification that the agent attached a local image via the view_image tool.
|
||||
@@ -506,6 +582,8 @@ pub enum EventMsg {
|
||||
|
||||
ExecApprovalRequest(ExecApprovalRequestEvent),
|
||||
|
||||
ElicitationRequest(ElicitationRequestEvent),
|
||||
|
||||
ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
|
||||
|
||||
/// Notification advising the user that something they are using has been
|
||||
@@ -540,6 +618,12 @@ pub enum EventMsg {
|
||||
/// List of custom prompts available to the agent.
|
||||
ListCustomPromptsResponse(ListCustomPromptsResponseEvent),
|
||||
|
||||
/// List of skills available to the agent.
|
||||
ListSkillsResponse(ListSkillsResponseEvent),
|
||||
|
||||
/// Notification that skill data may have been updated and clients may want to reload.
|
||||
SkillsUpdateAvailable,
|
||||
|
||||
PlanUpdate(UpdatePlanArgs),
|
||||
|
||||
TurnAborted(TurnAbortedEvent),
|
||||
@@ -563,6 +647,35 @@ pub enum EventMsg {
|
||||
ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
|
||||
}
|
||||
|
||||
/// Codex errors that we expose to clients.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum CodexErrorInfo {
|
||||
ContextWindowExceeded,
|
||||
UsageLimitExceeded,
|
||||
HttpConnectionFailed {
|
||||
http_status_code: Option<u16>,
|
||||
},
|
||||
/// Failed to connect to the response SSE stream.
|
||||
ResponseStreamConnectionFailed {
|
||||
http_status_code: Option<u16>,
|
||||
},
|
||||
InternalServerError,
|
||||
Unauthorized,
|
||||
BadRequest,
|
||||
SandboxError,
|
||||
/// The response SSE stream disconnected in the middle of a turnbefore completion.
|
||||
ResponseStreamDisconnected {
|
||||
http_status_code: Option<u16>,
|
||||
},
|
||||
/// Reached the retry limit for responses.
|
||||
ResponseTooManyFailedAttempts {
|
||||
http_status_code: Option<u16>,
|
||||
},
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
pub struct RawResponseItemEvent {
|
||||
pub item: ResponseItem,
|
||||
@@ -625,6 +738,9 @@ pub struct ReasoningContentDeltaEvent {
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
pub delta: String,
|
||||
// load with default value so it's backward compatible with the old format.
|
||||
#[serde(default)]
|
||||
pub summary_index: i64,
|
||||
}
|
||||
|
||||
impl HasLegacyEvent for ReasoningContentDeltaEvent {
|
||||
@@ -641,6 +757,9 @@ pub struct ReasoningRawContentDeltaEvent {
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
pub delta: String,
|
||||
// load with default value so it's backward compatible with the old format.
|
||||
#[serde(default)]
|
||||
pub content_index: i64,
|
||||
}
|
||||
|
||||
impl HasLegacyEvent for ReasoningRawContentDeltaEvent {
|
||||
@@ -681,6 +800,8 @@ pub struct ExitedReviewModeEvent {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ErrorEvent {
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub codex_error_info: Option<CodexErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
@@ -688,6 +809,9 @@ pub struct WarningEvent {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ContextCompactedEvent;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct TaskCompleteEvent {
|
||||
pub last_agent_message: Option<String>,
|
||||
@@ -785,6 +909,8 @@ pub struct TokenCountEvent {
|
||||
pub struct RateLimitSnapshot {
|
||||
pub primary: Option<RateLimitWindow>,
|
||||
pub secondary: Option<RateLimitWindow>,
|
||||
pub credits: Option<CreditsSnapshot>,
|
||||
pub plan_type: Option<crate::account::PlanType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
|
||||
@@ -799,6 +925,13 @@ pub struct RateLimitWindow {
|
||||
pub resets_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct CreditsSnapshot {
|
||||
pub has_credits: bool,
|
||||
pub unlimited: bool,
|
||||
pub balance: Option<String>,
|
||||
}
|
||||
|
||||
// Includes prompts, tools and space to call compact.
|
||||
const BASELINE_TOKENS: i64 = 12000;
|
||||
|
||||
@@ -931,7 +1064,13 @@ pub struct AgentReasoningRawContentDeltaEvent {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct AgentReasoningSectionBreakEvent {}
|
||||
pub struct AgentReasoningSectionBreakEvent {
|
||||
// load with default value so it's backward compatible with the old format.
|
||||
#[serde(default)]
|
||||
pub item_id: String,
|
||||
#[serde(default)]
|
||||
pub summary_index: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct AgentReasoningDeltaEvent {
|
||||
@@ -1066,6 +1205,29 @@ pub enum SubAgentSource {
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for SessionSource {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SessionSource::Cli => f.write_str("cli"),
|
||||
SessionSource::VSCode => f.write_str("vscode"),
|
||||
SessionSource::Exec => f.write_str("exec"),
|
||||
SessionSource::Mcp => f.write_str("mcp"),
|
||||
SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
|
||||
SessionSource::Unknown => f.write_str("unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SubAgentSource {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SubAgentSource::Review => f.write_str("review"),
|
||||
SubAgentSource::Compact => f.write_str("compact"),
|
||||
SubAgentSource::Other(other) => f.write_str(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
|
||||
pub struct SessionMeta {
|
||||
pub id: ConversationId,
|
||||
@@ -1115,6 +1277,8 @@ pub enum RolloutItem {
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
|
||||
pub struct CompactedItem {
|
||||
pub message: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub replacement_history: Option<Vec<ResponseItem>>,
|
||||
}
|
||||
|
||||
impl From<CompactedItem> for ResponseItem {
|
||||
@@ -1160,11 +1324,47 @@ pub struct GitInfo {
|
||||
pub repository_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Review request sent to the review session.
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReviewDelivery {
|
||||
Inline,
|
||||
Detached,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum ReviewTarget {
|
||||
/// Review the working tree: staged, unstaged, and untracked files.
|
||||
UncommittedChanges,
|
||||
|
||||
/// Review changes between the current branch and the given base branch.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
BaseBranch { branch: String },
|
||||
|
||||
/// Review the changes introduced by a specific commit.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Commit {
|
||||
sha: String,
|
||||
/// Optional human-readable label (e.g., commit subject) for UIs.
|
||||
title: Option<String>,
|
||||
},
|
||||
|
||||
/// Arbitrary instructions provided by the user.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Custom { instructions: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
/// Review request sent to the review session.
|
||||
pub struct ReviewRequest {
|
||||
pub prompt: String,
|
||||
pub user_facing_hint: String,
|
||||
pub target: ReviewTarget,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub user_facing_hint: Option<String>,
|
||||
}
|
||||
|
||||
/// Structured review result produced by a child review session.
|
||||
@@ -1211,25 +1411,68 @@ pub struct ReviewLineRange {
|
||||
pub end: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecCommandSource {
|
||||
Agent,
|
||||
UserShell,
|
||||
UnifiedExecStartup,
|
||||
UnifiedExecInteraction,
|
||||
}
|
||||
|
||||
impl Default for ExecCommandSource {
|
||||
fn default() -> Self {
|
||||
Self::Agent
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ExecCommandBeginEvent {
|
||||
/// Identifier so this can be paired with the ExecCommandEnd event.
|
||||
pub call_id: String,
|
||||
/// Identifier for the underlying PTY process (when available).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub process_id: Option<String>,
|
||||
/// Turn ID that this command belongs to.
|
||||
pub turn_id: String,
|
||||
/// The command to be executed.
|
||||
pub command: Vec<String>,
|
||||
/// The command's working directory if not the default cwd for the agent.
|
||||
pub cwd: PathBuf,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
/// True when this exec was initiated directly by the user (e.g. bang command),
|
||||
/// not by the agent/model. Defaults to false for backwards compatibility.
|
||||
/// Where the command originated. Defaults to Agent for backward compatibility.
|
||||
#[serde(default)]
|
||||
pub is_user_shell_command: bool,
|
||||
pub source: ExecCommandSource,
|
||||
/// Raw input sent to a unified exec session (if this is an interaction event).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub interaction_input: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ExecCommandEndEvent {
|
||||
/// Identifier for the ExecCommandBegin that finished.
|
||||
pub call_id: String,
|
||||
/// Identifier for the underlying PTY process (when available).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub process_id: Option<String>,
|
||||
/// Turn ID that this command belongs to.
|
||||
pub turn_id: String,
|
||||
/// The command that was executed.
|
||||
pub command: Vec<String>,
|
||||
/// The command's working directory if not the default cwd for the agent.
|
||||
pub cwd: PathBuf,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
/// Where the command originated. Defaults to Agent for backward compatibility.
|
||||
#[serde(default)]
|
||||
pub source: ExecCommandSource,
|
||||
/// Raw input sent to a unified exec session (if this is an interaction event).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub interaction_input: Option<String>,
|
||||
|
||||
/// Captured stdout
|
||||
pub stdout: String,
|
||||
/// Captured stderr
|
||||
@@ -1275,6 +1518,17 @@ pub struct ExecCommandOutputDeltaEvent {
|
||||
pub chunk: Vec<u8>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
pub struct TerminalInteractionEvent {
|
||||
/// Identifier for the ExecCommandBegin that produced this chunk.
|
||||
pub call_id: String,
|
||||
/// Process id associated with the running command.
|
||||
pub process_id: String,
|
||||
/// Stdin sent to the running session.
|
||||
pub stdin: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct BackgroundEventEvent {
|
||||
pub message: String,
|
||||
@@ -1305,6 +1559,8 @@ pub struct UndoCompletedEvent {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct StreamErrorEvent {
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub codex_error_info: Option<CodexErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
@@ -1316,6 +1572,10 @@ pub struct StreamInfoEvent {
|
||||
pub struct PatchApplyBeginEvent {
|
||||
/// Identifier so this can be paired with the PatchApplyEnd event.
|
||||
pub call_id: String,
|
||||
/// Turn ID that this patch belongs to.
|
||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
/// If true, there was no ApplyPatchApprovalRequest for this patch.
|
||||
pub auto_approved: bool,
|
||||
/// The changes to be applied.
|
||||
@@ -1326,12 +1586,19 @@ pub struct PatchApplyBeginEvent {
|
||||
pub struct PatchApplyEndEvent {
|
||||
/// Identifier for the PatchApplyBegin that finished.
|
||||
pub call_id: String,
|
||||
/// Turn ID that this patch belongs to.
|
||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
/// Captured stdout (summary printed by apply_patch).
|
||||
pub stdout: String,
|
||||
/// Captured stderr (parser errors, IO failures, etc.).
|
||||
pub stderr: String,
|
||||
/// Whether the patch was applied successfully.
|
||||
pub success: bool,
|
||||
/// The changes that were applied (mirrors PatchApplyBeginEvent::changes).
|
||||
#[serde(default)]
|
||||
pub changes: HashMap<PathBuf, FileChange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
@@ -1360,6 +1627,37 @@ pub struct McpListToolsResponseEvent {
|
||||
pub auth_statuses: std::collections::HashMap<String, McpAuthStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct McpStartupUpdateEvent {
|
||||
/// Server name being started.
|
||||
pub server: String,
|
||||
/// Current startup status.
|
||||
pub status: McpStartupStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case", tag = "state")]
|
||||
#[ts(rename_all = "snake_case", tag = "state")]
|
||||
pub enum McpStartupStatus {
|
||||
Starting,
|
||||
Ready,
|
||||
Failed { error: String },
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
|
||||
pub struct McpStartupCompleteEvent {
|
||||
pub ready: Vec<String>,
|
||||
pub failed: Vec<McpStartupFailure>,
|
||||
pub cancelled: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct McpStartupFailure {
|
||||
pub server: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
@@ -1388,7 +1686,43 @@ pub struct ListCustomPromptsResponseEvent {
|
||||
pub custom_prompts: Vec<CustomPrompt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
/// Response payload for `Op::ListSkills`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ListSkillsResponseEvent {
|
||||
pub skills: Vec<SkillsListEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum SkillScope {
|
||||
User,
|
||||
Repo,
|
||||
Public,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct SkillMetadata {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub path: PathBuf,
|
||||
pub scope: SkillScope,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct SkillErrorInfo {
|
||||
pub path: PathBuf,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct SkillsListEntry {
|
||||
pub cwd: PathBuf,
|
||||
pub skills: Vec<SkillMetadata>,
|
||||
pub errors: Vec<SkillErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct SessionConfiguredEvent {
|
||||
/// Name left as session_id instead of conversation_id for backwards compatibility.
|
||||
pub session_id: ConversationId,
|
||||
@@ -1396,6 +1730,18 @@ pub struct SessionConfiguredEvent {
|
||||
/// Tell the client what model is being queried.
|
||||
pub model: String,
|
||||
|
||||
pub model_provider_id: String,
|
||||
|
||||
/// When to escalate for approval for execution
|
||||
pub approval_policy: AskForApproval,
|
||||
|
||||
/// How to sandbox commands executed in the system
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
|
||||
/// Working directory that should be treated as the *root* of the
|
||||
/// session.
|
||||
pub cwd: PathBuf,
|
||||
|
||||
/// The effort the model is putting into reasoning about the user's request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
@@ -1415,14 +1761,18 @@ pub struct SessionConfiguredEvent {
|
||||
}
|
||||
|
||||
/// User's decision in response to an ExecApprovalRequest.
|
||||
#[derive(
|
||||
Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS,
|
||||
)]
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReviewDecision {
|
||||
/// User has approved this command and the agent should execute it.
|
||||
Approved,
|
||||
|
||||
/// User has approved this command and wants to apply the proposed execpolicy
|
||||
/// amendment so future matching commands are permitted.
|
||||
ApprovedExecpolicyAmendment {
|
||||
proposed_execpolicy_amendment: ExecPolicyAmendment,
|
||||
},
|
||||
|
||||
/// User has approved this command and wants to automatically approve any
|
||||
/// future identical instances (`command` and `cwd` match exactly) for the
|
||||
/// remainder of the session.
|
||||
@@ -1439,7 +1789,8 @@ pub enum ReviewDecision {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum FileChange {
|
||||
Add {
|
||||
content: String,
|
||||
@@ -1480,6 +1831,7 @@ mod tests {
|
||||
use crate::items::UserMessageItem;
|
||||
use crate::items::WebSearchItem;
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
@@ -1524,6 +1876,10 @@ mod tests {
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
model: "codex-mini-latest".to_string(),
|
||||
model_provider_id: "openai".to_string(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::ReadOnly,
|
||||
cwd: PathBuf::from("/home/user/project"),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
@@ -1538,6 +1894,12 @@ mod tests {
|
||||
"type": "session_configured",
|
||||
"session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||
"model": "codex-mini-latest",
|
||||
"model_provider_id": "openai",
|
||||
"approval_policy": "never",
|
||||
"sandbox_policy": {
|
||||
"type": "read-only"
|
||||
},
|
||||
"cwd": "/home/user/project",
|
||||
"reasoning_effort": "medium",
|
||||
"history_log_id": 0,
|
||||
"history_entry_count": 0,
|
||||
@@ -1565,4 +1927,47 @@ mod tests {
|
||||
assert_eq!(deserialized, event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_mcp_startup_update_event() -> Result<()> {
|
||||
let event = Event {
|
||||
id: "init".to_string(),
|
||||
msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
|
||||
server: "srv".to_string(),
|
||||
status: McpStartupStatus::Failed {
|
||||
error: "boom".to_string(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&event)?;
|
||||
assert_eq!(value["msg"]["type"], "mcp_startup_update");
|
||||
assert_eq!(value["msg"]["server"], "srv");
|
||||
assert_eq!(value["msg"]["status"]["state"], "failed");
|
||||
assert_eq!(value["msg"]["status"]["error"], "boom");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_mcp_startup_complete_event() -> Result<()> {
|
||||
let event = Event {
|
||||
id: "init".to_string(),
|
||||
msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
|
||||
ready: vec!["a".to_string()],
|
||||
failed: vec![McpStartupFailure {
|
||||
server: "b".to_string(),
|
||||
error: "bad".to_string(),
|
||||
}],
|
||||
cancelled: vec!["c".to_string()],
|
||||
}),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&event)?;
|
||||
assert_eq!(value["msg"]["type"], "mcp_startup_complete");
|
||||
assert_eq!(value["msg"]["ready"][0], "a");
|
||||
assert_eq!(value["msg"]["failed"][0]["server"], "b");
|
||||
assert_eq!(value["msg"]["failed"][0]["error"], "bad");
|
||||
assert_eq!(value["msg"]["cancelled"][0], "c");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,10 @@ pub enum UserInput {
|
||||
LocalImage {
|
||||
path: std::path::PathBuf,
|
||||
},
|
||||
|
||||
/// Skill selected by the user (name + path to SKILL.md).
|
||||
Skill {
|
||||
name: String,
|
||||
path: std::path::PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user