ToolRegistry构建流程
ToolRegistry 的数据结构并不能单独说明工具从哪里来。当前版本的构建过程由 spec_plan.rs 负责:先注册 Core runtime,再追加 MCP、extension 和 dynamic runtime,随后在 finalize 阶段插入 tool search/Code Mode executor, 最后才把冲突和 exposure 结果交给 ToolRouter。注册顺序会影响 external winner、synthetic runtime 的位置和 某些测试观察到的 source order。
本文面向已经读过ToolRegistry数据结构和SpecPlan生成算法 的读者。TLS008 解释 map 中保存什么,本文解释这张 map 如何被填充;不展开每个 handler 的业务参数和审批执行。 读完后,读者应能从 build_tool_router 判断某个 runtime 的注册阶段、冲突 winner 和是否会被特殊 executor 替换。
1. 构建入口
1.1 Core计划
build_tool_router 创建 CoreToolPlanContext 和空 Registry,先调用 add_core_tool_sources。basic session source 在这里拥有一条早返回路径:只构造有限的环境工具,不进入普通 MCP/extension/dynamic 追加。
相关源码:
codex-rs/core/src/tools/spec_plan.rs :: build_tool_routercodex-rs/core/src/tools/spec_plan.rs :: CoreToolPlanContextcodex-rs/core/src/tools/spec_plan.rs :: add_core_tool_sources
let context = CoreToolPlanContext {
turn_context,
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::default();
add_core_tool_sources(&context, &mut registry);
let hosted_specs = if crate::guardian::is_basic_session_source(
&turn_context.session_source,
) {
Vec::new()
} else {
// 普通路径继续追加外部来源。
Vec::new()
};这段片段的关键不是 hosted_specs 的省略内容,而是注册阶段先后:Core handler 先占据默认名称,外部工具后续 只能通过 external 注册规则竞争。Guardian 的收窄发生在外部追加之前,因此不是“全部注册后再隐藏”。
1.2 Core来源顺序
普通 Core 路径依次加入 shell family、MCP resource、utility 和 collaboration。每组内部还会根据环境、feature、 model capability 和 agent version 决定具体 handler。
源码位置:codex-rs/core/src/tools/spec_plan.rs :: add_core_tool_sources
add_shell_tools(context, registry);
add_mcp_resource_tools(context, registry);
add_core_utility_tools(context, registry);
add_collaboration_tools(context, registry);add_core_tool_sources 不直接生成模型可见列表,只负责把 Core runtime 放入 Registry。ToolExposure 和最终 namespace/spec 合并在之后处理;因此注册成功不等于模型一定看到工具。
2. 外部注入
2.1 MCP
普通路径先调用 append_mcp_tools,然后立即调用 apply_mcp_tool_exposure_policy。前者把 MCP runtime 注入 Registry,后者根据 server omit_tools_from、direct-only namespace 和 search/code mode 条件重写 exposure。
相关源码:
codex-rs/core/src/tools/spec_plan.rs :: build_tool_routercodex-rs/core/src/tools/spec_plan.rs :: apply_mcp_tool_exposure_policy
let registered_mcp_tools = append_mcp_tools(
mcp.tools(),
&turn_context.config,
apps_enabled,
&mcp.config().mcp_server_catalog,
search_tool_enabled(turn_context),
&mut registry,
);
apply_mcp_tool_exposure_policy(
turn_context,
mcp,
®istered_mcp_tools,
&mut registry,
);registered_mcp_tools 是 policy 的输入边界:没有成功进入 Registry 的 MCP tool 不会被 exposure policy 重新安排。 这避免对被过滤或未发现的工具写入虚假的 deferred/direct 状态。
2.2 Extension
extension executor 由 session extension registry 和当前 step_store 共同提供。append_extension_tool_executors 在遍历 executor 时先过滤 standalone web search 和 image generation 的门槛,再用 register_external 注入。
相关源码:
codex-rs/core/src/tools/spec_plan.rs :: extension_tool_executorscodex-rs/core/src/tools/spec_plan.rs :: append_extension_tool_executors
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)
{
continue;
}
let runtime = Arc::new(ExtensionToolAdapter::new(executor));
registry.register_external(runtime);
}这里的顺序有两个后果:过滤发生在 Registry 注入前;extension duplicate 会保留先到达的 runtime,并把 collision 交给后续 finalize。extension web/run 还会影响 hosted web search 是否生成,但 dynamic/MCP 同名不会写入这个 standalone winner 结果。
2.3 Dynamic
dynamic function 和 namespace function 都逐个构造 DynamicToolHandler,转换失败只记录错误并跳过当前条目, 不会回滚已经注册的其他 dynamic tools。
源码位置:codex-rs/core/src/tools/spec_plan.rs :: append_dynamic_tool_runtimes
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 Some(handler) = DynamicToolHandler::new_in_namespace(namespace, tool) else {
tracing::error!("Failed to convert dynamic tool {:?}.{:?}", namespace.name, tool.name);
continue;
};
registry.register_external(Arc::new(handler));
}
}
}
}2.4 外部顺序
普通路径的外部顺序是 MCP → exposure policy → extension → dynamic → hosted specs。这个顺序不是审美选择:hosted web search 是否生成要知道 extension 是否已注册 standalone web/run;MCP/dynamic 只影响 Registry,不改变 standalone extension winner。
3. Synthetic注册
3.1 ToolSearch
finalize_tool_router 发现存在 deferred 且可搜索 runtime 后,才注入 ToolSearch handler。它会删除同名旧 runtime, 删除占据 tool_search namespace 的普通 namespace runtime,再从当前 deferred entries 构建 search infos。
源码位置:codex-rs/core/src/tools/spec_plan.rs :: finalize_tool_router、append_tool_search_executor
if search_tool_enabled(turn_context)
&& registry.entries().any(|tool| {
tool.runtime.tool_name() != tool_search_name
&& tool.exposure.is_deferred()
&& tool.runtime.search_info().is_some()
})
{
if registry.remove(&tool_search_name).is_some() {
registry.record_collision(tool_search_name.clone());
}
append_tool_search_executor(turn_context, &mut registry, tool_search_handler_cache);
}ToolSearch 是 trusted synthetic runtime,服务的是 deferred runtime 的搜索投影,而不是一个外部工具来源。它的 注册发生在所有普通来源之后,因此能看到完整的 deferred 集合。
3.2 Code Mode
Code Mode executor 也在 finalize 阶段通过 prepend_trusted 加入 Registry。它先扫描当前可参与 code mode 的 runtime, 再将 enabled/deferred prompt definitions 交给 CodeModeExecuteHandler,最后把 execute 和 wait 放到 Registry 前端。
源码位置:codex-rs/core/src/tools/spec_plan.rs :: register_code_mode_executors
let execute_handler = CodeModeExecuteHandler::new(
create_code_mode_tool(
&enabled_tools,
&deferred_tools,
&namespace_descriptions,
default_exec_yield_time_ms,
tool_mode == ToolMode::CodeModeOnly,
),
code_mode_nested_tool_specs,
);
registry.prepend_trusted(Arc::new(CodeModeWaitHandler));
registry.prepend_trusted(Arc::new(execute_handler));两次 prepend 的顺序意味着后加入的 execute handler 在 IndexMap 更靠前;这会影响 synthetic source order,但不会让 Code Mode 自动绕过 runtime 的 exposure 或 duplicate 检查。
4. Winner与冲突
4.1 Source order
IndexMap 保留插入顺序。Core trusted 先进入,external 按 MCP/extension/dynamic 顺序尝试;external duplicate 不会替换 winner。Code Mode synthetic executor 通过 prepend 改变前端顺序。当前测试专门抽取几个关键名称验证顺序, 而不是把全部工具列表当成稳定 API。
源码位置:codex-rs/core/src/tools/spec_plan_tests.rs :: unified_tool_runtimes_preserve_source_order_and_collision_priority
let expected_source_order = [
"exec_command",
"write_stdin",
"mcp__first__echo",
"extension_echo",
"dynamic_echo",
];
let source_order = plan
.registered_names
.iter()
.filter(|name| expected_source_order.contains(name))
.collect::<Vec<_>>();
assert_eq!(source_order, expected_source_order);4.2 Strict模式
external duplicate 先在 Registry 中被跳过并记录 first_collision,strict collision 开关开启后, finalize_tool_router 才把它转换为 CodexErrorDetails::ToolCollision。同 namespace 内多个不同工具名可以共存; 相同名称但不同 namespace 也可以共存。
源码位置:codex-rs/core/src/tools/spec_plan_tests.rs :: strict_tool_collisions_reject_external_and_synthetic_duplicates
let result = finalize_tool_router(
&turn,
registry,
hosted_specs,
&Default::default(),
);
assert!(matches!(
result.expect_err("strict tool collision should fail tool planning").details(),
CodexErrorDetails::ToolCollision(_)
));4.3 Namespace合并
冲突处理发生在 runtime registry,namespace 合并发生在 model-visible spec 生成。两层不要混淆:Registry 允许 同 namespace 下多个不同工具 runtime;SpecPlan 最后才将 namespace children 合并、排序并补 description。
源码位置:codex-rs/core/src/tools/spec_plan.rs :: merge_into_namespaces
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 下不同 child name 不会在 Registry 层冲突,只有到 visible spec 阶段才会合并;canonical ToolName 相同则在注册阶段已经决定 winner。这个顺序是阅读 Registry 构建代码时最容易混淆的两个层次。
5. 测试路径
5.1 注册顺序
spec_plan_tests::unified_tool_runtimes_preserve_source_order_and_collision_priority 同时提供 MCP、extension 和 dynamic runtime,检查关键名称的 source order,并验证 duplicate external tool 没有替换已注册 winner。
5.2 条件注册
environment_count_controls_environment_backed_tools、request_user_input_tool_respects_experimental_config_gate 和 multi_agent_feature_selects_one_agent_tool_family 分别证明环境、feature 和 agent version 会改变 Core 注册面。
5.3 冲突收束
strict_tool_collisions_reject_external_and_synthetic_duplicates 证明 strict finalize 会失败; relaxed_tool_collisions_preserve_first_nonempty_namespace_description 证明 relaxed mode 继续运行并保留第一个非空 namespace description;strict_tool_collisions_allow_identical_names_in_different_namespaces 证明 namespace 是冲突 键的一部分。
相关测试:
codex-rs/core/src/tools/spec_plan_tests.rs :: unified_tool_runtimes_preserve_source_order_and_collision_prioritycodex-rs/core/src/tools/spec_plan_tests.rs :: environment_count_controls_environment_backed_toolscodex-rs/core/src/tools/spec_plan_tests.rs :: strict_tool_collisions_reject_external_and_synthetic_duplicatescodex-rs/core/src/tools/spec_plan_tests.rs :: relaxed_tool_collisions_preserve_first_nonempty_namespace_descriptioncodex-rs/core/src/tools/spec_plan_tests.rs :: strict_tool_collisions_allow_identical_names_in_different_namespaces
这些测试证明 Registry 构建的局部规则,不证明最终 provider wire shape、handler 外部副作用或所有 feature 组合。
6. 阅读练习
在 Codex 源码 workspace 中运行:
cargo test -p codex-core unified_tool_runtimes_preserve_source_order_and_collision_priority
cargo test -p codex-core environment_count_controls_environment_backed_tools
cargo test -p codex-core strict_tool_collisions_reject_external_and_synthetic_duplicates
cargo test -p codex-core relaxed_tool_collisions_preserve_first_nonempty_namespace_description
cargo test -p codex-core strict_tool_collisions_allow_identical_names_in_different_namespaces然后尝试回答:
- 为什么 MCP exposure policy 必须紧跟
append_mcp_tools,而不能等所有 external runtime 注册完再做? - 当一个 external tool 与 Core tool 同名时,哪个对象保留?
first_collision什么时候才变成用户可见错误? - Code Mode execute/wait 为什么使用 trusted prepend?这对 registry source order 和 duplicate 有什么影响?
- 同 namespace 不同 child name 为什么可以共存,而相同 canonical
ToolName不能共存? - 注册顺序与最终 namespace children 顺序为什么不是同一件事?分别指出 IndexMap 和
merge_into_namespaces的作用。
7. 边界
ToolRegistry 构建流程负责把 runtime 实例和 exposure 元数据装入当前 Step 的索引,不负责:
- schema 归一化、ToolSpec wire shape 或 hosted provider 执行;
- 调用 payload 解析、并行锁、取消 teardown 和输出回灌;
- handler 的审批、sandbox、hook 业务规则和外部副作用。
排查一个工具为什么没有出现时,应按“Core 条件注册 → MCP/extension/dynamic 注入 → external winner → synthetic executor → strict collision → model-visible merge”的顺序检查。不要只看最终工具列表,也不要把历史 archive 中的 ToolRegistryPlan 结构当作当前版本事实。
