use crate::agent::exceeds_thread_spawn_depth_limit; use crate::agent::next_thread_spawn_depth; use crate::environment_selection::TurnEnvironmentSnapshot; use crate::image_preparation::unified_image_budget_enabled; use crate::session::session::Session; use crate::session::turn_context::TurnContext; use crate::tools::code_mode::execute_spec::create_code_mode_tool; use crate::tools::effective_tool_mode; use crate::tools::handlers::ApplyPatchHandler; use crate::tools::handlers::CodeModeExecuteHandler; use crate::tools::handlers::CodeModeWaitHandler; use crate::tools::handlers::CurrentTimeHandler; use crate::tools::handlers::DynamicToolHandler; use crate::tools::handlers::ExecCommandHandler; use crate::tools::handlers::ExecCommandHandlerOptions; use crate::tools::handlers::GetContextRemainingHandler; use crate::tools::handlers::ListAvailablePluginsToInstallHandler; use crate::tools::handlers::ListMcpResourceTemplatesHandler; use crate::tools::handlers::ListMcpResourcesHandler; use crate::tools::handlers::NewContextWindowHandler; use crate::tools::handlers::PlanHandler; use crate::tools::handlers::ReadMcpResourceHandler; use crate::tools::handlers::RequestPermissionsHandler; use crate::tools::handlers::RequestPluginInstallHandler; use crate::tools::handlers::RequestUserInputAsyncHandler; use crate::tools::handlers::RequestUserInputHandler; use crate::tools::handlers::SendMessageToUserAsyncHandler; use crate::tools::handlers::SleepHandler; use crate::tools::handlers::TestSyncHandler; use crate::tools::handlers::ToolSearchHandlerCache; use crate::tools::handlers::ViewImageHandler; use crate::tools::handlers::WaitForEnvironmentHandler; use crate::tools::handlers::WriteStdinHandler; 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::MULTI_AGENT_V1_NAMESPACE; use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions; use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions; use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2; use crate::tools::handlers::multi_agents_v2::InterruptAgentHandler; 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::tool_search_spec::ToolSearchSourceListing; use crate::tools::handlers::view_image_spec::ViewImageToolOptions; use crate::tools::hosted_spec::WebSearchToolOptions; use crate::tools::hosted_spec::create_web_search_tool; use crate::tools::multi_agent_tool::multi_agent_v2_handler; #[cfg(test)] use crate::tools::registry::RegisteredTool; use crate::tools::registry::ToolExposure; use crate::tools::registry::ToolRegistry; use crate::tools::router::ToolRouter; use crate::tools::tool_namespaces_info::collect_tool_namespaces_info; use codex_connectors::apps_config_from_layer_stack; use codex_extension_api::ExtensionData; use codex_features::Feature; use codex_features::SleepToolMode; use codex_login::AuthManager; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_prompts::ResolvedModelMessages; use codex_protocol::DEFAULT_FUNCTION_NAMESPACE; use codex_protocol::account::PlanType; use codex_protocol::config_types::WebSearchMode; use codex_protocol::dynamic_tools::DynamicToolNamespaceTool; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::error::CodexErrorDetails; use codex_protocol::error::Result as CodexResult; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ToolMode; use codex_protocol::protocol::MultiAgentVersion; use codex_tools::IndirectNamespacePrefixes; 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::ToolExposures; use codex_tools::ToolName; use codex_tools::ToolSpec; use codex_tools::UnifiedExecShellMode; use codex_tools::can_request_original_image_detail; use codex_tools::collect_code_mode_exec_prompt_tool_definitions; use codex_tools::collect_request_plugin_install_entries; use codex_tools::default_namespace_description; use codex_tools::request_user_input_available_modes; use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::collections::btree_map::Entry; use std::sync::Arc; use tracing::instrument; const IMAGE_GEN_NAMESPACE: &str = "image_gen"; const IMAGEGEN_TOOL_NAME: &str = "imagegen"; #[derive(Clone, Copy)] struct CoreToolPlanContext<'a> { tool_policy: &'a codex_extension_api::ToolPolicy, turn_context: &'a TurnContext, model_info: &'a ModelInfo, environments: &'a TurnEnvironmentSnapshot, mcp: &'a codex_mcp::McpBinding, tool_suggest_candidates: Option<&'a crate::tools::router::ToolSuggestCandidates>, wait_for_environment_tool_config: Option<&'a Arc>, default_agent_type_description: &'a str, wait_agent_timeouts: WaitAgentTimeoutOptions, } #[instrument(level = "trace", skip_all)] #[allow(clippy::too_many_arguments)] pub(crate) fn build_tool_router( session: &Session, turn_context: &TurnContext, model_info: &ModelInfo, environments: &TurnEnvironmentSnapshot, mcp: &Arc, apps_enabled: bool, step_store: &ExtensionData, tool_suggest_candidates: Option<&crate::tools::router::ToolSuggestCandidates>, ) -> CodexResult { let default_agent_type_description = crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new()); let wait_for_environment_tool_config = session .services .thread_extension_data .get::(); let context = CoreToolPlanContext { tool_policy: &session.tool_policy, turn_context, model_info, environments, mcp, tool_suggest_candidates, wait_for_environment_tool_config: wait_for_environment_tool_config.as_ref(), default_agent_type_description: &default_agent_type_description, wait_agent_timeouts: wait_agent_timeout_options(turn_context), }; let mut registry = ToolRegistry::with_tool_policy(Arc::clone(&session.tool_policy)); add_core_tool_sources(&context, &mut registry); let registered_mcp_tools = session.services.mcp_handler_cache.append_mcp_tools( mcp, &turn_context.config, apps_enabled, &mcp.config().mcp_server_catalog, model_info.supports_search_tool, &mut registry, ); apply_mcp_tool_exposure_policy( turn_context, model_info, mcp, ®istered_mcp_tools, &mut registry, ); let standalone_web_search_tool = append_extension_tool_executors( turn_context, model_info, extension_tool_executors(session, step_store), &mut registry, ); append_dynamic_tool_runtimes(&turn_context.dynamic_tools, &mut registry); let hosted_specs = hosted_model_tool_specs( turn_context, model_info, standalone_web_search_tool.as_slice(), ); finalize_tool_router( turn_context, model_info, registry, hosted_specs, &session.services.tool_search_handler_cache, ) } /// Use the effective mode because the model can override the thread's configured mode. fn code_mode_only_strict_3p_tools(turn_context: &TurnContext, model_info: &ModelInfo) -> bool { effective_tool_mode(turn_context, model_info) == ToolMode::CodeModeOnly && turn_context .config .features .enabled(Feature::CodeModeOnlyStrictThirdPartyTools) } fn apply_mcp_tool_exposure_policy( turn_context: &TurnContext, model_info: &ModelInfo, mcp: &codex_mcp::McpBinding, registered_mcp_tools: &HashSet, registry: &mut ToolRegistry, ) { let mut omitted_exposures_by_tool = HashMap::new(); let apps_config = apps_config_from_layer_stack(&turn_context.config.config_layer_stack); for tool in mcp.tools() { let tool_name = tool.canonical_tool_name(); // Disabled, denied, app-only and over-budget MCP tools are absent from this set. if !registered_mcp_tools.contains(&tool_name) { continue; } let Some(server) = mcp.config().mcp_server_catalog.server(&tool.server_name) else { continue; }; omitted_exposures_by_tool .entry(tool_name) .or_insert_with(|| { let connector_omissions = (tool.server_name == CODEX_APPS_MCP_SERVER_NAME) .then_some(tool.connector_id.as_deref()) .flatten() .and_then(|id| apps_config.as_ref()?.apps.get(id)) .and_then(|app| app.omit_tools_from.as_deref()) .unwrap_or_default(); server .config() .omit_tools_from .as_deref() .unwrap_or_default() .iter() .chain(connector_omissions) .copied() .collect::() }); } for tool in registry.entries_mut() { let tool_name = tool.runtime.tool_name(); let Some(omitted_exposures) = omitted_exposures_by_tool.get(&tool_name) else { continue; }; if code_mode_only_strict_3p_tools(turn_context, model_info) { // Ignore MCP/app omit_tools_from; eligible tools must stay deferred in exec. // Log conflicts without adding a warning to the model's prompt. if omitted_exposures.intersects(ToolExposures::DEFERRED | ToolExposures::CODE_MODE) { tracing::warn!( tool_name = %tool_name, "Ignoring MCP/app omit_tools_from because code_mode_only_strict_3p_tools is enabled" ); } continue; } let tool_name = tool_name.with_default_namespace(); let mut exposures = ToolExposures::ALL.difference(*omitted_exposures); if tool_name.namespace.as_ref().is_some_and(|namespace| { turn_context .config .code_mode .direct_only_tool_namespaces .contains(namespace) }) { exposures = exposures.difference(ToolExposures::DEFERRED | ToolExposures::CODE_MODE); } exposures = if model_info.supports_search_tool && exposures.contains(ToolExposures::DEFERRED) && (effective_tool_mode(turn_context, model_info) != ToolMode::CodeModeOnly || exposures.contains(ToolExposures::CODE_MODE)) { exposures.difference(ToolExposures::DIRECT) } else { exposures.difference(ToolExposures::DEFERRED) }; tool.exposure = match ( exposures.contains(ToolExposures::DIRECT), exposures.contains(ToolExposures::DEFERRED), exposures.contains(ToolExposures::CODE_MODE), ) { (false, false, false) => ToolExposure::Hidden, (false, false, true) => ToolExposure::CodeModeOnly, (true, false, false) => ToolExposure::DirectModelOnly, (true, false, true) => ToolExposure::Direct, (false, true, false) => ToolExposure::DeferredModelOnly, (false, true, true) => ToolExposure::Deferred, (true, true, _) => unreachable!("direct and deferred exposure are mutually exclusive"), }; } } #[cfg(test)] #[instrument(level = "trace", skip_all)] pub(crate) fn build_core_tool_registry( turn_context: &TurnContext, model_info: &ModelInfo, environments: &TurnEnvironmentSnapshot, mcp: &codex_mcp::McpBinding, tool_suggest_candidates: Option<&crate::tools::router::ToolSuggestCandidates>, wait_for_environment_tool_config: Option<&Arc>, ) -> ToolRegistry { let default_agent_type_description = crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new()); let context = CoreToolPlanContext { tool_policy: &Default::default(), turn_context, model_info, environments, mcp, tool_suggest_candidates, wait_for_environment_tool_config, default_agent_type_description: &default_agent_type_description, wait_agent_timeouts: wait_agent_timeout_options(turn_context), }; let mut registry = ToolRegistry::default(); add_core_tool_sources(&context, &mut registry); registry } #[cfg(test)] #[instrument(level = "trace", skip_all)] pub(crate) fn append_source_tools( turn_context: &TurnContext, model_info: &ModelInfo, registry: &mut ToolRegistry, mcp_tools: Vec, extension_tool_executors: impl IntoIterator< Item = Arc ToolExecutor>>, >, dynamic_tools: &[DynamicToolSpec], ) -> Vec { for tool in mcp_tools { registry.register_external_with_exposure(tool.runtime, tool.exposure); } let standalone_web_search_tool = append_extension_tool_executors( turn_context, model_info, extension_tool_executors, registry, ); append_dynamic_tool_runtimes(dynamic_tools, registry); hosted_model_tool_specs( turn_context, model_info, standalone_web_search_tool.as_slice(), ) } #[instrument(level = "trace", skip_all)] pub(crate) fn extension_tool_executors<'a>( session: &'a Session, step_store: &'a ExtensionData, ) -> impl Iterator ToolExecutor>>> + 'a { session .services .extensions .tool_contributors() .iter() .flat_map(move |contributor| { contributor.tools_for_step( &session.services.session_extension_data, &session.services.thread_extension_data, step_store, ) }) } #[instrument(level = "trace", skip_all)] pub(crate) fn finalize_tool_router( turn_context: &TurnContext, model_info: &ModelInfo, mut registry: ToolRegistry, mut hosted_specs: Vec, tool_search_handler_cache: &ToolSearchHandlerCache, ) -> CodexResult { hosted_specs.retain(|spec| registry.tool_policy.allows(&ToolName::plain(spec.name()))); apply_direct_model_only_namespace_overrides(turn_context, &mut registry); enforce_strict_3p_tools(turn_context, model_info, &mut registry); let tool_mode = effective_tool_mode(turn_context, model_info); let code_mode_enabled = matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly); if code_mode_enabled { for tool_name in [ ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME), ToolName::plain(codex_code_mode::WAIT_TOOL_NAME), ] { if registry.remove(&tool_name).is_some() { registry.record_collision(tool_name); } } } let tool_search_name = ToolName::plain(TOOL_SEARCH_TOOL_NAME); if model_info.supports_search_tool && registry.entries().any(|tool| { tool.runtime.tool_name() != tool_search_name && tool.exposure.is_deferred() && tool.runtime.search_info().is_some() }) { if registry.remove(&tool_search_name).is_some() { registry.record_collision(tool_search_name.clone()); } // Special model tools own the namespace matching their wire identity, so // regular namespace tools cannot advertise that same model-visible surface. let conflicting_tool_names = registry .entries() .filter_map(|tool| { let owned_spec; let spec = if let Some(spec) = tool.runtime.immutable_spec() { spec.as_ref() } else { owned_spec = tool.runtime.spec(); &owned_spec }; let ToolSpec::Namespace(namespace) = spec else { return None; }; (namespace.name == tool_search_name.name).then(|| tool.runtime.tool_name()) }) .collect::>(); for tool_name in conflicting_tool_names { if registry.remove(&tool_name).is_some() { registry.record_collision(tool_name); } } append_tool_search_executor(turn_context, &mut registry, tool_search_handler_cache); } let model_messages = ResolvedModelMessages::from_model(model_info); let indirect_prefixes = IndirectNamespacePrefixes::new( model_messages.indirect_description_prefixes(), registry.mcp_namespaces(), ) .map_err(|error| CodexErrorDetails::InvalidRequest(error.to_string()))?; let code_mode_tool_names = register_code_mode_executors(turn_context, model_info, &mut registry, &indirect_prefixes); let include_tool_namespaces_info = turn_context .config .tool_registry .turn_metadata_includes_tool_info && model_info.use_responses_lite; if turn_context.config.tool_registry.error_on_tool_collisions { if let Some(tool_name) = registry.first_collision() { let namespace = tool_name.namespace.as_deref().unwrap_or("functions"); let name = format!("{namespace}.{}", tool_name.name); return Err(CodexErrorDetails::ToolCollision(name).into()); } let mut namespace_descriptions = BTreeMap::new(); let mut namespace_owners = BTreeMap::new(); for tool in registry.entries() { let owned_spec; let spec = if let Some(spec) = tool.runtime.immutable_spec() { spec.as_ref() } else { owned_spec = tool.runtime.spec(); &owned_spec }; if include_tool_namespaces_info && tool.exposure != ToolExposure::Hidden { let namespace_name = match spec { ToolSpec::Namespace(namespace) => namespace.name.as_str(), ToolSpec::Function(_) | ToolSpec::Freeform(_) => DEFAULT_FUNCTION_NAMESPACE, ToolSpec::ToolSearch { .. } => TOOL_SEARCH_TOOL_NAME, ToolSpec::WebSearch { .. } => continue, }; let owner = tool.runtime.mcp_server_name(); match namespace_owners.get(namespace_name) { Some(existing_owner) if existing_owner != &owner => { return Err( CodexErrorDetails::ToolCollision(namespace_name.to_string()).into() ); } Some(_) => {} None => { namespace_owners.insert(namespace_name.to_string(), owner); } } } let ToolSpec::Namespace(namespace) = spec else { continue; }; if namespace.description.trim().is_empty() { continue; } match namespace_descriptions.entry(namespace.name.clone()) { Entry::Vacant(entry) => { entry.insert(namespace.description.clone()); } Entry::Occupied(entry) if entry.get() != &namespace.description => { return Err(CodexErrorDetails::ToolCollision(entry.key().clone()).into()); } Entry::Occupied(_) => {} } } } let model_visible_specs = build_model_visible_specs( turn_context, model_info, ®istry, &code_mode_tool_names, hosted_specs, ); let tool_namespaces_info = include_tool_namespaces_info .then(|| { collect_tool_namespaces_info(®istry, &code_mode_tool_names, &model_visible_specs) }) .filter(|info| !info.is_empty()); let child_management_tools = required_child_management_tool_names(turn_context, model_info); let router = ToolRouter::from_parts( registry, model_visible_specs, tool_mode, code_mode_tool_names, tool_namespaces_info, &child_management_tools, ); // Internal workers can inherit MAv2 configuration without using the board. if multi_agent_v2_enabled(turn_context) && collab_tools_enabled(turn_context, model_info) && turn_context.config.multi_agent_v2.disable_direct_message && !turn_context.session_source.is_internal() { let post_tool = ToolName::new( turn_context.config.multi_agent_v2.tool_namespace.clone(), "post", ); if !router.exposes_tool(&post_tool) { return Err(CodexErrorDetails::InvalidRequest( "disable_direct_message requires an available agent message board in this session" .to_owned(), ) .into()); } } Ok(router) } fn apply_direct_model_only_namespace_overrides( turn_context: &TurnContext, registry: &mut ToolRegistry, ) { if turn_context .config .code_mode .direct_only_tool_namespaces .is_empty() { return; } for tool in registry.entries_mut() { let configured = tool .runtime .tool_name() .with_default_namespace() .namespace .as_ref() .is_some_and(|namespace| { turn_context .config .code_mode .direct_only_tool_namespaces .contains(namespace) }); if configured && tool.exposure.is_available_in_code_mode() { tool.exposure = ToolExposure::DirectModelOnly; } } } /// Run after namespace overrides so eligible third-party tools always remain deferred. fn enforce_strict_3p_tools( turn_context: &TurnContext, model_info: &ModelInfo, registry: &mut ToolRegistry, ) { if !code_mode_only_strict_3p_tools(turn_context, model_info) { return; } for tool in registry.entries_mut() { // Remaining Hidden entries failed eligibility or budget checks; keep them hidden. if !tool.runtime.is_third_party_tool() || tool.exposure == ToolExposure::Hidden { continue; } if tool.exposure != ToolExposure::Deferred { tracing::warn!( tool_name = %tool.runtime.tool_name().with_default_namespace(), "Deferring third-party tool because code_mode_only_strict_3p_tools is enabled" ); tool.exposure = ToolExposure::Deferred; } } } #[instrument(level = "trace", skip_all)] fn build_model_visible_specs( turn_context: &TurnContext, model_info: &ModelInfo, registry: &ToolRegistry, code_mode_tool_names: &BTreeMap, hosted_specs: Vec, ) -> Vec { let mut specs = Vec::new(); for tool in registry.entries() { let exposure = tool.exposure; if !exposure.is_direct() { continue; } let tool_name = tool.runtime.tool_name(); if is_hidden_by_code_mode_only(turn_context, model_info, &tool_name, exposure) { continue; } let spec = tool.runtime.spec(); specs.push(spec_for_model_request( turn_context, model_info, exposure, &tool_name, code_mode_tool_names, spec, )); } specs.extend(hosted_specs); merge_into_namespaces(specs) } fn spec_for_model_request( turn_context: &TurnContext, model_info: &ModelInfo, exposure: ToolExposure, tool_name: &ToolName, code_mode_tool_names: &BTreeMap, spec: ToolSpec, ) -> ToolSpec { let tool_mode = effective_tool_mode(turn_context, model_info); if matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) && exposure.is_available_in_code_mode() && !is_excluded_from_code_mode(turn_context, tool_name) && codex_code_mode::is_code_mode_nested_tool(spec.name()) && code_mode_tool_names .get(&codex_code_mode::normalize_code_mode_identifier( &codex_tools::code_mode_name_for_tool_name(tool_name), )) .is_some_and(|winner| winner == tool_name) { codex_tools::augment_tool_spec_for_code_mode( spec, turn_context.config.code_mode.tool_input_schema_max_bytes, ) } else { spec } } #[instrument(level = "trace", skip_all)] fn hosted_model_tool_specs( turn_context: &TurnContext, model_info: &ModelInfo, registered_extension_tool_names: &[ToolName], ) -> Vec { // Responses Lite accepts schemas for client-executed tools, not hosted Responses tools. if model_info.use_responses_lite { return Vec::new(); } let mut specs = Vec::new(); let standalone_web_search_available = standalone_web_search_enabled(turn_context, model_info) && registered_extension_tool_names.contains(&ToolName::namespaced("web", "run")); // `Some(Cached/Live/Disabled)` are the options for mode when standalone search is unavailable // and the provider supports hosted search. `None` prevents emitting a hosted search tool. let web_search_mode = (!standalone_web_search_available && turn_context.provider.capabilities().web_search) .then_some(turn_context.config.web_search_mode.value()); let web_search_config = web_search_mode .as_ref() .and(turn_context.config.web_search_config.as_ref()); if let Some(hosted_web_search_tool) = create_web_search_tool(WebSearchToolOptions { web_search_mode, web_search_config, web_search_tool_type: model_info.web_search_tool_type, }) { specs.push(hosted_web_search_tool); } specs } pub(crate) fn tool_suggest_enabled(turn_context: &TurnContext) -> bool { let features = turn_context.config.features.get(); features.enabled(Feature::ToolSuggest) && features.enabled(Feature::Apps) && features.enabled(Feature::Plugins) } fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool { turn_context.multi_agent_version == MultiAgentVersion::V2 } fn collab_tools_enabled(turn_context: &TurnContext, model_info: &ModelInfo) -> bool { match turn_context.multi_agent_version { MultiAgentVersion::Disabled => false, MultiAgentVersion::V1 => !exceeds_thread_spawn_depth_limit( next_thread_spawn_depth(&turn_context.session_source), turn_context.config.agent_max_depth, ), MultiAgentVersion::V2 => { turn_context.session_source.get_agent_path().is_none() || model_info.multi_agent_version == Some(MultiAgentVersion::V2) } } } fn required_child_management_tool_names( turn_context: &TurnContext, model_info: &ModelInfo, ) -> Vec { if !collab_tools_enabled(turn_context, model_info) { return Vec::new(); } let (namespace, names): (_, &[&str]) = match turn_context.multi_agent_version { MultiAgentVersion::Disabled => return Vec::new(), MultiAgentVersion::V1 => ( Some(MULTI_AGENT_V1_NAMESPACE), &["send_input", "wait_agent", "resume_agent", "close_agent"], ), MultiAgentVersion::V2 => ( turn_context.config.multi_agent_v2.tool_namespace.as_deref(), if turn_context.config.multi_agent_v2.disable_direct_message { &["interrupt_agent", "list_agents"] } else { &[ "send_message", "followup_task", "interrupt_agent", "list_agents", ] }, ), }; let mut tools = names .iter() .map(|name| ToolName::new(namespace.map(str::to_owned), (*name).to_owned())) .collect::>(); if multi_agent_v2_enabled(turn_context) && turn_context.config.multi_agent_v2.wait_agent_enabled { tools.push(ToolName::new(namespace.map(str::to_owned), "wait_agent")); } tools } fn image_generation_available(turn_context: &TurnContext, model_info: &ModelInfo) -> bool { if !turn_context .config .features .get() .enabled(Feature::ImageGeneration) { return false; } if turn_context .auth_manager .as_deref() .and_then(AuthManager::auth_cached) .and_then(|auth| auth.account_plan_type()) == Some(PlanType::Free) { return false; } if !turn_context.provider.capabilities().image_generation { return false; } if !model_info.input_modalities.contains(&InputModality::Image) { return false; } let provider = turn_context.provider.info(); provider.uses_openai_actor_authorization() || (provider.requires_openai_auth && turn_context .auth_manager .as_deref() .is_some_and(AuthManager::current_auth_uses_codex_backend)) } fn wait_agent_timeout_options(turn_context: &TurnContext) -> WaitAgentTimeoutOptions { if multi_agent_v2_enabled(turn_context) { return WaitAgentTimeoutOptions { default_timeout_ms: turn_context.config.multi_agent_v2.default_wait_timeout_ms, min_timeout_ms: turn_context.config.multi_agent_v2.min_wait_timeout_ms, max_timeout_ms: turn_context.config.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( turn_context: &TurnContext, default_agent_type_description: &str, ) -> String { let agent_type_description = crate::agent::role::spawn_tool_spec::build(&turn_context.config.agent_roles); if agent_type_description.is_empty() { default_agent_type_description.to_string() } else { agent_type_description } } fn is_hidden_by_code_mode_only( turn_context: &TurnContext, model_info: &ModelInfo, tool_name: &ToolName, exposure: ToolExposure, ) -> bool { let tool_mode = effective_tool_mode(turn_context, model_info); tool_mode == ToolMode::CodeModeOnly && exposure.is_available_in_code_mode() && codex_code_mode::is_code_mode_nested_tool(&codex_tools::code_mode_name_for_tool_name( tool_name, )) } fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName) -> bool { let tool_name = tool_name.clone().with_default_namespace(); tool_name.namespace.as_ref().is_some_and(|namespace| { turn_context .config .code_mode .excluded_tool_namespaces .contains(namespace) }) } fn register_code_mode_executors( turn_context: &TurnContext, model_info: &ModelInfo, registry: &mut ToolRegistry, indirect_prefixes: &IndirectNamespacePrefixes<'_>, ) -> BTreeMap { let tool_mode = effective_tool_mode(turn_context, model_info); if !matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) { return BTreeMap::new(); } let mut code_mode_tool_names = BTreeMap::new(); let mut code_mode_nested_tool_specs = Vec::new(); let mut exec_prompt_tool_specs = Vec::new(); let mut deferred_exec_prompt_tool_specs = Vec::new(); let mut included_deferred_mcp_output_schema = false; let deferred_tools_guidance_enabled = model_info.supports_search_tool; for tool in registry.entries() { let exposure = tool.exposure; if !exposure.is_available_in_code_mode() { continue; } let tool_name = tool.runtime.tool_name(); // Deferred third-party tools still need an exec route to be discoverable. if is_excluded_from_code_mode(turn_context, &tool_name) && !(code_mode_only_strict_3p_tools(turn_context, model_info) && tool.runtime.is_third_party_tool()) { continue; } let immutable_spec = tool.runtime.immutable_spec(); let cached_runtime = immutable_spec.map(|_| Arc::clone(&tool.runtime)); let spec = immutable_spec .map(Arc::clone) .unwrap_or_else(|| Arc::new(tool.runtime.spec())); // Derive the name without serializing and augmenting every tool schema. let code_mode_name = match spec.as_ref() { ToolSpec::Function(_) | ToolSpec::Freeform(_) => { codex_tools::code_mode_name_for_tool_name(&tool_name) } ToolSpec::Namespace(namespace) if !namespace.tools.is_empty() => { codex_tools::code_mode_name_for_tool_name(&tool_name) } ToolSpec::Namespace(_) | ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => { continue; } }; if !codex_code_mode::is_code_mode_nested_tool(&code_mode_name) { continue; } match code_mode_tool_names.entry(codex_code_mode::normalize_code_mode_identifier( &code_mode_name, )) { Entry::Vacant(entry) => { entry.insert(tool_name); } Entry::Occupied(_) => { tracing::warn!( tool_name = %tool_name, code_mode_name = %code_mode_name, "skipping tool with a duplicate normalized code-mode name" ); continue; } } if exposure == ToolExposure::Deferred { if deferred_tools_guidance_enabled && (cached_runtime.is_none() || !included_deferred_mcp_output_schema) { if cached_runtime.is_some() { included_deferred_mcp_output_schema = true; } deferred_exec_prompt_tool_specs.push(Arc::clone(&spec)); } } else { exec_prompt_tool_specs.push(spec.as_ref().clone()); } code_mode_nested_tool_specs.push((spec, cached_runtime)); } let mut namespace_descriptions = code_mode_namespace_descriptions(&exec_prompt_tool_specs); let code_mode_input_schema_max_bytes = turn_context.config.code_mode.tool_input_schema_max_bytes; let mut enabled_tools = collect_code_mode_exec_prompt_tool_definitions( exec_prompt_tool_specs.iter(), code_mode_input_schema_max_bytes, ); let deferred_tools = collect_code_mode_exec_prompt_tool_definitions( deferred_exec_prompt_tool_specs.iter().map(Arc::as_ref), code_mode_input_schema_max_bytes, ); let model_messages = ResolvedModelMessages::from_model(model_info); if tool_mode == ToolMode::CodeModeOnly { indirect_prefixes.apply_exec_prompt(&mut enabled_tools, &mut namespace_descriptions); } enabled_tools .sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions)); let execute_handler = CodeModeExecuteHandler::new( create_code_mode_tool( &enabled_tools, &deferred_tools, &namespace_descriptions, turn_context.config.code_mode.default_exec_yield_time_ms, tool_mode == ToolMode::CodeModeOnly, if unified_image_budget_enabled(&turn_context.config.features, model_info) { codex_code_mode::ImageDetailVisibility::Hidden } else { codex_code_mode::ImageDetailVisibility::Visible }, model_messages.code_mode(), ), code_mode_nested_tool_specs, ); registry.prepend_trusted(Arc::new(CodeModeWaitHandler::new( model_messages.code_mode_wait_description_override(), model_messages.code_mode_wait_parameters_override(), ))); registry.prepend_trusted(Arc::new(execute_handler)); code_mode_tool_names } #[instrument(level = "trace", skip_all, fields(tool_spec_count = specs.len()))] 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| { let left_name = match left { ResponsesApiNamespaceTool::Function(tool) => &tool.name, ResponsesApiNamespaceTool::Custom(tool) => &tool.name, }; let right_name = match right { ResponsesApiNamespaceTool::Function(tool) => &tool.name, ResponsesApiNamespaceTool::Custom(tool) => &tool.name, }; 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 } #[instrument(level = "trace", skip_all)] fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) { // The startup ceiling applies to the thread and every selected environment. if context.tool_policy.require_managed_sandbox && (!matches!( context.turn_context.permission_profile(), PermissionProfile::Managed { .. } ) || context.environments.turn_environments().any(|environment| { !matches!( environment.permission_profile(), PermissionProfile::Managed { .. } ) })) { return; } add_shell_tools(context, registry); add_mcp_resource_tools(context, registry); add_core_utility_tools(context, registry); add_collaboration_tools(context, registry); } fn standalone_web_search_enabled(turn_context: &TurnContext, model_info: &ModelInfo) -> bool { turn_context.provider.capabilities().web_search && (model_info.use_responses_lite || turn_context .config .features .get() .enabled(Feature::StandaloneWebSearch)) } fn tool_environment_mode(context: &CoreToolPlanContext<'_>) -> ToolEnvironmentMode { let count = if context .turn_context .config .features .get() .enabled(Feature::StableEnvironmentTools) { // Keep selectors stable as selected attachments change readiness. context.environments.environments.len() } else { context.environments.turn_environments().count() }; ToolEnvironmentMode::from_count(count) } fn should_include_windows_shell_guidance(environments: &TurnEnvironmentSnapshot) -> bool { let mut environments = environments.turn_environments(); let Some(environment) = environments.next() else { return false; }; let executor_platform_os = if environments.next().is_none() { environment.executor_platform_os.as_deref() } else { None }; // One tool schema can target any ready environment. Multi-environment turns and legacy // executors without platform OS information preserve the host-derived guidance. match executor_platform_os { Some(platform_os) => platform_os == "windows", None => cfg!(windows), } } #[instrument(level = "trace", skip_all)] fn add_shell_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) { let turn_context = context.turn_context; let features = turn_context.config.features.get(); let environment_mode = tool_environment_mode(context); let stable_environment_tools = features.enabled(Feature::StableEnvironmentTools); let advertise_environment_tools = stable_environment_tools || environment_mode.has_environment(); if !advertise_environment_tools || !features.enabled(Feature::ShellTool) || matches!(context.model_info.shell_type, ConfigShellToolType::Disabled) { return; } if context.tool_policy.require_unified_exec && !features.enabled(Feature::UnifiedExec) { return; } let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals) && context.tool_policy.expose_additional_permissions; let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); let options = ExecCommandHandlerOptions { include_login_parameter: stable_environment_tools || context .environments .turn_environments() .any(|environment| environment.config().allow_login_shell), include_shell_parameter: stable_environment_tools || !matches!( &turn_context.unified_exec_shell_mode, UnifiedExecShellMode::ZshFork(_) ) || context .environments .turn_environments() .any(|environment| environment.environment.is_remote()), allow_tty: features.enabled(Feature::UnifiedExecTty), exec_permission_approvals_enabled, include_environment_id, include_windows_shell_guidance: should_include_windows_shell_guidance(context.environments), }; if features.enabled(Feature::UnifiedExec) { registry.add(ExecCommandHandler::new(options)); registry.add(WriteStdinHandler); } else { // Managed requirements are the only configuration path that can keep // unified exec disabled. Preserve command execution without exposing a // resumable process or write_stdin authority prohibited by policy. registry.add(ExecCommandHandler::one_shot(options)); } } #[instrument(level = "trace", skip_all)] fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) { if context.mcp.has_servers() || effective_tool_mode(context.turn_context, context.model_info) == ToolMode::CodeModeOnly { let messages = ResolvedModelMessages::from_model(context.model_info).mcp_resources(); registry.add(ListMcpResourcesHandler::new( messages.and_then(|messages| messages.list_mcp_resources.as_ref()), )); registry.add(ListMcpResourceTemplatesHandler::new( messages.and_then(|messages| messages.list_mcp_resource_templates.as_ref()), )); registry.add(ReadMcpResourceHandler::new( messages.and_then(|messages| messages.read_mcp_resource.as_ref()), )); } } #[instrument(level = "trace", skip_all)] fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) { let turn_context = context.turn_context; let features = turn_context.config.features.get(); let environment_mode = tool_environment_mode(context); let stable_environment_tools = features.enabled(Feature::StableEnvironmentTools); let advertise_environment_tools = stable_environment_tools || environment_mode.has_environment(); if turn_context.config.update_plan_enabled { registry.add(PlanHandler); } if features.enabled(Feature::DeferredExecutor) { registry.add( context .wait_for_environment_tool_config .map(Arc::as_ref) .map_or_else( WaitForEnvironmentHandler::default, WaitForEnvironmentHandler::new, ), ); } if turn_context.config.experimental_request_user_input_enabled { registry.add_with_exposure( RequestUserInputHandler { available_modes: request_user_input_available_modes(features), }, ToolExposure::DirectModelOnly, ); } if !turn_context.session_source.is_non_root_agent() && context .model_info .experimental_supported_tools .iter() // Existing model catalogs still advertise the previous name. .any(|tool| { matches!( tool.as_str(), "request_user_input_async" | "send_user_message_async" ) }) { let model_messages = ResolvedModelMessages::from_model(context.model_info); registry.add_with_exposure( RequestUserInputAsyncHandler { description: model_messages .request_user_input_async_description() .to_string(), parameters: model_messages .request_user_input_async_parameters_override() .map(str::to_owned), }, ToolExposure::DirectModelOnly, ); } if !turn_context.session_source.is_non_root_agent() && (features.enabled(Feature::SendMessageToUserAsync) || context .model_info .experimental_supported_tools .iter() .any(|tool| tool == "send_message_to_user_async")) { registry.add_with_exposure(SendMessageToUserAsyncHandler, ToolExposure::DirectModelOnly); } if advertise_environment_tools && features.enabled(Feature::RequestPermissionsTool) { registry.add(RequestPermissionsHandler); } if features.enabled(Feature::TokenBudget) { registry.add_with_exposure(NewContextWindowHandler, ToolExposure::DirectModelOnly); registry.add(GetContextRemainingHandler); } let current_time_reminder_enabled = features.enabled(Feature::CurrentTimeReminder); let model_has_clock = context .model_info .experimental_supported_tools .iter() .any(|tool| tool == "clock"); if current_time_reminder_enabled || model_has_clock { registry.add(CurrentTimeHandler); } if features.enabled(Feature::SleepTool) && match turn_context.config.sleep_tool_mode { SleepToolMode::AlwaysOn => true, SleepToolMode::ModelDriven => { if current_time_reminder_enabled { turn_context .config .current_time_reminder .as_ref() .is_some_and(|config| config.sleep_tool) } else { model_has_clock } } } { registry.add(SleepHandler); } if tool_suggest_enabled(turn_context) && let Some(candidates) = context .tool_suggest_candidates .filter(|candidates| !candidates.tools.is_empty()) { if candidates.presentation == crate::tools::router::ToolSuggestPresentation::ListTool { registry.add(ListAvailablePluginsToInstallHandler::new( collect_request_plugin_install_entries(&candidates.tools), )); } registry.add(RequestPluginInstallHandler::new( candidates.tools.clone(), candidates.presentation, )); } if advertise_environment_tools && context.model_info.apply_patch_tool_type.is_some() { let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); registry.add(ApplyPatchHandler::new(include_environment_id)); } if context .model_info .experimental_supported_tools .iter() .any(|tool| tool == "test_sync_tool") { registry.add(TestSyncHandler); } if advertise_environment_tools && features.enabled(Feature::ViewImage) { let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); registry.add(ViewImageHandler::new(ViewImageToolOptions { can_request_original_image_detail: can_request_original_image_detail( context.model_info, ), unified_image_budget: unified_image_budget_enabled( &turn_context.config.features, context.model_info, ), include_environment_id, })); } } #[instrument(level = "trace", skip_all)] fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) { let turn_context = context.turn_context; if collab_tools_enabled(turn_context, context.model_info) { if multi_agent_v2_enabled(turn_context) { let model_messages = ResolvedModelMessages::from_model(context.model_info); let spawn_agent_description = model_messages.multi_agent_tool_description_override("spawn_agent"); let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only { ToolExposure::DirectModelOnly } else { ToolExposure::Direct }; let tool_namespace = turn_context.config.multi_agent_v2.tool_namespace.as_deref(); let agent_type_description = agent_type_description(turn_context, context.default_agent_type_description); let hide_spawn_agent_metadata = turn_context.config.multi_agent_v2.hide_spawn_agent_metadata; registry.register_trusted_with_exposure( multi_agent_v2_handler( SpawnAgentHandlerV2::new( SpawnAgentToolOptions { available_models: turn_context.available_models.clone(), multi_agent_version: turn_context.multi_agent_version, model_catalog_in_context: turn_context .config .features .enabled(Feature::ModelCatalogInContext), agent_type_description, expose_agent_type: !turn_context.config.agent_roles.is_empty(), hide_agent_type_model_reasoning: hide_spawn_agent_metadata, expose_spawn_agent_model_overrides: turn_context .config .multi_agent_v2 .expose_spawn_agent_model_overrides, usage_hint_text: turn_context .config .multi_agent_v2 .usage_hint_text .clone(), }, spawn_agent_description.map(str::to_owned), ), tool_namespace, // Spawn composes the selected description with inheritance and usage guidance. /*description_override*/ None, model_messages.multi_agent_tool_parameters_override("spawn_agent"), ), exposure, ); if !turn_context.config.multi_agent_v2.disable_direct_message { registry.register_trusted_with_exposure( multi_agent_v2_handler( SendMessageHandlerV2, tool_namespace, model_messages.multi_agent_tool_description_override("send_message"), model_messages.multi_agent_tool_parameters_override("send_message"), ), exposure, ); registry.register_trusted_with_exposure( multi_agent_v2_handler( FollowupTaskHandlerV2, tool_namespace, model_messages.multi_agent_tool_description_override("followup_task"), model_messages.multi_agent_tool_parameters_override("followup_task"), ), exposure, ); } if turn_context.config.multi_agent_v2.wait_agent_enabled { registry.register_trusted_with_exposure( multi_agent_v2_handler( WaitAgentHandlerV2::new(context.wait_agent_timeouts), tool_namespace, model_messages.multi_agent_tool_description_override("wait_agent"), model_messages.multi_agent_tool_parameters_override("wait_agent"), ), exposure, ); } registry.register_trusted_with_exposure( multi_agent_v2_handler( InterruptAgentHandler, tool_namespace, model_messages.multi_agent_tool_description_override("interrupt_agent"), model_messages.multi_agent_tool_parameters_override("interrupt_agent"), ), exposure, ); registry.register_trusted_with_exposure( multi_agent_v2_handler( ListAgentsHandlerV2, tool_namespace, model_messages.multi_agent_tool_description_override("list_agents"), model_messages.multi_agent_tool_parameters_override("list_agents"), ), exposure, ); } else { let agent_type_description = agent_type_description(turn_context, context.default_agent_type_description); let exposure = if context.model_info.supports_search_tool { ToolExposure::Deferred } else { ToolExposure::Direct }; registry.add_with_exposure( SpawnAgentHandler::new(SpawnAgentToolOptions { available_models: turn_context.available_models.clone(), multi_agent_version: turn_context.multi_agent_version, model_catalog_in_context: turn_context .config .features .enabled(Feature::ModelCatalogInContext), agent_type_description, expose_agent_type: !turn_context.config.agent_roles.is_empty(), hide_agent_type_model_reasoning: false, expose_spawn_agent_model_overrides: true, usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(), }), exposure, ); registry.add_with_exposure(SendInputHandler, exposure); registry.add_with_exposure(ResumeAgentHandler, exposure); registry .add_with_exposure(WaitAgentHandler::new(context.wait_agent_timeouts), exposure); registry.add_with_exposure(CloseAgentHandler, exposure); } } } #[instrument(level = "trace", skip_all, fields(dynamic_tool_count = dynamic_tools.len()))] fn append_dynamic_tool_runtimes(dynamic_tools: &[DynamicToolSpec], registry: &mut ToolRegistry) { for spec in dynamic_tools { match spec { DynamicToolSpec::Function(tool) => { let Some(handler) = DynamicToolHandler::new(tool) else { tracing::error!( "Failed to convert dynamic tool {:?} to OpenAI tool", tool.name ); continue; }; registry.register_external(Arc::new(handler)); } DynamicToolSpec::Namespace(namespace) => { for tool in &namespace.tools { let DynamicToolNamespaceTool::Function(tool) = tool; let Some(handler) = DynamicToolHandler::new_in_namespace(namespace, tool) else { tracing::error!( "Failed to convert dynamic tool {:?}.{:?} to OpenAI tool", namespace.name, tool.name ); continue; }; registry.register_external(Arc::new(handler)); } } } } } #[instrument(level = "trace", skip_all)] fn append_tool_search_executor( turn_context: &TurnContext, registry: &mut ToolRegistry, tool_search_handler_cache: &ToolSearchHandlerCache, ) { let source_listing = if turn_context .config .features .enabled(Feature::DeferredToolWorldState) { ToolSearchSourceListing::Omit } else { ToolSearchSourceListing::Include }; let handler = tool_search_handler_cache.get_or_build(registry, source_listing); registry.register_trusted(handler); } fn append_extension_tool_executors( turn_context: &TurnContext, model_info: &ModelInfo, executors: impl IntoIterator ToolExecutor>>>, registry: &mut ToolRegistry, ) -> Option { let standalone_web_search_enabled = standalone_web_search_enabled(turn_context, model_info); let web_search_mode_on = turn_context.config.web_search_mode.value() != WebSearchMode::Disabled; let mut standalone_web_search_tool = None; for executor in executors { let tool_name = executor.tool_name(); let is_standalone_web_search = tool_name == ToolName::namespaced("web", "run"); if is_standalone_web_search && (!standalone_web_search_enabled || !web_search_mode_on) { continue; } if tool_name == ToolName::namespaced(IMAGE_GEN_NAMESPACE, IMAGEGEN_TOOL_NAME) && !image_generation_available(turn_context, model_info) { continue; } let runtime = Arc::new(ExtensionToolAdapter::new(executor)); if registry.register_external(runtime) && is_standalone_web_search { standalone_web_search_tool = Some(tool_name); } } standalone_web_search_tool } 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_tests.rs"] mod tests;