From 4fd857d8567b3aa55940c022abe611ce1edec332 Mon Sep 17 00:00:00 2001 From: Friel Date: Tue, 31 Mar 2026 22:09:55 +0000 Subject: [PATCH] agent tools namespaced and legacy aliases preserved --- codex-rs/core/src/tools/spec.rs | 120 ++++++------ codex-rs/core/src/tools/spec_tests.rs | 217 +++++++-------------- codex-rs/core/tests/suite/search_tool.rs | 11 +- codex-rs/core/tests/suite/tool_suggest.rs | 4 +- codex-rs/tools/src/code_mode.rs | 3 +- codex-rs/tools/src/tool_discovery.rs | 7 +- codex-rs/tools/src/tool_discovery_tests.rs | 6 +- codex-rs/tools/src/tool_spec.rs | 4 + codex-rs/tools/src/tool_spec_tests.rs | 28 +++ 9 files changed, 179 insertions(+), 221 deletions(-) diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index d0614fc35b..b6781b46a5 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -39,6 +39,8 @@ use codex_protocol::protocol::SubAgentSource; use codex_tools::CommandToolOptions; use codex_tools::DiscoverableTool; use codex_tools::DiscoverableToolType; +use codex_tools::ResponsesApiNamespace; +use codex_tools::ResponsesApiNamespaceTool; use codex_tools::ShellToolOptions; use codex_tools::SpawnAgentToolOptions; use codex_tools::ToolSearchAppInfo; @@ -86,6 +88,7 @@ use serde::Serialize; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::PathBuf; +use std::sync::Arc; pub type JsonSchema = codex_tools::JsonSchema; @@ -93,6 +96,8 @@ pub type JsonSchema = codex_tools::JsonSchema; pub(crate) use codex_tools::mcp_call_tool_result_output_schema; const WEB_SEARCH_CONTENT_TYPES: [&str; 2] = ["text", "image"]; +const AGENT_TOOLS_NAMESPACE: &str = "agents"; +const AGENT_TOOLS_NAMESPACE_DESCRIPTION: &str = "Agent collaboration tools for spawning, messaging, waiting on, listing, and closing subagents."; #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ShellCommandBackendConfig { @@ -382,6 +387,30 @@ fn push_tool_spec( } } +fn create_agent_tools_namespace(tools: Vec) -> ToolSpec { + let tools = tools + .into_iter() + .filter_map(|tool| match tool { + ToolSpec::Function(tool) => Some(ResponsesApiNamespaceTool::Function(tool)), + _ => None, + }) + .collect(); + + ToolSpec::Namespace(ResponsesApiNamespace { + name: AGENT_TOOLS_NAMESPACE.to_string(), + description: AGENT_TOOLS_NAMESPACE_DESCRIPTION.to_string(), + tools, + }) +} + +fn register_agent_tool_handler(builder: &mut ToolRegistryBuilder, name: &str, handler: Arc) +where + H: crate::tools::registry::ToolHandler + 'static, +{ + builder.register_handler(name, handler.clone()); + builder.register_handler(tool_handler_key(name, Some(AGENT_TOOLS_NAMESPACE)), handler); +} + /// Builds the tool registry builder while collecting tool specs for later serialization. #[cfg(test)] pub(crate) fn build_specs( @@ -436,8 +465,6 @@ pub(crate) fn build_specs_with_discoverable_tools( use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2; use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2; - use std::sync::Arc; - let mut builder = ToolRegistryBuilder::new(); let shell_handler = Arc::new(ShellHandler); @@ -626,9 +653,9 @@ pub(crate) fn build_specs_with_discoverable_tools( builder.register_handler("request_permissions", request_permissions_handler); } - if config.search_tool - && let Some(app_tools) = app_tools - { + let include_tool_search = config.search_tool || config.collab_tools; + if include_tool_search && (app_tools.is_some() || config.collab_tools) { + let app_tools = app_tools.unwrap_or_default(); let search_tool_handler = Arc::new(ToolSearchHandler::new(app_tools.clone())); push_tool_spec( &mut builder, @@ -775,102 +802,67 @@ pub(crate) fn build_specs_with_discoverable_tools( if config.collab_tools { if config.multi_agent_v2 { - push_tool_spec( - &mut builder, + let agent_tools = vec![ create_spawn_agent_tool_v2(SpawnAgentToolOptions { available_models: &config.available_models, agent_type_description: crate::agent::role::spawn_tool_spec::build( &config.agent_roles, ), }), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, create_send_message_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, create_assign_task_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, create_wait_agent_tool_v2(WaitAgentTimeoutOptions { default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS, min_timeout_ms: MIN_WAIT_TIMEOUT_MS, max_timeout_ms: MAX_WAIT_TIMEOUT_MS, }), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, create_close_agent_tool_v2(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, create_list_agents_tool(), + ]; + push_tool_spec( + &mut builder, + create_agent_tools_namespace(agent_tools), /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, ); - builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandlerV2)); - builder.register_handler("send_message", Arc::new(SendMessageHandlerV2)); - builder.register_handler("assign_task", Arc::new(AssignTaskHandlerV2)); - builder.register_handler("wait_agent", Arc::new(WaitAgentHandlerV2)); - builder.register_handler("close_agent", Arc::new(CloseAgentHandlerV2)); - builder.register_handler("list_agents", Arc::new(ListAgentsHandlerV2)); - } else { - push_tool_spec( + register_agent_tool_handler(&mut builder, "spawn_agent", Arc::new(SpawnAgentHandlerV2)); + register_agent_tool_handler( &mut builder, + "send_message", + Arc::new(SendMessageHandlerV2), + ); + register_agent_tool_handler(&mut builder, "assign_task", Arc::new(AssignTaskHandlerV2)); + register_agent_tool_handler(&mut builder, "wait_agent", Arc::new(WaitAgentHandlerV2)); + register_agent_tool_handler(&mut builder, "close_agent", Arc::new(CloseAgentHandlerV2)); + register_agent_tool_handler(&mut builder, "list_agents", Arc::new(ListAgentsHandlerV2)); + } else { + let agent_tools = vec![ create_spawn_agent_tool_v1(SpawnAgentToolOptions { available_models: &config.available_models, agent_type_description: crate::agent::role::spawn_tool_spec::build( &config.agent_roles, ), }), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, create_send_input_tool_v1(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, create_resume_agent_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler)); - push_tool_spec( - &mut builder, create_wait_agent_tool_v1(WaitAgentTimeoutOptions { default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS, min_timeout_ms: MIN_WAIT_TIMEOUT_MS, max_timeout_ms: MAX_WAIT_TIMEOUT_MS, }), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); + create_close_agent_tool_v1(), + ]; push_tool_spec( &mut builder, - create_close_agent_tool_v1(), + create_agent_tools_namespace(agent_tools), /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, ); - builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandler)); - builder.register_handler("send_input", Arc::new(SendInputHandler)); - builder.register_handler("wait_agent", Arc::new(WaitAgentHandler)); - builder.register_handler("close_agent", Arc::new(CloseAgentHandler)); + register_agent_tool_handler(&mut builder, "spawn_agent", Arc::new(SpawnAgentHandler)); + register_agent_tool_handler(&mut builder, "send_input", Arc::new(SendInputHandler)); + register_agent_tool_handler(&mut builder, "resume_agent", Arc::new(ResumeAgentHandler)); + register_agent_tool_handler(&mut builder, "wait_agent", Arc::new(WaitAgentHandler)); + register_agent_tool_handler(&mut builder, "close_agent", Arc::new(CloseAgentHandler)); } } diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 8bc27a0729..03ab33ee46 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -16,6 +16,7 @@ use codex_tools::ConfiguredToolSpec; use codex_tools::DiscoverablePluginInfo; use codex_tools::DiscoverableTool; use codex_tools::FreeformTool; +use codex_tools::ResponsesApiNamespaceTool; use codex_tools::ResponsesApiTool; use codex_tools::ResponsesApiWebSearchFilters; use codex_tools::ResponsesApiWebSearchUserLocation; @@ -197,6 +198,25 @@ fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a Co .unwrap_or_else(|| panic!("expected tool {expected_name}")) } +fn find_namespaced_tool<'a>( + tools: &'a [ConfiguredToolSpec], + namespace: &str, + expected_name: &str, +) -> &'a ResponsesApiTool { + let namespace_tool = find_tool(tools, namespace); + let ToolSpec::Namespace(namespace) = &namespace_tool.spec else { + panic!("expected {namespace} namespace tool"); + }; + namespace + .tools + .iter() + .find_map(|tool| match tool { + ResponsesApiNamespaceTool::Function(tool) if tool.name == expected_name => Some(tool), + _ => None, + }) + .expect("expected tool in namespace") +} + fn strip_descriptions_schema(schema: &mut JsonSchema) { match schema { JsonSchema::Boolean { description } @@ -229,7 +249,8 @@ fn strip_descriptions_tool(spec: &mut ToolSpec) { ToolSpec::Function(ResponsesApiTool { parameters, .. }) => { strip_descriptions_schema(parameters); } - ToolSpec::Freeform(_) + ToolSpec::Namespace(_) + | ToolSpec::Freeform(_) | ToolSpec::LocalShell {} | ToolSpec::ImageGeneration { .. } | ToolSpec::WebSearch { .. } => {} @@ -425,18 +446,12 @@ fn test_build_specs_collab_tools_enabled() { &[], ) .build(); - assert_contains_tool_names( - &tools, - &["spawn_agent", "send_input", "wait_agent", "close_agent"], - ); + assert_contains_tool_names(&tools, &["agents"]); assert_lacks_tool_name(&tools, "spawn_agents_on_csv"); assert_lacks_tool_name(&tools, "list_agents"); - let spawn_agent = find_tool(&tools, "spawn_agent"); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &spawn_agent.spec else { - panic!("spawn_agent should be a function tool"); - }; - let JsonSchema::Object { properties, .. } = parameters else { + let spawn_agent = find_namespaced_tool(&tools, "agents", "spawn_agent"); + let JsonSchema::Object { properties, .. } = &spawn_agent.parameters else { panic!("spawn_agent should use object params"); }; assert!(properties.contains_key("fork_context")); @@ -467,64 +482,41 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { &[], ) .build(); - assert_contains_tool_names( - &tools, - &[ - "spawn_agent", - "send_message", - "assign_task", - "wait_agent", - "close_agent", - "list_agents", - ], - ); + assert_contains_tool_names(&tools, &["agents"]); - let spawn_agent = find_tool(&tools, "spawn_agent"); - let ToolSpec::Function(ResponsesApiTool { - parameters, - output_schema, + let spawn_agent = find_namespaced_tool(&tools, "agents", "spawn_agent"); + let JsonSchema::Object { + properties: _, + required, .. - }) = &spawn_agent.spec + } = &spawn_agent.parameters else { panic!("spawn_agent should be a function tool"); }; - let JsonSchema::Object { - properties, - required, - .. - } = parameters - else { - panic!("spawn_agent should use object params"); - }; - assert!(properties.contains_key("task_name")); - assert!(properties.contains_key("items")); - assert!(properties.contains_key("fork_turns")); - assert!(!properties.contains_key("message")); - assert!(!properties.contains_key("fork_context")); + let output_schema = spawn_agent + .output_schema + .as_ref() + .expect("spawn_agent should define output schema"); assert_eq!( required.as_ref(), Some(&vec!["task_name".to_string(), "items".to_string()]) ); - let output_schema = output_schema - .as_ref() - .expect("spawn_agent should define output schema"); assert_eq!( output_schema["required"], json!(["agent_id", "task_name", "nickname"]) ); - let send_message = find_tool(&tools, "send_message"); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &send_message.spec else { - panic!("send_message should be a function tool"); - }; + let send_message = find_namespaced_tool(&tools, "agents", "send_message"); let JsonSchema::Object { properties, required, .. - } = parameters + } = &send_message.parameters else { - panic!("send_message should use object params"); + panic!("send_message should be a function tool"); }; + assert!(properties.contains_key("items")); + assert!(!properties.contains_key("message")); assert!(properties.contains_key("target")); assert!(!properties.contains_key("interrupt")); assert!(!properties.contains_key("message")); @@ -533,17 +525,14 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { Some(&vec!["target".to_string(), "items".to_string()]) ); - let assign_task = find_tool(&tools, "assign_task"); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &assign_task.spec else { - panic!("assign_task should be a function tool"); - }; + let assign_task = find_namespaced_tool(&tools, "agents", "assign_task"); let JsonSchema::Object { properties, required, .. - } = parameters + } = &assign_task.parameters else { - panic!("assign_task should use object params"); + panic!("assign_task should be a function tool"); }; assert!(properties.contains_key("target")); assert!(!properties.contains_key("message")); @@ -552,56 +541,42 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { Some(&vec!["target".to_string(), "items".to_string()]) ); - let wait_agent = find_tool(&tools, "wait_agent"); - let ToolSpec::Function(ResponsesApiTool { - parameters, - output_schema, - .. - }) = &wait_agent.spec - else { - panic!("wait_agent should be a function tool"); - }; + let wait_agent = find_namespaced_tool(&tools, "agents", "wait_agent"); let JsonSchema::Object { properties, required, .. - } = parameters + } = &wait_agent.parameters else { - panic!("wait_agent should use object params"); + panic!("wait_agent should be a function tool"); }; + let output_schema = wait_agent + .output_schema + .as_ref() + .expect("wait_agent should define output schema"); assert!(!properties.contains_key("targets")); assert!(properties.contains_key("timeout_ms")); assert_eq!(required, &None); - let output_schema = output_schema - .as_ref() - .expect("wait_agent should define output schema"); assert_eq!( output_schema["properties"]["message"]["description"], json!("Brief wait summary without the agent's final content.") ); - let list_agents = find_tool(&tools, "list_agents"); - let ToolSpec::Function(ResponsesApiTool { - parameters, - output_schema, - .. - }) = &list_agents.spec - else { - panic!("list_agents should be a function tool"); - }; + let list_agents = find_namespaced_tool(&tools, "agents", "list_agents"); let JsonSchema::Object { properties, required, .. - } = parameters + } = &list_agents.parameters else { - panic!("list_agents should use object params"); + panic!("list_agents should be a function tool"); }; - assert!(properties.contains_key("path_prefix")); - assert_eq!(required.as_ref(), None); - let output_schema = output_schema + let output_schema = list_agents + .output_schema .as_ref() .expect("list_agents should define output schema"); + assert!(properties.contains_key("path_prefix")); + assert_eq!(required.as_ref(), None); assert_eq!( output_schema["properties"]["agents"]["items"]["required"], json!(["agent_name", "agent_status", "last_task_message"]) @@ -634,16 +609,7 @@ fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { &[], ) .build(); - assert_contains_tool_names( - &tools, - &[ - "spawn_agent", - "send_input", - "wait_agent", - "close_agent", - "spawn_agents_on_csv", - ], - ); + assert_contains_tool_names(&tools, &["agents", "spawn_agents_on_csv"]); } #[test] @@ -752,15 +718,7 @@ fn test_build_specs_agent_job_worker_tools_enabled() { .build(); assert_contains_tool_names( &tools, - &[ - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", - "spawn_agents_on_csv", - "report_agent_job_result", - ], + &["agents", "spawn_agents_on_csv", "report_agent_job_result"], ); assert_lacks_tool_name(&tools, "request_user_input"); } @@ -1374,11 +1332,7 @@ fn test_build_specs_gpt5_codex_default() { "apply_patch", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -1397,11 +1351,7 @@ fn test_build_specs_gpt51_codex_default() { "apply_patch", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -1422,11 +1372,7 @@ fn test_build_specs_gpt5_codex_unified_exec_web_search() { "apply_patch", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -1447,11 +1393,7 @@ fn test_build_specs_gpt51_codex_unified_exec_web_search() { "apply_patch", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -1470,11 +1412,7 @@ fn test_gpt_5_1_codex_max_defaults() { "apply_patch", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -1493,11 +1431,7 @@ fn test_codex_5_1_mini_defaults() { "apply_patch", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -1515,11 +1449,7 @@ fn test_gpt_5_defaults() { "request_user_input", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -1538,11 +1468,7 @@ fn test_gpt_5_1_defaults() { "apply_patch", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -1563,11 +1489,7 @@ fn test_gpt_5_1_codex_max_unified_exec_web_search() { "apply_patch", "web_search", "view_image", - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", + "agents", ], ); } @@ -2227,7 +2149,8 @@ fn search_tool_description_handles_no_enabled_apps() { panic!("expected tool_search tool"); }; - assert!(description.contains("None currently enabled.")); + assert!(description.contains("agents")); + assert!(description.contains("Agent collaboration tools for spawning, messaging, waiting on, listing, and closing subagents.")); assert!(!description.contains("{{app_descriptions}}")); } diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 423c57a8c6..984ef0cbf9 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -36,6 +36,7 @@ const SEARCH_TOOL_DESCRIPTION_SNIPPETS: [&str; 2] = [ "- Calendar: Plan events and manage your calendar.", ]; const TOOL_SEARCH_TOOL_NAME: &str = "tool_search"; +const AGENTS_NAMESPACE_TOOL_NAME: &str = "agents"; const CALENDAR_CREATE_TOOL: &str = "mcp__codex_apps__calendar_create_event"; const CALENDAR_LIST_TOOL: &str = "mcp__codex_apps__calendar_list_events"; const SEARCH_CALENDAR_NAMESPACE: &str = "mcp__codex_apps__calendar"; @@ -217,7 +218,7 @@ async fn tool_search_disabled_by_default_exposes_apps_tools_directly() -> Result } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> { +async fn search_tool_keeps_agents_namespace_for_api_key_auth() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -247,8 +248,12 @@ async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> { let body = mock.single_request().body_json(); let tools = tool_names(&body); assert!( - !tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME), - "tools list should not include {TOOL_SEARCH_TOOL_NAME} for API key auth: {tools:?}" + tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME), + "tools list should include {TOOL_SEARCH_TOOL_NAME} for the always-present agents namespace: {tools:?}" + ); + assert!( + tools.iter().any(|name| name == AGENTS_NAMESPACE_TOOL_NAME), + "tools list should include the {AGENTS_NAMESPACE_TOOL_NAME} namespace: {tools:?}" ); Ok(()) diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index 460c7366a3..01c42e7e1a 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -122,8 +122,8 @@ async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts( let body = mock.single_request().body_json(); let tools = tool_names(&body); assert!( - !tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME), - "tools list should not include {TOOL_SEARCH_TOOL_NAME}: {tools:?}" + tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME), + "tools list should include {TOOL_SEARCH_TOOL_NAME}: {tools:?}" ); assert!( tools.iter().any(|name| name == TOOL_SUGGEST_TOOL_NAME), diff --git a/codex-rs/tools/src/code_mode.rs b/codex-rs/tools/src/code_mode.rs index 814621f347..2090f36870 100644 --- a/codex-rs/tools/src/code_mode.rs +++ b/codex-rs/tools/src/code_mode.rs @@ -132,7 +132,8 @@ fn code_mode_tool_definition_for_spec(spec: &ToolSpec) -> Option None, diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index 0216cc1703..af21de07cd 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -7,6 +7,8 @@ use serde::Serialize; use std::collections::BTreeMap; const TUI_CLIENT_NAME: &str = "codex-tui"; +const AGENT_TOOLS_NAMESPACE: &str = "agents"; +const AGENT_TOOLS_NAMESPACE_DESCRIPTION: &str = "Agent collaboration tools for spawning, messaging, waiting on, listing, and closing subagents."; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ToolSearchAppInfo { @@ -138,7 +140,10 @@ pub fn create_tool_search_tool(app_tools: &[ToolSearchAppInfo], default_limit: u ), ]); - let mut app_descriptions = BTreeMap::new(); + let mut app_descriptions = BTreeMap::from([( + AGENT_TOOLS_NAMESPACE.to_string(), + Some(AGENT_TOOLS_NAMESPACE_DESCRIPTION.to_string()), + )]); for app_tool in app_tools { app_descriptions .entry(app_tool.name.clone()) diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index 1b241c5844..4ef2719538 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -27,9 +27,9 @@ fn create_tool_search_tool_deduplicates_and_renders_enabled_apps() { ], /*default_limit*/ 8, ), - ToolSpec::ToolSearch { - execution: "client".to_string(), - description: "# Apps (Connectors) tool discovery\n\nSearches over apps/connectors tool metadata with BM25 and exposes matching tools for the next model call.\n\nYou have access to all the tools of the following apps/connectors:\n- Google Drive: Use Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work.\n- Slack\nSome of the tools may not have been provided to you upfront, and you should use this tool (`tool_search`) to search for the required tools and load them for the apps mentioned above. For the apps mentioned above, always use `tool_search` instead of `list_mcp_resources` or `list_mcp_resource_templates` for tool discovery.".to_string(), + ToolSpec::ToolSearch { + execution: "client".to_string(), + description: "# Apps (Connectors) tool discovery\n\nSearches over apps/connectors tool metadata with BM25 and exposes matching tools for the next model call.\n\nYou have access to all the tools of the following apps/connectors:\n- Google Drive: Use Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work.\n- Slack\n- agents: Agent collaboration tools for spawning, messaging, waiting on, listing, and closing subagents.\nSome of the tools may not have been provided to you upfront, and you should use this tool (`tool_search`) to search for the required tools and load them for the apps mentioned above. For the apps mentioned above, always use `tool_search` instead of `list_mcp_resources` or `list_mcp_resource_templates` for tool discovery.".to_string(), parameters: JsonSchema::Object { properties: BTreeMap::from([ ( diff --git a/codex-rs/tools/src/tool_spec.rs b/codex-rs/tools/src/tool_spec.rs index f537388853..e9810d5157 100644 --- a/codex-rs/tools/src/tool_spec.rs +++ b/codex-rs/tools/src/tool_spec.rs @@ -1,5 +1,6 @@ use crate::FreeformTool; use crate::JsonSchema; +use crate::ResponsesApiNamespace; use crate::ResponsesApiTool; use codex_protocol::config_types::WebSearchContextSize; use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters; @@ -15,6 +16,8 @@ use serde_json::Value; pub enum ToolSpec { #[serde(rename = "function")] Function(ResponsesApiTool), + #[serde(rename = "namespace")] + Namespace(ResponsesApiNamespace), #[serde(rename = "tool_search")] ToolSearch { execution: String, @@ -52,6 +55,7 @@ impl ToolSpec { pub fn name(&self) -> &str { match self { ToolSpec::Function(tool) => tool.name.as_str(), + ToolSpec::Namespace(namespace) => namespace.name.as_str(), ToolSpec::ToolSearch { .. } => "tool_search", ToolSpec::LocalShell {} => "local_shell", ToolSpec::ImageGeneration { .. } => "image_generation", diff --git a/codex-rs/tools/src/tool_spec_tests.rs b/codex-rs/tools/src/tool_spec_tests.rs index c84ea94a36..ae1000aeb0 100644 --- a/codex-rs/tools/src/tool_spec_tests.rs +++ b/codex-rs/tools/src/tool_spec_tests.rs @@ -6,6 +6,7 @@ use crate::AdditionalProperties; use crate::FreeformTool; use crate::FreeformToolFormat; use crate::JsonSchema; +use crate::ResponsesApiNamespace; use crate::ResponsesApiTool; use crate::create_tools_json_for_responses_api; use codex_protocol::config_types::WebSearchContextSize; @@ -34,6 +35,15 @@ fn tool_spec_name_covers_all_variants() { .name(), "lookup_order" ); + assert_eq!( + ToolSpec::Namespace(ResponsesApiNamespace { + name: "agents".to_string(), + description: "Agent tools".to_string(), + tools: Vec::new(), + }) + .name(), + "agents" + ); assert_eq!( ToolSpec::ToolSearch { execution: "sync".to_string(), @@ -166,6 +176,24 @@ fn create_tools_json_for_responses_api_includes_top_level_name() { ); } +#[test] +fn namespace_tool_spec_serializes_expected_wire_shape() { + assert_eq!( + serde_json::to_value(ToolSpec::Namespace(ResponsesApiNamespace { + name: "agents".to_string(), + description: "Agent collaboration tools.".to_string(), + tools: Vec::new(), + })) + .expect("serialize namespace"), + json!({ + "type": "namespace", + "name": "agents", + "description": "Agent collaboration tools.", + "tools": [] + }) + ); +} + #[test] fn web_search_tool_spec_serializes_expected_wire_shape() { assert_eq!(