diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index fe592236c1..1b753de1c5 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -9,10 +9,7 @@ use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::models::WebSearchAction; -use codex_protocol::models::is_image_close_tag_text; -use codex_protocol::models::is_image_open_tag_text; -use codex_protocol::models::is_local_image_close_tag_text; -use codex_protocol::models::is_local_image_open_tag_text; +use codex_protocol::models::is_local_image_label_text; use codex_protocol::user_input::UserInput; use tracing::warn; use uuid::Uuid; @@ -39,11 +36,8 @@ fn parse_user_message(message: &[ContentItem]) -> Option { for (idx, content_item) in message.iter().enumerate() { match content_item { ContentItem::InputText { text } => { - if (is_local_image_open_tag_text(text) || is_image_open_tag_text(text)) - && (matches!(message.get(idx + 1), Some(ContentItem::InputImage { .. }))) - || (idx > 0 - && (is_local_image_close_tag_text(text) || is_image_close_tag_text(text)) - && matches!(message.get(idx - 1), Some(ContentItem::InputImage { .. }))) + if is_local_image_label_text(text) + && matches!(message.get(idx + 1), Some(ContentItem::InputImage { .. })) { continue; } @@ -148,6 +142,7 @@ mod tests { use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::models::WebSearchAction; + use codex_protocol::models::local_image_label_text; use codex_protocol::user_input::UserInput; use pretty_assertions::assert_eq; @@ -192,7 +187,7 @@ mod tests { #[test] fn skips_local_image_label_text() { let image_url = "data:image/png;base64,abc".to_string(); - let label = codex_protocol::models::local_image_open_tag_text(1); + let label = local_image_label_text(std::path::Path::new("/tmp/example.png")); let user_text = "Please review this image.".to_string(); let item = ResponseItem::Message { @@ -203,46 +198,6 @@ mod tests { ContentItem::InputImage { image_url: image_url.clone(), }, - ContentItem::InputText { - text: "".to_string(), - }, - ContentItem::InputText { - text: user_text.clone(), - }, - ], - }; - - let turn_item = parse_turn_item(&item).expect("expected user message turn item"); - - match turn_item { - TurnItem::UserMessage(user) => { - let expected_content = vec![ - UserInput::Image { image_url }, - UserInput::Text { text: user_text }, - ]; - assert_eq!(user.content, expected_content); - } - other => panic!("expected TurnItem::UserMessage, got {other:?}"), - } - } - - #[test] - fn skips_unnamed_image_label_text() { - let image_url = "data:image/png;base64,abc".to_string(); - let label = codex_protocol::models::image_open_tag_text(); - let user_text = "Please review this image.".to_string(); - - let item = ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ - ContentItem::InputText { text: label }, - ContentItem::InputImage { - image_url: image_url.clone(), - }, - ContentItem::InputText { - text: codex_protocol::models::image_close_tag_text(), - }, ContentItem::InputText { text: user_text.clone(), }, diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index 87dd7207b1..2791097298 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -13,7 +13,7 @@ use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; -use codex_protocol::models::local_image_content_items_with_label_number; +use codex_protocol::models::local_image_content_items; pub struct ViewImageHandler; @@ -65,8 +65,7 @@ impl ToolHandler for ViewImageHandler { } let event_path = abs_path.clone(); - let content: Vec = - local_image_content_items_with_label_number(&abs_path, None); + let content: Vec = local_image_content_items(&abs_path, false); let input = ResponseInputItem::Message { role: "user".to_string(), content, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 0a66b41403..a26aca2502 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -5,8 +5,6 @@ use crate::features::Features; use crate::tools::handlers::PLAN_TOOL; use crate::tools::handlers::apply_patch::create_apply_patch_freeform_tool; use crate::tools::handlers::apply_patch::create_apply_patch_json_tool; -use crate::tools::handlers::collab::DEFAULT_WAIT_TIMEOUT_MS; -use crate::tools::handlers::collab::MAX_WAIT_TIMEOUT_MS; use crate::tools::registry::ToolRegistryBuilder; use codex_protocol::models::VIEW_IMAGE_TOOL_NAME; use codex_protocol::openai_models::ApplyPatchToolType; @@ -25,7 +23,6 @@ pub(crate) struct ToolsConfig { pub apply_patch_tool_type: Option, pub web_search_request: bool, pub web_search_cached: bool, - pub collab_tools: bool, pub experimental_supported_tools: Vec, } @@ -43,7 +40,6 @@ impl ToolsConfig { let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform); let include_web_search_request = features.enabled(Feature::WebSearchRequest); let include_web_search_cached = features.enabled(Feature::WebSearchCached); - let include_collab_tools = features.enabled(Feature::Collab); let shell_type = if !features.enabled(Feature::ShellTool) { ConfigShellToolType::Disabled @@ -75,7 +71,6 @@ impl ToolsConfig { apply_patch_tool_type, web_search_request: include_web_search_request, web_search_cached: include_web_search_cached, - collab_tools: include_collab_tools, experimental_supported_tools: model_info.experimental_supported_tools.clone(), } } @@ -138,72 +133,70 @@ impl From for AdditionalProperties { } fn create_exec_command_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "cmd".to_string(), - JsonSchema::String { - description: Some("Shell command to execute.".to_string()), - }, - ), - ( - "workdir".to_string(), - JsonSchema::String { - description: Some( - "Optional working directory to run the command in; defaults to the turn cwd." - .to_string(), - ), - }, - ), - ( - "shell".to_string(), - JsonSchema::String { - description: Some("Shell binary to launch. Defaults to /bin/bash.".to_string()), - }, - ), - ( - "login".to_string(), - JsonSchema::Boolean { - description: Some( - "Whether to run the shell with -l/-i semantics. Defaults to true.".to_string(), - ), - }, - ), - ( - "yield_time_ms".to_string(), - JsonSchema::Number { - description: Some( - "How long to wait (in milliseconds) for output before yielding.".to_string(), - ), - }, - ), - ( - "max_output_tokens".to_string(), - JsonSchema::Number { - description: Some( - "Maximum number of tokens to return. Excess output will be truncated." - .to_string(), - ), - }, - ), - ( - "sandbox_permissions".to_string(), - JsonSchema::String { - description: Some( - "Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"." - .to_string(), - ), - }, - ), - ( - "justification".to_string(), - JsonSchema::String { - description: Some( - "Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command." - .to_string(), - ), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "cmd".to_string(), + JsonSchema::String { + description: Some("Shell command to execute.".to_string()), + }, + ); + properties.insert( + "workdir".to_string(), + JsonSchema::String { + description: Some( + "Optional working directory to run the command in; defaults to the turn cwd." + .to_string(), + ), + }, + ); + properties.insert( + "shell".to_string(), + JsonSchema::String { + description: Some("Shell binary to launch. Defaults to /bin/bash.".to_string()), + }, + ); + properties.insert( + "login".to_string(), + JsonSchema::Boolean { + description: Some( + "Whether to run the shell with -l/-i semantics. Defaults to true.".to_string(), + ), + }, + ); + properties.insert( + "yield_time_ms".to_string(), + JsonSchema::Number { + description: Some( + "How long to wait (in milliseconds) for output before yielding.".to_string(), + ), + }, + ); + properties.insert( + "max_output_tokens".to_string(), + JsonSchema::Number { + description: Some( + "Maximum number of tokens to return. Excess output will be truncated.".to_string(), + ), + }, + ); + properties.insert( + "sandbox_permissions".to_string(), + JsonSchema::String { + description: Some( + "Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"." + .to_string(), + ), + }, + ); + properties.insert( + "justification".to_string(), + JsonSchema::String { + description: Some( + "Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command." + .to_string(), + ), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "exec_command".to_string(), @@ -220,37 +213,35 @@ fn create_exec_command_tool() -> ToolSpec { } fn create_write_stdin_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "session_id".to_string(), - JsonSchema::Number { - description: Some("Identifier of the running unified exec session.".to_string()), - }, - ), - ( - "chars".to_string(), - JsonSchema::String { - description: Some("Bytes to write to stdin (may be empty to poll).".to_string()), - }, - ), - ( - "yield_time_ms".to_string(), - JsonSchema::Number { - description: Some( - "How long to wait (in milliseconds) for output before yielding.".to_string(), - ), - }, - ), - ( - "max_output_tokens".to_string(), - JsonSchema::Number { - description: Some( - "Maximum number of tokens to return. Excess output will be truncated." - .to_string(), - ), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "session_id".to_string(), + JsonSchema::Number { + description: Some("Identifier of the running unified exec session.".to_string()), + }, + ); + properties.insert( + "chars".to_string(), + JsonSchema::String { + description: Some("Bytes to write to stdin (may be empty to poll).".to_string()), + }, + ); + properties.insert( + "yield_time_ms".to_string(), + JsonSchema::Number { + description: Some( + "How long to wait (in milliseconds) for output before yielding.".to_string(), + ), + }, + ); + properties.insert( + "max_output_tokens".to_string(), + JsonSchema::Number { + description: Some( + "Maximum number of tokens to return. Excess output will be truncated.".to_string(), + ), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "write_stdin".to_string(), @@ -267,39 +258,39 @@ fn create_write_stdin_tool() -> ToolSpec { } fn create_shell_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "command".to_string(), - JsonSchema::Array { - items: Box::new(JsonSchema::String { description: None }), - description: Some("The command to execute".to_string()), - }, - ), - ( - "workdir".to_string(), - JsonSchema::String { - description: Some("The working directory to execute the command in".to_string()), - }, - ), - ( - "timeout_ms".to_string(), - JsonSchema::Number { - description: Some("The timeout for the command in milliseconds".to_string()), - }, - ), - ( - "sandbox_permissions".to_string(), - JsonSchema::String { - description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()), - }, - ), - ( - "justification".to_string(), - JsonSchema::String { - description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "command".to_string(), + JsonSchema::Array { + items: Box::new(JsonSchema::String { description: None }), + description: Some("The command to execute".to_string()), + }, + ); + properties.insert( + "workdir".to_string(), + JsonSchema::String { + description: Some("The working directory to execute the command in".to_string()), + }, + ); + properties.insert( + "timeout_ms".to_string(), + JsonSchema::Number { + description: Some("The timeout for the command in milliseconds".to_string()), + }, + ); + + properties.insert( + "sandbox_permissions".to_string(), + JsonSchema::String { + description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()), + }, + ); + properties.insert( + "justification".to_string(), + JsonSchema::String { + description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()), + }, + ); let description = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"]. @@ -331,49 +322,48 @@ Examples of valid command strings: } fn create_shell_command_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "command".to_string(), - JsonSchema::String { - description: Some( - "The shell script to execute in the user's default shell".to_string(), - ), - }, - ), - ( - "workdir".to_string(), - JsonSchema::String { - description: Some("The working directory to execute the command in".to_string()), - }, - ), - ( - "login".to_string(), - JsonSchema::Boolean { - description: Some( - "Whether to run the shell with login shell semantics. Defaults to true." - .to_string(), - ), - }, - ), - ( - "timeout_ms".to_string(), - JsonSchema::Number { - description: Some("The timeout for the command in milliseconds".to_string()), - }, - ), - ( - "sandbox_permissions".to_string(), - JsonSchema::String { - description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()), - }, - ), - ( - "justification".to_string(), - JsonSchema::String { - description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "command".to_string(), + JsonSchema::String { + description: Some( + "The shell script to execute in the user's default shell".to_string(), + ), + }, + ); + properties.insert( + "workdir".to_string(), + JsonSchema::String { + description: Some("The working directory to execute the command in".to_string()), + }, + ); + properties.insert( + "login".to_string(), + JsonSchema::Boolean { + description: Some( + "Whether to run the shell with login shell semantics. Defaults to true." + .to_string(), + ), + }, + ); + properties.insert( + "timeout_ms".to_string(), + JsonSchema::Number { + description: Some("The timeout for the command in milliseconds".to_string()), + }, + ); + properties.insert( + "sandbox_permissions".to_string(), + JsonSchema::String { + description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()), + }, + ); + properties.insert( + "justification".to_string(), + JsonSchema::String { + description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()), + }, + ); let description = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. @@ -405,17 +395,17 @@ Examples of valid command strings: fn create_view_image_tool() -> ToolSpec { // Support only local filesystem path. - let properties = BTreeMap::from([( + let mut properties = BTreeMap::new(); + properties.insert( "path".to_string(), JsonSchema::String { description: Some("Local filesystem path to an image file".to_string()), }, - )]); + ); ToolSpec::Function(ResponsesApiTool { name: VIEW_IMAGE_TOOL_NAME.to_string(), - description: "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags)." - .to_string(), + description: "Preview a local image from the filesystem.".to_string(), strict: false, parameters: JsonSchema::Object { properties, @@ -425,158 +415,55 @@ fn create_view_image_tool() -> ToolSpec { }) } -fn create_spawn_agent_tool() -> ToolSpec { +fn create_test_sync_tool() -> ToolSpec { let mut properties = BTreeMap::new(); properties.insert( - "message".to_string(), - JsonSchema::String { - description: Some("Initial message to send to the new agent.".to_string()), + "sleep_before_ms".to_string(), + JsonSchema::Number { + description: Some("Optional delay in milliseconds before any other action".to_string()), + }, + ); + properties.insert( + "sleep_after_ms".to_string(), + JsonSchema::Number { + description: Some( + "Optional delay in milliseconds after completing the barrier".to_string(), + ), }, ); - ToolSpec::Function(ResponsesApiTool { - name: "spawn_agent".to_string(), - description: "Spawn a new agent and return its id.".to_string(), - strict: false, - parameters: JsonSchema::Object { - properties, - required: Some(vec!["message".to_string()]), - additional_properties: Some(false.into()), - }, - }) -} - -fn create_send_input_tool() -> ToolSpec { - let mut properties = BTreeMap::new(); - properties.insert( + let mut barrier_properties = BTreeMap::new(); + barrier_properties.insert( "id".to_string(), JsonSchema::String { - description: Some("Identifier of the agent to message.".to_string()), + description: Some( + "Identifier shared by concurrent calls that should rendezvous".to_string(), + ), }, ); - properties.insert( - "message".to_string(), - JsonSchema::String { - description: Some("Message to send to the agent.".to_string()), + barrier_properties.insert( + "participants".to_string(), + JsonSchema::Number { + description: Some( + "Number of tool calls that must arrive before the barrier opens".to_string(), + ), }, ); - - ToolSpec::Function(ResponsesApiTool { - name: "send_input".to_string(), - description: "Send a message to an existing agent.".to_string(), - strict: false, - parameters: JsonSchema::Object { - properties, - required: Some(vec!["id".to_string(), "message".to_string()]), - additional_properties: Some(false.into()), - }, - }) -} - -fn create_wait_tool() -> ToolSpec { - let mut properties = BTreeMap::new(); - properties.insert( - "id".to_string(), - JsonSchema::String { - description: Some("Identifier of the agent to wait on.".to_string()), - }, - ); - properties.insert( + barrier_properties.insert( "timeout_ms".to_string(), JsonSchema::Number { - description: Some(format!( - "Optional timeout in milliseconds. Defaults to {DEFAULT_WAIT_TIMEOUT_MS} and max {MAX_WAIT_TIMEOUT_MS}." - )), + description: Some("Maximum time in milliseconds to wait at the barrier".to_string()), }, ); - ToolSpec::Function(ResponsesApiTool { - name: "wait".to_string(), - description: "Wait for an agent and return its status.".to_string(), - strict: false, - parameters: JsonSchema::Object { - properties, - required: Some(vec!["id".to_string()]), - additional_properties: Some(false.into()), - }, - }) -} - -fn create_close_agent_tool() -> ToolSpec { - let mut properties = BTreeMap::new(); properties.insert( - "id".to_string(), - JsonSchema::String { - description: Some("Identifier of the agent to close.".to_string()), - }, - ); - - ToolSpec::Function(ResponsesApiTool { - name: "close_agent".to_string(), - description: "Close an agent and return its last known status.".to_string(), - strict: false, - parameters: JsonSchema::Object { - properties, - required: Some(vec!["id".to_string()]), + "barrier".to_string(), + JsonSchema::Object { + properties: barrier_properties, + required: Some(vec!["id".to_string(), "participants".to_string()]), additional_properties: Some(false.into()), }, - }) -} - -fn create_test_sync_tool() -> ToolSpec { - let barrier_properties = BTreeMap::from([ - ( - "id".to_string(), - JsonSchema::String { - description: Some( - "Identifier shared by concurrent calls that should rendezvous".to_string(), - ), - }, - ), - ( - "participants".to_string(), - JsonSchema::Number { - description: Some( - "Number of tool calls that must arrive before the barrier opens".to_string(), - ), - }, - ), - ( - "timeout_ms".to_string(), - JsonSchema::Number { - description: Some( - "Maximum time in milliseconds to wait at the barrier".to_string(), - ), - }, - ), - ]); - - let properties = BTreeMap::from([ - ( - "sleep_before_ms".to_string(), - JsonSchema::Number { - description: Some( - "Optional delay in milliseconds before any other action".to_string(), - ), - }, - ), - ( - "sleep_after_ms".to_string(), - JsonSchema::Number { - description: Some( - "Optional delay in milliseconds after completing the barrier".to_string(), - ), - }, - ), - ( - "barrier".to_string(), - JsonSchema::Object { - properties: barrier_properties, - required: Some(vec!["id".to_string(), "participants".to_string()]), - additional_properties: Some(false.into()), - }, - ), - ]); + ); ToolSpec::Function(ResponsesApiTool { name: "test_sync_tool".to_string(), @@ -591,41 +478,40 @@ fn create_test_sync_tool() -> ToolSpec { } fn create_grep_files_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "pattern".to_string(), - JsonSchema::String { - description: Some("Regular expression pattern to search for.".to_string()), - }, - ), - ( - "include".to_string(), - JsonSchema::String { - description: Some( - "Optional glob that limits which files are searched (e.g. \"*.rs\" or \ - \"*.{ts,tsx}\")." - .to_string(), - ), - }, - ), - ( - "path".to_string(), - JsonSchema::String { - description: Some( - "Directory or file path to search. Defaults to the session's working directory." - .to_string(), - ), - }, - ), - ( - "limit".to_string(), - JsonSchema::Number { - description: Some( - "Maximum number of file paths to return (defaults to 100).".to_string(), - ), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "pattern".to_string(), + JsonSchema::String { + description: Some("Regular expression pattern to search for.".to_string()), + }, + ); + properties.insert( + "include".to_string(), + JsonSchema::String { + description: Some( + "Optional glob that limits which files are searched (e.g. \"*.rs\" or \ + \"*.{ts,tsx}\")." + .to_string(), + ), + }, + ); + properties.insert( + "path".to_string(), + JsonSchema::String { + description: Some( + "Directory or file path to search. Defaults to the session's working directory." + .to_string(), + ), + }, + ); + properties.insert( + "limit".to_string(), + JsonSchema::Number { + description: Some( + "Maximum number of file paths to return (defaults to 100).".to_string(), + ), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "grep_files".to_string(), @@ -642,93 +528,88 @@ fn create_grep_files_tool() -> ToolSpec { } fn create_read_file_tool() -> ToolSpec { - let indentation_properties = BTreeMap::from([ - ( - "anchor_line".to_string(), - JsonSchema::Number { - description: Some( - "Anchor line to center the indentation lookup on (defaults to offset)." - .to_string(), - ), - }, - ), - ( - "max_levels".to_string(), - JsonSchema::Number { - description: Some( - "How many parent indentation levels (smaller indents) to include.".to_string(), - ), - }, - ), - ( - "include_siblings".to_string(), - JsonSchema::Boolean { - description: Some( - "When true, include additional blocks that share the anchor indentation." - .to_string(), - ), - }, - ), - ( - "include_header".to_string(), - JsonSchema::Boolean { - description: Some( - "Include doc comments or attributes directly above the selected block." - .to_string(), - ), - }, - ), - ( - "max_lines".to_string(), - JsonSchema::Number { - description: Some( - "Hard cap on the number of lines returned when using indentation mode." - .to_string(), - ), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "file_path".to_string(), + JsonSchema::String { + description: Some("Absolute path to the file".to_string()), + }, + ); + properties.insert( + "offset".to_string(), + JsonSchema::Number { + description: Some( + "The line number to start reading from. Must be 1 or greater.".to_string(), + ), + }, + ); + properties.insert( + "limit".to_string(), + JsonSchema::Number { + description: Some("The maximum number of lines to return.".to_string()), + }, + ); + properties.insert( + "mode".to_string(), + JsonSchema::String { + description: Some( + "Optional mode selector: \"slice\" for simple ranges (default) or \"indentation\" \ + to expand around an anchor line." + .to_string(), + ), + }, + ); - let properties = BTreeMap::from([ - ( - "file_path".to_string(), - JsonSchema::String { - description: Some("Absolute path to the file".to_string()), - }, - ), - ( - "offset".to_string(), - JsonSchema::Number { - description: Some( - "The line number to start reading from. Must be 1 or greater.".to_string(), - ), - }, - ), - ( - "limit".to_string(), - JsonSchema::Number { - description: Some("The maximum number of lines to return.".to_string()), - }, - ), - ( - "mode".to_string(), - JsonSchema::String { - description: Some( - "Optional mode selector: \"slice\" for simple ranges (default) or \"indentation\" \ - to expand around an anchor line." - .to_string(), - ), - }, - ), - ( - "indentation".to_string(), - JsonSchema::Object { - properties: indentation_properties, - required: None, - additional_properties: Some(false.into()), - }, - ), - ]); + let mut indentation_properties = BTreeMap::new(); + indentation_properties.insert( + "anchor_line".to_string(), + JsonSchema::Number { + description: Some( + "Anchor line to center the indentation lookup on (defaults to offset).".to_string(), + ), + }, + ); + indentation_properties.insert( + "max_levels".to_string(), + JsonSchema::Number { + description: Some( + "How many parent indentation levels (smaller indents) to include.".to_string(), + ), + }, + ); + indentation_properties.insert( + "include_siblings".to_string(), + JsonSchema::Boolean { + description: Some( + "When true, include additional blocks that share the anchor indentation." + .to_string(), + ), + }, + ); + indentation_properties.insert( + "include_header".to_string(), + JsonSchema::Boolean { + description: Some( + "Include doc comments or attributes directly above the selected block.".to_string(), + ), + }, + ); + indentation_properties.insert( + "max_lines".to_string(), + JsonSchema::Number { + description: Some( + "Hard cap on the number of lines returned when using indentation mode.".to_string(), + ), + }, + ); + properties.insert( + "indentation".to_string(), + JsonSchema::Object { + properties: indentation_properties, + required: None, + additional_properties: Some(false.into()), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "read_file".to_string(), @@ -745,36 +626,35 @@ fn create_read_file_tool() -> ToolSpec { } fn create_list_dir_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "dir_path".to_string(), - JsonSchema::String { - description: Some("Absolute path to the directory to list.".to_string()), - }, - ), - ( - "offset".to_string(), - JsonSchema::Number { - description: Some( - "The entry number to start listing from. Must be 1 or greater.".to_string(), - ), - }, - ), - ( - "limit".to_string(), - JsonSchema::Number { - description: Some("The maximum number of entries to return.".to_string()), - }, - ), - ( - "depth".to_string(), - JsonSchema::Number { - description: Some( - "The maximum directory depth to traverse. Must be 1 or greater.".to_string(), - ), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "dir_path".to_string(), + JsonSchema::String { + description: Some("Absolute path to the directory to list.".to_string()), + }, + ); + properties.insert( + "offset".to_string(), + JsonSchema::Number { + description: Some( + "The entry number to start listing from. Must be 1 or greater.".to_string(), + ), + }, + ); + properties.insert( + "limit".to_string(), + JsonSchema::Number { + description: Some("The maximum number of entries to return.".to_string()), + }, + ); + properties.insert( + "depth".to_string(), + JsonSchema::Number { + description: Some( + "The maximum directory depth to traverse. Must be 1 or greater.".to_string(), + ), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "list_dir".to_string(), @@ -791,26 +671,25 @@ fn create_list_dir_tool() -> ToolSpec { } fn create_list_mcp_resources_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "server".to_string(), - JsonSchema::String { - description: Some( - "Optional MCP server name. When omitted, lists resources from every configured server." - .to_string(), - ), - }, - ), - ( - "cursor".to_string(), - JsonSchema::String { - description: Some( - "Opaque cursor returned by a previous list_mcp_resources call for the same server." - .to_string(), - ), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "server".to_string(), + JsonSchema::String { + description: Some( + "Optional MCP server name. When omitted, lists resources from every configured server." + .to_string(), + ), + }, + ); + properties.insert( + "cursor".to_string(), + JsonSchema::String { + description: Some( + "Opaque cursor returned by a previous list_mcp_resources call for the same server." + .to_string(), + ), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "list_mcp_resources".to_string(), @@ -825,26 +704,25 @@ fn create_list_mcp_resources_tool() -> ToolSpec { } fn create_list_mcp_resource_templates_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "server".to_string(), - JsonSchema::String { - description: Some( - "Optional MCP server name. When omitted, lists resource templates from all configured servers." - .to_string(), - ), - }, - ), - ( - "cursor".to_string(), - JsonSchema::String { - description: Some( - "Opaque cursor returned by a previous list_mcp_resource_templates call for the same server." - .to_string(), - ), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "server".to_string(), + JsonSchema::String { + description: Some( + "Optional MCP server name. When omitted, lists resource templates from all configured servers." + .to_string(), + ), + }, + ); + properties.insert( + "cursor".to_string(), + JsonSchema::String { + description: Some( + "Opaque cursor returned by a previous list_mcp_resource_templates call for the same server." + .to_string(), + ), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "list_mcp_resource_templates".to_string(), @@ -859,26 +737,25 @@ fn create_list_mcp_resource_templates_tool() -> ToolSpec { } fn create_read_mcp_resource_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "server".to_string(), - JsonSchema::String { - description: Some( - "MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources." - .to_string(), - ), - }, - ), - ( - "uri".to_string(), - JsonSchema::String { - description: Some( - "Resource URI to read. Must be one of the URIs returned by list_mcp_resources." - .to_string(), - ), - }, - ), - ]); + let mut properties = BTreeMap::new(); + properties.insert( + "server".to_string(), + JsonSchema::String { + description: Some( + "MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources." + .to_string(), + ), + }, + ); + properties.insert( + "uri".to_string(), + JsonSchema::String { + description: Some( + "Resource URI to read. Must be one of the URIs returned by list_mcp_resources." + .to_string(), + ), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "read_mcp_resource".to_string(), @@ -1103,7 +980,6 @@ pub(crate) fn build_specs( mcp_tools: Option>, ) -> ToolRegistryBuilder { use crate::tools::handlers::ApplyPatchHandler; - use crate::tools::handlers::CollabHandler; use crate::tools::handlers::GrepFilesHandler; use crate::tools::handlers::ListDirHandler; use crate::tools::handlers::McpHandler; @@ -1230,18 +1106,6 @@ pub(crate) fn build_specs( builder.push_spec_with_parallel_support(create_view_image_tool(), true); builder.register_handler("view_image", view_image_handler); - if config.collab_tools { - let collab_handler = Arc::new(CollabHandler); - builder.push_spec(create_spawn_agent_tool()); - builder.push_spec(create_send_input_tool()); - builder.push_spec(create_wait_tool()); - builder.push_spec(create_close_agent_tool()); - builder.register_handler("spawn_agent", collab_handler.clone()); - builder.register_handler("send_input", collab_handler.clone()); - builder.register_handler("wait", collab_handler.clone()); - builder.register_handler("close_agent", collab_handler); - } - if let Some(mcp_tools) = mcp_tools { let mut entries: Vec<(String, mcp_types::Tool)> = mcp_tools.into_iter().collect(); entries.sort_by(|a, b| a.0.cmp(&b.0)); @@ -1375,7 +1239,7 @@ mod tests { // Build actual map name -> spec use std::collections::BTreeMap; use std::collections::HashSet; - let mut actual: BTreeMap = BTreeMap::from([]); + let mut actual: BTreeMap = BTreeMap::new(); let mut duplicate_names = Vec::new(); for t in &tools { let name = tool_name(&t.spec).to_string(); @@ -1389,7 +1253,7 @@ mod tests { ); // Build expected from the same helpers used by the builder. - let mut expected: BTreeMap = BTreeMap::from([]); + let mut expected: BTreeMap = BTreeMap::new(); for spec in [ create_exec_command_tool(), create_write_stdin_tool(), @@ -1421,23 +1285,6 @@ mod tests { } } - #[test] - fn test_build_specs_collab_tools_enabled() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::Collab); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - features: &features, - }); - let (tools, _) = build_specs(&tools_config, None).build(); - assert_contains_tool_names( - &tools, - &["spawn_agent", "send_input", "wait", "close_agent"], - ); - } - fn assert_model_tools(model_slug: &str, features: &Features, expected_tools: &[&str]) { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline(model_slug, &config); @@ -2311,8 +2158,8 @@ Examples of valid command strings: #[test] fn chat_tools_include_top_level_name() { - let properties = - BTreeMap::from([("foo".to_string(), JsonSchema::String { description: None })]); + let mut properties = BTreeMap::new(); + properties.insert("foo".to_string(), JsonSchema::String { description: None }); let tools = vec![ToolSpec::Function(ResponsesApiTool { name: "demo".to_string(), description: "A demo tool".to_string(), diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index bec2a35f5a..137b12916b 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -182,44 +182,24 @@ fn local_image_error_placeholder( pub const VIEW_IMAGE_TOOL_NAME: &str = "view_image"; -const IMAGE_OPEN_TAG: &str = ""; -const IMAGE_CLOSE_TAG: &str = ""; -const LOCAL_IMAGE_OPEN_TAG_PREFIX: &str = ""; -const LOCAL_IMAGE_CLOSE_TAG: &str = IMAGE_CLOSE_TAG; - -pub fn image_open_tag_text() -> String { - IMAGE_OPEN_TAG.to_string() +fn local_image_label_suffix() -> String { + format!(" follows (you can see it without using the {VIEW_IMAGE_TOOL_NAME} tool):") } -pub fn image_close_tag_text() -> String { - IMAGE_CLOSE_TAG.to_string() +pub fn local_image_label_text(path: &std::path::Path) -> String { + format!("Image {}{}", path.display(), local_image_label_suffix()) } -pub fn local_image_label_text(label_number: usize) -> String { - format!("[Image #{label_number}]") +pub fn is_local_image_label_text(text: &str) -> bool { + let suffix = local_image_label_suffix(); + let trimmed = text.trim(); + trimmed.starts_with("Image ") && trimmed.ends_with(&suffix) } -pub fn local_image_open_tag_text(label_number: usize) -> String { - let label = local_image_label_text(label_number); - format!("{LOCAL_IMAGE_OPEN_TAG_PREFIX}{label}{LOCAL_IMAGE_OPEN_TAG_SUFFIX}") -} - -pub fn is_local_image_open_tag_text(text: &str) -> bool { - text.strip_prefix(LOCAL_IMAGE_OPEN_TAG_PREFIX) - .is_some_and(|rest| rest.ends_with(LOCAL_IMAGE_OPEN_TAG_SUFFIX)) -} - -pub fn is_local_image_close_tag_text(text: &str) -> bool { - is_image_close_tag_text(text) -} - -pub fn is_image_open_tag_text(text: &str) -> bool { - text == IMAGE_OPEN_TAG -} - -pub fn is_image_close_tag_text(text: &str) -> bool { - text == IMAGE_CLOSE_TAG +fn local_image_label(path: &std::path::Path) -> ContentItem { + ContentItem::InputText { + text: local_image_label_text(path), + } } fn invalid_image_error_placeholder( @@ -245,26 +225,16 @@ fn unsupported_image_error_placeholder(path: &std::path::Path, mime: &str) -> Co } } -pub fn local_image_content_items_with_label_number( - path: &std::path::Path, - label_number: Option, -) -> Vec { +pub fn local_image_content_items(path: &std::path::Path, include_label: bool) -> Vec { match load_and_resize_to_fit(path) { Ok(image) => { - let mut items = Vec::with_capacity(3); - if let Some(label_number) = label_number { - items.push(ContentItem::InputText { - text: local_image_open_tag_text(label_number), - }); + let mut items = Vec::with_capacity(2); + if include_label { + items.push(local_image_label(path)); } items.push(ContentItem::InputImage { image_url: image.into_data_url(), }); - if label_number.is_some() { - items.push(ContentItem::InputText { - text: LOCAL_IMAGE_CLOSE_TAG.to_string(), - }); - } items } Err(err) => { @@ -385,26 +355,14 @@ pub enum ReasoningItemContent { impl From> for ResponseInputItem { fn from(items: Vec) -> Self { - let mut image_index = 0; Self::Message { role: "user".to_string(), content: items .into_iter() .flat_map(|c| match c { UserInput::Text { text } => vec![ContentItem::InputText { text }], - UserInput::Image { image_url } => vec![ - ContentItem::InputText { - text: image_open_tag_text(), - }, - ContentItem::InputImage { image_url }, - ContentItem::InputText { - text: image_close_tag_text(), - }, - ], - UserInput::LocalImage { path } => { - image_index += 1; - local_image_content_items_with_label_number(&path, Some(image_index)) - } + UserInput::Image { image_url } => vec![ContentItem::InputImage { image_url }], + UserInput::LocalImage { path } => local_image_content_items(&path, true), UserInput::Skill { .. } => Vec::new(), // Skill bodies are injected later in core }) .collect::>(), @@ -845,33 +803,6 @@ mod tests { Ok(()) } - #[test] - fn wraps_image_user_input_with_tags() -> Result<()> { - let image_url = "data:image/png;base64,abc".to_string(); - - let item = ResponseInputItem::from(vec![UserInput::Image { - image_url: image_url.clone(), - }]); - - match item { - ResponseInputItem::Message { content, .. } => { - let expected = vec![ - ContentItem::InputText { - text: image_open_tag_text(), - }, - ContentItem::InputImage { image_url }, - ContentItem::InputText { - text: image_close_tag_text(), - }, - ]; - assert_eq!(content, expected); - } - other => panic!("expected message response but got {other:?}"), - } - - Ok(()) - } - #[test] fn local_image_read_error_adds_placeholder() -> Result<()> { let dir = tempdir()?;