Files
codex/codex-rs/core/src/tools/spec_plan.rs
T
Steve Coffey f088e0c4ff Allow replacing dynamic tools between model steps (#57553)
## What changed

Add `CodexThread::replace_dynamic_tools` to replace the tool catalog without starting a turn. Updates apply to subsequent model steps and future turns while preserving in-flight calls; an empty list clears the catalog.

Require host opt-in through `allow_persistent_dynamic_tool_updates` for persistent threads. Persist replacements before publishing them, restore the latest catalog on resume, and preserve replacements and explicit clears through compaction. Keep explicit resume overrides separate from the persisted catalog.

## Testing

Add coverage for pending calls during replacement, replacement and clearing across restart, compaction recovery, resume overrides, submission ordering after caller cancellation, host opt-in, and persistence failures.

GitOrigin-RevId: fd2634906895a9b72ae7892ddd9435dced45eccb
2026-10-27 19:50:41 +00:00

1699 lines
65 KiB
Rust

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;
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::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<crate::WaitForEnvironmentToolConfig>>,
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<codex_mcp::McpBinding>,
apps_enabled: bool,
step_store: &ExtensionData,
tool_suggest_candidates: Option<&crate::tools::router::ToolSuggestCandidates>,
dynamic_tools: &[DynamicToolSpec],
) -> CodexResult<ToolRouter> {
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::<crate::WaitForEnvironmentToolConfig>();
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,
session.services.mcp_manager.shared_definitions(),
&mut registry,
);
apply_mcp_tool_exposure_policy(
turn_context,
model_info,
mcp,
&registered_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(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<ToolName>,
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::<ToolExposures>()
});
}
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<crate::WaitForEnvironmentToolConfig>>,
) -> 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<RegisteredTool>,
extension_tool_executors: impl IntoIterator<
Item = Arc<dyn for<'call> ToolExecutor<ExtensionToolCall<'call>>>,
>,
dynamic_tools: &[DynamicToolSpec],
) -> Vec<ToolSpec> {
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<Item = Arc<dyn for<'call> ToolExecutor<ExtensionToolCall<'call>>>> + '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<ToolSpec>,
tool_search_handler_cache: &ToolSearchHandlerCache,
) -> CodexResult<ToolRouter> {
let model_messages = ResolvedModelMessages::from_model(model_info);
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);
let search_exposure = if model_info.supports_search_tool
&& registry.tool_policy.allows(&tool_search_name)
&& registry.entries().any(|tool| {
tool.runtime.tool_name() != tool_search_name
&& tool.exposure.is_deferred()
&& tool.runtime.search_info().is_some()
}) {
Some(
if turn_context
.config
.features
.enabled(Feature::CodeModeToolSearch)
{
tool_exposure_with_namespace_override(
turn_context,
tool_search_name.clone(),
ToolExposure::Direct,
)
} else {
ToolExposure::Direct
},
)
} else {
None
};
if search_exposure.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::<Vec<_>>();
for tool_name in conflicting_tool_names {
if registry.remove(&tool_name).is_some() {
registry.record_collision(tool_name);
}
}
}
prepare_function_description_prefixes(
turn_context,
model_info,
&mut registry,
search_exposure,
)?;
if let Some(exposure) = search_exposure {
append_tool_search_executor(
turn_context,
&mut registry,
tool_search_handler_cache,
exposure,
);
}
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,
&registry,
&code_mode_tool_names,
hosted_specs,
);
let tool_namespaces_info = include_tool_namespaces_info
.then(|| {
collect_tool_namespaces_info(&registry, &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);
Ok(ToolRouter::from_parts(
registry,
model_visible_specs,
tool_mode,
code_mode_tool_names,
tool_namespaces_info,
&child_management_tools,
))
}
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() {
tool.exposure = tool_exposure_with_namespace_override(
turn_context,
tool.runtime.tool_name(),
tool.exposure,
);
}
}
fn tool_exposure_with_namespace_override(
turn_context: &TurnContext,
tool_name: ToolName,
exposure: ToolExposure,
) -> ToolExposure {
if exposure.is_available_in_code_mode()
&& tool_name
.with_default_namespace()
.namespace
.as_ref()
.is_some_and(|namespace| {
turn_context
.config
.code_mode
.direct_only_tool_namespaces
.contains(namespace)
})
{
ToolExposure::DirectModelOnly
} else {
exposure
}
}
/// 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<String, ToolName>,
hosted_specs: Vec<ToolSpec>,
) -> Vec<ToolSpec> {
let mut specs = Vec::new();
for tool in registry.entries() {
let exposure = tool.exposure;
if !is_directly_visible(turn_context, model_info, tool) {
continue;
}
let tool_name = tool.runtime.tool_name();
let spec = Arc::unwrap_or_clone(tool.spec());
specs.push(spec_for_model_request(
turn_context,
model_info,
exposure,
&tool_name,
code_mode_tool_names,
spec,
));
}
specs.extend(hosted_specs);
let mcp_namespaces = registry
.mcp_namespaces()
.map(|(_, namespace)| namespace)
.collect();
merge_into_namespaces(specs, &mcp_namespaces)
}
fn spec_for_model_request(
turn_context: &TurnContext,
model_info: &ModelInfo,
exposure: ToolExposure,
tool_name: &ToolName,
code_mode_tool_names: &BTreeMap<String, ToolName>,
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<ToolSpec> {
// 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.config.multi_agent_v2.disable_native_tools
&& (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<ToolName> {
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::<Vec<_>>();
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_directly_visible(
turn_context: &TurnContext,
model_info: &ModelInfo,
tool: &RegisteredTool,
) -> bool {
tool.exposure.is_direct()
&& !is_hidden_by_code_mode_only(
turn_context,
model_info,
&tool.runtime.tool_name(),
tool.exposure,
)
}
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 prepare_function_description_prefixes(
turn_context: &TurnContext,
model_info: &ModelInfo,
registry: &mut ToolRegistry,
search_exposure: Option<ToolExposure>,
) -> CodexResult<()> {
let model_messages = ResolvedModelMessages::from_model(model_info);
let prefixes = model_messages.functions_namespace_functions_description_prefixes();
let mut available_tools = Vec::new();
if prefixes.is_some_and(|prefixes| !prefixes.is_empty()) {
let code_mode_enabled = matches!(
effective_tool_mode(turn_context, model_info),
ToolMode::CodeMode | ToolMode::CodeModeOnly
);
let search_name = ToolName::plain(TOOL_SEARCH_TOOL_NAME);
let search_visible = search_exposure.is_some_and(|exposure| {
!is_hidden_by_code_mode_only(turn_context, model_info, &search_name, exposure)
|| (code_mode_enabled
&& registry
.tool_policy
.allows(&ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME))
&& turn_context
.config
.features
.enabled(Feature::CodeModeToolSearch)
&& exposure.is_available_in_code_mode()
&& !is_excluded_from_code_mode(turn_context, &search_name))
});
available_tools.extend(
registry
.entries()
.filter(|tool| {
is_directly_visible(turn_context, model_info, tool)
|| (search_visible
&& tool.exposure.is_deferred()
&& tool.runtime.search_info().is_some())
})
.map(|tool| tool.runtime.tool_name()),
);
available_tools.extend(
code_mode_tools(turn_context, model_info, registry)
.map(|(_, _, tool)| tool.runtime.tool_name()),
);
// These wrappers are generated after their inner descriptions are ready.
if code_mode_enabled {
available_tools.extend(
[
codex_code_mode::PUBLIC_TOOL_NAME,
codex_code_mode::WAIT_TOOL_NAME,
]
.into_iter()
.map(ToolName::plain)
.filter(|name| registry.tool_policy.allows(name)),
);
}
}
registry
.apply_functions_namespace_function_prefixes(prefixes, available_tools)
.map_err(|error| CodexErrorDetails::InvalidRequest(error.to_owned()).into())
}
/// Select the same nested tools for prefix budgeting and Code Mode construction.
/// A registered tool alone is not reachable when policy disallows the exec wrapper.
fn code_mode_tools<'a>(
turn_context: &'a TurnContext,
model_info: &'a ModelInfo,
registry: &'a ToolRegistry,
) -> impl Iterator<Item = (String, Arc<ToolSpec>, &'a RegisteredTool)> {
let tool_mode = effective_tool_mode(turn_context, model_info);
let enabled = matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly)
&& registry
.tool_policy
.allows(&ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME));
let mut seen = HashSet::new();
registry.entries().filter_map(move |tool| {
if !enabled || !tool.exposure.is_available_in_code_mode() {
return None;
}
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())
{
return None;
}
let spec = tool.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::ToolSearch { .. }
if turn_context
.config
.features
.enabled(Feature::CodeModeToolSearch) =>
{
codex_tools::code_mode_name_for_tool_name(&tool_name)
}
ToolSpec::Namespace(_) | ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => {
return None;
}
};
if !codex_code_mode::is_code_mode_nested_tool(&code_mode_name) {
return None;
}
let code_mode_name = codex_code_mode::normalize_code_mode_identifier(&code_mode_name);
if !seen.insert(code_mode_name.clone()) {
tracing::warn!(
tool_name = %tool_name,
code_mode_name = %code_mode_name,
"skipping tool with a duplicate normalized code-mode name"
);
return None;
}
Some((code_mode_name, spec, tool))
})
}
fn register_code_mode_executors(
turn_context: &TurnContext,
model_info: &ModelInfo,
registry: &mut ToolRegistry,
indirect_prefixes: &IndirectNamespacePrefixes<'_>,
) -> BTreeMap<String, ToolName> {
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 (name, spec, tool) in code_mode_tools(turn_context, model_info, registry) {
code_mode_tool_names.insert(name, tool.runtime.tool_name());
let exposure = tool.exposure;
let cached_runtime = tool.cached_runtime().cloned();
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 deferred_tool_discovery = if exec_prompt_tool_specs
.iter()
.any(|spec| matches!(spec, ToolSpec::ToolSearch { .. }))
{
codex_code_mode::DeferredToolDiscovery::RankedSearch
} else {
codex_code_mode::DeferredToolDiscovery::Catalog
};
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 exec_description = codex_code_mode::build_exec_tool_description(
&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
},
deferred_tool_discovery,
model_messages.code_mode(),
);
let execute_handler = CodeModeExecuteHandler::new(
create_code_mode_tool(exec_description),
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<ToolSpec>, mcp_namespaces: &HashSet<&str>) -> 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| {
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)
});
// Native tools resolve their own defaults, including intentional empty descriptions.
if namespace.description.trim().is_empty()
&& mcp_namespaces.contains(namespace.name.as_str())
{
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_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(environments: &TurnEnvironmentSnapshot) -> ToolEnvironmentMode {
// Keep environment selectors stable as selected attachments change readiness.
ToolEnvironmentMode::from_count(environments.environments.len())
}
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.environments);
if !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 {
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.environments);
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.config.experimental_request_user_input_enabled
&& !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 features.enabled(Feature::RequestPermissionsTool) {
registry.add(RequestPermissionsHandler);
}
if features.enabled(Feature::TokenBudget) {
let model_messages = ResolvedModelMessages::from_model(context.model_info);
let messages = model_messages.context_management();
registry.add_with_exposure(
NewContextWindowHandler::new(
messages.and_then(|messages| messages.new_context.as_ref()),
),
ToolExposure::DirectModelOnly,
);
registry.add(GetContextRemainingHandler::new(
messages.and_then(|messages| messages.get_context_remaining.as_ref()),
));
}
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 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 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,
exposure: ToolExposure,
) {
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_with_exposure(handler, exposure);
}
fn append_extension_tool_executors(
turn_context: &TurnContext,
model_info: &ModelInfo,
executors: impl IntoIterator<Item = Arc<dyn for<'call> ToolExecutor<ExtensionToolCall<'call>>>>,
registry: &mut ToolRegistry,
) -> Option<ToolName> {
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<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;