雨天小六

读懂 Codex(9.8):ToolRegistry、ToolPlan 与同 Step Router

· 更新于 2026-08-03 · 专栏:读懂 Codex

#Codex#Agent Runtime#Python#软件架构

具体问题与边界

工具目录在模型采样期间发生刷新时,怎样防止模型按旧 Schema 生成参数却由新 Handler 执行?

Registry 持有可变目录和 generation;Planner 每 Step 取一次 snapshot;同一 ToolPlan 同时产生 specs 与 Router。 本节以官方 Codex 锁定提交为事实基线,以 Mini Codex 的可执行断言检验 Python 设计;二者不是逐行翻译关系。

协议、类型与状态所有权

对象创建/所有者生命周期与作用域是否持久化
ToolRegistry handlers/sourceRuntime 配置/MCP refresh跨 Step 可变
generationToolRegistry每次 register/replace_source 递增
RegistrySnapshotToolPlanner一次 plan 调用
ToolPlan specs/router一个 Step模型请求到工具执行结束

正常路径

ToolRegistry、ToolPlan 与同 Step Router正常路径图
图 9.8-1:工具目录在模型采样期间发生刷新时,怎样防止模型按旧 Schema 生成参数却由新 Handler 执行?
  1. Builtin 注册和 MCP source replacement 都修改 Registry,并递增 generation。
  2. Step 开始时 Planner 只调用一次 snapshot(),复制 handler map。之后 Registry 的任何变化都不修改这份 map。
  3. ToolSpec tuple 从 snapshot handlers 生成;ToolRouter 也用同一 snapshot handlers 构造。
  4. ModelRequest 保存 tool_generation 便于测试与诊断;模型返回 ToolCall 后不再查询全局 Registry。
  5. 下一 Step 再 plan,才看到新的 generation 和目录。这把热更新可见性固定到明确边界。

调用链与等待点

1. Builtin 注册和 MCP source replacement 都修改 Registry,并递增 generation
→ 2. Step 开始时 Planner 只调用一次 `snapshot()`,复制 handler map
→ 3. ToolSpec tuple 从 snapshot handlers 生成;ToolRouter 也用同一 snapshot handlers 构造
→ 4. ModelRequest 保存 tool_generation 便于测试与诊断;模型返回 ToolCall 后不再查询全局 Registry
→ 5. 下一 Step 再 plan,才看到新的 generation 和目录

每个 await 都是状态可被取消、外部副作用可能已经发生或其他任务能够推进的边界。正文因此同时写清输入形状、所有者、成功产物和失败残留,而不是只列方法名。

Python 风格伪代码

class ToolRegistry:
    def register(handler, source="builtin"):
        handlers[handler.spec.name] = handler
        sources[handler.spec.name] = source
        generation += 1

    def snapshot():
        return RegistrySnapshot(generation, handlers.copy())

class ToolPlanner:
    def plan():
        snapshot = registry.snapshot()          # exactly once
        return ToolPlan(
            generation=snapshot.generation,
            specs=tuple(h.spec for h in snapshot.handlers.values()),
            router=ToolRouter(snapshot.handlers, hooks),
        )

results = await tool_plan.router.dispatch(response.tool_calls, context)

伪代码表达顺序和责任。真实可运行实现没有把万能调用当作未解释黑箱;对应模块见 Mini 导航。

失败、取消与恢复

ToolRegistry、ToolPlan 与同 Step Router失败路径图
图 9.8-2:失败分支按实际状态所有者收口,模型可见失败与 Runtime fatal 分开。
故障点已发生/残留状态处理与模型可见结果
返回后查全局 Registry当前 Step 发生 spec-handler time travelRouter 冻结 snapshot map
MCP 刷新逐个覆盖模型可能看见半套目录replace_source 一次替换同 source
同名不同 source静默覆盖 builtin 或其他 serverRegistry 抛 collision
未知工具Call 无执行者生成同 call_id error ToolResult

必须保持的不变量

一次模型采样展示的 ToolSpec 与其 ToolCall 真正调用的 Handler 必须来自同一个 RegistrySnapshot。

设计取舍

复制 handler map 有小额内存成本,却把动态目录更新限制在 Step 边界。Mini Registry 不实现官方 Tool Search、deferred loading、feature gate 和并行门。

“为什么”的表述是从源码状态机、调用顺序和测试反推的工程解释;源码未声明的动机不写成官方承诺。

测试与复现实验

cd examples/mini-codex
uv run pytest -q -k 'test_router_uses_same_registry_snapshot_as_tool_specs or test_mcp_refresh_replaces_one_server_catalog_generation'
uv run mypy src

关键断言:

  • test_router_uses_same_registry_snapshot_as_tool_specs
  • test_mcp_refresh_replaces_one_server_catalog_generation

官方源码导航

Mini Codex 对照

  • src/mini_codex/tools/registry.py:generation、source replacement 与 snapshot
  • src/mini_codex/tools/planner.py:同快照 ToolPlan
  • src/mini_codex/tools/router.py:冻结 Handler map 的执行

本节边界

已经证明:一次模型采样展示的 ToolSpec 与其 ToolCall 真正调用的 Handler 必须来自同一个 RegistrySnapshot。

阅读导航

上一节:9.7 · 下一节:9.9

评论


← 返回文章列表