雨天小六

读懂 Codex(10.5):工具生命周期与副作用控制

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

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

具体问题与边界

工具调用不是普通函数调用。模型已经产生 ToolCall 时,参数可能非法;审批等待期间可能被取消;进程 启动后可能超时;结果写入前进程可能退出。Runtime 必须知道副作用尚未发生、正在发生还是已经发生, 才能决定 Pre Hook、审批、Post Hook、ToolResult 和 Rollout 的顺序。

本节讨论通用生命周期屏障,不把 Shell、Patch、MCP 各自的内部算法重新展开。

生命周期和提交点

阶段状态所有者可否安全拒绝必须产出
ToolCall acceptedTurn/History完整 Call 记录
payload validationRouter/Handler参数错误 ToolResult
PreToolUse/PolicyHook/Policyallow、rewrite 或 deny
approval waitSession waiter可取消有作用域的 decision
executionExecutionEnvironment通常不可无损撤销stdout/patch/MCP result 或错误
PostToolUseHook engine不能回滚观察、additional context、block follow-up
result commitTurn/Recorder结果已确定同 call_id ToolResult + 事件

正常路径

ToolCall 从校验、Hook、审批、执行到结果回灌的副作用控制时序图
图 10.5-1:所有可阻止副作用的检查都位于执行前;执行后只能编码事实、记录结果并约束后续流程。
  1. 先把模型产生的完整 ToolCall 记入 Turn,使后续失败也有可关联的 call_id。
  2. Router 按 payload variant 解析并校验;失败时不进入执行环境,直接构造 ToolResult。
  3. Pre Hook 可以按受限合同改写输入或阻断;Policy/Approval 再对最终具体动作做判定。
  4. ExecutionEnvironment 获得明确的 cwd、权限、取消 Token 和超时,执行真实副作用。
  5. 输出经过结构化编码与硬上限截断;Post Hook 观察真实结果,但不声称回滚。
  6. ToolResult 以原 call_id 提交 History/Rollout,下一次模型采样只读取已配对结果。

Call 先记录并不表示副作用已经执行。它建立的是协议事实;真正不可逆的提交点由具体工具决定,例如 子进程 spawn、Patch 第一次 replace 或远端 MCP 服务确认写入。

Python 风格伪代码

async def invoke_tool(call: ToolCall, step: StepSnapshot, turn: TurnSnapshot) -> ToolResult:
    await turn.history.append(call)
    try:
        payload = step.tool_plan.parse(call)
        payload = await hooks.run_pre(call.name, payload, turn.cancellation)
        decision = policy.evaluate(payload, turn.permissions)
        await require_approval_if_needed(decision, call.call_id, turn.cancellation)
        raw = await step.tool_plan.execute(payload, turn.cancellation)
        bounded = encode_and_truncate(raw)
        post = await hooks.run_post(call.name, bounded, turn.cancellation)
        result = ToolResult.success(call.call_id, bounded, post.additional_context)
    except RecoverableToolError as error:
        result = ToolResult.failure(call.call_id, encode_error(error))
    except asyncio.CancelledError:
        result = ToolResult.failure(call.call_id, "cancelled after acceptance")
    await turn.history.append(result)
    await turn.rollout.flush_pair(call, result)
    return result

生产实现对不同工具有更细的事件和执行器,但“接受后的每个出口都形成配对结果”是可迁移合同。

失败、取消与部分副作用

工具在审批、执行和结果提交阶段失败时的残留状态图
图 10.5-2:错误处理首先记录残留状态;“返回失败”不等于没有进程、文件或远端副作用。
故障点副作用状态模型应看见恢复动作
payload 非法未发生参数错误 Result允许模型修正参数
Pre Hook/Policy 拒绝未发生明确拒绝 Result不自动升级
Approval 被拒/取消未发生拒绝或取消 Result清理 waiter
进程超时可能已输出/写文件timeout + 有界输出kill、reap、报告残留
Patch 中途失败可能部分文件已改已完成/未完成列表不宣称事务回滚
Post Hook 失败副作用已发生工具事实仍保留Hook 错误单独编码
Result 写入失败副作用已发生、历史未确认不可盲目重跑工具先恢复写入/重建配对

设计判断

通用 Router 应统一配对、Hook、事件和错误编码;具体执行器仍要拥有自己的提交点和清理逻辑。把所有 工具强塞进一个事务接口会制造虚假保证,尤其是 Shell 与远端 MCP 无法被本地 Rollback 撤销。

这种生命周期增加样板代码,但使故障测试可以断言“错误发生后剩下什么”。对纯计算、无副作用工具, 可以缩短 Policy/Approval 路径;仍应保留 call_id 配对和输出上限。

证据与 Mini Codex

生产锚点包括 Tool Router/parallel、Unified Exec ProcessManager、Apply Patch Handler、Hook dispatcher 与 Rollout recorder;6.13—6.20、6.25—6.40 已逐段证明调用链。

Mini Codex 覆盖 Hook rewrite、审批拒绝、超时 kill/reap、Patch 预验证和 ToolResult 配对:

cd examples/mini-codex
uv run pytest -q -k 'hook or approval or timeout or patch or paired'

它没有 PTY、跨平台进程组和真实远端副作用,故障矩阵只适用于已实现边界。

本节边界

本节证明的是副作用的时间顺序。是否允许执行、谁能强制限制资源,必须继续拆成策略、审批和沙箱三层。 详细源码映射由研究仓库中的配套索引维护。

阅读导航

上一节:10.4 · 下一节:10.6

评论


← 返回文章列表