[codex] Make handlers own parallel tool support (#22254)

## Why

`ToolRouter::tool_supports_parallel()` was still consulting configured
specs when a handler lookup missed, even though parallel schedulability
is really a property of the executable handler. Keeping that metadata on
`ConfiguredToolSpec` duplicated state between the model-visible spec
layer and the runtime handler layer.

This change makes handlers the sole source of truth for parallel tool
support and removes the extra spec wrapper that only existed to carry
duplicated metadata.

## What changed

- removed `ConfiguredToolSpec` and store plain `ToolSpec` values in the
registry/router builder path
- changed `ToolRouter::tool_supports_parallel()` to consult only the
handler registry and fall back to `false`
- simplified spec collection and test helpers to operate directly on
`ToolSpec`
- updated router/spec tests to cover handler-owned parallel behavior and
the no-handler fallback

## Validation

- `cargo test -p codex-tools`
- `cargo test -p codex-core mcp_parallel_support_uses_handler_data`
- `cargo test -p codex-core
deferred_responses_api_tool_serializes_with_defer_loading`
- `cargo test -p codex-core
tools_without_handlers_do_not_support_parallel`
- `cargo test -p codex-core
request_plugin_install_can_be_registered_without_search_tool`

## Docs

No documentation updates needed.
This commit is contained in:
pakrym-oai authored and GitHub committed 2026-05-11 22:26:33 -07:00
1 parent 79c65f816c
commit c9e46ed639
10 files changed
+115 -185

No files matched your search

+11 -12
View File
@@ -17,7 +17,6 @@ use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::SessionSource;
use codex_tools::AdditionalProperties;
use codex_tools::ConfiguredToolSpec;
use codex_tools::DiscoverableTool;
use codex_tools::JsonSchema;
use codex_tools::LoadableToolSpec;
@@ -163,11 +162,11 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() {
}
// Avoid order-based assertions; compare via set containment instead.
fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&str]) {
fn assert_contains_tool_names(tools: &[ToolSpec], expected_subset: &[&str]) {
use std::collections::HashSet;
let mut names = HashSet::new();
let mut duplicates = Vec::new();
for name in tools.iter().map(ConfiguredToolSpec::name) {
for name in tools.iter().map(ToolSpec::name) {
if !names.insert(name) {
duplicates.push(name);
}
@@ -194,7 +193,7 @@ fn shell_tool_name(config: &ToolsConfig) -> Option<&'static str> {
}
}
fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a ConfiguredToolSpec {
fn find_tool<'a>(tools: &'a [ToolSpec], expected_name: &str) -> &'a ToolSpec {
tools
.iter()
.find(|tool| tool.name() == expected_name)
@@ -202,12 +201,12 @@ fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a Co
}
fn find_namespace_function_tool<'a>(
tools: &'a [ConfiguredToolSpec],
tools: &'a [ToolSpec],
expected_namespace: &str,
expected_name: &str,
) -> &'a ResponsesApiTool {
let namespace_tool = find_tool(tools, expected_namespace);
let ToolSpec::Namespace(namespace) = &namespace_tool.spec else {
let ToolSpec::Namespace(namespace) = namespace_tool else {
panic!("expected namespace tool {expected_namespace}");
};
namespace
@@ -249,7 +248,7 @@ fn multi_agent_v2_spawn_agent_description(tools_config: &ToolsConfig) -> String
)
.build();
let spawn_agent = find_tool(&tools, "spawn_agent");
let ToolSpec::Function(ResponsesApiTool { description, .. }) = &spawn_agent.spec else {
let ToolSpec::Function(ResponsesApiTool { description, .. }) = spawn_agent else {
panic!("spawn_agent should be a function tool");
};
description.clone()
@@ -326,7 +325,7 @@ async fn get_memory_requires_feature_flag() {
)
.build();
assert!(
!tools.iter().any(|t| t.spec.name() == "get_memory"),
!tools.iter().any(|t| t.name() == "get_memory"),
"get_memory should be disabled when memory_tool feature is off"
);
}
@@ -778,7 +777,7 @@ async fn multi_agent_v2_wait_agent_schema_uses_configured_min_timeout() {
)
.build();
let wait_agent = find_tool(&tools, "wait_agent");
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &wait_agent.spec else {
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = wait_agent else {
panic!("wait_agent should be a function tool");
};
let timeout_description = parameters
@@ -867,7 +866,7 @@ async fn search_tool_description_handles_no_enabled_mcp_tools() {
)
.build();
let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME);
let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else {
let ToolSpec::ToolSearch { description, .. } = search_tool else {
panic!("expected tool_search tool");
};
@@ -916,7 +915,7 @@ async fn search_tool_description_falls_back_to_connector_name_without_descriptio
)
.build();
let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME);
let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else {
let ToolSpec::ToolSearch { description, .. } = search_tool else {
panic!("expected tool_search tool");
};
@@ -1123,7 +1122,7 @@ async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
description,
parameters,
..
}) = &tool.spec
}) = tool
else {
panic!("unavailable MCP tool should be exposed as a function tool");
};