具体问题与边界
Pre Hook 改参数或阻断、Tool 执行、Post Hook 观察失败时,怎样保持一个 Call 对应一个模型可见 Result?
ToolHookRunner 串行运行 Pre/Post Hook;Router 保留 Call/Result 所有权;Hook 生命周期事件用于诊断,不替代 ToolResult。 本节区分“从官方源码得到的生产事实”和“为教学实现作出的 Python 选择”,不会把后一种包装成 Codex 的等价实现。
状态所有权
| 对象 | 所有者 | 生命周期 | 持久化 |
|---|---|---|---|
| Pre/Post hook list | ToolHookRunner | ToolPlan/Runtime 生命周期 | 否 |
| mutable arguments copy | Pre chain | 单 ToolCall | 否 |
| ToolExecution | Handler | 单 ToolCall | 转换成 ToolResult |
| ToolHookEvent list | HookRunner | 诊断生命周期 | Mini 仅内存 |
| ToolResult | Router | 跨后续 Step | 持久化 |
正常路径
- Router 找到 Handler 后先把 ToolCall.arguments 复制给 Pre Hook 链;Hook 不能直接改写规范 ToolCall Item。
- 每个 Pre Hook 可返回新 arguments 或 blocked_reason。下一个 Hook 看到前一个改写;任一阻断立即停止 Tool 执行。
- Pre Hook 抛异常按 fail-closed 处理:形成 blocked error Result,同时记录 HookEvent。
- Tool 正常执行后调用 Post Hook。Post Hook 失败不删除已经得到的 tool output;Mini 将失败说明追加并标记 Result error。
- 所有分支最终仍由 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)
失败、取消与恢复
| 故障点 | 已留下的状态 | 处理 |
|---|---|---|
| Pre rewrite 返回坏参数 | Tool 自己验证后错误 | 同 call_id error Result |
| Pre blocked | Tool 未执行 | blocked_reason 进入 Result |
| Pre exception | 不继续副作用 | fail-closed Result + HookEvent |
| Tool exception | Post 不运行 | tool failed Result |
| Post exception | Tool 副作用可能已经发生 | 保留 output,追加 hook failure 并标 error |
| Interrupt | 未执行后续 Call | aborted 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_lifecycletest_tool_result_is_paired_before_follow_up_sample
官方源码导航
- codex-rs/hooks/src/events/pre_tool_use.rs:Pre Tool 输入与输出合同
- codex-rs/hooks/src/events/post_tool_use.rs:Post Tool 事件合同
- codex-rs/hooks/src/engine/dispatcher.rs:Hook 调度、matcher 与结果
- codex-rs/core/src/hook_runtime.rs:Core 接入和 additional context
- codex-rs/core/src/tools/tool_dispatch_trace.rs:工具分派生命周期追踪
Mini Codex 对照
src/mini_codex/tools/hooks.py:Pre/Post Protocol、Outcome 与 Eventsrc/mini_codex/tools/router.py:Hook 与 ToolResult 配对src/mini_codex/tools/planner.py:同 ToolPlan 注入 HookRunner
本节结论
Hook 无论改写、阻断还是失败,都不能让已接收 ToolCall 在模型历史中失去 Result;Post 失败不能抹掉可能已经发生的 Tool 副作用。
评论
登录后即可评论