青雲的博客
拆开 Codex 第六部:Agent runtime 怎样被承载与验证 第 36 章

没有证据的“跑通”不算跑通

一次命令返回 0 只说明某个观察面没有报错。本章把 source、确定性测试、真实本地二进制、rollout、trace、OTel 与终端投影放进同一张证据账本,也把外部服务、审批沙箱和平台跳过明确留在账本之外。

源码版本
rust-v0.144.6
验证日期
Commit
5d1fbf26c43abc65a203928b2e31561cb039e06d

前面三十五章已经把入口、Turn、工具、会话、Thread、Realtime 和 Hook 拆开了。到这里,最容易出现一种假完成:命令返回了 0,页面也出现了文字,于是我们说“整个 Agent 跑通了”。

这句话缺少三个东西:跑通的是哪一个 claim,谁拥有这个 claim,观察结果落在哪一层。没有这三项,passed 只是一个没有上下文的布尔值。

flowchart LR
  accTitle: 从源码主张到可复核证据
  accDescr: 每个主张先绑定固定源码,再用确定性测试或真实本地任务产生观察物;观察物只能覆盖明确的层,剩余边界必须单独列为未证明。
  CLAIM["claim: 这件事应该成立"] --> SOURCE["pinned source: owner 与 contract"]
  SOURCE --> TEST["deterministic test or trace"]
  TEST --> OBS["observed artifact: request / file / rollout / screen"]
  OBS --> LAYER["observed layer"]
  LAYER --> BOUNDARY["unproven boundary"]
  SOURCE -. "不能代替运行" .-> OBS
  OBS -. "不能自动扩大范围" .-> BOUNDARY

先把“跑通”写成一张账本

我用下面这张表作为本章的阅读规则。每一行都必须能回答五个问题:主张是什么,源码 owner 在哪里,本次有没有确定性测试或 trace,实际看到了什么,还有哪一段没有被覆盖。没有运行证据时必须明确写成 source-known,不能用预期测试或可用 backend 冒充本次观察。

claimsource ownerdeterministic test / traceobserved layerunproven boundary
-copenai_base_url 真正进入 exec binaryTestCodexExecBuilder::cmd_with_server 与 CLI 的 config path本地 codex-exec 命名 E2E进程启动、Wiremock request真实 provider 的认证、路由和配额
模型能看到可调用的 exec contractTestCodexExecBuilder 之外的 Responses Lite request 构造与 fixture assertion首个 captured request 检查 additional_toolswire body 与 tool schema随机模型是否会选择它
nested ApplyPatch 的副作用与 ownercode-mode runtime 与 rollout-trace reducer文件 SHA-256 与 trace replay本地文件、reduced tool graphapproval/sandbox 真实策略
rollout 保存顶层 exec、JS source 与 outputexec suite 的 rollout lookup helper 与 JSONL writer对同一 rollout JSONL 做 substring 检查durable text recordstructured nested ApplyPatch owner
resume --last 续写同一个会话extract_conversation_id 对 SessionMeta 的读取第二次命令与路径/UUID 比较rollout identity、append size跨版本迁移和用户选择其他 rollout
trace graph 能还原 inference、code cell、tool ownerThreadTraceContextRolloutTracereplay_bundlereplay 每个 bundle 并检查 status/ownerreduced graphtrace 初始化失败时的完整性
GuardianWarning 从 core 到终端每层有各自契约core emitter、app-server protocol、TUI target、ChatWidget四个独立 named testsevent queue、JSON、target、history cellemitter-to-wire-to-render 的单次真实链路
OpenTelemetry(OTel)能观察成功/失败和敏感字段策略SessionTelemetrysource-known;本次未运行 instrumentation test 或现场采样本次无 telemetry 观察物telemetry 丢失不等于业务没有执行

这张表故意把“源码解释”和“运行观察”分开。SourceEvidence 能证明固定版本里存在某个 owner 和分支,但它不声称这条分支在本次运行被走到;测试能证明一个输入被处理,却不自动证明上游发出了这个输入。后文引用的 VT100-compatible terminal backend 也只说明仓库提供了可观察终端内容的测试能力;本次没有把它登记成已运行截图或端到端终端采样。

本节源码依据(3 处)

一条本地任务,必须把事实串起来

本章的主实验不是把十几个孤立测试排成列表,而是让一个真实编译出来的 codex-exec 连续完成一件受控工作。固定源码基线是 openai/codexrust-v0.144.6,commit 为 5d1fbf26c43abc65a203928b2e31561cb039e06d。runner 先确认 tag peel 和 HEAD 都指向这个 commit,再从它创建 detached 临时 worktree;fixture 只改两个文件。每次运行都在同一个 scratch root 下另建唯一的 CARGO_TARGET_DIR,它是 source worktree 的同级目录,不会让两个实验共享旧编译产物。

这个 tag 的 workspace manifest 已是 0.144.6,锁文件里的 132 个 local package 仍是 0.0.0。runner 因而先在临时 worktree 执行 cargo update --workspace --offline,只允许 132 组本地版本替换,以及 fixture 新增的两条 dependency edge。无依赖 metadata 核对 package 数量和版本后,还要用 --locked --all-features --filter-platform <host> 解析一次完整依赖图;两层校验都通过,才进入 just test

运行命令固定为:

: "${BLOG_ROOT:?先执行第六部导读的准备脚本,或把 BLOG_ROOT 指向博客仓库根目录}"
: "${SOURCE_ROOT:?先执行第六部导读的准备脚本,或把 SOURCE_ROOT 指向固定 Codex checkout}"
test -f "$BLOG_ROOT/package.json"
test -f "$BLOG_ROOT/scripts/verify-codex-handbook-e2e.ts"
cd "$BLOG_ROOT"
pnpm verify:handbook:e2e -- --source-dir "$SOURCE_ROOT"

fixture 的 SHA-256 是 3c8149b62ba0f2428307c6f99d12351a8766a1bc649d3442150b10ff777a662f。这不是装饰性版本号:runner 在实验开始前记录固定 source checkout 的 status、diff、untracked fingerprint 和 worktree 列表;patch 只应用到临时 worktree。finally 移除临时 worktree 后,runner 再捕获一次固定 checkout 状态并与起点比较,要求它保持原样。

任务的六个观察点

  1. 测试通过真实 -c 路径注入 openai_base_url="{wiremock}/v1",而不是在 core helper 里直接替换 provider。
  2. fake Responses provider 返回固定 SSE:第一条 response 是顶层 exec custom tool call,第二条是工具输出后的完成消息,第三条供 resume --last 使用。
  3. 第一份 captured request 必须同时包含用户 marker 和 additional_tools 中的 exec contract;contract 的 format 是 Lark grammar,描述里明确出现 apply_patch
  4. exec 的 JavaScript 运行时执行 tools.apply_patch,写入 handbook-e2e-side-effect.txt,内容固定为 handbook e2e side effect\n;SHA-256 必须是 a907eb01bd391443cf4b48174c04ec6155d728d3d59aa29d99f08b8559e9908e
  5. 测试从 sessions 目录找到包含初始 marker 的 rollout JSONL,对 prompt、顶层 exec call id、JavaScript 字面量 tools.apply_patch、custom tool output 和 completed response 做子串检查;随后运行 codex exec resume --last,要求同一路径、同一 conversation UUID 且文件长度增加。
  6. 两次运行产生的 trace bundle 都交给 codex_rollout_trace::replay_bundle。每个 bundle 的 rollout status 必须是 Completed,总计三次 completed inference、一个 code cell 和一个成功的 nested ApplyPatch;tool requester 必须是 code_cell:handbook-e2e-exec

tests/fixtures/codex-handbook-e2e.patch:200 里的 rollout JSONL 校验只是 substring 子串检查:它证明顶层 exec 标识、JavaScript 中的字面量 tools.apply_patch、custom tool output 与完成标记被持久化,不能单独证明 structured nested ApplyPatch 的 owner。tests/fixtures/codex-handbook-e2e.patch:254 才对每个 bundle 做 trace replay,用 ToolCallKind::ApplyPatch、执行状态和 ToolCallRequester::CodeCell 证明 nested ApplyPatch 的 requester。

这里有一个值得单独记下的名称差异:模型看到的是顶层 execapply_patch 是 code-mode JavaScript 里嵌套的 runtime tool。把它们写成“模型直接调用 apply_patch”,会把模型可见 owner 和运行时 owner 合并掉,trace 的 requester assertion 也就失去意义。

本节源码依据(4 处)

实验结果:通过的是一条受控证据链

runner 通过 just test 得到的 nextest 输出是:

PASS [  0.42s] suite::resume::handbook_full_task_config_request_patch_rollout_resume_trace
Summary: 1 tests run: 1 passed, 68 skipped

runner 从测试输出中的唯一 HANDBOOK_E2E_SUMMARY 解析出下面的结果:

观察项结果它确实说明了什么
Responses request count3初始请求、tool follow-up、resume 请求都被 fake provider 收到
changed filehandbook-e2e-side-effect.txt本地工作目录发生了预期文件副作用
side-effect SHA-256a907eb01bd391443cf4b48174c04ec6155d728d3d59aa29d99f08b8559e9908e副作用内容是固定字节,而不是只检查文件存在
trace bundle count2初始运行和 resume 各留下一个可 replay bundle
completed inferences3reducer 图里三次 inference 都收到了 Completed 状态
code cell count1所有 bundle 合并后只有 code_cell:handbook-e2e-exec
completed nested apply_patch1一个 ApplyPatch 调用成功,且 requester 是上述 code cell
resume same rollouttrue第二次 prompt 追加到同一 rollout identity
rollout identity本次运行动态生成的 UUIDrunner 只验证 UUID 格式,以及 resume 前后仍是同一个 identity

trace event classes 至少包括 rollout_startedinference_startedinference_completedcode_cell_startedcode_cell_initial_responsecode_cell_endedtool_call_startedtool_call_endedrollout_ended。事件类名可以帮助定位缺哪一层,但它们本身不等于 payload 已经被正确关联,所以 runner 还要 replay graph 并检查 code-cell、tool requester、execution status 和 raw payload references。

这条链的强度来自交叉对账:request 里的 marker 要能在 rollout 找到,rollout 里的 tool call 要能在 trace graph 找到,文件 hash 要与 tool result 相符,resume 的 prompt 要落在同一 SessionMeta identity 下。任何一项孤立地通过,都不能推出其余项。

本节源码依据(4 处)

四个 GuardianWarning 绿灯,为什么仍然不是 E2E

第 31 章已经用 GuardianWarning 说明了 TUI 的 thread routing。这里把同一事件横着再看一遍,是为了给“相邻测试不能自动拼成端到端”一个具体例子。

固定 commit 导出的测试副本中有四个都能独立通过的测试:

named test测试主动构造了什么绿灯只覆盖到哪里
core emitterguardian_review_surfaces_responses_api_errors_in_rejection_reasonmock Responses API 返回 400,再读取 Session event queueGuardianWarning 包含底层 API error;不证明 app-server 接收或投影
app-server wireverify_guardian_warning_notification_serialization直接构造 ServerNotification::GuardianWarningJSON-RPC method、threadIdmessage 的序列化;不触发 core emitter
TUI routingguardian_warning_notifications_route_to_threads直接构造带 ThreadId 的 notificationtarget classifier 返回对应 Thread;不证明 app-server 发过这条消息
TUI renderlive_app_server_guardian_warning_notification_renders_message手工把 notification 送进 ChatWidgethistory cell 显示文字;不证明 routing、wire 或 core 发生

四条命令可以这样运行:

: "${ARCHIVE_CODEX_RS:?先执行第六部导读的 archive 准备脚本}"
cd "$ARCHIVE_CODEX_RS"
just test --locked -p codex-core guardian_review_surfaces_responses_api_errors_in_rejection_reason
just test --locked -p codex-app-server verify_guardian_warning_notification_serialization
just test --locked -p codex-tui guardian_warning_notifications_route_to_threads
just test --locked -p codex-tui live_app_server_guardian_warning_notification_renders_message

固定 commit 导出的测试副本的实际结果是四条命令各执行 1 test、各 1 passed。nextest 同时报告 core 2968 skipped、app-server 946 skipped、两次 TUI 各 2991 skipped;这些数字只是被名称过滤掉的 suite inventory,不是额外通过的业务合同,也不能填补四层之间的交接。

它们分别是四种 layer test,不是一个共享 fixture 的 emitter-to-wire-to-render 旅程。要把它们升级成 E2E,必须让 core 真的发出 event,让 app-server 的 listener 真的选择 projection,让客户端真的收到 JSON,并在正确 Thread 的 widget 中渲染;当前四个测试没有共同的运行实例,也没有共同的 event id 可对账。

本节源码依据(4 处)
本节源码依据(4 处)

这四层之间还有一个容易漏掉的 replay 边界:TUI 的 event_is_notice 把 GuardianWarning 识别为可显示 notice,但这只是 buffered UI event 的筛选规则;它不从 rollout JSONL 重新生成事件,也不保证被淘汰的 buffer 还能恢复。UI replay 和 rollout replay 是两套 owner。

本节源码依据(2 处)

证据强度:不是一条自动升级的直线

不同证据不是从“弱”线性升级到“强”。它们回答的问题不同:unit test 适合验证折叠规则,wire fixture 适合验证字段名,snapshot 适合验证 projection,local binary 适合验证本机跨模块路径;真实外部服务、OTel、trace 和截图则各自增加另一种观察面。

证据面能回答不能回答
source / SourceEvidence固定版本里谁拥有状态、分支和 contract本次运行是否走到该分支
unit test一个函数或 reducer 在给定输入下的局部语义上游是否真的调用它
wire fixtureJSON-RPC method、字段名、枚举和 schemalistener 是否选择了这条消息
snapshot / VT100当前 projection 产生了哪些 cell 或终端文字emitter、wire、工具副作用
local binary本机真实 CLI、配置、进程、文件与持久化能否闭合外部账号、服务端行为、所有平台
real external service认证、网络、服务端路由与真实响应随机模型输出的稳定性和可重复性
OTelspan/log/metric 是否记录成功、失败、token 和脱敏后的 prompt业务副作用一定发生,或 telemetry 一定送达
rollout trace事件能否严格还原成带 owner 的 reduced graph没有 trace 时业务一定没发生
screenshot某一 viewport 的最终视觉投影隐藏状态、请求体、磁盘内容和跨平台行为
skipped test当前环境明确没有执行某个分支该分支通过了

“Skipped” 要单独放在账本里。它不是失败,也不是通过;它是一个待补证据。把 skip_if_no_network!skip_if_sandbox!skip_if_wine_exec! 后面的 test count 当作 pass,会把环境前提误写成业务结论。

本节源码依据(3 处)

OpenTelemetry(OTel)与 rollout trace:两个观察面,不是一份日志

OTel 记录“发生过什么类型的观测”

SessionTelemetry 的 metadata 包含 conversation id、auth mode、originator、session source、model、terminal type 和是否记录用户 prompt。Responses event 会把 function call 的 tool name、completed token usage 和 SSE 成功/失败分别写入 instrumentation;prompt 默认可以只记录长度和计数,具体文本由 log_user_prompts 控制,关闭时写成 [REDACTED]

这对排查“服务端返回了什么、某次 SSE 是否失败”很有用,但 OTel 仍是旁观者。一个 codex.sse_event success=true 不拥有文件写入;一个 telemetry exporter 失败也不代表 Turn 没有完成。业务 owner 仍然是 core/session、tool runtime 或 rollout writer。

本节源码依据(5 处)

rollout trace 记录“能否被严格还原”

ThreadTraceContext::start_root_or_disabledCODEX_ROLLOUT_TRACE_ROOT 启动 bundle,且 trace 初始化失败时只记录 warning 并禁用 trace;这说明 trace 是诊断证据,不是 session 可用性的前置条件。启用后,context 为 code cell、tool dispatch 和 inference attempt 分别创建 handle。reducer 读取 manifest 和 raw event log,遇到不符合 owner 顺序的事件就失败,而不是用当前 active thread 猜一个归属。

本实验因此同时检查两件事:trace bundle 存在,以及 replay 后的 graph 关系正确。只 ls trace.jsonl 不能证明 ApplyPatch 属于 code cell,也不能证明 inference status 是 Completed。

本节源码依据(3 处)

这次 E2E 明确没有覆盖什么

把边界写出来,结论才不会膨胀成宣传语。

未证明边界原因下次需要的证据
真实 OpenAI Responses API测试把 base URL 指向本地 Wiremock,SSE 序列完全固定带隔离账号、配额和网络审计的外部服务实验
随机模型的规划与工具选择fake provider 直接返回 exec call,不让模型自行决定版本化 prompt/response 采样和人工审阅;不能拿随机输出当稳定 contract
approval 与 sandbox命令使用 --dangerously-bypass-approvals-and-sandbox,只为闭合受控副作用独立 approval UI、Permission Profile、sandbox policy 的 integration/E2E
TUI、Realtime、Hooks、多 Agentfixture 只穿过 exec、code cell、apply_patch、rollout、resume、trace各自 owner 的跨层任务,并为事件/Thread/transport 建立共同 correlation id
发布产物运行的是本机 checkout 编译的 codex-execnpm、Homebrew、桌面包和目标平台的 artifact verification
Windows、Wine 和无网络环境某些 upstream test 在这些条件下显式 skip在对应 runner 上真正执行,而不是把 skip 计作 pass
Trace 可用性在故障时的保证trace 初始化是 best-effort;失败会禁用诊断而不阻止 session专门测试磁盘满、权限错误和 partial bundle 的可观测性 contract

固定源码里还有一个很直观的 skipped-test 例子:Guardian 的 Responses API 错误测试先调用 skip_if_no_network!,真实 host-native denial suite 又同时检查 sandbox 与 Wine 前提。它们说明测试作者知道外部条件是边界;它们没有说明边界已经被本地运行覆盖。

本节源码依据(1 处)

读完整本书时,最后只保留这条纪律

读源码时先写 claim,再找 owner;跑测试时先看实际执行了几个 test,再看 passed;看日志时把 request、side effect、rollout、trace 和 UI projection 分开;遇到 skip 就把它标成缺口;遇到 fake provider、bypass flag 或平台限制,就在结论里原样保留。

这本小册最后留下的不是“Codex 已经被完全证明”这句话,而是一套可以继续复用的账本格式:

claim
  -> pinned source owner
  -> named deterministic test / trace
  -> observed artifact
  -> observed layer
  -> unproven boundary

第 31 至 35 章的 TUI、Thread、MultiAgentV2、Realtime 和 Hooks,已经分别给出了自己的状态与生命周期;本章把它们放回证据边界,不把它们拼成一条不存在的线性流水线。到这里全书结束。后续如果源码版本变化,应该新建一次带新 commit、新 fixture 和新实验记录的账本,而不是把这次本地通过结果延伸成永久保证。