Appearance
Run 怎样安全暂停再继续:Pause、Checkpoint 与 Pending Writes
前置:Retry / Idempotency / Unknown Outcome;Run Lifecycle State Machine
核心问题:进程退出后,Run 为什么还能从正确的位置继续,而不会丢状态或重复已经完成的副作用?
审批可以跨小时:服务释放 worker,保存 pending approval;批准事件到达后新进程从 checkpoint 继续。每个 graph super-step 后 checkpoint,能在 crash 后从最近完成批次继续,而不是重跑全部历史。
Pause / Resume
Pause 不是让 Python 协程一直挂着,而是把当前运行转换成一个可以释放进程资源、以后再恢复的 durable state。
例如等待人工审批:
text
RUNNING
→ Tool 需要 approval
→ 提交 checkpoint + approval_request
→ Run = WAITING_APPROVAL
→ Worker 释放
两小时后:
approval event
→ 新 Worker load checkpoint
→ Run = RUNNING
→ 从下一安全边界继续所以 Resume 的关键不是“重新调用 run()”,而是恢复时能判断哪些结果已经提交、哪些动作尚未发生,避免把已完成副作用再做一次。
Checkpoint Boundary
Checkpoint 是一个内部状态彼此一致、可以安全恢复的切面,不是随便把当前对象序列化一下。
如果 Node A 已写入结果,但 Scheduler 的 completed_nodes 还没更新,就不能把这两个不同版本一起保存成 checkpoint。恢复后 Runtime 会得到一个自相矛盾的世界。
边界越细,恢复时重复工作越少,但写放大越高;边界越粗,存储便宜,但 crash 后需要重新计算更多内容。选择粒度时要同时考虑恢复成本和外部副作用风险。
🔍 源码对照:Checkpoint 是 prepare → persist → commit 的恢复边界
精确定位
- 文件:
source/host/runner/turn-settle.ts - Symbol:
persistCheckpoint()→persistStepCheckpoint()→persistFinalState() - 深入索引:Code Atlas:06-durable-settlement
ts
async function persistCheckpoint(ctx, checkpoint, finalize) {
await transcriptMirror?.prepareCheckpoint(ctx, checkpoint, finalize)
try {
await agentStore.handleCheckpoint(ctx, checkpoint)
} catch (e) {
await transcriptMirror?.abortCheckpoint(ctx)
throw e
}
await transcriptMirror?.commitCheckpoint(ctx)
}对照理解
- 这里不是简单
save(checkpoint):先 prepare transcript mirror,再提交 agent state;主存储失败时 abort 已准备的 mirror。 - 这体现了 checkpoint 是一致恢复边界,而不是定时序列化 Python 对象。
- 如果主 checkpoint 已持久化但 transcript commit 失败,源码专门抛
TranscriptAppendAfterCheckpointError,说明“两个事实之间的 crash window”必须显式建模。
源码说明
这个案例有 conversation state、durable file policy、storage、crash guard 多条持久化相关边界,恰好适合反向检查本文的核心问题:Checkpoint 不是“把内存对象 pickle 一下”,而是要决定哪些事实必须在 pause/crash 后仍然成立。阅读源码时不要追求找到一个名为 CheckpointStore 的类,而要找 durable fact 的写入边界。
工程边界
从 reconstruction 看到的持久化材料不能直接证明它实现了通用 exactly-once resume;尤其外部 side effect 仍需独立 execution record / reconciliation。
Shadow Harness 对照
- Checkpoint 只保存恢复所需的 authoritative state + version,不把临时 task/future 当 durable state。
- WAITING_APPROVAL 等 pause reason 与 pending side-effect metadata 必须可恢复。
验证
在 tool side effect success 与 checkpoint commit 之间注入 crash;恢复时必须识别 outcome unknown,而不是无脑重新执行。
Pending Writes
Pending writes 记录同一并行 step 中已成功子任务的 durable output,即使整个 step 因 sibling 失败尚未形成完整 checkpoint。恢复时据此避免重跑成功节点,LangGraph 当前 persistence 就使用这一思想。
要确保这些写与执行记录有稳定 identity。
一个 Approval Pause 的完整状态快照
假设并行 step 中:search_logs 已成功,prepare_patch 已生成 patch,apply_patch 需要人工批准。此时 Runtime 不应该让 worker 挂几个小时,而是保存类似:
json
{
"run_id": "r-42",
"state_version": 17,
"status": "WAITING_APPROVAL",
"committed": ["search_logs"],
"pending_writes": {
"prepare_patch": "artifact://patches/p-9"
},
"waiting": {
"type": "approval",
"node_id": "apply_patch",
"approval_id": "a-7"
}
}然后释放 worker。两小时后 approval event 到达,新进程读取 checkpoint,把 prepare_patch 的 durable output 视为已经存在,只继续 apply_patch。
这里最容易出错的是 crash window:Tool 已经成功,但结果还没进入 checkpoint。如果 Tool 有外部副作用,resume 不能仅凭“checkpoint 里没有成功记录”就盲目重试;还需要 execution identity、pending write 或 reconciliation 来判断真实世界是否已经发生过动作。Checkpoint 解决“从哪里恢复”,并不自动解决“外部动作是否恰好执行过一次”。
Failure Modes
Checkpoint 没有形成一致恢复边界
症状: snapshot 已写但 node/tool result 没写,或反过来。
为什么危险: resume 不知道哪一步已提交,可能重复副作用或跳过工作。
处理: checkpoint 与必要 execution record/pending writes 在明确一致边界提交;resume 只调度未提交部分。
如何验证
| Case | 断言 |
|---|---|
| Checkpoint 没有形成一致恢复边界 | 在不同提交点注入 crash,恢复后 logical node 已提交副作用最多一次。 |
关联源码
自测
- Tool 已成功产生副作用,但进程在写 checkpoint 前 crash。恢复时为什么既不能假设没执行,也不能直接重放?
- Checkpoint 必须和哪些 pending writes / execution identity 一起形成一致恢复边界,才能避免丢状态或重复副作用?