Skip to content

ToolRegistry数据结构

从当前 ToolRegistry 的 IndexMap 与 RegisteredTool 出发,解释名称归一化、注册优先级、exposure、查询投影和冲突记录。

基于rust-v0.150.0
CodexRustToolsRuntime

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 :: RegisteredTool
  • codex-rs/core/src/tools/registry.rs :: ToolRegistry
rust
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_exposure
  • codex-rs/core/src/tools/registry.rs :: register_external_with_exposure
rust
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

rust
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

rust
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_exposure
  • codex-rs/core/src/tools/registry.rs :: prepend_trusted
rust
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

rust
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

rust
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 :: tool
  • codex-rs/core/src/tools/registry.rs :: entries
  • codex-rs/core/src/tools/registry.rs :: entries_mut
  • codex-rs/core/src/tools/registry.rs :: remove

源码位置:codex-rs/core/src/tools/registry.rs :: entries

rust
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_calls
  • codex-rs/core/src/tools/registry.rs :: waits_for_runtime_cancellation
rust
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

rust
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 :: CoreToolRuntime
  • codex-rs/core/src/tools/registry.rs :: create_diff_consumer
rust
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

rust
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_namespace
  • codex-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_builtin
  • codex-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_rewrites
  • codex-rs/core/src/tools/registry_tests.rs :: dispatch_uses_canonical_tool_names_for_lifecycle_contributors

7. 阅读练习 ​

在 Codex 源码 workspace 中运行:

bash
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

然后尝试回答:

  1. plain lookup、functions.lookup、mcp__calendar.lookup 在 Registry 中分别是什么关系?
  2. external duplicate 为什么不立即 panic,而 trusted duplicate 为什么调用 error_or_panic?
  3. Hidden runtime 为什么即使声明 parallel-safe,supports_parallel_tool_calls 仍返回 false?
  4. first_collision 和最终 strict collision error 分别在哪两个阶段产生?
  5. 一个 deferred namespace 没有 ToolSpec::Namespace description 时,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。