From 12ee9eb6e0021ed8e1c22ea68b2de1b2bbf7283a Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 10 Mar 2026 19:20:15 -0700 Subject: [PATCH] Add snippets annotated with types to tools when code mode enabled (#14284) Main purpose is for code mode to understand the return type. --- codex-rs/core/src/tools/code_mode.rs | 30 +- .../core/src/tools/code_mode_description.rs | 388 ++++++++++++++++++ codex-rs/core/src/tools/mod.rs | 1 + codex-rs/core/src/tools/spec.rs | 359 +++++++++++++--- 4 files changed, 699 insertions(+), 79 deletions(-) create mode 100644 codex-rs/core/src/tools/code_mode_description.rs diff --git a/codex-rs/core/src/tools/code_mode.rs b/codex-rs/core/src/tools/code_mode.rs index 6e2c704b7..e8ca460ff 100644 --- a/codex-rs/core/src/tools/code_mode.rs +++ b/codex-rs/core/src/tools/code_mode.rs @@ -10,6 +10,7 @@ use crate::exec_env::create_env; use crate::features::Feature; use crate::function_tool::FunctionCallError; use crate::tools::ToolRouter; +use crate::tools::code_mode_description::code_mode_tool_reference; use crate::tools::context::FunctionToolOutput; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::ToolPayload; @@ -347,25 +348,6 @@ fn truncate_code_mode_result( async fn build_enabled_tools(exec: &ExecContext) -> Vec { let router = build_nested_router(exec).await; - let mcp_tool_names = exec - .session - .services - .mcp_connection_manager - .read() - .await - .list_all_tools() - .await - .into_iter() - .map(|(qualified_name, tool_info)| { - ( - qualified_name, - ( - vec!["mcp".to_string(), tool_info.server_name], - tool_info.tool_name, - ), - ) - }) - .collect::>(); let mut out = Vec::new(); for spec in router.specs() { let tool_name = spec.name().to_string(); @@ -373,16 +355,12 @@ async fn build_enabled_tools(exec: &ExecContext) -> Vec { continue; } - let (namespace, name) = if let Some((namespace, name)) = mcp_tool_names.get(&tool_name) { - (namespace.clone(), name.clone()) - } else { - (Vec::new(), tool_name.clone()) - }; + let reference = code_mode_tool_reference(&tool_name); out.push(EnabledTool { tool_name, - namespace, - name, + namespace: reference.namespace, + name: reference.tool_key, kind: tool_kind_for_spec(&spec), }); } diff --git a/codex-rs/core/src/tools/code_mode_description.rs b/codex-rs/core/src/tools/code_mode_description.rs new file mode 100644 index 000000000..b801ac035 --- /dev/null +++ b/codex-rs/core/src/tools/code_mode_description.rs @@ -0,0 +1,388 @@ +use crate::client_common::tools::ToolSpec; +use crate::mcp::split_qualified_tool_name; +use crate::tools::code_mode::PUBLIC_TOOL_NAME; +use serde_json::Value as JsonValue; + +pub(crate) struct CodeModeToolReference { + pub(crate) module_path: String, + pub(crate) namespace: Vec, + pub(crate) tool_key: String, +} + +pub(crate) fn code_mode_tool_reference(tool_name: &str) -> CodeModeToolReference { + if let Some((server_name, tool_key)) = split_qualified_tool_name(tool_name) { + let namespace = vec!["mcp".to_string(), server_name]; + return CodeModeToolReference { + module_path: format!("tools/{}.js", namespace.join("/")), + namespace, + tool_key, + }; + } + + CodeModeToolReference { + module_path: "tools.js".to_string(), + namespace: Vec::new(), + tool_key: tool_name.to_string(), + } +} + +pub(crate) fn augment_tool_spec_for_code_mode(spec: ToolSpec, code_mode_enabled: bool) -> ToolSpec { + if !code_mode_enabled { + return spec; + } + + match spec { + ToolSpec::Function(mut tool) => { + if tool.name != PUBLIC_TOOL_NAME { + tool.description = append_code_mode_sample( + &tool.description, + &tool.name, + "args", + serde_json::to_value(&tool.parameters) + .ok() + .as_ref() + .map(render_json_schema_to_typescript) + .unwrap_or_else(|| "unknown".to_string()), + tool.output_schema + .as_ref() + .map(render_json_schema_to_typescript) + .unwrap_or_else(|| "unknown".to_string()), + ); + } + ToolSpec::Function(tool) + } + ToolSpec::Freeform(mut tool) => { + if tool.name != PUBLIC_TOOL_NAME { + tool.description = append_code_mode_sample( + &tool.description, + &tool.name, + "input", + "string".to_string(), + "unknown".to_string(), + ); + } + ToolSpec::Freeform(tool) + } + other => other, + } +} + +fn append_code_mode_sample( + description: &str, + tool_name: &str, + input_name: &str, + input_type: String, + output_type: String, +) -> String { + let reference = code_mode_tool_reference(tool_name); + let local_name = code_mode_local_name(&reference.tool_key); + + format!( + "{description}\n\nCode mode declaration:\n```ts\nimport {{ tools }} from \"{}\";\ndeclare function {local_name}({input_name}: {input_type}): Promise<{output_type}>;\n```", + reference.module_path + ) +} + +fn code_mode_local_name(tool_key: &str) -> String { + let mut identifier = String::new(); + + for (index, ch) in tool_key.chars().enumerate() { + let is_valid = if index == 0 { + ch == '_' || ch == '$' || ch.is_ascii_alphabetic() + } else { + ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() + }; + + if is_valid { + identifier.push(ch); + } else { + identifier.push('_'); + } + } + + if identifier.is_empty() { + return "tool_call".to_string(); + } + + if identifier == "tools" { + identifier.push_str("_tool"); + } + + if identifier + .chars() + .next() + .is_some_and(|ch| ch.is_ascii_digit()) + { + identifier.insert(0, '_'); + } + + identifier +} + +fn render_json_schema_to_typescript(schema: &JsonValue) -> String { + render_json_schema_to_typescript_inner(schema, 0) +} + +fn render_json_schema_to_typescript_inner(schema: &JsonValue, indent: usize) -> String { + match schema { + JsonValue::Bool(true) => "unknown".to_string(), + JsonValue::Bool(false) => "never".to_string(), + JsonValue::Object(map) => { + if let Some(value) = map.get("const") { + return render_json_schema_literal(value); + } + + if let Some(values) = map.get("enum").and_then(serde_json::Value::as_array) { + let rendered = values + .iter() + .map(render_json_schema_literal) + .collect::>(); + if !rendered.is_empty() { + return rendered.join(" | "); + } + } + + for key in ["anyOf", "oneOf"] { + if let Some(variants) = map.get(key).and_then(serde_json::Value::as_array) { + let rendered = variants + .iter() + .map(|variant| render_json_schema_to_typescript_inner(variant, indent)) + .collect::>(); + if !rendered.is_empty() { + return rendered.join(" | "); + } + } + } + + if let Some(variants) = map.get("allOf").and_then(serde_json::Value::as_array) { + let rendered = variants + .iter() + .map(|variant| render_json_schema_to_typescript_inner(variant, indent)) + .collect::>(); + if !rendered.is_empty() { + return rendered.join(" & "); + } + } + + if let Some(schema_type) = map.get("type") { + if let Some(types) = schema_type.as_array() { + let rendered = types + .iter() + .filter_map(serde_json::Value::as_str) + .map(|schema_type| { + render_json_schema_type_keyword(map, schema_type, indent) + }) + .collect::>(); + if !rendered.is_empty() { + return rendered.join(" | "); + } + } + + if let Some(schema_type) = schema_type.as_str() { + return render_json_schema_type_keyword(map, schema_type, indent); + } + } + + if map.contains_key("properties") + || map.contains_key("additionalProperties") + || map.contains_key("required") + { + return render_json_schema_object(map, indent); + } + + if map.contains_key("items") || map.contains_key("prefixItems") { + return render_json_schema_array(map, indent); + } + + "unknown".to_string() + } + _ => "unknown".to_string(), + } +} + +fn render_json_schema_type_keyword( + map: &serde_json::Map, + schema_type: &str, + indent: usize, +) -> String { + match schema_type { + "string" => "string".to_string(), + "number" | "integer" => "number".to_string(), + "boolean" => "boolean".to_string(), + "null" => "null".to_string(), + "array" => render_json_schema_array(map, indent), + "object" => render_json_schema_object(map, indent), + _ => "unknown".to_string(), + } +} + +fn render_json_schema_array(map: &serde_json::Map, indent: usize) -> String { + if let Some(items) = map.get("items") { + let item_type = render_json_schema_to_typescript_inner(items, indent + 2); + return format!("Array<{item_type}>"); + } + + if let Some(items) = map.get("prefixItems").and_then(serde_json::Value::as_array) { + let item_types = items + .iter() + .map(|item| render_json_schema_to_typescript_inner(item, indent + 2)) + .collect::>(); + if !item_types.is_empty() { + return format!("[{}]", item_types.join(", ")); + } + } + + "unknown[]".to_string() +} + +fn render_json_schema_object(map: &serde_json::Map, indent: usize) -> String { + let required = map + .get("required") + .and_then(serde_json::Value::as_array) + .map(|items| { + items + .iter() + .filter_map(serde_json::Value::as_str) + .collect::>() + }) + .unwrap_or_default(); + let properties = map + .get("properties") + .and_then(serde_json::Value::as_object) + .cloned() + .unwrap_or_default(); + + let mut sorted_properties = properties.iter().collect::>(); + sorted_properties.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)); + + let mut lines = sorted_properties + .into_iter() + .map(|(name, value)| { + let optional = if required.iter().any(|required_name| required_name == name) { + "" + } else { + "?" + }; + let property_name = render_json_schema_property_name(name); + let property_type = render_json_schema_to_typescript_inner(value, indent + 2); + format!( + "{}{property_name}{optional}: {property_type};", + " ".repeat(indent + 2) + ) + }) + .collect::>(); + + if let Some(additional_properties) = map.get("additionalProperties") { + let additional_type = match additional_properties { + JsonValue::Bool(true) => Some("unknown".to_string()), + JsonValue::Bool(false) => None, + value => Some(render_json_schema_to_typescript_inner(value, indent + 2)), + }; + + if let Some(additional_type) = additional_type { + lines.push(format!( + "{}[key: string]: {additional_type};", + " ".repeat(indent + 2) + )); + } + } else if properties.is_empty() { + lines.push(format!("{}[key: string]: unknown;", " ".repeat(indent + 2))); + } + + if lines.is_empty() { + return "{}".to_string(); + } + + format!("{{\n{}\n{}}}", lines.join("\n"), " ".repeat(indent)) +} + +fn render_json_schema_property_name(name: &str) -> String { + if code_mode_local_name(name) == name { + name.to_string() + } else { + serde_json::to_string(name).unwrap_or_else(|_| format!("\"{}\"", name.replace('"', "\\\""))) + } +} + +fn render_json_schema_literal(value: &JsonValue) -> String { + serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string()) +} + +#[cfg(test)] +mod tests { + use super::render_json_schema_to_typescript; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[test] + fn render_json_schema_to_typescript_renders_object_properties() { + let schema = json!({ + "type": "object", + "properties": { + "path": {"type": "string"}, + "recursive": {"type": "boolean"} + }, + "required": ["path"], + "additionalProperties": false + }); + + assert_eq!( + render_json_schema_to_typescript(&schema), + "{\n path: string;\n recursive?: boolean;\n}" + ); + } + + #[test] + fn render_json_schema_to_typescript_renders_anyof_unions() { + let schema = json!({ + "anyOf": [ + {"const": "pending"}, + {"const": "done"}, + {"type": "number"} + ] + }); + + assert_eq!( + render_json_schema_to_typescript(&schema), + "\"pending\" | \"done\" | number" + ); + } + + #[test] + fn render_json_schema_to_typescript_renders_additional_properties() { + let schema = json!({ + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": {"type": "string"} + } + }, + "additionalProperties": {"type": "integer"} + }); + + assert_eq!( + render_json_schema_to_typescript(&schema), + "{\n tags?: Array;\n [key: string]: number;\n}" + ); + } + + #[test] + fn render_json_schema_to_typescript_sorts_object_properties() { + let schema = json!({ + "type": "object", + "properties": { + "structuredContent": {"type": "string"}, + "_meta": {"type": "string"}, + "isError": {"type": "boolean"}, + "content": {"type": "array", "items": {"type": "string"}} + }, + "required": ["content"] + }); + + assert_eq!( + render_json_schema_to_typescript(&schema), + "{\n _meta?: string;\n content: Array;\n isError?: boolean;\n structuredContent?: string;\n}" + ); + } +} diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index 677e9d5f9..20808325b 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -1,4 +1,5 @@ pub mod code_mode; +pub(crate) mod code_mode_description; pub mod context; pub mod events; pub(crate) mod handlers; diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index a34ca7315..8aab13979 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -8,6 +8,7 @@ use crate::features::Features; use crate::mcp_connection_manager::ToolInfo; use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::tools::code_mode::PUBLIC_TOOL_NAME; +use crate::tools::code_mode_description::augment_tool_spec_for_code_mode; use crate::tools::handlers::PLAN_TOOL; use crate::tools::handlers::SEARCH_TOOL_BM25_DEFAULT_LIMIT; use crate::tools::handlers::SEARCH_TOOL_BM25_TOOL_NAME; @@ -1764,6 +1765,20 @@ pub fn create_tools_json_for_responses_api( Ok(tools_json) } +fn push_tool_spec( + builder: &mut ToolRegistryBuilder, + spec: ToolSpec, + supports_parallel_tool_calls: bool, + code_mode_enabled: bool, +) { + let spec = augment_tool_spec_for_code_mode(spec, code_mode_enabled); + if supports_parallel_tool_calls { + builder.push_spec_with_parallel_support(spec, true); + } else { + builder.push_spec(spec); + } +} + pub(crate) fn mcp_tool_to_openai_tool( fully_qualified_name: String, tool: rmcp::model::Tool, @@ -2031,26 +2046,45 @@ pub(crate) fn build_specs( .collect::>(); enabled_tool_names.sort(); enabled_tool_names.dedup(); - builder.push_spec(create_code_mode_tool(&enabled_tool_names)); + push_tool_spec( + &mut builder, + create_code_mode_tool(&enabled_tool_names), + false, + config.code_mode_enabled, + ); builder.register_handler(PUBLIC_TOOL_NAME, code_mode_handler); } match &config.shell_type { ConfigShellToolType::Default => { - builder.push_spec_with_parallel_support( + push_tool_spec( + &mut builder, create_shell_tool(request_permission_enabled), true, + config.code_mode_enabled, ); } ConfigShellToolType::Local => { - builder.push_spec_with_parallel_support(ToolSpec::LocalShell {}, true); + push_tool_spec( + &mut builder, + ToolSpec::LocalShell {}, + true, + config.code_mode_enabled, + ); } ConfigShellToolType::UnifiedExec => { - builder.push_spec_with_parallel_support( + push_tool_spec( + &mut builder, create_exec_command_tool(config.allow_login_shell, request_permission_enabled), true, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_write_stdin_tool(), + false, + config.code_mode_enabled, ); - builder.push_spec(create_write_stdin_tool()); builder.register_handler("exec_command", unified_exec_handler.clone()); builder.register_handler("write_stdin", unified_exec_handler); } @@ -2058,9 +2092,11 @@ pub(crate) fn build_specs( // Do nothing. } ConfigShellToolType::ShellCommand => { - builder.push_spec_with_parallel_support( + push_tool_spec( + &mut builder, create_shell_command_tool(config.allow_login_shell, request_permission_enabled), true, + config.code_mode_enabled, ); } } @@ -2074,49 +2110,104 @@ pub(crate) fn build_specs( } if mcp_tools.is_some() { - builder.push_spec_with_parallel_support(create_list_mcp_resources_tool(), true); - builder.push_spec_with_parallel_support(create_list_mcp_resource_templates_tool(), true); - builder.push_spec_with_parallel_support(create_read_mcp_resource_tool(), true); + push_tool_spec( + &mut builder, + create_list_mcp_resources_tool(), + true, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_list_mcp_resource_templates_tool(), + true, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_read_mcp_resource_tool(), + true, + config.code_mode_enabled, + ); builder.register_handler("list_mcp_resources", mcp_resource_handler.clone()); builder.register_handler("list_mcp_resource_templates", mcp_resource_handler.clone()); builder.register_handler("read_mcp_resource", mcp_resource_handler); } - builder.push_spec(PLAN_TOOL.clone()); + push_tool_spec( + &mut builder, + PLAN_TOOL.clone(), + false, + config.code_mode_enabled, + ); builder.register_handler("update_plan", plan_handler); if config.js_repl_enabled { - builder.push_spec(create_js_repl_tool()); - builder.push_spec(create_js_repl_reset_tool()); + push_tool_spec( + &mut builder, + create_js_repl_tool(), + false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_js_repl_reset_tool(), + false, + config.code_mode_enabled, + ); builder.register_handler("js_repl", js_repl_handler); builder.register_handler("js_repl_reset", js_repl_reset_handler); } if config.request_user_input { - builder.push_spec(create_request_user_input_tool(CollaborationModesConfig { - default_mode_request_user_input: config.default_mode_request_user_input, - })); + push_tool_spec( + &mut builder, + create_request_user_input_tool(CollaborationModesConfig { + default_mode_request_user_input: config.default_mode_request_user_input, + }), + false, + config.code_mode_enabled, + ); builder.register_handler("request_user_input", request_user_input_handler); } if config.request_permissions_tool_enabled { - builder.push_spec(create_request_permissions_tool()); + push_tool_spec( + &mut builder, + create_request_permissions_tool(), + false, + config.code_mode_enabled, + ); builder.register_handler("request_permissions", request_permissions_handler); } if config.search_tool { let app_tools = app_tools.unwrap_or_default(); - builder.push_spec_with_parallel_support(create_search_tool_bm25_tool(&app_tools), true); + push_tool_spec( + &mut builder, + create_search_tool_bm25_tool(&app_tools), + true, + config.code_mode_enabled, + ); builder.register_handler(SEARCH_TOOL_BM25_TOOL_NAME, search_tool_handler); } if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type { match apply_patch_tool_type { ApplyPatchToolType::Freeform => { - builder.push_spec(create_apply_patch_freeform_tool()); + push_tool_spec( + &mut builder, + create_apply_patch_freeform_tool(), + false, + config.code_mode_enabled, + ); } ApplyPatchToolType::Function => { - builder.push_spec(create_apply_patch_json_tool()); + push_tool_spec( + &mut builder, + create_apply_patch_json_tool(), + false, + config.code_mode_enabled, + ); } } builder.register_handler("apply_patch", apply_patch_handler); @@ -2127,7 +2218,12 @@ pub(crate) fn build_specs( .contains(&"grep_files".to_string()) { let grep_files_handler = Arc::new(GrepFilesHandler); - builder.push_spec_with_parallel_support(create_grep_files_tool(), true); + push_tool_spec( + &mut builder, + create_grep_files_tool(), + true, + config.code_mode_enabled, + ); builder.register_handler("grep_files", grep_files_handler); } @@ -2136,7 +2232,12 @@ pub(crate) fn build_specs( .contains(&"read_file".to_string()) { let read_file_handler = Arc::new(ReadFileHandler); - builder.push_spec_with_parallel_support(create_read_file_tool(), true); + push_tool_spec( + &mut builder, + create_read_file_tool(), + true, + config.code_mode_enabled, + ); builder.register_handler("read_file", read_file_handler); } @@ -2146,7 +2247,12 @@ pub(crate) fn build_specs( .any(|tool| tool == "list_dir") { let list_dir_handler = Arc::new(ListDirHandler); - builder.push_spec_with_parallel_support(create_list_dir_tool(), true); + push_tool_spec( + &mut builder, + create_list_dir_tool(), + true, + config.code_mode_enabled, + ); builder.register_handler("list_dir", list_dir_handler); } @@ -2155,7 +2261,12 @@ pub(crate) fn build_specs( .contains(&"test_sync_tool".to_string()) { let test_sync_handler = Arc::new(TestSyncHandler); - builder.push_spec_with_parallel_support(create_test_sync_tool(), true); + push_tool_spec( + &mut builder, + create_test_sync_tool(), + true, + config.code_mode_enabled, + ); builder.register_handler("test_sync_tool", test_sync_handler); } @@ -2176,45 +2287,90 @@ pub(crate) fn build_specs( ), }; - builder.push_spec(ToolSpec::WebSearch { - external_web_access: Some(external_web_access), - filters: config - .web_search_config - .as_ref() - .and_then(|cfg| cfg.filters.clone().map(Into::into)), - user_location: config - .web_search_config - .as_ref() - .and_then(|cfg| cfg.user_location.clone().map(Into::into)), - search_context_size: config - .web_search_config - .as_ref() - .and_then(|cfg| cfg.search_context_size), - search_content_types, - }); + push_tool_spec( + &mut builder, + ToolSpec::WebSearch { + external_web_access: Some(external_web_access), + filters: config + .web_search_config + .as_ref() + .and_then(|cfg| cfg.filters.clone().map(Into::into)), + user_location: config + .web_search_config + .as_ref() + .and_then(|cfg| cfg.user_location.clone().map(Into::into)), + search_context_size: config + .web_search_config + .as_ref() + .and_then(|cfg| cfg.search_context_size), + search_content_types, + }, + false, + config.code_mode_enabled, + ); } if config.image_gen_tool { - builder.push_spec(ToolSpec::ImageGeneration { - output_format: "png".to_string(), - }); + push_tool_spec( + &mut builder, + ToolSpec::ImageGeneration { + output_format: "png".to_string(), + }, + false, + config.code_mode_enabled, + ); } - builder.push_spec_with_parallel_support(create_view_image_tool(), true); + push_tool_spec( + &mut builder, + create_view_image_tool(), + true, + config.code_mode_enabled, + ); builder.register_handler("view_image", view_image_handler); if config.artifact_tools { - builder.push_spec(create_artifacts_tool()); + push_tool_spec( + &mut builder, + create_artifacts_tool(), + false, + config.code_mode_enabled, + ); builder.register_handler("artifacts", artifacts_handler); } if config.collab_tools { let multi_agent_handler = Arc::new(MultiAgentHandler); - builder.push_spec(create_spawn_agent_tool(config)); - builder.push_spec(create_send_input_tool()); - builder.push_spec(create_resume_agent_tool()); - builder.push_spec(create_wait_tool()); - builder.push_spec(create_close_agent_tool()); + push_tool_spec( + &mut builder, + create_spawn_agent_tool(config), + false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_send_input_tool(), + false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_resume_agent_tool(), + false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_wait_tool(), + false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_close_agent_tool(), + false, + config.code_mode_enabled, + ); builder.register_handler("spawn_agent", multi_agent_handler.clone()); builder.register_handler("send_input", multi_agent_handler.clone()); builder.register_handler("resume_agent", multi_agent_handler.clone()); @@ -2224,10 +2380,20 @@ pub(crate) fn build_specs( if config.agent_jobs_tools { let agent_jobs_handler = Arc::new(BatchJobHandler); - builder.push_spec(create_spawn_agents_on_csv_tool()); + push_tool_spec( + &mut builder, + create_spawn_agents_on_csv_tool(), + false, + config.code_mode_enabled, + ); builder.register_handler("spawn_agents_on_csv", agent_jobs_handler.clone()); if config.agent_jobs_worker_tools { - builder.push_spec(create_report_agent_job_result_tool()); + push_tool_spec( + &mut builder, + create_report_agent_job_result_tool(), + false, + config.code_mode_enabled, + ); builder.register_handler("report_agent_job_result", agent_jobs_handler); } } @@ -2239,7 +2405,12 @@ pub(crate) fn build_specs( for (name, tool) in entries.into_iter() { match mcp_tool_to_openai_tool(name.clone(), tool.clone()) { Ok(converted_tool) => { - builder.push_spec(ToolSpec::Function(converted_tool)); + push_tool_spec( + &mut builder, + ToolSpec::Function(converted_tool), + false, + config.code_mode_enabled, + ); builder.register_handler(name, mcp_handler.clone()); } Err(e) => { @@ -2253,7 +2424,12 @@ pub(crate) fn build_specs( for tool in dynamic_tools { match dynamic_tool_to_openai_tool(tool) { Ok(converted_tool) => { - builder.push_spec(ToolSpec::Function(converted_tool)); + push_tool_spec( + &mut builder, + ToolSpec::Function(converted_tool), + false, + config.code_mode_enabled, + ); builder.register_handler(tool.name.clone(), dynamic_tool_handler.clone()); } Err(e) => { @@ -4179,6 +4355,83 @@ Examples of valid command strings: ); } + #[test] + fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { + let config = test_config(); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut features = Features::with_defaults(); + features.enable(Feature::CodeMode); + features.enable(Feature::UnifiedExec); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + }); + + let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); + let ToolSpec::Function(ResponsesApiTool { description, .. }) = + &find_tool(&tools, "view_image").spec + else { + panic!("expected function tool"); + }; + + assert_eq!( + 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).\n\nCode mode declaration:\n```ts\nimport { tools } from \"tools.js\";\ndeclare function view_image(args: {\n path: string;\n}): Promise;\n```" + ); + } + + #[test] + fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { + let config = test_config(); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut features = Features::with_defaults(); + features.enable(Feature::CodeMode); + features.enable(Feature::UnifiedExec); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + }); + + let (tools, _) = build_specs( + &tools_config, + Some(HashMap::from([( + "mcp__sample__echo".to_string(), + mcp_tool( + "echo", + "Echo text", + serde_json::json!({ + "type": "object", + "properties": { + "message": {"type": "string"} + }, + "required": ["message"], + "additionalProperties": false + }), + ), + )])), + None, + &[], + ) + .build(); + + let ToolSpec::Function(ResponsesApiTool { description, .. }) = + &find_tool(&tools, "mcp__sample__echo").spec + else { + panic!("expected function tool"); + }; + + assert_eq!( + description, + "Echo text\n\nCode mode declaration:\n```ts\nimport { tools } from \"tools/mcp/sample.js\";\ndeclare function echo(args: {\n message: string;\n}): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n}>;\n```" + ); + } + #[test] fn chat_tools_include_top_level_name() { let properties =