第 15 讲:Tool Registry 与 Spec Plan

Codex 源码解析 · 第 15 讲

Codex 源码解析系列 第 15 讲:Tool Registry 与 Spec Plan 基于 OpenAI Codex 源码 · 2026-10-09 💡 本讲一句话: Codex 的工具系统分两层—— ToolRegistry (core/src/tools/registry.rs)管"谁能被调用", spec_plan (core/src/tools/spec_plan.rs)管"模型每轮能看到谁"。前者用 trusted/external 双轨注册防冲突,后者按 feature flag、环境、MCP server、Code Mode 现场裁剪出六档曝光位;所有调用最终汇入 dispatch_any_with_terminal_outcome 一条流水线:PreToolUse hooks → 执行 → PostToolUse hooks。读完你能说清一个工具从"注册"到"模型可见"再到"被调用"的完整链路。 一、为什么工具清单不能是静态的 第 14 讲我们拆了上下文压缩——那是"历史太长怎么办"。本讲换个方向:模型每轮请求里带的 tools 列表,到底是谁、按什么规则拼出来的?答案藏在 core/src/tools/ 这个目录里,核心就四个文件: registry.rs (826 行)、 spec_plan.rs (1552 行)、 router.rs (385 行)、 context.rs 。工具清单必须动态,原因至少有四个:feature flag 开关(UnifiedExec、TokenBudget……)随时变;MCP server 是用户配置的,每个 server 的工具集不同;Code Mode 开启后嵌套工具面要重新洗牌;Guardian 审查会话只允许三个工具。所以 Codex 的做法是: 每个 turn 现场 build 一份 ToolRegistry + 模型可见 spec ,而不是全局单例。 📌 本讲地图: ① ToolRegistry 双轨注册(trusted vs external)→ ② dispatch 单入口流水线 → ③ spec_plan 按 turn 组装计划 → ④ MCP 曝光位策略 → ⑤ 模型可见 spec 与命名空间合并。每节都带真实源码行号。 二、ToolRegistry:双轨注册,冲突策略完全不同 先看数据结构本体。整个 registry 就两个字段——一个保序的 IndexMap ,加一个"第一次撞名"的记录: 📄 codex-rs/core/src/tools/registry.rs (第 286-350 行) #[derive(Default)] // 派生 Default:空 registry,两个字段都有默认值 pub struct ToolRegistry { // 工具注册表本体——一个 turn 一份,不是全局单例 tools: IndexMap<ToolName, RegisteredTool>, // 保序映射:插入顺序即模型可见顺序(IndexMap 而非 HashMap) first_collision: Option<ToolName>, // 只记第一次撞名的工具名——后面统一报错用,不覆盖 } impl ToolRegistry { // 注册表方法集——下面对比 trusted/external 双轨 pub(crate) fn register_trusted_with_exposure( // trusted = Codex 自己内置的工具,注册冲突是编程错误 &mut self, runtime: Arc<dyn CoreToolRuntime>, exposure: ToolExposure, // 参数:运行时实例 + 指定曝光位(trusted/external 共用签名) ) { let tool_name = runtime.tool_name().with_default_namespace(); // 补默认命名空间,保证 key 归一化 match self.tools.entry(tool_name) { // entry API:一次拿到"空位/已占"两种情况 Entry::Vacant(entry) => { // …

阅读全文:Codex 源码解析 第 15 讲