ToolRegistry数据结构
ToolRouter 负责把模型调用变成统一的 ToolCall,但真正回答“这个名称对应哪个可执行 runtime”的对象是 ToolRegistry。当前实现不是一个保存完整规格列表的旧式 Builder,而是一张以规范化 ToolName 为键的 IndexMap:每个键只绑定一个 runtime 和当前 Step 的 ToolExposure,另有一个 first_collision 记录外部注册 冲突。
本文面向已经读过ToolPayload调用模型和SpecPlan生成算法 的读者。本文只讲 Registry 的存储、注册、查找和调度元数据,不展开完整 Hook 管线或并行执行器。读完后,读者 应能解释 plain name、functions namespace 和 MCP namespace 为什么映射到不同或相同的键,判断 trusted/external 注册谁获胜,并定位一个工具为什么“已注册但不可并行”或“找不到 runtime”。
1. 一条索引
1.1 存储结构
Registry 的核心只有两个字段:tools 保存有序名称到 RegisteredTool 的映射,first_collision 保存第一个 需要由 finalize 决策的冲突名称。RegisteredTool 把 runtime 和 exposure 放在一起,意味着 execution owner 与 当前 Step 的模型暴露策略使用同一条注册记录。
相关源码:
codex-rs/core/src/tools/registry.rs :: RegisteredToolcodex-rs/core/src/tools/registry.rs :: ToolRegistry
pub(crate) struct RegisteredTool {
pub(crate) runtime: Arc<dyn CoreToolRuntime>,
pub(crate) exposure: ToolExposure,
}
#[derive(Default)]
pub struct ToolRegistry {
tools: IndexMap<ToolName, RegisteredTool>,
first_collision: Option<ToolName>,
}这里没有单独的 ToolSpec 字段。需要模型规格时,Registry 从 runtime.spec() 读取;需要执行时,从同一个 runtime 调 handle;需要并行、取消、搜索或 diff 信息时,也继续询问这个 runtime。这样可以避免“规格说支持某 能力、实际 handler 却是另一个对象”的双重事实源。
1.2 所有权关系
Arc<dyn CoreToolRuntime> 让 registry、并行 runtime、extension adapter 和测试共享同一个执行对象; ToolExposure 则是可复制的值,能在 apply_mcp_tool_exposure_policy 或 Code Mode 规划阶段被重写,而不需要 替换 runtime。
2. 名称规范化
2.1 注册键
所有注册入口都会先调用 runtime.tool_name().with_default_namespace()。plain name、空 namespace 和默认 functions namespace 因而使用同一个键;非默认 namespace 会保留,用于区分 MCP、extension 或协作工具。
相关源码:
codex-rs/core/src/tools/registry.rs :: register_trusted_with_exposurecodex-rs/core/src/tools/registry.rs :: register_external_with_exposure
let tool_name = runtime.tool_name().with_default_namespace();
match self.tools.entry(tool_name) {
Entry::Vacant(entry) => {
entry.insert(RegisteredTool { runtime, exposure });
}
Entry::Occupied(entry) => {
let tool_name = entry.key();
error_or_panic(format!("tool {tool_name} already registered"));
}
}查找也重新做同样归一化:
源码位置:codex-rs/core/src/tools/registry.rs :: ToolRegistry::tool
pub(crate) fn tool(&self, name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
self.tools
.get(&name.clone().with_default_namespace())
.map(|tool| Arc::clone(&tool.runtime))
}因此,调用方不需要预先知道 plain name 是否曾以 functions 注册;但调用方必须保留非默认 namespace,否则 mcp__calendar.echo 会被误查成默认 echo,得到 None 或错误 runtime。
2.2 默认别名冲突
规范化也意味着 plain lookup 与 functions.lookup 不是两个可并存工具。测试将两种注册顺序都覆盖,第二次 注册会失败,第一次 runtime 继续作为 winner。
源码位置:codex-rs/core/src/tools/registry_tests.rs :: registry_rejects_default_namespace_alias_collisions
let plain_name = ToolName::plain("lookup");
let namespaced_name = ToolName::namespaced(DEFAULT_FUNCTION_NAMESPACE, "lookup");
let winner = Arc::new(TestHandler {
tool_name: plain_name.clone(),
}) as Arc<dyn CoreToolRuntime>;
let mut registry = ToolRegistry::from_tools([Arc::clone(&winner)]);
assert!(!registry.register_external(Arc::new(TestHandler {
tool_name: namespaced_name.clone(),
})));
assert!(registry
.tool(&namespaced_name)
.is_some_and(|handler| Arc::ptr_eq(&handler, &winner)));这个测试证明的是 Registry key 归一化,不是模型 namespace wire shape。模型请求的 namespace 合并仍由 SpecPlan 处理;Registry 只关心最终可执行名称。
3. 注册策略
3.1 Trusted工具
Core 自己创建的 runtime 使用 add/register_trusted;Code Mode executor 使用 prepend_trusted,将其插入 IndexMap 首位。trusted duplicate 会调用 error_or_panic,因为这通常是内部装配不变量破坏,而不是外部输入 可以修复的普通冲突。
相关源码:
codex-rs/core/src/tools/registry.rs :: register_trusted_with_exposurecodex-rs/core/src/tools/registry.rs :: prepend_trusted
pub(crate) fn prepend_trusted(&mut self, runtime: Arc<dyn CoreToolRuntime>) {
let tool_name = runtime.tool_name().with_default_namespace();
if self.tools.contains_key(&tool_name) {
error_or_panic(format!("tool {tool_name} already registered"));
return;
}
let exposure = runtime.exposure();
self.tools
.shift_insert(0, tool_name, RegisteredTool { runtime, exposure });
}prepend_trusted 的顺序效果会被 entries()、Code Mode prompt 收集和某些 collision winner 观察到;最终模型 namespace children 仍会在 SpecPlan 的 merge_into_namespaces 中按名称排序,因此不能把注册顺序直接当成 wire 顺序。
3.2 External工具
MCP、extension 和 dynamic runtime 使用 register_external。外部重复注册不会 panic,而是记录 warning,将已有 runtime 保留为 winner,并把 canonical key 写入 first_collision。之后是否报错由 finalize 阶段的 error_on_tool_collisions 决定。
源码位置:codex-rs/core/src/tools/registry.rs :: register_external_with_exposure
match self.tools.entry(tool_name) {
Entry::Vacant(entry) => {
entry.insert(RegisteredTool { runtime, exposure });
true
}
Entry::Occupied(entry) => {
tracing::warn!(
tool_name = %entry.key(),
"skipping duplicate external tool that is already registered"
);
self.first_collision
.get_or_insert_with(|| entry.key().clone());
false
}
}这种“保留 winner、延迟决定是否失败”的设计允许 relaxed mode 继续运行,同时让 strict mode 在计划收束时拒绝 不确定的工具面。
3.3 Reserved名称
default namespace 下的 external shell_command 是保留名称。没有 builtin 时它直接被拒绝且不记录 collision;有 builtin 时才记录 collision。带其他 namespace 的 client.shell_command 不受这条 default-name 规则影响。
源码位置:codex-rs/core/src/tools/registry.rs :: register_external_with_exposure
if tool_name.is_default_namespace() && tool_name.name == "shell_command" {
tracing::warn!(tool_name = %tool_name, "skipping external tool with reserved name");
if self.tools.contains_key(&tool_name) {
self.record_collision(tool_name);
}
return false;
}4. 查询投影
4.1 Runtime查询
tool 返回 runtime 的 clone;entries/entries_mut 提供计划阶段遍历和 exposure 重写;remove 删除规范化 键并返回 runtime。它们分别服务 lookup、SpecPlan 策略和特殊 executor 替换。
相关源码:
codex-rs/core/src/tools/registry.rs :: toolcodex-rs/core/src/tools/registry.rs :: entriescodex-rs/core/src/tools/registry.rs :: entries_mutcodex-rs/core/src/tools/registry.rs :: remove
源码位置:codex-rs/core/src/tools/registry.rs :: entries
pub(crate) fn entries(&self) -> impl Iterator<Item = &RegisteredTool> {
self.tools.values()
}
pub(crate) fn entries_mut(&mut self) -> impl Iterator<Item = &mut RegisteredTool> {
self.tools.values_mut()
}
pub(crate) fn remove(&mut self, tool_name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
self.tools
.shift_remove(&tool_name.clone().with_default_namespace())
.map(|tool| tool.runtime)
}源码位置:codex-rs/core/src/tools/registry.rs :: entries、entries_mut、remove
entries 是只读计划视图,entries_mut 是 exposure policy 的写入口,remove 则用于冲突或特殊 executor 替换;三者都围绕同一张 IndexMap 工作。
4.2 Exposure查询
supports_parallel_tool_calls 不只询问 runtime capability,还要求 exposure 不是 Hidden;隐藏 runtime 即使自身 声明 parallel-safe,也不会被当作普通模型并行能力使用。waits_for_runtime_cancellation 则只询问 runtime,因为 取消 teardown 是执行资源属性,不是模型可见性属性。
相关源码:
codex-rs/core/src/tools/registry.rs :: supports_parallel_tool_callscodex-rs/core/src/tools/registry.rs :: waits_for_runtime_cancellation
pub(crate) fn supports_parallel_tool_calls(&self, name: &ToolName) -> Option<bool> {
let tool = self.tools.get(&name.clone().with_default_namespace())?;
Some(tool.exposure != ToolExposure::Hidden && tool.runtime.supports_parallel_tool_calls())
}
pub(crate) fn waits_for_runtime_cancellation(&self, name: &ToolName) -> Option<bool> {
let tool = self.tool(name)?;
Some(tool.waits_for_runtime_cancellation())
}4.3 Deferred names
deferred_tool_namespaces 只收集非默认 namespace 的 deferred entries,并尝试从 namespace spec 取得 description。 Function、Freeform、ToolSearch 和 WebSearch 没有 namespace description,因此不会凭工具名伪造一段描述。
源码位置:codex-rs/core/src/tools/registry.rs :: deferred_tool_namespaces
for (name, tool) in &self.tools {
if !tool.exposure.is_deferred() || name.is_default_namespace() {
continue;
}
let Some(namespace) = &name.namespace else {
continue;
};
let description = match tool.runtime.spec() {
ToolSpec::Namespace(namespace) => namespace.description,
ToolSpec::Function(_)
| ToolSpec::Freeform(_)
| ToolSpec::ToolSearch { .. }
| ToolSpec::WebSearch { .. } => String::new(),
};
if !description.trim().is_empty() {
namespaces.entry(namespace.clone()).or_default().clone_from(&description);
}
}5. 调度元数据
5.1 Readiness
MCP runtime 可以覆盖 wait_until_ready,Registry 不缓存 readiness 状态,只把 runtime 的等待 future 暴露给 ToolCallRuntime。因此同一个工具名称的 readiness 必须由对应 runtime 自己拥有,不能由 Registry 用全局 server 状态替代。
5.2 Diff与Hook
create_diff_consumer 从 runtime 获取 streamed argument diff consumer;pre_tool_use_payload、 with_updated_hook_input 和 post_tool_use_payload 也属于 runtime 扩展点,Registry 只在 dispatch 流程中调用。 这使 apply_patch 可以消费 Custom payload,而普通 function runtime 使用默认 JSON hook 适配。
相关源码:
codex-rs/core/src/tools/registry.rs :: CoreToolRuntimecodex-rs/core/src/tools/registry.rs :: create_diff_consumer
pub(crate) fn create_diff_consumer(
&self,
name: &ToolName,
) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
self.tool(name)?.create_diff_consumer()
}5.3 Dispatch入口
Registry dispatch 先增加 active-turn tool call 计数,再查找 runtime、检查 payload kind、通知 lifecycle、执行 PreToolUse hook,最后交给 handler。Registry 的数据结构因此不仅是“名称到函数”的 map,也是策略 metadata 的 集中入口。
源码位置:codex-rs/core/src/tools/registry.rs :: dispatch_any_with_terminal_outcome
let tool = match self.tool(&tool_name) {
Some(tool) => tool,
None => {
let message = unsupported_tool_call_message(&invocation.payload, &tool_name);
return Err(FunctionCallError::RespondToModel(message));
}
};
if !tool.matches_kind(&invocation.payload) {
let message = format!("tool {tool_name} invoked with incompatible payload");
return Err(FunctionCallError::Fatal(message));
}
notify_tool_start(&invocation).await;6. 测试路径
6.1 名称与注册
handler_normalizes_only_the_default_namespace 验证 plain/default namespace alias 指向同一 runtime,而非默认 namespace 保持独立。registry_preserves_external_winners_and_trusted_synthetic_order 验证 external duplicate 保留 已有 winner,trusted prepend 放到 IndexMap 首位。
相关测试:
codex-rs/core/src/tools/registry_tests.rs :: handler_normalizes_only_the_default_namespacecodex-rs/core/src/tools/registry_tests.rs :: registry_preserves_external_winners_and_trusted_synthetic_order
6.2 保留名与并行
reserved_command_tools_reject_external_runtimes_without_a_builtin 验证 default shell_command 被拒绝,但 namespaced client.shell_command 可以注册。readiness_selects_exact_tool_with_registry_owned_exposure 验证 readiness 选择依据规范化后精确名称,而不是只看工具名字符串。
相关测试:
codex-rs/core/src/tools/registry_tests.rs :: reserved_command_tools_reject_external_runtimes_without_a_builtincodex-rs/core/src/tools/registry_tests.rs :: readiness_selects_exact_tool_with_registry_owned_exposure
6.3 Hook与生命周期
function_tools_expose_default_hook_payloads_and_rewrites 验证默认 function hook input 和 rewrite; dispatch_uses_canonical_tool_names_for_lifecycle_contributors 验证 plain/default namespace dispatch 对 lifecycle consumer 使用 canonical name,并分别记录成功输出与 handler error。
相关测试:
codex-rs/core/src/tools/registry_tests.rs :: function_tools_expose_default_hook_payloads_and_rewritescodex-rs/core/src/tools/registry_tests.rs :: dispatch_uses_canonical_tool_names_for_lifecycle_contributors
7. 阅读练习
在 Codex 源码 workspace 中运行:
cargo test -p codex-core handler_normalizes_only_the_default_namespace
cargo test -p codex-core registry_preserves_external_winners_and_trusted_synthetic_order
cargo test -p codex-core reserved_command_tools_reject_external_runtimes_without_a_builtin
cargo test -p codex-core readiness_selects_exact_tool_with_registry_owned_exposure
cargo test -p codex-core dispatch_uses_canonical_tool_names_for_lifecycle_contributors然后尝试回答:
- plain
lookup、functions.lookup、mcp__calendar.lookup在 Registry 中分别是什么关系? - external duplicate 为什么不立即 panic,而 trusted duplicate 为什么调用
error_or_panic? - Hidden runtime 为什么即使声明 parallel-safe,
supports_parallel_tool_calls仍返回 false? first_collision和最终 strict collision error 分别在哪两个阶段产生?- 一个 deferred namespace 没有
ToolSpec::Namespacedescription 时,deferred_tool_namespaces为什么不会凭空生成说明?
8. 边界
ToolRegistry 是当前 Step 的 runtime 索引和调度元数据入口,不是完整工具计划:
- 规格如何合并、hosted spec 如何追加、provider 如何过滤属于 SpecPlan;
- payload 如何从 ResponseItem 归一化属于 ToolRouter;
- 并行锁、取消 teardown 和结果回灌属于 ToolCallRuntime;
- schema、approval、sandbox 和 handler 业务校验属于各自专题。
排查工具问题时,先用 canonical ToolName 检查 Registry lookup,再看 RegisteredTool.exposure,然后检查 runtime 的 matches_kind、readiness、parallel 和 cancellation metadata。不要因为模型看不到工具,就断言它没有注册; 也不要因为 runtime 已注册,就断言它一定会进入当前请求的 Prompt.tools。
