具体问题与边界
模型看见的是名称、描述和 JSON Schema,真正执行的是 Registry 中的 Handler。如果这两张表分别从 可变全局状态构造,模型可能按旧 Schema 生成参数,Runtime 却交给新 Handler。Codex 的 ToolPlan 在一个 Step 内同时产出模型可见规格和执行路由,时间一致性比“是否能查到同名函数”更重要。
本节不要求可见规格集合与本地 Handler 集合完全相等:Hosted、Deferred 和隐藏工具是明确例外。
合同与状态所有权
| 对象 | 创建点 | Step 内可变性 | 作用 |
|---|---|---|---|
| planned tools | 能力/Feature/MCP 规划 | 创建后冻结 | 描述候选工具及来源 |
| model-visible specs | ToolPlan | 冻结 | 发给模型的调用合同 |
| runtime registry | 同一 ToolPlan | 冻结 | 名称到执行器的绑定 |
| ToolRouter | StepContext | 冻结 | 解析 ResponseItem 并分派 |
| MCP catalog revision | binding snapshot | 冻结 | 证明动态目录版本 |
| ToolCall runtime | 原 Step 引用 | 执行期间保持 | 防止晚完成时漂移 |
正常路径
- Runtime 汇合模型能力、Feature、工具模式、MCP revision 和环境可用性,得到 planned tools。
- 每个计划项同时贡献 ToolSpec 与 PlannedRuntime;冲突在计划阶段检测,不等调用后才猜测。
- ToolPlan 生成
model_visible_specs和 Registry,并装配成 Step 的 ToolRouter。 - 模型只能根据这组 specs 产生 ToolCall;Router 使用同一 Step 的 registry 解释名称与 payload。
- 新工具目录只影响下一 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。
失败与热更新
| 故障 | 模型合同 | Runtime 状态 | 处理 |
|---|---|---|---|
| 重复 runtime name | 不确定 | 两个 Handler 竞争 | 构建计划时拒绝冲突 |
| 未知工具名 | 模型可能输出陈旧/非法名 | 无 Handler | 返回配对失败结果,不 panic |
| 参数不满足 Schema | ToolCall 已产生 | 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.rs、tools/src/tool_spec.rs、tools/src/tool_executor.rs 和
session/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 的同快照不变量。
本节边界
同快照解决“调用由谁解释”,并没有解决“副作用何时获准、结果怎样提交”。这些属于下一节的工具 生命周期。详细源码映射由研究仓库中的配套索引维护。
评论
登录后即可评论