use crate::config::DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS; use crate::config::DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS; use crate::config::DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS; use crate::tools::code_mode::execute_spec::create_code_mode_tool; use crate::tools::context::ToolInvocation; use crate::tools::handlers::ApplyPatchHandler; use crate::tools::handlers::CodeModeExecuteHandler; use crate::tools::handlers::CodeModeWaitHandler; use crate::tools::handlers::CreateGoalHandler; use crate::tools::handlers::DynamicToolHandler; use crate::tools::handlers::ExecCommandHandler; use crate::tools::handlers::ExecCommandHandlerOptions; use crate::tools::handlers::GetGoalHandler; use crate::tools::handlers::ListMcpResourceTemplatesHandler; use crate::tools::handlers::ListMcpResourcesHandler; use crate::tools::handlers::McpHandler; use crate::tools::handlers::PlanHandler; use crate::tools::handlers::ReadMcpResourceHandler; use crate::tools::handlers::RequestPermissionsHandler; use crate::tools::handlers::RequestPluginInstallHandler; use crate::tools::handlers::RequestUserInputHandler; use crate::tools::handlers::ShellCommandHandler; use crate::tools::handlers::ShellCommandHandlerOptions; use crate::tools::handlers::TestSyncHandler; use crate::tools::handlers::ToolSearchHandler; use crate::tools::handlers::UpdateGoalHandler; use crate::tools::handlers::ViewImageHandler; use crate::tools::handlers::WriteStdinHandler; use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler; use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler; use crate::tools::handlers::extension_tools::ExtensionToolAdapter; use crate::tools::handlers::multi_agents::CloseAgentHandler; use crate::tools::handlers::multi_agents::ResumeAgentHandler; use crate::tools::handlers::multi_agents::SendInputHandler; use crate::tools::handlers::multi_agents::SpawnAgentHandler; use crate::tools::handlers::multi_agents::WaitAgentHandler; use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions; use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions; use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2; use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2; 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 crate::tools::handlers::view_image_spec::ViewImageToolOptions; use crate::tools::hosted_spec::WebSearchToolOptions; use crate::tools::hosted_spec::create_image_generation_tool; use crate::tools::hosted_spec::create_web_search_tool; use crate::tools::registry::CoreToolRuntime; use crate::tools::registry::ToolExposure; use crate::tools::registry::ToolRegistry; use crate::tools::registry::override_tool_exposure; use crate::tools::router::ToolRouter; use crate::tools::router::ToolRouterParams; use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::openai_models::ConfigShellToolType; use codex_tools::DiscoverableTool; use codex_tools::ResponsesApiNamespace; use codex_tools::ResponsesApiNamespaceTool; use codex_tools::TOOL_SEARCH_TOOL_NAME; use codex_tools::ToolCall as ExtensionToolCall; use codex_tools::ToolEnvironmentMode; use codex_tools::ToolExecutor; use codex_tools::ToolName; use codex_tools::ToolOutput; use codex_tools::ToolSpec; use codex_tools::ToolsConfig; use codex_tools::collect_code_mode_exec_prompt_tool_definitions; use codex_tools::default_namespace_description; use std::collections::BTreeMap; use std::collections::HashSet; use std::sync::Arc; use tracing::warn; const MULTI_AGENT_V2_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents."; #[derive(Clone, Copy)] struct ToolRegistryBuildParams<'a> { mcp_tools: Option<&'a [ToolInfo]>, deferred_mcp_tools: Option<&'a [ToolInfo]>, discoverable_tools: Option<&'a [DiscoverableTool]>, extension_tool_executors: &'a [Arc>], dynamic_tools: &'a [DynamicToolSpec], default_agent_type_description: &'a str, wait_agent_timeouts: WaitAgentTimeoutOptions, } pub(crate) fn build_tool_router(config: &ToolsConfig, params: ToolRouterParams<'_>) -> ToolRouter { let (model_visible_specs, registry) = build_tool_specs_and_registry(config, params); ToolRouter::from_parts(registry, model_visible_specs) } fn build_tool_specs_and_registry( config: &ToolsConfig, params: ToolRouterParams<'_>, ) -> (Vec, ToolRegistry) { let ToolRouterParams { mcp_tools, deferred_mcp_tools, discoverable_tools, extension_tool_executors, dynamic_tools, } = params; let default_agent_type_description = crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new()); let mut executors = collect_tool_executors( config, ToolRegistryBuildParams { mcp_tools: mcp_tools.as_deref(), deferred_mcp_tools: deferred_mcp_tools.as_deref(), discoverable_tools: discoverable_tools.as_deref(), extension_tool_executors: &extension_tool_executors, dynamic_tools, default_agent_type_description: &default_agent_type_description, wait_agent_timeouts: wait_agent_timeout_options(config), }, ); append_tool_search_executor(config, &mut executors); prepend_code_mode_executors(config, &mut executors); build_model_visible_specs_and_registry(config, executors, hosted_model_tool_specs(config)) } fn build_model_visible_specs_and_registry( config: &ToolsConfig, executors: Vec>, hosted_specs: Vec, ) -> (Vec, ToolRegistry) { let mut specs = Vec::new(); let mut seen_tool_names = HashSet::new(); for executor in &executors { let tool_name = executor.tool_name(); if !seen_tool_names.insert(tool_name.clone()) { continue; } let exposure = executor.exposure(); if exposure.is_direct() && !is_hidden_by_code_mode_only(config, &tool_name, exposure) && let Some(spec) = executor.spec() { specs.push(spec_for_model_request(config, exposure, spec)); } } for spec in hosted_specs { if !is_hidden_by_code_mode_only(config, &ToolName::plain(spec.name()), ToolExposure::Direct) { specs.push(spec); } } let registry = ToolRegistry::from_tools(executors); let model_visible_specs = merge_into_namespaces(specs) .into_iter() .filter(|spec| config.namespace_tools || !matches!(spec, ToolSpec::Namespace(_))) .collect(); (model_visible_specs, registry) } fn spec_for_model_request( config: &ToolsConfig, exposure: ToolExposure, spec: ToolSpec, ) -> ToolSpec { if config.code_mode_enabled && exposure != ToolExposure::DirectModelOnly && codex_code_mode::is_code_mode_nested_tool(spec.name()) { codex_tools::augment_tool_spec_for_code_mode(spec) } else { spec } } pub(crate) fn hosted_model_tool_specs(config: &ToolsConfig) -> Vec { let mut specs = Vec::new(); if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions { web_search_mode: config.web_search_mode, web_search_config: config.web_search_config.as_ref(), web_search_tool_type: config.web_search_tool_type, }) { specs.push(web_search_tool); } if config.image_gen_tool { specs.push(create_image_generation_tool("png")); } specs } fn wait_agent_timeout_options(config: &ToolsConfig) -> WaitAgentTimeoutOptions { if config.multi_agent_v2 { return WaitAgentTimeoutOptions { default_timeout_ms: config .wait_agent_default_timeout_ms .unwrap_or(DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS), min_timeout_ms: config .wait_agent_min_timeout_ms .unwrap_or(DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS), max_timeout_ms: config .wait_agent_max_timeout_ms .unwrap_or(DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS), }; } WaitAgentTimeoutOptions { default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS, min_timeout_ms: MIN_WAIT_TIMEOUT_MS, max_timeout_ms: MAX_WAIT_TIMEOUT_MS, } } fn agent_type_description(config: &ToolsConfig, default_agent_type_description: &str) -> String { if config.agent_type_description.is_empty() { default_agent_type_description.to_string() } else { config.agent_type_description.clone() } } fn is_hidden_by_code_mode_only( config: &ToolsConfig, tool_name: &ToolName, exposure: ToolExposure, ) -> bool { config.code_mode_only_enabled && exposure != ToolExposure::DirectModelOnly && codex_code_mode::is_code_mode_nested_tool(&codex_tools::code_mode_name_for_tool_name( tool_name, )) } fn build_code_mode_executors( config: &ToolsConfig, executors: &[Arc], deferred_tools_available: bool, ) -> Vec> { if !config.code_mode_enabled { return vec![]; } let code_mode_nested_tool_specs = executors .iter() .filter_map(|executor| { if executor.exposure() == ToolExposure::DirectModelOnly { return None; } executor.spec() }) .collect::>(); let namespace_descriptions = code_mode_namespace_descriptions(&code_mode_nested_tool_specs); let mut enabled_tools = collect_code_mode_exec_prompt_tool_definitions(code_mode_nested_tool_specs.iter()); enabled_tools .sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions)); vec![ Arc::new(CodeModeExecuteHandler::new( create_code_mode_tool( &enabled_tools, &namespace_descriptions, config.code_mode_only_enabled, deferred_tools_available, ), code_mode_nested_tool_specs, )), Arc::new(CodeModeWaitHandler), ] } fn merge_into_namespaces(specs: Vec) -> Vec { let mut merged_specs = Vec::with_capacity(specs.len()); let mut namespace_indices = BTreeMap::::new(); for spec in specs { match spec { ToolSpec::Namespace(mut namespace) => { if let Some(index) = namespace_indices.get(&namespace.name).copied() { let ToolSpec::Namespace(existing_namespace) = &mut merged_specs[index] else { unreachable!("namespace index must point to a namespace spec"); }; if existing_namespace.description.trim().is_empty() && !namespace.description.trim().is_empty() { existing_namespace.description = namespace.description; } existing_namespace.tools.append(&mut namespace.tools); continue; } namespace_indices.insert(namespace.name.clone(), merged_specs.len()); merged_specs.push(ToolSpec::Namespace(namespace)); } spec => merged_specs.push(spec), } } for spec in &mut merged_specs { let ToolSpec::Namespace(namespace) = spec else { continue; }; namespace.tools.sort_by(|left, right| match (left, right) { ( ResponsesApiNamespaceTool::Function(left), ResponsesApiNamespaceTool::Function(right), ) => left.name.cmp(&right.name), }); if namespace.description.trim().is_empty() { namespace.description = default_namespace_description(&namespace.name); } } merged_specs } fn code_mode_namespace_descriptions( specs: &[ToolSpec], ) -> BTreeMap { let mut namespace_descriptions = BTreeMap::new(); for spec in specs { let ToolSpec::Namespace(namespace) = spec else { continue; }; let entry = namespace_descriptions .entry(namespace.name.clone()) .or_insert_with(|| codex_code_mode::ToolNamespaceDescription { name: namespace.name.clone(), description: namespace.description.clone(), }); if entry.description.trim().is_empty() && !namespace.description.trim().is_empty() { entry.description = namespace.description.clone(); } } namespace_descriptions } fn collect_tool_executors( config: &ToolsConfig, params: ToolRegistryBuildParams<'_>, ) -> Vec> { let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled; let mut executors = Vec::>::new(); if config.environment_mode.has_environment() { let include_environment_id = matches!(config.environment_mode, ToolEnvironmentMode::Multiple); match &config.shell_type { ConfigShellToolType::UnifiedExec => { executors.push(Arc::new(ExecCommandHandler::new( ExecCommandHandlerOptions { allow_login_shell: config.allow_login_shell, exec_permission_approvals_enabled, include_environment_id, }, ))); executors.push(Arc::new(WriteStdinHandler)); } ConfigShellToolType::Disabled => {} ConfigShellToolType::Default | ConfigShellToolType::Local | ConfigShellToolType::ShellCommand => { executors.push(Arc::new(ShellCommandHandler::new( ShellCommandHandlerOptions { backend_config: config.shell_command_backend, allow_login_shell: config.allow_login_shell, exec_permission_approvals_enabled, }, ))); } } } if config.environment_mode.has_environment() && config.shell_type != ConfigShellToolType::Disabled { match &config.shell_type { ConfigShellToolType::UnifiedExec => { executors.push(Arc::new(ShellCommandHandler::from( config.shell_command_backend, ))); } ConfigShellToolType::Default | ConfigShellToolType::Local | ConfigShellToolType::ShellCommand | ConfigShellToolType::Disabled => {} } } if params.mcp_tools.is_some() { executors.push(Arc::new(ListMcpResourcesHandler)); executors.push(Arc::new(ListMcpResourceTemplatesHandler)); executors.push(Arc::new(ReadMcpResourceHandler)); } executors.push(Arc::new(PlanHandler)); if config.goal_tools { executors.push(Arc::new(GetGoalHandler)); executors.push(Arc::new(CreateGoalHandler)); executors.push(Arc::new(UpdateGoalHandler)); } executors.push(Arc::new(RequestUserInputHandler { available_modes: config.request_user_input_available_modes.clone(), })); if config.request_permissions_tool_enabled { executors.push(Arc::new(RequestPermissionsHandler)); } if config.tool_suggest && let Some(discoverable_tools) = params.discoverable_tools.filter(|tools| !tools.is_empty()) { executors.push(Arc::new(RequestPluginInstallHandler::new( discoverable_tools, ))); } if config.environment_mode.has_environment() && config.apply_patch_tool_type.is_some() { let include_environment_id = matches!(config.environment_mode, ToolEnvironmentMode::Multiple); executors.push(Arc::new(ApplyPatchHandler::new(include_environment_id))); } if config .experimental_supported_tools .iter() .any(|tool| tool == "test_sync_tool") { executors.push(Arc::new(TestSyncHandler)); } if config.environment_mode.has_environment() { let include_environment_id = matches!(config.environment_mode, ToolEnvironmentMode::Multiple); executors.push(Arc::new(ViewImageHandler::new(ViewImageToolOptions { can_request_original_image_detail: config.can_request_original_image_detail, include_environment_id, }))); } if config.collab_tools { if config.multi_agent_v2 { let exposure = if config.multi_agent_v2_non_code_mode_only { ToolExposure::DirectModelOnly } else { ToolExposure::Direct }; let tool_namespace = config .namespace_tools .then_some(config.multi_agent_v2_tool_namespace.as_deref()) .flatten(); let agent_type_description = agent_type_description(config, params.default_agent_type_description); executors.push(multi_agent_v2_handler( SpawnAgentHandlerV2::new(SpawnAgentToolOptions { available_models: config.available_models.clone(), agent_type_description, hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata, include_usage_hint: config.spawn_agent_usage_hint, usage_hint_text: config.spawn_agent_usage_hint_text.clone(), max_concurrent_threads_per_session: config.max_concurrent_threads_per_session, }), exposure, tool_namespace, )); executors.push(multi_agent_v2_handler( SendMessageHandlerV2, exposure, tool_namespace, )); executors.push(multi_agent_v2_handler( FollowupTaskHandlerV2, exposure, tool_namespace, )); executors.push(multi_agent_v2_handler( WaitAgentHandlerV2::new(params.wait_agent_timeouts), exposure, tool_namespace, )); executors.push(multi_agent_v2_handler( CloseAgentHandlerV2, exposure, tool_namespace, )); executors.push(multi_agent_v2_handler( ListAgentsHandlerV2, exposure, tool_namespace, )); } else { let agent_type_description = agent_type_description(config, params.default_agent_type_description); executors.push(Arc::new(SpawnAgentHandler::new(SpawnAgentToolOptions { available_models: config.available_models.clone(), agent_type_description, hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata, include_usage_hint: config.spawn_agent_usage_hint, usage_hint_text: config.spawn_agent_usage_hint_text.clone(), max_concurrent_threads_per_session: config.max_concurrent_threads_per_session, }))); executors.push(Arc::new(SendInputHandler)); executors.push(Arc::new(ResumeAgentHandler)); executors.push(Arc::new(WaitAgentHandler::new(params.wait_agent_timeouts))); executors.push(Arc::new(CloseAgentHandler)); } } if config.agent_jobs_tools { executors.push(Arc::new(SpawnAgentsOnCsvHandler)); if config.agent_jobs_worker_tools { executors.push(Arc::new(ReportAgentJobResultHandler)); } } if let Some(mcp_tools) = params.mcp_tools { for tool in mcp_tools { executors.push(Arc::new(McpHandler::new(tool.clone()))); } } if let Some(deferred_mcp_tools) = params.deferred_mcp_tools { for tool in deferred_mcp_tools { executors.push(Arc::new(McpHandler::with_exposure( tool.clone(), ToolExposure::Deferred, ))); } } for tool in params.dynamic_tools { let Some(handler) = DynamicToolHandler::new(tool).map(Arc::new) else { tracing::error!( "Failed to convert dynamic tool {:?} to OpenAI tool", tool.name ); continue; }; executors.push(handler); } append_extension_tool_executors(config, params.extension_tool_executors, &mut executors); executors } fn append_tool_search_executor( config: &ToolsConfig, executors: &mut Vec>, ) { if !(config.search_tool && config.namespace_tools) { return; } let search_infos = executors .iter() .filter(|executor| executor.exposure() == ToolExposure::Deferred) .filter_map(|executor| executor.search_info()) .collect::>(); if search_infos.is_empty() { return; } executors.push(Arc::new(ToolSearchHandler::new(search_infos))); } fn prepend_code_mode_executors( config: &ToolsConfig, executors: &mut Vec>, ) { let deferred_tools_available = config.search_tool && executors .iter() .any(|executor| executor.exposure() == ToolExposure::Deferred); let code_mode_executors = build_code_mode_executors(config, executors, deferred_tools_available); executors.splice(0..0, code_mode_executors); } fn append_extension_tool_executors( config: &ToolsConfig, executors: &[Arc>], registered_executors: &mut Vec>, ) { if executors.is_empty() { return; } let mut reserved_tool_names = registered_executors .iter() .map(|executor| executor.tool_name()) .collect::>(); if config.code_mode_enabled { reserved_tool_names.insert(ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME)); reserved_tool_names.insert(ToolName::plain(codex_code_mode::WAIT_TOOL_NAME)); } if config.search_tool && config.namespace_tools && registered_executors .iter() .any(|executor| executor.exposure() == ToolExposure::Deferred) { reserved_tool_names.insert(ToolName::plain(TOOL_SEARCH_TOOL_NAME)); } for executor in executors.iter().cloned() { let tool_name = executor.tool_name(); if !reserved_tool_names.insert(tool_name.clone()) { warn!("Skipping extension tool `{tool_name}`: tool already registered"); continue; } registered_executors.push(Arc::new(ExtensionToolAdapter::new(executor))); } } fn multi_agent_v2_handler( handler: impl CoreToolRuntime + 'static, exposure: ToolExposure, namespace: Option<&str>, ) -> Arc { let handler: Arc = match namespace { Some(namespace) => Arc::new(MultiAgentV2NamespaceOverride { handler: Arc::new(handler), namespace: namespace.to_string(), }), None => Arc::new(handler), }; override_tool_exposure(handler, exposure) } struct MultiAgentV2NamespaceOverride { handler: Arc, namespace: String, } #[async_trait::async_trait] impl ToolExecutor for MultiAgentV2NamespaceOverride { fn tool_name(&self) -> ToolName { ToolName::namespaced(self.namespace.clone(), self.handler.tool_name().name) } fn spec(&self) -> Option { match self.handler.spec()? { ToolSpec::Function(tool) => Some(ToolSpec::Namespace(ResponsesApiNamespace { name: self.namespace.clone(), description: MULTI_AGENT_V2_NAMESPACE_DESCRIPTION.to_string(), tools: vec![ResponsesApiNamespaceTool::Function(tool)], })), spec => Some(spec), } } fn exposure(&self) -> ToolExposure { self.handler.exposure() } fn supports_parallel_tool_calls(&self) -> bool { self.handler.supports_parallel_tool_calls() } async fn handle( &self, invocation: ToolInvocation, ) -> Result, codex_tools::FunctionCallError> { self.handler.handle(invocation).await } } impl CoreToolRuntime for MultiAgentV2NamespaceOverride { fn matches_kind(&self, payload: &crate::tools::context::ToolPayload) -> bool { self.handler.matches_kind(payload) } fn search_info(&self) -> Option { self.handler.search_info() } fn create_diff_consumer( &self, ) -> Option> { self.handler.create_diff_consumer() } } fn compare_code_mode_tools( left: &codex_code_mode::ToolDefinition, right: &codex_code_mode::ToolDefinition, namespace_descriptions: &BTreeMap, ) -> std::cmp::Ordering { let left_namespace = code_mode_namespace_name(left, namespace_descriptions); let right_namespace = code_mode_namespace_name(right, namespace_descriptions); left_namespace .cmp(&right_namespace) .then_with(|| left.tool_name.name.cmp(&right.tool_name.name)) .then_with(|| left.name.cmp(&right.name)) } fn code_mode_namespace_name<'a>( tool: &codex_code_mode::ToolDefinition, namespace_descriptions: &'a BTreeMap, ) -> Option<&'a str> { tool.tool_name .namespace .as_ref() .and_then(|namespace| namespace_descriptions.get(namespace)) .map(|namespace_description| namespace_description.name.as_str()) } #[cfg(test)] #[path = "spec_plan_model_tests.rs"] mod model_tests; #[cfg(test)] #[path = "spec_plan_tests.rs"] mod tests;