雨天小六

读懂 Codex(9.13):Tool Hook、生命周期事件和失败结果回灌

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

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

具体问题与边界

Pre Hook 改参数或阻断、Tool 执行、Post Hook 观察失败时,怎样保持一个 Call 对应一个模型可见 Result?

ToolHookRunner 串行运行 Pre/Post Hook;Router 保留 Call/Result 所有权;Hook 生命周期事件用于诊断,不替代 ToolResult。 本节区分“从官方源码得到的生产事实”和“为教学实现作出的 Python 选择”,不会把后一种包装成 Codex 的等价实现。

状态所有权

对象所有者生命周期持久化
Pre/Post hook listToolHookRunnerToolPlan/Runtime 生命周期
mutable arguments copyPre chain单 ToolCall
ToolExecutionHandler单 ToolCall转换成 ToolResult
ToolHookEvent listHookRunner诊断生命周期Mini 仅内存
ToolResultRouter跨后续 Step持久化

正常路径

Tool Hook、生命周期事件和失败结果回灌正常路径图
图 9.13-1:Pre Hook 改参数或阻断、Tool 执行、Post Hook 观察失败时,怎样保持一个 Call 对应一个模型可见 Result?
  1. Router 找到 Handler 后先把 ToolCall.arguments 复制给 Pre Hook 链;Hook 不能直接改写规范 ToolCall Item。
  2. 每个 Pre Hook 可返回新 arguments 或 blocked_reason。下一个 Hook 看到前一个改写;任一阻断立即停止 Tool 执行。
  3. Pre Hook 抛异常按 fail-closed 处理:形成 blocked error Result,同时记录 HookEvent。
  4. Tool 正常执行后调用 Post Hook。Post Hook 失败不删除已经得到的 tool output;Mini 将失败说明追加并标记 Result error。
  5. 所有分支最终仍由 Router 用原 call_id 构造 ToolResult,后续模型能判断是策略、Hook、Tool 还是未知工具失败。

顺序为什么不能交换

Router 找到 Handler 后先把 ToolCall.arguments 复制给 Pre Hook 链;Hook 不能直接改写规范 ToolCall Item
→ 每个 Pre Hook 可返回新 arguments 或 blocked_reason
→ Pre Hook 抛异常按 fail-closed 处理:形成 blocked error Result,同时记录 HookEvent
→ Tool 正常执行后调用 Post Hook
→ 所有分支最终仍由 Router 用原 call_id 构造 ToolResult,后续模型能判断是策略、Hook、Tool 还是未知工具失败

箭头代表可见性与所有权转移,不是松散依赖。Policy、Approval、外部副作用、规范 Item 和 durability 各自有提交点,后一步不能替前一步作更强承诺。

Python 风格伪代码

async def dispatch_one(call, ctx):
    handler = snapshot_handlers.get(call.name)
    if handler is None:
        return error_result(call.call_id, "unknown tool")

    pre = await hooks.run_pre(call, ctx)
    if pre.blocked_reason:
        return error_result(call.call_id, pre.blocked_reason)

    try:
        execution = await handler.execute(pre.arguments, ctx)
    except TurnCancelled:
        return aborted_result(call.call_id)
    except Exception as exc:
        return error_result(call.call_id, f"tool failed: {exc}")

    post_failure = await hooks.run_post(call, execution, ctx)
    if post_failure:
        return ToolResult(call.call_id,
                          output=execution.output + post_failure,
                          is_error=True)
    return ToolResult(call.call_id, execution.output, execution.is_error)

失败、取消与恢复

Tool Hook、生命周期事件和失败结果回灌失败路径图
图 9.13-2:每个失败点都列出已经发生的副作用和仍可安全执行的恢复动作。
故障点已留下的状态处理
Pre rewrite 返回坏参数Tool 自己验证后错误同 call_id error Result
Pre blockedTool 未执行blocked_reason 进入 Result
Pre exception不继续副作用fail-closed Result + HookEvent
Tool exceptionPost 不运行tool failed Result
Post exceptionTool 副作用可能已经发生保留 output,追加 hook failure 并标 error
Interrupt未执行后续 Callaborted Result 后向 Turn 传播取消

不变量

Hook 无论改写、阻断还是失败,都不能让已接收 ToolCall 在模型历史中失去 Result;Post 失败不能抹掉可能已经发生的 Tool 副作用。

设计思路与限制

Mini 选择 Pre fail-closed、Post 保留 output 并标错,这是可测试的教学策略;官方 Hook schema、trust、matcher、command timeout、additional context 和 UI lifecycle 更完整。

测试与复现

cd examples/mini-codex
uv run pytest -q -k 'test_pre_tool_hook_rewrites_arguments_and_emits_lifecycle or test_tool_result_is_paired_before_follow_up_sample'
uv run mypy src
  • test_pre_tool_hook_rewrites_arguments_and_emits_lifecycle
  • test_tool_result_is_paired_before_follow_up_sample

官方源码导航

Mini Codex 对照

  • src/mini_codex/tools/hooks.py:Pre/Post Protocol、Outcome 与 Event
  • src/mini_codex/tools/router.py:Hook 与 ToolResult 配对
  • src/mini_codex/tools/planner.py:同 ToolPlan 注入 HookRunner

本节结论

Hook 无论改写、阻断还是失败,都不能让已接收 ToolCall 在模型历史中失去 Result;Post 失败不能抹掉可能已经发生的 Tool 副作用。

阅读导航

上一节:9.12 · 下一节:9.14

评论


← 返回文章列表