diff --git a/codex-rs/code-mode/src/description.rs b/codex-rs/code-mode/src/description.rs index 97320c8c5..0cba45d7e 100644 --- a/codex-rs/code-mode/src/description.rs +++ b/codex-rs/code-mode/src/description.rs @@ -1,6 +1,7 @@ use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; +use std::collections::BTreeMap; use crate::PUBLIC_TOOL_NAME; @@ -57,6 +58,12 @@ pub struct ToolDefinition { pub output_schema: Option, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ToolNamespaceDescription { + pub name: String, + pub description: String, +} + #[derive(Debug, Default, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] struct CodeModeExecPragma { @@ -163,6 +170,7 @@ pub fn is_code_mode_nested_tool(tool_name: &str) -> bool { pub fn build_exec_tool_description( enabled_tools: &[(String, String)], + namespace_descriptions: &BTreeMap, code_mode_only: bool, ) -> String { if !code_mode_only { @@ -175,17 +183,38 @@ pub fn build_exec_tool_description( ]; if !enabled_tools.is_empty() { - let nested_tool_reference = enabled_tools - .iter() - .map(|(name, nested_description)| { - let global_name = normalize_code_mode_identifier(name); - format!( - "### `{global_name}` (`{name}`)\n{}", - nested_description.trim() - ) - }) - .collect::>() - .join("\n\n"); + let mut current_namespace: Option<&str> = None; + let mut nested_tool_sections = Vec::with_capacity(enabled_tools.len()); + + for (name, nested_description) in enabled_tools { + let next_namespace = namespace_descriptions + .get(name) + .map(|namespace_description| namespace_description.name.as_str()); + if next_namespace != current_namespace { + if let Some(namespace_description) = namespace_descriptions.get(name) { + let namespace_description_text = namespace_description.description.trim(); + if !namespace_description_text.is_empty() { + nested_tool_sections.push(format!( + "## {}\n{namespace_description_text}", + namespace_description.name + )); + } + } + current_namespace = next_namespace; + } + + let global_name = normalize_code_mode_identifier(name); + let nested_description = nested_description.trim(); + if nested_description.is_empty() { + nested_tool_sections.push(format!("### `{global_name}` (`{name}`)")); + } else { + nested_tool_sections.push(format!( + "### `{global_name}` (`{name}`)\n{nested_description}" + )); + } + } + + let nested_tool_reference = nested_tool_sections.join("\n\n"); sections.push(nested_tool_reference); } @@ -524,12 +553,14 @@ mod tests { use super::CodeModeToolKind; use super::ParsedExecSource; use super::ToolDefinition; + use super::ToolNamespaceDescription; use super::augment_tool_definition; use super::build_exec_tool_description; use super::normalize_code_mode_identifier; use super::parse_exec_source; use pretty_assertions::assert_eq; use serde_json::json; + use std::collections::BTreeMap; #[test] fn parse_exec_source_without_pragma() { @@ -646,6 +677,7 @@ mod tests { fn code_mode_only_description_includes_nested_tools() { let description = build_exec_tool_description( &[("foo".to_string(), "bar".to_string())], + &BTreeMap::new(), /*code_mode_only*/ true, ); assert!(description.contains("### `foo` (`foo`)")); @@ -653,8 +685,67 @@ mod tests { #[test] fn exec_description_mentions_timeout_helpers() { - let description = build_exec_tool_description(&[], /*code_mode_only*/ false); + let description = + build_exec_tool_description(&[], &BTreeMap::new(), /*code_mode_only*/ false); assert!(description.contains("`setTimeout(callback: () => void, delayMs?: number)`")); assert!(description.contains("`clearTimeout(timeoutId?: number)`")); } + + #[test] + fn code_mode_only_description_groups_namespace_instructions_once() { + let namespace_descriptions = BTreeMap::from([ + ( + "mcp__sample__alpha".to_string(), + ToolNamespaceDescription { + name: "mcp__sample".to_string(), + description: "Shared namespace guidance.".to_string(), + }, + ), + ( + "mcp__sample__beta".to_string(), + ToolNamespaceDescription { + name: "mcp__sample".to_string(), + description: "Shared namespace guidance.".to_string(), + }, + ), + ]); + let description = build_exec_tool_description( + &[ + ("mcp__sample__alpha".to_string(), "First tool".to_string()), + ("mcp__sample__beta".to_string(), "Second tool".to_string()), + ], + &namespace_descriptions, + /*code_mode_only*/ true, + ); + assert_eq!(description.matches("## mcp__sample").count(), 1); + assert!(description.contains( + r#"## mcp__sample +Shared namespace guidance. + +### `mcp__sample__alpha` (`mcp__sample__alpha`) +First tool + +### `mcp__sample__beta` (`mcp__sample__beta`) +Second tool"# + )); + } + + #[test] + fn code_mode_only_description_omits_empty_namespace_sections() { + let namespace_descriptions = BTreeMap::from([( + "mcp__sample__alpha".to_string(), + ToolNamespaceDescription { + name: "mcp__sample".to_string(), + description: String::new(), + }, + )]); + let description = build_exec_tool_description( + &[("mcp__sample__alpha".to_string(), "First tool".to_string())], + &namespace_descriptions, + /*code_mode_only*/ true, + ); + + assert!(!description.contains("## mcp__sample")); + assert!(description.contains("### `mcp__sample__alpha` (`mcp__sample__alpha`)")); + } } diff --git a/codex-rs/code-mode/src/lib.rs b/codex-rs/code-mode/src/lib.rs index 841e568be..f7ab0d48e 100644 --- a/codex-rs/code-mode/src/lib.rs +++ b/codex-rs/code-mode/src/lib.rs @@ -6,6 +6,7 @@ mod service; pub use description::CODE_MODE_PRAGMA_PREFIX; pub use description::CodeModeToolKind; pub use description::ToolDefinition; +pub use description::ToolNamespaceDescription; pub use description::append_code_mode_sample; pub use description::augment_tool_definition; pub use description::build_exec_tool_description; diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager.rs b/codex-rs/codex-mcp/src/mcp_connection_manager.rs index c90088c7d..bfbd5f5b2 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager.rs @@ -186,6 +186,8 @@ pub struct ToolInfo { pub server_name: String, pub tool_name: String, pub tool_namespace: String, + #[serde(default)] + pub server_instructions: Option, pub tool: Tool, pub connector_id: Option, pub connector_name: Option, @@ -356,6 +358,7 @@ struct ManagedClient { tools: Vec, tool_filter: ToolFilter, tool_timeout: Option, + server_instructions: Option, server_supports_sandbox_state_capability: bool, codex_apps_tools_cache_context: Option, } @@ -842,6 +845,7 @@ impl McpConnectionManager { CODEX_APPS_MCP_SERVER_NAME, &managed_client.client, managed_client.tool_timeout, + managed_client.server_instructions.as_deref(), ) .await .with_context(|| { @@ -1374,9 +1378,14 @@ async fn start_server_task( let list_start = Instant::now(); let fetch_start = Instant::now(); - let tools = list_tools_for_client_uncached(&server_name, &client, startup_timeout) - .await - .map_err(StartupOutcomeError::from)?; + let tools = list_tools_for_client_uncached( + &server_name, + &client, + startup_timeout, + initialize_result.instructions.as_deref(), + ) + .await + .map_err(StartupOutcomeError::from)?; emit_duration( MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC, fetch_start.elapsed(), @@ -1407,6 +1416,7 @@ async fn start_server_task( tools, tool_timeout: Some(tool_timeout), tool_filter, + server_instructions: initialize_result.instructions, server_supports_sandbox_state_capability, codex_apps_tools_cache_context, }; @@ -1587,6 +1597,7 @@ async fn list_tools_for_client_uncached( server_name: &str, client: &Arc, timeout: Option, + server_instructions: Option<&str>, ) -> Result> { let resp = client .list_tools_with_connector_ids(/*params*/ None, timeout) @@ -1617,6 +1628,7 @@ async fn list_tools_for_client_uncached( server_name: server_name.to_owned(), tool_name, tool_namespace, + server_instructions: server_instructions.map(str::to_string), tool: tool_def, connector_id: tool.connector_id, connector_name, diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs b/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs index b34a5cc04..835ea7087 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs @@ -15,6 +15,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo { } else { server_name.to_string() }, + server_instructions: None, tool: Tool { name: tool_name.to_string().into(), title: None, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 57ecbd442..cbc3cc8c1 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -6888,16 +6888,18 @@ pub(crate) async fn built_tools( } else { app_tools }; + let mcp_tool_router_inputs = + has_mcp_servers.then(|| crate::tools::router::map_mcp_tool_infos(&mcp_tools)); Ok(Arc::new(ToolRouter::from_config( &turn_context.tools_config, ToolRouterParams { - mcp_tools: has_mcp_servers.then(|| { - mcp_tools - .into_iter() - .map(|(name, tool)| (name, tool.tool)) - .collect() - }), + mcp_tools: mcp_tool_router_inputs + .as_ref() + .map(|inputs| inputs.mcp_tools.clone()), + tool_namespaces: mcp_tool_router_inputs + .as_ref() + .map(|inputs| inputs.tool_namespaces.clone()), app_tools, discoverable_tools, dynamic_tools: turn_context.dynamic_tools.as_slice(), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 9a5740f15..427f5ac61 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -121,7 +121,7 @@ mod guardian_tests; struct InstructionsTestCase { slug: &'static str, - expects_apply_patch_instructions: bool, + expects_apply_patch_description: bool, } fn user_message(text: &str) -> ResponseItem { @@ -305,6 +305,7 @@ fn test_tool_runtime(session: Arc, turn_context: Arc) -> T &turn_context.tools_config, crate::tools::router::ToolRouterParams { mcp_tools: None, + tool_namespaces: None, app_tools: None, discoverable_tools: None, dynamic_tools: turn_context.dynamic_tools.as_slice(), @@ -413,6 +414,7 @@ fn make_mcp_tool( server_name: server_name.to_string(), tool_name: tool_name.to_string(), tool_namespace, + server_instructions: None, tool: Tool { name: tool_name.to_string().into(), title: None, @@ -695,19 +697,19 @@ async fn get_base_instructions_no_user_content() { let test_cases = vec![ InstructionsTestCase { slug: "gpt-5", - expects_apply_patch_instructions: false, + expects_apply_patch_description: false, }, InstructionsTestCase { slug: "gpt-5.1", - expects_apply_patch_instructions: false, + expects_apply_patch_description: false, }, InstructionsTestCase { slug: "gpt-5.1-codex", - expects_apply_patch_instructions: false, + expects_apply_patch_description: false, }, InstructionsTestCase { slug: "gpt-5.1-codex-max", - expects_apply_patch_instructions: false, + expects_apply_patch_description: false, }, ]; @@ -716,7 +718,7 @@ async fn get_base_instructions_no_user_content() { for test_case in test_cases { let model_info = model_info_for_slug(test_case.slug, &config); - if test_case.expects_apply_patch_instructions { + if test_case.expects_apply_patch_description { assert_eq!( model_info.base_instructions.as_str(), prompt_with_apply_patch_instructions @@ -5292,15 +5294,12 @@ async fn fatal_tool_error_stops_turn_and_reports_error() { .await }; let app_tools = Some(tools.clone()); + let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&tools); let router = ToolRouter::from_config( &turn_context.tools_config, crate::tools::router::ToolRouterParams { - mcp_tools: Some( - tools - .into_iter() - .map(|(name, tool)| (name, tool.tool)) - .collect(), - ), + mcp_tools: Some(mcp_tool_router_inputs.mcp_tools), + tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces), app_tools, discoverable_tools: None, dynamic_tools: turn_context.dynamic_tools.as_slice(), diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 7a45a97a1..57be9f3a4 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -112,6 +112,7 @@ fn codex_app_tool( server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), tool_name: tool_name.to_string(), tool_namespace, + server_instructions: None, tool: test_tool_definition(tool_name), connector_id: Some(connector_id.to_string()), connector_name: connector_name.map(ToOwned::to_owned), @@ -190,6 +191,7 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() { server_name: "sample".to_string(), tool_name: "echo".to_string(), tool_namespace: "sample".to_string(), + server_instructions: None, tool: test_tool_definition("echo"), connector_id: None, connector_name: None, @@ -314,6 +316,7 @@ fn accessible_connectors_from_mcp_tools_preserves_description() { server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), tool_name: "calendar_create_event".to_string(), tool_namespace: "mcp__codex_apps__calendar".to_string(), + server_instructions: None, tool: Tool { name: "calendar_create_event".to_string().into(), title: None, diff --git a/codex-rs/core/src/tools/code_mode/mod.rs b/codex-rs/core/src/tools/code_mode/mod.rs index 21fca4689..e6a10b967 100644 --- a/codex-rs/core/src/tools/code_mode/mod.rs +++ b/codex-rs/core/src/tools/code_mode/mod.rs @@ -257,15 +257,14 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter { .read() .await .list_all_tools() - .await - .into_iter() - .map(|(name, tool_info)| (name, tool_info.tool)) - .collect(); + .await; + let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&mcp_tools); ToolRouter::from_config( &nested_tools_config, ToolRouterParams { - mcp_tools: Some(mcp_tools), + mcp_tools: Some(mcp_tool_router_inputs.mcp_tools), + tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces), app_tools: None, discoverable_tools: None, dynamic_tools: exec.turn.dynamic_tools.as_slice(), diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 0cfbd2a60..b00ecde8e 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1561,16 +1561,13 @@ impl JsReplManager { .await .list_all_tools() .await; + let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&mcp_tools); let router = ToolRouter::from_config( &exec.turn.tools_config, crate::tools::router::ToolRouterParams { - mcp_tools: Some( - mcp_tools - .into_iter() - .map(|(name, tool)| (name, tool.tool)) - .collect(), - ), + mcp_tools: Some(mcp_tool_router_inputs.mcp_tools), + tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces), app_tools: None, discoverable_tools: None, dynamic_tools: exec.turn.dynamic_tools.as_slice(), diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index aad8c1b7d..030152e0f 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -16,6 +16,7 @@ use codex_protocol::models::SearchToolCallParams; use codex_protocol::models::ShellToolCallParams; use codex_tools::ConfiguredToolSpec; use codex_tools::DiscoverableTool; +use codex_tools::ToolNamespace; use codex_tools::ToolSpec; use codex_tools::ToolsConfig; use rmcp::model::Tool; @@ -41,15 +42,43 @@ pub struct ToolRouter { pub(crate) struct ToolRouterParams<'a> { pub(crate) mcp_tools: Option>, + pub(crate) tool_namespaces: Option>, pub(crate) app_tools: Option>, pub(crate) discoverable_tools: Option>, pub(crate) dynamic_tools: &'a [DynamicToolSpec], } +pub(crate) struct McpToolRouterInputs { + pub(crate) mcp_tools: HashMap, + pub(crate) tool_namespaces: HashMap, +} + +pub(crate) fn map_mcp_tool_infos(mcp_tools: &HashMap) -> McpToolRouterInputs { + McpToolRouterInputs { + mcp_tools: mcp_tools + .iter() + .map(|(name, tool)| (name.clone(), tool.tool.clone())) + .collect(), + tool_namespaces: mcp_tools + .iter() + .map(|(name, tool)| { + ( + name.clone(), + ToolNamespace { + name: tool.tool_namespace.clone(), + description: tool.server_instructions.clone(), + }, + ) + }) + .collect(), + } +} + impl ToolRouter { pub fn from_config(config: &ToolsConfig, params: ToolRouterParams<'_>) -> Self { let ToolRouterParams { mcp_tools, + tool_namespaces, app_tools, discoverable_tools, dynamic_tools, @@ -58,6 +87,7 @@ impl ToolRouter { config, mcp_tools, app_tools, + tool_namespaces, discoverable_tools, dynamic_tools, ); diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index 641adb56d..0ce760134 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -35,6 +35,7 @@ async fn js_repl_tools_only_blocks_direct_tool_calls() -> anyhow::Result<()> { .map(|(name, tool)| (name, tool.tool)) .collect(), ), + tool_namespaces: None, app_tools, discoverable_tools: None, dynamic_tools: turn.dynamic_tools.as_slice(), @@ -93,6 +94,7 @@ async fn js_repl_tools_only_allows_js_repl_source_calls() -> anyhow::Result<()> .map(|(name, tool)| (name, tool.tool)) .collect(), ), + tool_namespaces: None, app_tools, discoverable_tools: None, dynamic_tools: turn.dynamic_tools.as_slice(), diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 37a68d408..ac1ab97f5 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -10,6 +10,7 @@ use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_tools::DiscoverableTool; use codex_tools::ToolHandlerKind; +use codex_tools::ToolNamespace; use codex_tools::ToolRegistryPlanAppTool; use codex_tools::ToolRegistryPlanParams; use codex_tools::ToolUserShellType; @@ -33,6 +34,7 @@ pub(crate) fn build_specs_with_discoverable_tools( config: &ToolsConfig, mcp_tools: Option>, app_tools: Option>, + tool_namespaces: Option>, discoverable_tools: Option>, dynamic_tools: &[DynamicToolSpec], ) -> ToolRegistryBuilder { @@ -86,6 +88,7 @@ pub(crate) fn build_specs_with_discoverable_tools( config, ToolRegistryPlanParams { mcp_tools: mcp_tools.as_ref(), + tool_namespaces: tool_namespaces.as_ref(), app_tools: app_tool_sources.as_deref(), discoverable_tools: discoverable_tools.as_deref(), dynamic_tools, diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 023c513a4..d13644b51 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -181,6 +181,7 @@ fn build_specs( config, mcp_tools, app_tools, + /*tool_namespaces*/ None, /*discoverable_tools*/ None, dynamic_tools, ) @@ -261,6 +262,7 @@ fn assert_model_tools( &tools_config, ToolRouterParams { mcp_tools: None, + tool_namespaces: None, app_tools: None, discoverable_tools: None, dynamic_tools: &[], @@ -628,6 +630,7 @@ fn tool_suggest_requires_apps_and_plugins_features() { &tools_config, /*mcp_tools*/ None, /*app_tools*/ None, + /*tool_namespaces*/ None, discoverable_tools.clone(), &[], ) @@ -701,6 +704,7 @@ fn search_tool_description_falls_back_to_connector_name_without_description() { server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), tool_name: "_create_event".to_string(), tool_namespace: "mcp__codex_apps__calendar".to_string(), + server_instructions: None, tool: mcp_tool( "calendar_create_event", "Create calendar event", @@ -751,6 +755,7 @@ fn search_tool_registers_namespaced_app_tool_aliases() { server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), tool_name: "_create_event".to_string(), tool_namespace: "mcp__codex_apps__calendar".to_string(), + server_instructions: None, tool: mcp_tool( "calendar-create-event", "Create calendar event", @@ -768,6 +773,7 @@ fn search_tool_registers_namespaced_app_tool_aliases() { server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), tool_name: "_list_events".to_string(), tool_namespace: "mcp__codex_apps__calendar".to_string(), + server_instructions: None, tool: mcp_tool( "calendar-list-events", "List calendar events", diff --git a/codex-rs/tools/src/code_mode.rs b/codex-rs/tools/src/code_mode.rs index 07e236ebd..c2b82e1da 100644 --- a/codex-rs/tools/src/code_mode.rs +++ b/codex-rs/tools/src/code_mode.rs @@ -96,6 +96,7 @@ pub fn create_wait_tool() -> ToolSpec { pub fn create_code_mode_tool( enabled_tools: &[(String, String)], + namespace_descriptions: &BTreeMap, code_mode_only_enabled: bool, ) -> ToolSpec { const CODE_MODE_FREEFORM_GRAMMAR: &str = r#" @@ -112,6 +113,7 @@ SOURCE: /[\s\S]+/ name: codex_code_mode::PUBLIC_TOOL_NAME.to_string(), description: codex_code_mode::build_exec_tool_description( enabled_tools, + namespace_descriptions, code_mode_only_enabled, ), format: FreeformToolFormat { diff --git a/codex-rs/tools/src/code_mode_tests.rs b/codex-rs/tools/src/code_mode_tests.rs index 39bdd1f91..a38acacf6 100644 --- a/codex-rs/tools/src/code_mode_tests.rs +++ b/codex-rs/tools/src/code_mode_tests.rs @@ -20,10 +20,14 @@ fn augment_tool_spec_for_code_mode_augments_function_tools() { description: "Look up an order".to_string(), strict: false, defer_loading: Some(true), - parameters: JsonSchema::object(BTreeMap::from([( + parameters: JsonSchema::object( + BTreeMap::from([( "order_id".to_string(), JsonSchema::string(/*description*/ None), - )]), Some(vec!["order_id".to_string()]), Some(AdditionalProperties::Boolean(false))), + )]), + Some(vec!["order_id".to_string()]), + Some(AdditionalProperties::Boolean(false)) + ), output_schema: Some(json!({ "type": "object", "properties": { @@ -34,13 +38,23 @@ fn augment_tool_spec_for_code_mode_augments_function_tools() { })), ToolSpec::Function(ResponsesApiTool { name: "lookup_order".to_string(), - description: "Look up an order\n\nexec tool declaration:\n```ts\ndeclare const tools: { lookup_order(args: { order_id: string; }): Promise<{ ok: boolean; }>; };\n```".to_string(), + description: r#"Look up an order + +exec tool declaration: +```ts +declare const tools: { lookup_order(args: { order_id: string; }): Promise<{ ok: boolean; }>; }; +```"# + .to_string(), strict: false, defer_loading: Some(true), - parameters: JsonSchema::object(BTreeMap::from([( + parameters: JsonSchema::object( + BTreeMap::from([( "order_id".to_string(), JsonSchema::string(/*description*/ None), - )]), Some(vec!["order_id".to_string()]), Some(AdditionalProperties::Boolean(false))), + )]), + Some(vec!["order_id".to_string()]), + Some(AdditionalProperties::Boolean(false)) + ), output_schema: Some(json!({ "type": "object", "properties": { @@ -92,7 +106,13 @@ fn tool_spec_to_code_mode_tool_definition_returns_augmented_nested_tools() { tool_spec_to_code_mode_tool_definition(&spec), Some(codex_code_mode::ToolDefinition { name: "apply_patch".to_string(), - description: "Apply a patch\n\nexec tool declaration:\n```ts\ndeclare const tools: { apply_patch(input: string): Promise; };\n```".to_string(), + description: r#"Apply a patch + +exec tool declaration: +```ts +declare const tools: { apply_patch(input: string): Promise; }; +```"# + .to_string(), kind: codex_code_mode::CodeModeToolKind::Freeform, input_schema: None, output_schema: None, @@ -165,11 +185,16 @@ fn create_code_mode_tool_matches_expected_spec() { let enabled_tools = vec![("update_plan".to_string(), "Update the plan".to_string())]; assert_eq!( - create_code_mode_tool(&enabled_tools, /*code_mode_only_enabled*/ true), + create_code_mode_tool( + &enabled_tools, + &BTreeMap::new(), + /*code_mode_only_enabled*/ true, + ), ToolSpec::Freeform(FreeformTool { name: codex_code_mode::PUBLIC_TOOL_NAME.to_string(), description: codex_code_mode::build_exec_tool_description( &enabled_tools, + &BTreeMap::new(), /*code_mode_only*/ true ), format: FreeformToolFormat { diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 4fa90783f..8bfd3c312 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -114,6 +114,7 @@ pub use tool_discovery::filter_tool_suggest_discoverable_tools_for_client; pub use tool_registry_plan::build_tool_registry_plan; pub use tool_registry_plan_types::ToolHandlerKind; pub use tool_registry_plan_types::ToolHandlerSpec; +pub use tool_registry_plan_types::ToolNamespace; pub use tool_registry_plan_types::ToolRegistryPlan; pub use tool_registry_plan_types::ToolRegistryPlanAppTool; pub use tool_registry_plan_types::ToolRegistryPlanParams; diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index dc76c12d3..c597e4903 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -61,6 +61,7 @@ use crate::tool_registry_plan_types::agent_type_description; use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; use rmcp::model::Tool as McpTool; +use std::collections::BTreeMap; pub fn build_tool_registry_plan( config: &ToolsConfig, @@ -70,6 +71,20 @@ pub fn build_tool_registry_plan( let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled; if config.code_mode_enabled { + let namespace_descriptions = params + .tool_namespaces + .into_iter() + .flatten() + .map(|(name, detail)| { + ( + name.clone(), + codex_code_mode::ToolNamespaceDescription { + name: detail.name.clone(), + description: detail.description.clone().unwrap_or_default(), + }, + ) + }) + .collect::>(); let nested_config = config.for_code_mode_nested_tools(); let nested_plan = build_tool_registry_plan( &nested_config, @@ -78,7 +93,7 @@ pub fn build_tool_registry_plan( ..params }, ); - let enabled_tools = collect_code_mode_tool_definitions( + let mut enabled_tools = collect_code_mode_tool_definitions( nested_plan .specs .iter() @@ -87,8 +102,15 @@ pub fn build_tool_registry_plan( .into_iter() .map(|tool| (tool.name, tool.description)) .collect::>(); + enabled_tools.sort_by(|(left_name, _), (right_name, _)| { + compare_code_mode_tool_names(left_name, right_name, &namespace_descriptions) + }); plan.push_spec( - create_code_mode_tool(&enabled_tools, config.code_mode_only_enabled), + create_code_mode_tool( + &enabled_tools, + &namespace_descriptions, + config.code_mode_only_enabled, + ), /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, ); @@ -494,6 +516,41 @@ pub fn build_tool_registry_plan( plan } +fn compare_code_mode_tool_names( + left_name: &str, + right_name: &str, + namespace_descriptions: &BTreeMap, +) -> std::cmp::Ordering { + let left_namespace = code_mode_namespace_name(left_name, namespace_descriptions); + let right_namespace = code_mode_namespace_name(right_name, namespace_descriptions); + + left_namespace + .cmp(&right_namespace) + .then_with(|| { + code_mode_function_name(left_name, left_namespace) + .cmp(code_mode_function_name(right_name, right_namespace)) + }) + .then_with(|| left_name.cmp(right_name)) +} + +fn code_mode_namespace_name<'a>( + name: &str, + namespace_descriptions: &'a BTreeMap, +) -> Option<&'a str> { + namespace_descriptions + .get(name) + .map(|namespace_description| namespace_description.name.as_str()) +} + +fn code_mode_function_name<'a>(name: &'a str, namespace: Option<&str>) -> &'a str { + namespace + .and_then(|namespace| { + name.strip_prefix(namespace) + .and_then(|suffix| suffix.strip_prefix("__")) + }) + .unwrap_or(name) +} + #[cfg(test)] #[path = "tool_registry_plan_tests.rs"] mod tests; diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 51a0fe3c9..de69e310f 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -11,6 +11,7 @@ use crate::ResponsesApiTool; use crate::ResponsesApiWebSearchFilters; use crate::ResponsesApiWebSearchUserLocation; use crate::ToolHandlerSpec; +use crate::ToolNamespace; use crate::ToolRegistryPlanAppTool; use crate::ToolsConfigParams; use crate::WaitAgentTimeoutOptions; @@ -1332,6 +1333,7 @@ fn tool_suggest_is_not_registered_without_feature_flag() { &tools_config, /*mcp_tools*/ None, /*app_tools*/ None, + /*tool_namespaces*/ None, Some(vec![discoverable_connector( "connector_2128aebfecb84f64a069897515042a44", "Google Calendar", @@ -1371,6 +1373,7 @@ fn tool_suggest_can_be_registered_without_search_tool() { &tools_config, /*mcp_tools*/ None, /*app_tools*/ None, + /*tool_namespaces*/ None, Some(vec![discoverable_connector( "connector_2128aebfecb84f64a069897515042a44", "Google Calendar", @@ -1438,6 +1441,7 @@ fn tool_suggest_description_lists_discoverable_tools() { &tools_config, /*mcp_tools*/ None, /*app_tools*/ None, + /*tool_namespaces*/ None, Some(discoverable_tools), &[], ); @@ -1501,6 +1505,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { let model_info = model_info(); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); + features.enable(Feature::CodeModeOnly); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -1542,7 +1547,12 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { assert_eq!( description, - "Echo text\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array; isError?: boolean; structuredContent?: unknown; }>; };\n```" + r#"Echo text + +exec tool declaration: +```ts +declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array; isError?: boolean; structuredContent?: unknown; }>; }; +```"# ); } @@ -1789,6 +1799,7 @@ fn build_specs<'a>( config, mcp_tools, app_tools, + /*tool_namespaces*/ None, /*discoverable_tools*/ None, dynamic_tools, ) @@ -1798,6 +1809,25 @@ fn build_specs_with_discoverable_tools<'a>( config: &ToolsConfig, mcp_tools: Option>, app_tools: Option>>, + tool_namespaces: Option>, + discoverable_tools: Option>, + dynamic_tools: &[DynamicToolSpec], +) -> (Vec, Vec) { + build_specs_with_optional_tool_namespaces( + config, + mcp_tools, + tool_namespaces, + app_tools, + discoverable_tools, + dynamic_tools, + ) +} + +fn build_specs_with_optional_tool_namespaces<'a>( + config: &ToolsConfig, + mcp_tools: Option>, + tool_namespaces: Option>, + app_tools: Option>>, discoverable_tools: Option>, dynamic_tools: &[DynamicToolSpec], ) -> (Vec, Vec) { @@ -1805,6 +1835,7 @@ fn build_specs_with_discoverable_tools<'a>( config, ToolRegistryPlanParams { mcp_tools: mcp_tools.as_ref(), + tool_namespaces: tool_namespaces.as_ref(), app_tools: app_tools.as_deref(), discoverable_tools: discoverable_tools.as_deref(), dynamic_tools, diff --git a/codex-rs/tools/src/tool_registry_plan_types.rs b/codex-rs/tools/src/tool_registry_plan_types.rs index d15cf15d5..bf77090aa 100644 --- a/codex-rs/tools/src/tool_registry_plan_types.rs +++ b/codex-rs/tools/src/tool_registry_plan_types.rs @@ -58,6 +58,7 @@ pub struct ToolRegistryPlan { #[derive(Debug, Clone, Copy)] pub struct ToolRegistryPlanParams<'a> { pub mcp_tools: Option<&'a HashMap>, + pub tool_namespaces: Option<&'a HashMap>, pub app_tools: Option<&'a [ToolRegistryPlanAppTool<'a>]>, pub discoverable_tools: Option<&'a [DiscoverableTool]>, pub dynamic_tools: &'a [DynamicToolSpec], @@ -66,6 +67,12 @@ pub struct ToolRegistryPlanParams<'a> { pub codex_apps_mcp_server_name: &'a str, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ToolNamespace { + pub name: String, + pub description: Option, +} + #[derive(Debug, Clone, Copy)] pub struct ToolRegistryPlanAppTool<'a> { pub tool_name: &'a str,