Codex Handbook
core/src/tools/spec_plan.rs 1045 lines
use crate::agent::exceeds_thread_spawn_depth_limit;use crate::agent::next_thread_spawn_depth;use crate::session::turn_context::TurnContext;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::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::McpHandler;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::RequestUserInputHandler;use crate::tools::handlers::ShellCommandHandler;use crate::tools::handlers::ShellCommandHandlerOptions;use crate::tools::handlers::SleepHandler;use crate::tools::handlers::TestSyncHandler;use crate::tools::handlers::ToolSearchHandlerCache;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::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::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_features::Feature;use codex_login::AuthManager;use codex_mcp::ToolInfo;use codex_protocol::config_types::WebSearchMode;use codex_protocol::dynamic_tools::DynamicToolNamespaceTool;use codex_protocol::dynamic_tools::DynamicToolSpec;use codex_protocol::openai_models::ConfigShellToolType;use codex_protocol::openai_models::InputModality;use codex_protocol::openai_models::ToolMode;use codex_protocol::protocol::MultiAgentVersion;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::SubAgentSource;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::ToolSearchInfo;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 codex_tools::shell_command_backend_for_features;use codex_tools::shell_type_for_model_and_features;use std::collections::BTreeMap;use std::collections::HashSet;use std::sync::Arc;use tracing::instrument;use tracing::warn;const MULTI_AGENT_V2_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";const IMAGE_GEN_NAMESPACE: &str = "image_gen";const IMAGEGEN_TOOL_NAME: &str = "imagegen";type PlannedRuntime = Arc<dyn CoreToolRuntime>;#[derive(Default)]struct PlannedTools {    runtimes: Vec<PlannedRuntime>,    hosted_specs: Vec<ToolSpec>,}impl PlannedTools {    fn add<T>(&mut self, handler: T)    where        T: CoreToolRuntime + 'static,    {        self.runtimes.push(Arc::new(handler));    }    fn add_arc(&mut self, handler: PlannedRuntime) {        self.runtimes.push(handler);    }    fn add_with_exposure<T>(&mut self, handler: T, exposure: ToolExposure)    where        T: CoreToolRuntime + 'static,    {        self.runtimes            .push(override_tool_exposure(Arc::new(handler), exposure));    }    fn add_dispatch_only<T>(&mut self, handler: T)    where        T: CoreToolRuntime + 'static,    {        self.add_with_exposure(handler, ToolExposure::Hidden);    }    fn add_hosted_spec(&mut self, spec: ToolSpec) {        self.hosted_specs.push(spec);    }    fn runtimes(&self) -> &[PlannedRuntime] {        &self.runtimes    }}#[derive(Clone, Copy)]struct CoreToolPlanContext<'a> {    turn_context: &'a TurnContext,    mcp_tools: Option<&'a [ToolInfo]>,    deferred_mcp_tools: Option<&'a [ToolInfo]>,    discoverable_tools: Option<&'a [DiscoverableTool]>,    extension_tool_executors: &'a [Arc<dyn ToolExecutor<ExtensionToolCall>>],    dynamic_tools: &'a [DynamicToolSpec],    tool_search_handler_cache: &'a ToolSearchHandlerCache,    default_agent_type_description: &'a str,    wait_agent_timeouts: WaitAgentTimeoutOptions,}#[instrument(level = "trace", skip_all)]pub(crate) fn build_tool_router(    turn_context: &TurnContext,    params: ToolRouterParams<'_>,    tool_search_handler_cache: &ToolSearchHandlerCache,) -> ToolRouter {    let (model_visible_specs, registry) =        build_tool_specs_and_registry(turn_context, params, tool_search_handler_cache);    ToolRouter::from_parts(registry, model_visible_specs)}#[instrument(level = "trace", skip_all)]fn build_tool_specs_and_registry(    turn_context: &TurnContext,    params: ToolRouterParams<'_>,    tool_search_handler_cache: &ToolSearchHandlerCache,) -> (Vec<ToolSpec>, 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 context = CoreToolPlanContext {        turn_context,        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,        tool_search_handler_cache,        default_agent_type_description: &default_agent_type_description,        wait_agent_timeouts: wait_agent_timeout_options(turn_context),    };    let mut planned_tools = PlannedTools::default();    add_tool_sources(&context, &mut planned_tools);    append_tool_search_executor(&context, &mut planned_tools);    prepend_code_mode_executors(&context, &mut planned_tools);    build_model_visible_specs_and_registry(turn_context, planned_tools)}#[instrument(level = "trace", skip_all)]fn build_model_visible_specs_and_registry(    turn_context: &TurnContext,    planned_tools: PlannedTools,) -> (Vec<ToolSpec>, ToolRegistry) {    let PlannedTools {        runtimes,        hosted_specs,    } = planned_tools;    let mut specs = Vec::new();    let mut seen_tool_names = HashSet::new();    for runtime in &runtimes {        let tool_name = runtime.tool_name();        if !seen_tool_names.insert(tool_name.clone()) {            continue;        }        let exposure = runtime.exposure();        if exposure.is_direct() && !is_hidden_by_code_mode_only(turn_context, &tool_name, exposure)        {            let spec = runtime.spec();            specs.push(spec_for_model_request(                turn_context,                exposure,                &tool_name,                spec,            ));        }    }    specs.extend(hosted_specs);    let registry = ToolRegistry::from_tools(runtimes);    let model_visible_specs = merge_into_namespaces(specs)        .into_iter()        .filter(|spec| {            namespace_tools_enabled(turn_context) || !matches!(spec, ToolSpec::Namespace(_))        })        .collect();    (model_visible_specs, registry)}fn spec_for_model_request(    turn_context: &TurnContext,    exposure: ToolExposure,    tool_name: &ToolName,    spec: ToolSpec,) -> ToolSpec {    if matches!(        turn_context.tool_mode,        ToolMode::CodeMode | ToolMode::CodeModeOnly    ) && exposure != ToolExposure::DirectModelOnly        && !is_excluded_from_code_mode(turn_context, tool_name)        && codex_code_mode::is_code_mode_nested_tool(spec.name())    {        codex_tools::augment_tool_spec_for_code_mode(spec)    } else {        spec    }}fn hosted_model_tool_specs(context: &CoreToolPlanContext<'_>) -> Vec<ToolSpec> {    let turn_context = context.turn_context;    // Responses Lite accepts schemas for client-executed tools, not hosted Responses tools.    if turn_context.model_info.use_responses_lite {        return Vec::new();    }    let mut specs = Vec::new();    let standalone_web_search_available = standalone_web_search_enabled(turn_context)        && context            .extension_tool_executors            .iter()            .any(|executor| executor.tool_name() == 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: turn_context.model_info.web_search_tool_type,    }) {        specs.push(hosted_web_search_tool);    }    // TODO: Remove hosted image generation once the standalone extension is ready.    if image_generation_tool_enabled(turn_context)        && !standalone_image_generation_available(turn_context, context.extension_tool_executors)    {        specs.push(create_image_generation_tool("png"));    }    specs}pub(crate) fn search_tool_enabled(turn_context: &TurnContext) -> bool {    turn_context.model_info.supports_search_tool}pub(crate) fn tool_suggest_enabled(turn_context: &TurnContext) -> bool {    let features = turn_context.features.get();    features.enabled(Feature::ToolSuggest)        && features.enabled(Feature::Apps)        && features.enabled(Feature::Plugins)}fn namespace_tools_enabled(turn_context: &TurnContext) -> bool {    turn_context.provider.capabilities().namespace_tools}fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool {    turn_context.multi_agent_version == MultiAgentVersion::V2}fn collab_tools_enabled(turn_context: &TurnContext) -> 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 => true,    }}fn agent_jobs_tools_enabled(turn_context: &TurnContext) -> bool {    turn_context.features.get().enabled(Feature::SpawnCsv) && collab_tools_enabled(turn_context)}fn agent_jobs_worker_tools_enabled(turn_context: &TurnContext) -> bool {    agent_jobs_tools_enabled(turn_context)        && matches!(            &turn_context.session_source,            SessionSource::SubAgent(SubAgentSource::Other(label))                if label.starts_with("agent_job:")        )}fn image_generation_tool_enabled(turn_context: &TurnContext) -> bool {    image_generation_runtime_enabled(turn_context)        && turn_context            .features            .get()            .enabled(Feature::ImageGeneration)}fn image_generation_runtime_enabled(turn_context: &TurnContext) -> bool {    turn_context        .auth_manager        .as_deref()        .is_some_and(AuthManager::current_auth_uses_codex_backend)        && turn_context.provider.capabilities().image_generation        && turn_context            .model_info            .input_modalities            .contains(&InputModality::Image)}fn standalone_image_generation_model_visible(turn_context: &TurnContext) -> bool {    if !image_generation_runtime_enabled(turn_context) || !namespace_tools_enabled(turn_context) {        return false;    }    if turn_context.model_info.use_responses_lite {        return true;    }    turn_context.features.get().enabled(Feature::ImageGenExt)}fn standalone_image_generation_available(    turn_context: &TurnContext,    extension_tools: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],) -> bool {    standalone_image_generation_model_visible(turn_context)        && extension_tools.iter().any(|executor| {            executor.tool_name() == ToolName::namespaced(IMAGE_GEN_NAMESPACE, IMAGEGEN_TOOL_NAME)        })}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,    tool_name: &ToolName,    exposure: ToolExposure,) -> bool {    turn_context.tool_mode == ToolMode::CodeModeOnly        && exposure != ToolExposure::DirectModelOnly        && 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 {    tool_name.namespace.as_ref().is_some_and(|namespace| {        turn_context            .config            .code_mode            .excluded_tool_namespaces            .contains(namespace)    })}fn build_code_mode_executors(    turn_context: &TurnContext,    executors: &[Arc<dyn CoreToolRuntime>],) -> Vec<Arc<dyn CoreToolRuntime>> {    if !matches!(        turn_context.tool_mode,        ToolMode::CodeMode | ToolMode::CodeModeOnly    ) {        return vec![];    }    let mut code_mode_nested_tool_specs = Vec::new();    let mut exec_prompt_tool_specs = Vec::new();    let mut deferred_tools_available = false;    let deferred_tools_guidance_enabled = search_tool_enabled(turn_context);    for executor in executors {        let exposure = executor.exposure();        if exposure == ToolExposure::DirectModelOnly {            continue;        }        if exposure == ToolExposure::Hidden {            continue;        }        if is_excluded_from_code_mode(turn_context, &executor.tool_name()) {            continue;        }        let spec = executor.spec();        if exposure == ToolExposure::Deferred {            // Only show deferred-tool guidance when supported and an included spec is usable by code mode.            deferred_tools_available |= deferred_tools_guidance_enabled                && !collect_code_mode_exec_prompt_tool_definitions(std::iter::once(&spec))                    .is_empty();        } else {            exec_prompt_tool_specs.push(spec.clone());        }        code_mode_nested_tool_specs.push(spec);    }    let namespace_descriptions = code_mode_namespace_descriptions(&exec_prompt_tool_specs);    let mut enabled_tools =        collect_code_mode_exec_prompt_tool_definitions(exec_prompt_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,                turn_context.tool_mode == ToolMode::CodeModeOnly,                deferred_tools_available,            ),            code_mode_nested_tool_specs,        )),        Arc::new(CodeModeWaitHandler),    ]}fn merge_into_namespaces(specs: Vec<ToolSpec>) -> Vec<ToolSpec> {    let mut merged_specs = Vec::with_capacity(specs.len());    let mut namespace_indices = BTreeMap::<String, usize>::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<String, codex_code_mode::ToolNamespaceDescription> {    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_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {    add_shell_tools(context, planned_tools);    add_mcp_resource_tools(context, planned_tools);    add_core_utility_tools(context, planned_tools);    add_collaboration_tools(context, planned_tools);    add_mcp_runtime_tools(context, planned_tools);    add_extension_tools(context, planned_tools);    add_dynamic_tools(context, planned_tools);    for spec in hosted_model_tool_specs(context) {        planned_tools.add_hosted_spec(spec);    }}fn standalone_web_search_enabled(turn_context: &TurnContext) -> bool {    namespace_tools_enabled(turn_context)        && (turn_context.model_info.use_responses_lite            || turn_context                .features                .get()                .enabled(Feature::StandaloneWebSearch))}fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {    let turn_context = context.turn_context;    let features = turn_context.features.get();    let environment_mode = turn_context.tool_environment_mode();    if !environment_mode.has_environment() {        return;    }    let allow_login_shell = turn_context.config.permissions.allow_login_shell;    let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals);    let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);    let shell_command_options = ShellCommandHandlerOptions {        backend_config: shell_command_backend_for_features(features),        allow_login_shell,        exec_permission_approvals_enabled,    };    match shell_type_for_model_and_features(&turn_context.model_info, features) {        ConfigShellToolType::UnifiedExec => {            planned_tools.add(ExecCommandHandler::new(ExecCommandHandlerOptions {                allow_login_shell,                exec_permission_approvals_enabled,                include_environment_id,                include_shell_parameter: unified_exec_should_include_shell_parameter(turn_context),            }));            planned_tools.add(WriteStdinHandler);            // Keep the legacy shell tool registered while unified exec is            // model-visible.            planned_tools.add_dispatch_only(ShellCommandHandler::new(shell_command_options));        }        ConfigShellToolType::Disabled => {}        ConfigShellToolType::Default        | ConfigShellToolType::Local        | ConfigShellToolType::ShellCommand => {            planned_tools.add(ShellCommandHandler::new(shell_command_options));        }    }}fn unified_exec_should_include_shell_parameter(turn_context: &TurnContext) -> bool {    !matches!(        &turn_context.unified_exec_shell_mode,        UnifiedExecShellMode::ZshFork(_)    ) || turn_context        .environments        .turn_environments        .iter()        .any(|environment| environment.environment.is_remote())}fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {    if context.mcp_tools.is_some() {        planned_tools.add(ListMcpResourcesHandler);        planned_tools.add(ListMcpResourceTemplatesHandler);        planned_tools.add(ReadMcpResourceHandler);    }}fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {    let turn_context = context.turn_context;    let features = turn_context.features.get();    let environment_mode = turn_context.tool_environment_mode();    planned_tools.add(PlanHandler);    if turn_context.config.experimental_request_user_input_enabled {        planned_tools.add_with_exposure(            RequestUserInputHandler {                available_modes: request_user_input_available_modes(features),            },            ToolExposure::DirectModelOnly,        );    }    if features.enabled(Feature::RequestPermissionsTool) {        planned_tools.add(RequestPermissionsHandler);    }    if features.enabled(Feature::TokenBudget) {        planned_tools.add_with_exposure(NewContextWindowHandler, ToolExposure::DirectModelOnly);        planned_tools.add(GetContextRemainingHandler);    }    if features.enabled(Feature::SleepTool) {        planned_tools.add(SleepHandler);    }    if tool_suggest_enabled(turn_context)        && let Some(discoverable_tools) =            context.discoverable_tools.filter(|tools| !tools.is_empty())    {        planned_tools.add(ListAvailablePluginsToInstallHandler::new(            collect_request_plugin_install_entries(discoverable_tools),        ));        planned_tools.add(RequestPluginInstallHandler::new(            discoverable_tools.to_vec(),        ));    }    if environment_mode.has_environment() && turn_context.model_info.apply_patch_tool_type.is_some()    {        let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);        planned_tools.add(ApplyPatchHandler::new(include_environment_id));    }    if turn_context        .model_info        .experimental_supported_tools        .iter()        .any(|tool| tool == "test_sync_tool")    {        planned_tools.add(TestSyncHandler);    }    if environment_mode.has_environment() {        let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);        planned_tools.add(ViewImageHandler::new(ViewImageToolOptions {            can_request_original_image_detail: can_request_original_image_detail(                &turn_context.model_info,            ),            include_environment_id,        }));    }}fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {    let turn_context = context.turn_context;    if collab_tools_enabled(turn_context) {        if multi_agent_v2_enabled(turn_context) {            let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only {                ToolExposure::DirectModelOnly            } else {                ToolExposure::Direct            };            let tool_namespace = namespace_tools_enabled(turn_context)                .then_some(turn_context.config.multi_agent_v2.tool_namespace.as_deref())                .flatten();            let agent_type_description =                agent_type_description(turn_context, context.default_agent_type_description);            planned_tools.add_arc(override_tool_exposure(                multi_agent_v2_handler(                    SpawnAgentHandlerV2::new(SpawnAgentToolOptions {                        available_models: turn_context.available_models.clone(),                        agent_type_description,                        hide_agent_type_model_reasoning: turn_context                            .config                            .multi_agent_v2                            .hide_spawn_agent_metadata,                        include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,                        usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),                    }),                    tool_namespace,                ),                exposure,            ));            planned_tools.add_arc(override_tool_exposure(                multi_agent_v2_handler(SendMessageHandlerV2, tool_namespace),                exposure,            ));            planned_tools.add_arc(override_tool_exposure(                multi_agent_v2_handler(FollowupTaskHandlerV2, tool_namespace),                exposure,            ));            planned_tools.add_arc(override_tool_exposure(                multi_agent_v2_handler(                    WaitAgentHandlerV2::new(context.wait_agent_timeouts),                    tool_namespace,                ),                exposure,            ));            planned_tools.add_arc(override_tool_exposure(                multi_agent_v2_handler(InterruptAgentHandler, tool_namespace),                exposure,            ));            planned_tools.add_arc(override_tool_exposure(                multi_agent_v2_handler(ListAgentsHandlerV2, tool_namespace),                exposure,            ));        } else {            let agent_type_description =                agent_type_description(turn_context, context.default_agent_type_description);            let exposure =                if search_tool_enabled(turn_context) && namespace_tools_enabled(turn_context) {                    ToolExposure::Deferred                } else {                    ToolExposure::Direct                };            planned_tools.add_with_exposure(                SpawnAgentHandler::new(SpawnAgentToolOptions {                    available_models: turn_context.available_models.clone(),                    agent_type_description,                    hide_agent_type_model_reasoning: false,                    include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,                    usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),                }),                exposure,            );            planned_tools.add_with_exposure(SendInputHandler, exposure);            planned_tools.add_with_exposure(ResumeAgentHandler, exposure);            planned_tools                .add_with_exposure(WaitAgentHandler::new(context.wait_agent_timeouts), exposure);            planned_tools.add_with_exposure(CloseAgentHandler, exposure);        }    }    if agent_jobs_tools_enabled(turn_context) {        planned_tools.add(SpawnAgentsOnCsvHandler);        if agent_jobs_worker_tools_enabled(turn_context) {            planned_tools.add(ReportAgentJobResultHandler);        }    }}fn add_mcp_runtime_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {    if let Some(mcp_tools) = context.mcp_tools {        for tool in mcp_tools {            match McpHandler::new(tool.clone()) {                Ok(handler) => planned_tools.add(handler),                Err(err) => warn!(                    "Skipping MCP tool `{}`: failed to build tool spec: {err}",                    tool.canonical_tool_name()                ),            }        }    }    if let Some(deferred_mcp_tools) = context.deferred_mcp_tools {        for tool in deferred_mcp_tools {            match McpHandler::new(tool.clone()) {                Ok(handler) => planned_tools.add_with_exposure(handler, ToolExposure::Deferred),                Err(err) => warn!(                    "Skipping deferred MCP tool `{}`: failed to build tool spec: {err}",                    tool.canonical_tool_name()                ),            }        }    }}fn add_dynamic_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {    for spec in context.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;                };                planned_tools.add(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;                    };                    planned_tools.add(handler);                }            }        }    }}fn add_extension_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {    // Extension ToolContributor implementations are resolved into executors    // before planning. Core only adapts those executors into its runtime set.    append_extension_tool_executors(        context.turn_context,        context.extension_tool_executors,        planned_tools,    );}#[instrument(level = "trace", skip_all)]fn append_tool_search_executor(    context: &CoreToolPlanContext<'_>,    planned_tools: &mut PlannedTools,) {    let turn_context = context.turn_context;    if !(search_tool_enabled(turn_context) && namespace_tools_enabled(turn_context)) {        return;    }    let search_infos = planned_tools        .runtimes()        .iter()        .filter(|executor| executor.exposure() == ToolExposure::Deferred)        .filter_map(|executor| executor.search_info())        .collect::<Vec<_>>();    if search_infos.is_empty() {        return;    }    let handler: PlannedRuntime = context.tool_search_handler_cache.get_or_build(search_infos);    planned_tools.add_arc(handler);}fn prepend_code_mode_executors(    context: &CoreToolPlanContext<'_>,    planned_tools: &mut PlannedTools,) {    let turn_context = context.turn_context;    let code_mode_executors = build_code_mode_executors(turn_context, planned_tools.runtimes());    planned_tools.runtimes.splice(0..0, code_mode_executors);}fn append_extension_tool_executors(    turn_context: &TurnContext,    executors: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],    planned_tools: &mut PlannedTools,) {    if executors.is_empty() {        return;    }    let mut reserved_tool_names = planned_tools        .runtimes()        .iter()        .map(|executor| executor.tool_name())        .collect::<HashSet<_>>();    if matches!(        turn_context.tool_mode,        ToolMode::CodeMode | ToolMode::CodeModeOnly    ) {        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 search_tool_enabled(turn_context)        && namespace_tools_enabled(turn_context)        && planned_tools            .runtimes()            .iter()            .any(|executor| executor.exposure() == ToolExposure::Deferred)    {        reserved_tool_names.insert(ToolName::plain(TOOL_SEARCH_TOOL_NAME));    }    let standalone_web_search_enabled = standalone_web_search_enabled(turn_context);    let web_search_mode_on = turn_context.config.web_search_mode.value() != WebSearchMode::Disabled;    for executor in executors.iter().cloned() {        let tool_name = executor.tool_name();        if tool_name == ToolName::namespaced("web", "run")            && (!standalone_web_search_enabled || !web_search_mode_on)        {            continue;        }        if tool_name == ToolName::namespaced(IMAGE_GEN_NAMESPACE, IMAGEGEN_TOOL_NAME)            && !standalone_image_generation_model_visible(turn_context)        {            continue;        }        if !reserved_tool_names.insert(tool_name.clone()) {            warn!("Skipping extension tool `{tool_name}`: tool already registered");            continue;        }        planned_tools.add(ExtensionToolAdapter::new(executor));    }}fn multi_agent_v2_handler(    handler: impl CoreToolRuntime + 'static,    namespace: Option<&str>,) -> Arc<dyn CoreToolRuntime> {    match namespace {        Some(namespace) => Arc::new(MultiAgentV2NamespaceOverride {            handler: Arc::new(handler),            namespace: namespace.to_string(),        }),        None => Arc::new(handler),    }}struct MultiAgentV2NamespaceOverride {    handler: Arc<dyn CoreToolRuntime>,    namespace: String,}impl ToolExecutor<ToolInvocation> for MultiAgentV2NamespaceOverride {    fn tool_name(&self) -> ToolName {        ToolName::namespaced(self.namespace.clone(), self.handler.tool_name().name)    }    fn spec(&self) -> ToolSpec {        match self.handler.spec() {            ToolSpec::Function(tool) => ToolSpec::Namespace(ResponsesApiNamespace {                name: self.namespace.clone(),                description: MULTI_AGENT_V2_NAMESPACE_DESCRIPTION.to_string(),                tools: vec![ResponsesApiNamespaceTool::Function(tool)],            }),            spec => spec,        }    }    fn exposure(&self) -> ToolExposure {        self.handler.exposure()    }    fn supports_parallel_tool_calls(&self) -> bool {        self.handler.supports_parallel_tool_calls()    }    fn search_info(&self) -> Option<ToolSearchInfo> {        self.handler.search_info()    }    fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {        self.handler.handle(invocation)    }}impl CoreToolRuntime for MultiAgentV2NamespaceOverride {    fn matches_kind(&self, payload: &crate::tools::context::ToolPayload) -> bool {        self.handler.matches_kind(payload)    }    fn create_diff_consumer(        &self,    ) -> Option<Box<dyn crate::tools::registry::ToolArgumentDiffConsumer>> {        self.handler.create_diff_consumer()    }}fn compare_code_mode_tools(    left: &codex_code_mode::ToolDefinition,    right: &codex_code_mode::ToolDefinition,    namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,) -> 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<String, codex_code_mode::ToolNamespaceDescription>,) -> 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;