Refactor namespaced tool spec registration (#22256)

## 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`.
This commit is contained in:
pakrym-oai
2026-05-12 17:09:14 -07:00
committed by GitHub
parent b6e718591b
commit 0173f71143
14 changed files with 342 additions and 358 deletions
@@ -8,17 +8,20 @@ use codex_tools::ToolSpec;
use super::ExecContext;
use super::PUBLIC_TOOL_NAME;
use super::build_enabled_tools;
use super::handle_runtime_response;
use super::is_exec_tool_name;
pub struct CodeModeExecuteHandler {
spec: ToolSpec,
nested_tool_specs: Vec<ToolSpec>,
}
impl CodeModeExecuteHandler {
pub(crate) fn new(spec: ToolSpec) -> Self {
Self { spec }
pub(crate) fn new(spec: ToolSpec, nested_tool_specs: Vec<ToolSpec>) -> Self {
Self {
spec,
nested_tool_specs,
}
}
async fn execute(
@@ -31,7 +34,8 @@ impl CodeModeExecuteHandler {
let args =
codex_code_mode::parse_exec_source(&code).map_err(FunctionCallError::RespondToModel)?;
let exec = ExecContext { session, turn };
let enabled_tools = build_enabled_tools(&exec).await;
let enabled_tools =
codex_tools::collect_code_mode_tool_definitions(&self.nested_tool_specs);
let stored_values = exec
.session
.services
-33
View File
@@ -29,12 +29,9 @@ use crate::tools::context::ToolPayload;
use crate::tools::parallel::ToolCallRuntime;
use crate::tools::router::ToolCall;
use crate::tools::router::ToolCallSource;
use crate::tools::router::ToolRouterParams;
use crate::tools::router::extension_tool_bundles;
use crate::unified_exec::resolve_max_tokens;
use codex_features::Feature;
use codex_tools::ToolName;
use codex_tools::collect_code_mode_tool_definitions;
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_output_truncation::formatted_truncate_text_content_items_with_policy;
use codex_utils_output_truncation::truncate_function_output_items_with_policy;
@@ -260,36 +257,6 @@ fn truncate_code_mode_result(
truncate_function_output_items_with_policy(&items, policy)
}
pub(super) async fn build_enabled_tools(
exec: &ExecContext,
) -> Vec<codex_code_mode::ToolDefinition> {
let router = build_nested_router(exec).await;
let specs = router.specs();
collect_code_mode_tool_definitions(&specs)
}
#[expect(
clippy::await_holding_invalid_type,
reason = "nested tool router construction reads through the session-owned manager guard"
)]
async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
let nested_tools_config = exec.turn.tools_config.for_code_mode_nested_tools();
let mcp_connection_manager = exec.session.services.mcp_connection_manager.read().await;
let listed_mcp_tools = mcp_connection_manager.list_all_tools().await;
ToolRouter::from_config(
&nested_tools_config,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: Some(listed_mcp_tools),
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: extension_tool_bundles(exec.session.as_ref()),
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
},
)
}
async fn call_nested_tool(
_exec: ExecContext,
tool_runtime: ToolCallRuntime,