Skip to content

ToolRegistry构建流程

从 SpecPlan 的 Core 注册入口追踪 MCP、extension、dynamic 和 synthetic runtime 的注入顺序、winner 规则与冲突收束。

基于rust-v0.150.0
CodexRustToolsRuntime

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_router
  • codex-rs/core/src/tools/spec_plan.rs :: CoreToolPlanContext
  • codex-rs/core/src/tools/spec_plan.rs :: add_core_tool_sources
rust
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

rust
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_router
  • codex-rs/core/src/tools/spec_plan.rs :: apply_mcp_tool_exposure_policy
rust
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,
    &registered_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_executors
  • codex-rs/core/src/tools/spec_plan.rs :: append_extension_tool_executors
rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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_priority
  • codex-rs/core/src/tools/spec_plan_tests.rs :: environment_count_controls_environment_backed_tools
  • codex-rs/core/src/tools/spec_plan_tests.rs :: strict_tool_collisions_reject_external_and_synthetic_duplicates
  • codex-rs/core/src/tools/spec_plan_tests.rs :: relaxed_tool_collisions_preserve_first_nonempty_namespace_description
  • codex-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 中运行:

bash
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

然后尝试回答:

  1. 为什么 MCP exposure policy 必须紧跟 append_mcp_tools,而不能等所有 external runtime 注册完再做?
  2. 当一个 external tool 与 Core tool 同名时,哪个对象保留?first_collision 什么时候才变成用户可见错误?
  3. Code Mode execute/wait 为什么使用 trusted prepend?这对 registry source order 和 duplicate 有什么影响?
  4. 同 namespace 不同 child name 为什么可以共存,而相同 canonical ToolName 不能共存?
  5. 注册顺序与最终 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 结构当作当前版本事实。