mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
## Summary This refactor makes tool handlers the owner of the specs they can publish, so registry construction can register handlers once and separately publish only the specs that should be model-visible. The main motivation is deferred tools: MCP and dynamic tools still need handlers registered up front, but deferred tools should be discoverable through `tool_search` rather than emitted in the initial tool spec list. ## What changed - `McpHandler` and `DynamicToolHandler` can return their own `ToolSpec`. - `build_tool_registry_builder` now collects handlers, registers them through the no-spec path, and publishes only non-deferred handler specs. - Deferred MCP and dynamic tool names are combined into one `all_deferred_tools` set that drives spec filtering, code-mode deferred-tool signaling, and `tool_search` registration. - `tool_search` registration now requires both deferred tools and `namespace_tools`. - Namespace specs are merged in `spec_plan`, preserving top-level spec order, sorting tools within each namespace, and backfilling empty namespace descriptions. - Hosted web search and image-generation specs are included in the collected spec vector before namespace merge/publication, and tool-name tests that should not care about hosted relative order now compare sets. ## Testing - `cargo test -p codex-core tools::spec::tests:: -- --nocapture` - `cargo test -p codex-core tools::spec_plan::tests:: -- --nocapture` - `cargo test -p codex-core tools::router::tests::specs_filter_deferred_dynamic_tools -- --nocapture` - `cargo test -p codex-core suite::prompt_caching::prompt_tools_are_consistent_across_requests -- --nocapture` - `just fmt` - `just fix -p codex-core` - `cargo test -p codex-core -- --skip tools::handlers::multi_agents::tests::tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed` passed the library suite after skipping the known stack-overflowing unit test. Full `cargo test -p codex-core` currently hits a stack overflow in `tools::handlers::multi_agents::tests::tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed`; the same focused test reproduces on `origin/main`.
474 lines
19 KiB
Rust
474 lines
19 KiB
Rust
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
|
|
use crate::tools::handlers::ApplyPatchHandler;
|
|
use crate::tools::handlers::CodeModeExecuteHandler;
|
|
use crate::tools::handlers::CodeModeWaitHandler;
|
|
use crate::tools::handlers::ContainerExecHandler;
|
|
use crate::tools::handlers::CreateGoalHandler;
|
|
use crate::tools::handlers::DynamicToolHandler;
|
|
use crate::tools::handlers::ExecCommandHandler;
|
|
use crate::tools::handlers::ExecCommandHandlerOptions;
|
|
use crate::tools::handlers::GetGoalHandler;
|
|
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
|
|
use crate::tools::handlers::ListMcpResourcesHandler;
|
|
use crate::tools::handlers::LocalShellHandler;
|
|
use crate::tools::handlers::McpHandler;
|
|
use crate::tools::handlers::PlanHandler;
|
|
use crate::tools::handlers::ReadMcpResourceHandler;
|
|
use crate::tools::handlers::RequestPermissionsHandler;
|
|
use crate::tools::handlers::RequestPluginInstallHandler;
|
|
use crate::tools::handlers::RequestUserInputHandler;
|
|
use crate::tools::handlers::ShellCommandHandler;
|
|
use crate::tools::handlers::ShellCommandHandlerOptions;
|
|
use crate::tools::handlers::ShellHandler;
|
|
use crate::tools::handlers::TestSyncHandler;
|
|
use crate::tools::handlers::ToolSearchHandler;
|
|
use crate::tools::handlers::UpdateGoalHandler;
|
|
use crate::tools::handlers::ViewImageHandler;
|
|
use crate::tools::handlers::WriteStdinHandler;
|
|
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
|
|
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
|
|
use crate::tools::handlers::extension_tools::extension_tool_spec;
|
|
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_spec::SpawnAgentToolOptions;
|
|
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
|
|
use crate::tools::handlers::shell_spec::ShellToolOptions;
|
|
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::AnyToolHandler;
|
|
use crate::tools::registry::ToolRegistryBuilder;
|
|
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
|
|
use crate::tools::spec_plan_types::agent_type_description;
|
|
use codex_protocol::openai_models::ConfigShellToolType;
|
|
use codex_tools::ResponsesApiNamespaceTool;
|
|
use codex_tools::ToolEnvironmentMode;
|
|
use codex_tools::ToolName;
|
|
use codex_tools::ToolSearchSource;
|
|
use codex_tools::ToolSearchSourceInfo;
|
|
use codex_tools::ToolSpec;
|
|
use codex_tools::ToolsConfig;
|
|
use codex_tools::collect_code_mode_exec_prompt_tool_definitions;
|
|
use codex_tools::collect_tool_search_source_infos;
|
|
use codex_tools::default_namespace_description;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
|
|
pub fn build_tool_registry_builder(
|
|
config: &ToolsConfig,
|
|
params: ToolRegistryBuildParams<'_>,
|
|
) -> ToolRegistryBuilder {
|
|
let mut builder = ToolRegistryBuilder::new();
|
|
let all_deferred_tools = params
|
|
.deferred_mcp_tools
|
|
.into_iter()
|
|
.flatten()
|
|
.map(codex_mcp::ToolInfo::canonical_tool_name)
|
|
.chain(
|
|
params
|
|
.dynamic_tools
|
|
.iter()
|
|
.filter(|tool| tool.defer_loading)
|
|
.map(|tool| ToolName::new(tool.namespace.clone(), tool.name.clone())),
|
|
)
|
|
.collect::<HashSet<_>>();
|
|
let handlers = collect_handler_tools(config, params);
|
|
|
|
if config.code_mode_enabled {
|
|
let namespace_descriptions = params
|
|
.tool_namespaces
|
|
.into_iter()
|
|
.flatten()
|
|
.map(|(namespace, detail)| {
|
|
(
|
|
namespace.clone(),
|
|
codex_code_mode::ToolNamespaceDescription {
|
|
name: detail.name.clone(),
|
|
description: detail.description.clone().unwrap_or_default(),
|
|
},
|
|
)
|
|
})
|
|
.collect::<BTreeMap<_, _>>();
|
|
let mut code_mode_nested_tool_specs = handlers
|
|
.iter()
|
|
.filter_map(|handler| handler.spec())
|
|
.collect::<Vec<_>>();
|
|
code_mode_nested_tool_specs.extend(
|
|
params
|
|
.extension_tool_bundles
|
|
.iter()
|
|
.filter_map(|bundle| extension_tool_spec(bundle.spec()).ok()),
|
|
);
|
|
let mut enabled_tools =
|
|
collect_code_mode_exec_prompt_tool_definitions(code_mode_nested_tool_specs.iter());
|
|
enabled_tools
|
|
.sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions));
|
|
builder.register_handler(Arc::new(CodeModeExecuteHandler::new(
|
|
create_code_mode_tool(
|
|
&enabled_tools,
|
|
&namespace_descriptions,
|
|
config.code_mode_only_enabled,
|
|
config.search_tool && !all_deferred_tools.is_empty(),
|
|
),
|
|
code_mode_nested_tool_specs,
|
|
)));
|
|
builder.register_handler(Arc::new(CodeModeWaitHandler));
|
|
}
|
|
|
|
let mut non_deferred_specs = Vec::new();
|
|
for handler in &handlers {
|
|
let tool_name = handler.tool_name();
|
|
if !all_deferred_tools.contains(&tool_name)
|
|
&& let Some(spec) = handler.spec()
|
|
{
|
|
non_deferred_specs.push(spec);
|
|
}
|
|
}
|
|
|
|
if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions {
|
|
web_search_mode: config.web_search_mode,
|
|
web_search_config: config.web_search_config.as_ref(),
|
|
web_search_tool_type: config.web_search_tool_type,
|
|
}) {
|
|
non_deferred_specs.push(web_search_tool);
|
|
}
|
|
if config.image_gen_tool {
|
|
non_deferred_specs.push(create_image_generation_tool("png"));
|
|
}
|
|
|
|
for spec in merge_into_namespaces(non_deferred_specs) {
|
|
if !config.namespace_tools && matches!(spec, ToolSpec::Namespace(_)) {
|
|
continue;
|
|
}
|
|
let spec = if config.code_mode_enabled {
|
|
codex_tools::augment_tool_spec_for_code_mode(spec)
|
|
} else {
|
|
spec
|
|
};
|
|
builder.push_spec(spec);
|
|
}
|
|
|
|
for handler in handlers {
|
|
builder.register_any_handler_without_spec(handler);
|
|
}
|
|
|
|
if config.search_tool && config.namespace_tools && !all_deferred_tools.is_empty() {
|
|
let mut search_source_infos = params
|
|
.deferred_mcp_tools
|
|
.map(|mcp_tools| {
|
|
collect_tool_search_source_infos(mcp_tools.iter().map(|tool| ToolSearchSource {
|
|
server_name: tool.server_name.as_str(),
|
|
connector_name: tool.connector_name.as_deref(),
|
|
description: tool.namespace_description.as_deref(),
|
|
}))
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
if params.dynamic_tools.iter().any(|tool| {
|
|
all_deferred_tools.contains(&ToolName::new(tool.namespace.clone(), tool.name.clone()))
|
|
}) {
|
|
search_source_infos.push(ToolSearchSourceInfo {
|
|
name: "Dynamic tools".to_string(),
|
|
description: Some("Tools provided by the current Codex thread.".to_string()),
|
|
});
|
|
}
|
|
|
|
builder.register_handler(Arc::new(ToolSearchHandler::new(
|
|
params.tool_search_entries.to_vec(),
|
|
search_source_infos,
|
|
)));
|
|
}
|
|
|
|
for bundle in params.extension_tool_bundles.iter().cloned() {
|
|
builder.register_tool_bundle(bundle);
|
|
}
|
|
|
|
builder
|
|
}
|
|
|
|
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 collect_handler_tools(
|
|
config: &ToolsConfig,
|
|
params: ToolRegistryBuildParams<'_>,
|
|
) -> Vec<Arc<dyn AnyToolHandler>> {
|
|
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
|
|
let mut handlers = Vec::<Arc<dyn AnyToolHandler>>::new();
|
|
|
|
if config.environment_mode.has_environment() {
|
|
let include_environment_id =
|
|
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
|
|
match &config.shell_type {
|
|
ConfigShellToolType::Default => {
|
|
handlers.push(Arc::new(ShellHandler::new(ShellToolOptions {
|
|
exec_permission_approvals_enabled,
|
|
})));
|
|
}
|
|
ConfigShellToolType::Local => {
|
|
handlers.push(Arc::new(LocalShellHandler::new()));
|
|
}
|
|
ConfigShellToolType::UnifiedExec => {
|
|
handlers.push(Arc::new(ExecCommandHandler::new(
|
|
ExecCommandHandlerOptions {
|
|
allow_login_shell: config.allow_login_shell,
|
|
exec_permission_approvals_enabled,
|
|
include_environment_id,
|
|
},
|
|
)));
|
|
handlers.push(Arc::new(WriteStdinHandler));
|
|
}
|
|
ConfigShellToolType::Disabled => {}
|
|
ConfigShellToolType::ShellCommand => {
|
|
handlers.push(Arc::new(ShellCommandHandler::new(
|
|
ShellCommandHandlerOptions {
|
|
backend_config: config.shell_command_backend,
|
|
allow_login_shell: config.allow_login_shell,
|
|
exec_permission_approvals_enabled,
|
|
},
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
if config.environment_mode.has_environment()
|
|
&& config.shell_type != ConfigShellToolType::Disabled
|
|
{
|
|
match &config.shell_type {
|
|
ConfigShellToolType::Default => {
|
|
handlers.push(Arc::new(ContainerExecHandler));
|
|
handlers.push(Arc::new(LocalShellHandler::default()));
|
|
handlers.push(Arc::new(ShellCommandHandler::from(
|
|
config.shell_command_backend,
|
|
)));
|
|
}
|
|
ConfigShellToolType::Local => {
|
|
handlers.push(Arc::new(ShellHandler::default()));
|
|
handlers.push(Arc::new(ContainerExecHandler));
|
|
handlers.push(Arc::new(ShellCommandHandler::from(
|
|
config.shell_command_backend,
|
|
)));
|
|
}
|
|
ConfigShellToolType::UnifiedExec => {
|
|
handlers.push(Arc::new(ShellHandler::default()));
|
|
handlers.push(Arc::new(ContainerExecHandler));
|
|
handlers.push(Arc::new(LocalShellHandler::default()));
|
|
handlers.push(Arc::new(ShellCommandHandler::from(
|
|
config.shell_command_backend,
|
|
)));
|
|
}
|
|
ConfigShellToolType::ShellCommand => {
|
|
handlers.push(Arc::new(ShellHandler::default()));
|
|
handlers.push(Arc::new(ContainerExecHandler));
|
|
handlers.push(Arc::new(LocalShellHandler::default()));
|
|
}
|
|
ConfigShellToolType::Disabled => {}
|
|
}
|
|
}
|
|
|
|
if params.mcp_tools.is_some() {
|
|
handlers.push(Arc::new(ListMcpResourcesHandler));
|
|
handlers.push(Arc::new(ListMcpResourceTemplatesHandler));
|
|
handlers.push(Arc::new(ReadMcpResourceHandler));
|
|
}
|
|
|
|
handlers.push(Arc::new(PlanHandler));
|
|
if config.goal_tools {
|
|
handlers.push(Arc::new(GetGoalHandler));
|
|
handlers.push(Arc::new(CreateGoalHandler));
|
|
handlers.push(Arc::new(UpdateGoalHandler));
|
|
}
|
|
|
|
handlers.push(Arc::new(RequestUserInputHandler {
|
|
available_modes: config.request_user_input_available_modes.clone(),
|
|
}));
|
|
|
|
if config.request_permissions_tool_enabled {
|
|
handlers.push(Arc::new(RequestPermissionsHandler));
|
|
}
|
|
|
|
if config.tool_suggest
|
|
&& let Some(discoverable_tools) =
|
|
params.discoverable_tools.filter(|tools| !tools.is_empty())
|
|
{
|
|
handlers.push(Arc::new(RequestPluginInstallHandler::new(
|
|
discoverable_tools,
|
|
)));
|
|
}
|
|
|
|
if config.environment_mode.has_environment() && config.apply_patch_tool_type.is_some() {
|
|
let include_environment_id =
|
|
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
|
|
handlers.push(Arc::new(ApplyPatchHandler::new(include_environment_id)));
|
|
}
|
|
|
|
if config
|
|
.experimental_supported_tools
|
|
.iter()
|
|
.any(|tool| tool == "test_sync_tool")
|
|
{
|
|
handlers.push(Arc::new(TestSyncHandler));
|
|
}
|
|
|
|
if config.environment_mode.has_environment() {
|
|
let include_environment_id =
|
|
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
|
|
handlers.push(Arc::new(ViewImageHandler::new(ViewImageToolOptions {
|
|
can_request_original_image_detail: config.can_request_original_image_detail,
|
|
include_environment_id,
|
|
})));
|
|
}
|
|
|
|
if config.collab_tools {
|
|
if config.multi_agent_v2 {
|
|
let agent_type_description =
|
|
agent_type_description(config, params.default_agent_type_description);
|
|
handlers.push(Arc::new(SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
|
|
available_models: config.available_models.clone(),
|
|
agent_type_description,
|
|
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
|
|
include_usage_hint: config.spawn_agent_usage_hint,
|
|
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
|
|
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
|
|
})));
|
|
handlers.push(Arc::new(SendMessageHandlerV2));
|
|
handlers.push(Arc::new(FollowupTaskHandlerV2));
|
|
handlers.push(Arc::new(WaitAgentHandlerV2::new(
|
|
params.wait_agent_timeouts,
|
|
)));
|
|
handlers.push(Arc::new(CloseAgentHandlerV2));
|
|
handlers.push(Arc::new(ListAgentsHandlerV2));
|
|
} else {
|
|
let agent_type_description =
|
|
agent_type_description(config, params.default_agent_type_description);
|
|
handlers.push(Arc::new(SpawnAgentHandler::new(SpawnAgentToolOptions {
|
|
available_models: config.available_models.clone(),
|
|
agent_type_description,
|
|
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
|
|
include_usage_hint: config.spawn_agent_usage_hint,
|
|
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
|
|
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
|
|
})));
|
|
handlers.push(Arc::new(SendInputHandler));
|
|
handlers.push(Arc::new(ResumeAgentHandler));
|
|
handlers.push(Arc::new(WaitAgentHandler::new(params.wait_agent_timeouts)));
|
|
handlers.push(Arc::new(CloseAgentHandler));
|
|
}
|
|
}
|
|
|
|
if config.agent_jobs_tools {
|
|
handlers.push(Arc::new(SpawnAgentsOnCsvHandler));
|
|
if config.agent_jobs_worker_tools {
|
|
handlers.push(Arc::new(ReportAgentJobResultHandler));
|
|
}
|
|
}
|
|
|
|
if let Some(mcp_tools) = params.mcp_tools {
|
|
for tool in mcp_tools {
|
|
handlers.push(Arc::new(McpHandler::new(tool.clone())));
|
|
}
|
|
}
|
|
|
|
if let Some(deferred_mcp_tools) = params.deferred_mcp_tools {
|
|
for tool in deferred_mcp_tools {
|
|
handlers.push(Arc::new(McpHandler::new(tool.clone())));
|
|
}
|
|
}
|
|
|
|
for tool in params.dynamic_tools {
|
|
let Some(handler) = DynamicToolHandler::new(tool).map(Arc::new) else {
|
|
tracing::error!(
|
|
"Failed to convert dynamic tool {:?} to OpenAI tool",
|
|
tool.name
|
|
);
|
|
continue;
|
|
};
|
|
|
|
handlers.push(handler);
|
|
}
|
|
|
|
handlers
|
|
}
|
|
|
|
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;
|