雨天小六

读懂 Codex(10.4):Tool Spec 与 Runtime 的同快照契约

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

#Codex#Agent Runtime#软件架构#系统设计

具体问题与边界

模型看见的是名称、描述和 JSON Schema,真正执行的是 Registry 中的 Handler。如果这两张表分别从 可变全局状态构造,模型可能按旧 Schema 生成参数,Runtime 却交给新 Handler。Codex 的 ToolPlan 在一个 Step 内同时产出模型可见规格和执行路由,时间一致性比“是否能查到同名函数”更重要。

本节不要求可见规格集合与本地 Handler 集合完全相等:Hosted、Deferred 和隐藏工具是明确例外。

合同与状态所有权

对象创建点Step 内可变性作用
planned tools能力/Feature/MCP 规划创建后冻结描述候选工具及来源
model-visible specsToolPlan冻结发给模型的调用合同
runtime registry同一 ToolPlan冻结名称到执行器的绑定
ToolRouterStepContext冻结解析 ResponseItem 并分派
MCP catalog revisionbinding snapshot冻结证明动态目录版本
ToolCall runtime原 Step 引用执行期间保持防止晚完成时漂移

正常路径

能力规划同时构造模型可见工具规格与运行时注册表的正常流程
图 10.4-1:一个 PlannedTool 只在一个地方展开成模型合同和 Runtime 绑定,随后随 StepContext 一起冻结。
  1. Runtime 汇合模型能力、Feature、工具模式、MCP revision 和环境可用性,得到 planned tools。
  2. 每个计划项同时贡献 ToolSpec 与 PlannedRuntime;冲突在计划阶段检测,不等调用后才猜测。
  3. ToolPlan 生成 model_visible_specs 和 Registry,并装配成 Step 的 ToolRouter。
  4. 模型只能根据这组 specs 产生 ToolCall;Router 使用同一 Step 的 registry 解释名称与 payload。
  5. 新工具目录只影响下一 Step;旧 ToolCall 完成时继续使用旧 Router。

Hosted Web Search 可以只存在于模型面;Deferred 工具可以先注册搜索信息而不立即可见。正确不变量是 每个模型可返回且需要本地执行的调用都有唯一兼容 Runtime,而不是两个集合简单相等。

Python 风格伪代码

@dataclass(frozen=True)
class ToolPlan:
    specs: tuple[ToolSpec, ...]
    handlers: Mapping[str, ToolHandler]
    generation: int

def build_tool_plan(candidates: Sequence[PlannedTool], generation: int) -> ToolPlan:
    specs: list[ToolSpec] = []
    handlers: dict[str, ToolHandler] = {}
    for tool in candidates:
        if tool.model_visible:
            specs.append(tool.spec)
        if tool.handler is not None:
            if tool.runtime_name in handlers:
                raise DuplicateRuntime(tool.runtime_name)
            handlers[tool.runtime_name] = tool.handler
    validate_local_coverage(specs, handlers)
    return ToolPlan(tuple(stable_sort(specs)), MappingProxyType(handlers), generation)

async def route(call: ToolCall, plan: ToolPlan, context: InvocationContext) -> ToolResult:
    handler = plan.handlers.get(call.name)
    if handler is None:
        return ToolResult.failure(call.call_id, "unsupported local tool")
    return await handler.invoke(call.arguments, context)

validate_local_coverage 必须识别 hosted/deferred 例外;若只做集合相等,会把合法服务端工具误判为 错误,或为了通过校验注册一个永远不会正确执行的空 Handler。

失败与热更新

动态工具目录更新导致旧 ToolCall 被新 Handler 解释的失败路径
图 10.4-2:目录刷新应替换下一代 ToolPlan,不能原地修改正在被 Tool Future 使用的 Registry。
故障模型合同Runtime 状态处理
重复 runtime name不确定两个 Handler 竞争构建计划时拒绝冲突
未知工具名模型可能输出陈旧/非法名无 Handler返回配对失败结果,不 panic
参数不满足 SchemaToolCall 已产生Handler 不应开始副作用Router/Handler 校验并回灌错误
MCP refresh旧 spec 已发给模型新目录可用旧 Step 继续,新 Step 换代
Hosted tool 无本地 Handler合法服务端能力本地 Registry 缺项标记 Hosted,不交本地 Router
Handler 完成很晚原 Step 已结束采样新 generation 已建立Future 保留旧计划引用

设计判断

同快照契约让热更新不需要全局大锁,也使录制回放可以记录 generation。成本是旧计划在并发调用结束前 不能释放,规划层还要表达 Hosted/Deferred/Hidden 等可见性语义。固定工具集的小型 Agent 可以启动时 构建一次,但仍应让 spec 和 handler 从同一声明产生。

证据与 Mini Codex

生产锚点为 core/src/tools/spec_plan.rstools/src/tool_spec.rstools/src/tool_executor.rssession/step_context.rs;6.4—6.13 已覆盖 Spec 组装、动态来源、冲突和 Router。

Mini Codex 的 ToolRegistry generation/ToolPlanner 测试直接制造热更新:

cd examples/mini-codex
uv run pytest -q -k 'tool_plan or registry'

它省略 Hosted Tool 与 Tool Search,只证明本地 Function Tool 的同快照不变量。

本节边界

同快照解决“调用由谁解释”,并没有解决“副作用何时获准、结果怎样提交”。这些属于下一节的工具 生命周期。详细源码映射由研究仓库中的配套索引维护。

阅读导航

上一节:10.3 · 下一节:10.5

评论


← 返回文章列表