可靠性工程不是给 agent 外面包一层 try/catch。生产级 coding agent 要把每一步都设计成“可观察、可中断、可恢复、可证明没有重复副作用”的动作。
本课的核心模型是:先保护工作区,再执行工具;先记录检查点,再尝试重试;先判断副作用是否幂等,再恢复任务。Pi 源码已经给了几个重要锚点:provider 层有 timeoutMs、maxRetries 和 AbortSignal;AgentSession 层有 auto-retry、abort、session persistence;bash 工具有 timeout、输出截断和进程树取消;extension 示例里有 dirty repo guard。我们要做的是把这些点组合成自己的可靠性策略。
阶段四的 forge-code task create "修复登录测试失败" 已经能跑起来,但真实仓库会不断遇到这些情况:
- 模型流式请求超时,assistant message 只写了一半。
npm test卡住,子进程还在后台跑。edit已经改了一个文件,第二个工具失败。- agent 在用户本来就有未提交修改的工作区里继续写代码。
- 用户按下取消后,UI 停了,但 shell 命令或 retry sleep 没停。
- 重启进程后,不知道上次任务是可以继续、应该回滚,还是必须人工接管。
这节课回答一个问题:怎么让 agent 失败时停在一个可解释、可检查、可恢复的位置,而不是留下不可追踪的半成品。
可靠性可以拆成五个控制面:
第一是时间边界。模型请求、WebSocket 连接、bash 命令、外部 API、子 agent 任务都要有 timeout。没有 timeout 的任务不叫长期任务,只是无人看守的挂起状态。
第二是重试边界。重试只适合 transient failure,例如 network_error、429、503、timeout。如果错误已经产生不可重复副作用,例如 commit、push、npm publish,就不能直接重放同一个动作。
第三是幂等边界。读操作天然更容易重试;写文件可以通过“先读版本、再 diff、再写入”接近幂等;发布、提交、外部工单更新必须有 idempotency key、状态查询或人工确认。
第四是工作区边界。coding agent 不应该默认拥有当前目录里的所有改动。执行前要检查脏工作区,区分用户已有修改、agent 本轮修改、测试产生的缓存文件。
第五是恢复边界。每次任务都要留下 session、trace、checkpoint、changed files、last successful step 和 failure classification。恢复不是“继续 prompt 一下”,而是基于这些证据决定 resume、retry、rollback、handoff。
1. 先把失败分类
Section titled “1. 先把失败分类”不要一上来写重试循环。先把失败分成四类:
- 可重试:provider overloaded、网络断开、HTTP 5xx、短暂 timeout。
- 不可重试但可恢复:测试失败、类型错误、工具参数不合法。
- 有副作用的 partial failure:文件写了一半、commit 创建了、外部系统状态变了。
- 用户控制流:用户取消、权限拒绝、脏工作区阻断。
这个分类决定后续动作。比如测试失败不是“系统不可靠”,而是 agent 应该读失败输出继续修复;npm publish 成功后网络断开则不能简单重试,因为包可能已经发布。
2. 读 Pi 的 timeout 入口
Section titled “2. 读 Pi 的 timeout 入口”Pi 在 packages/ai/src/types.ts 的 provider options 里定义了 timeoutMs、websocketConnectTimeoutMs、maxRetries 和 maxRetryDelayMs。这些是模型请求层的时间和重试边界。
在 coding agent 层,SettingsManager 把这些配置暴露成 retry.provider.timeoutMs、retry.provider.maxRetries、httpIdleTimeoutMs、websocketConnectTimeoutMs。这说明生产 agent 不能把 timeout 写死在代码里,要允许不同团队按网络环境和模型供应商调整。
3. 读 Pi 的 session auto-retry
Section titled “3. 读 Pi 的 session auto-retry”AgentSession 会检查 assistant error message,识别 overloaded、rate limit、429、500、502、503、504、network error、connection lost、timeout 等可重试错误,然后用指数退避准备下一次 continue。
关键点是:它会把错误 assistant message 从 agent state 里移除,但 session history 仍然保留失败证据。这是一种很实用的恢复策略:模型下一次继续时不被失败 message 污染,但审计时仍能看到失败发生过。
4. 读 bash timeout 和取消
Section titled “4. 读 bash timeout 和取消”bash 工具的 timeout 不是简单返回错误。它会启动子进程,注册 AbortSignal,timeout 或 abort 时杀掉 process tree,最后把已收集输出、截断信息、full output path 和错误状态一起返回。
这比“命令失败了”更有用,因为恢复时要知道命令是否已经跑出部分输出、是否被截断、是否超时、是否由用户取消。
5. 把 partial failure 设计成检查点
Section titled “5. 把 partial failure 设计成检查点”coding agent 的一次任务通常不是一个原子事务。比如:
read files -> edit file A -> edit file B -> run tests -> update summary如果 edit file A 成功、edit file B 失败,不应该假装整个任务没发生。正确做法是记录:
- 完成到哪一步。
- 哪些文件被修改。
- 下一步是否可安全重试。
- 是否需要先跑
git diff或恢复 worktree。 - 是否需要用户决定保留还是丢弃已有改动。
6. 把用户中断作为正常路径
Section titled “6. 把用户中断作为正常路径”用户取消不是异常小角落,而是 coding agent 的核心控制流。取消要同时打断 retry sleep、provider stream、tool execution、bash subprocess 和后台 task runner。否则 UI 停了,工作区仍在变化。
Pi 的 AgentSession.abort() 同时调用 abortRetry()、agent.abort() 并等待 idle;abortBash() 取消正在运行的 bash 命令。这些锚点说明:取消要向下传递,不要只改 UI 状态。
本课 lab starter 是 /Users/ryanchen/codespace/pi-agent-course-lab/src/20-reliability-scenarios.ts。它已经把场景压缩成四类:
type Scenario = "timeout" | "dirty-worktree" | "tool-failure" | "user-cancel";课堂实践不是把这张表扩成更长的表,而是把每个 scenario 变成一个可执行的恢复决策:
timeout:记录 partial trace,标记 task retryable,但先确认没有重复副作用。dirty-worktree:在写入前停止,报告 changed files,让用户决定归属。tool-failure:记录 tool name、参数摘要、错误类别,判断 retryable 还是 permanent。user-cancel:取消 provider 和 tools,持久化 checkpoint,让 worktree 保持可检查。
课堂代码推演
Section titled “课堂代码推演”先从 starter 的 mitigation() 出发,把“建议列表”升级成“恢复计划”:
type RecoveryPlan = { action: "retry" | "stop" | "resume" | "handoff"; retryable: boolean; requireUserDecision: boolean; evidence: string[];};
function planRecovery(scenario: Scenario, changedFiles: string[]): RecoveryPlan { if (scenario === "dirty-worktree") { return { action: "stop", retryable: false, requireUserDecision: true, evidence: ["git status --porcelain", ...changedFiles], }; }
if (scenario === "timeout") { return { action: "retry", retryable: true, requireUserDecision: changedFiles.length > 0, evidence: ["trace.jsonl", "last tool call", "timeoutMs"], }; }
if (scenario === "user-cancel") { return { action: "handoff", retryable: false, requireUserDecision: true, evidence: ["checkpoint", "abort reason", "changed files"], }; }
return { action: "resume", retryable: false, requireUserDecision: false, evidence: ["tool name", "args summary", "stderr"], };}课堂上重点看变量怎么流:
scenario来自 runtime 观察,不是模型猜测。比如 timeout 来自 provider 或 bash executor;dirty worktree 来自git status --porcelain。changedFiles决定能不能自动继续。只要有未归属写入,就不要盲目 retry。retryable只回答“技术上能不能重试”,不代表“应该立刻重试”。evidence是恢复入口。下一次 resume 时先读 evidence,再决定继续或交给用户。
然后把它接到任务 runner:
const before = await gitStatus();if (before.dirty) return planRecovery("dirty-worktree", before.files);
try { await runAgentTask({ timeoutMs: 120_000 });} catch (error) { const after = await gitStatus(); return planRecovery(classify(error), after.files);}这里的控制点在任务开始前和异常捕获后。开始前保护用户修改,失败后保护 agent 已产生的证据。
/Users/ryanchen/codespace/pi-agent-course-lab/src/20-reliability-scenarios.ts:本课 lab starter,用场景表建立恢复策略入口。/Users/ryanchen/codespace/external-sources/pi/packages/ai/src/types.tsat61babc2:StreamOptions/ImagesOptions定义signal、timeoutMs、maxRetries、maxRetryDelayMs。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/settings-manager.tsat61babc2:RetrySettings、provider retry 配置、HTTP idle timeout、配置迁移和 settings 持久化。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session.tsat61babc2:AgentSession处理 session persistence、auto-retry、abort()、abortRetry()、abortBash()。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/bash-executor.tsat61babc2:统一 bash 执行结果,记录 output、exitCode、cancelled、truncated 和 fullOutputPath。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/bash.tsat61babc2:bash tool 的 timeout、AbortSignal、进程树取消、输出截断和错误包装。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/dirty-repo-guard.tsat61babc2:通过session_before_switch/session_before_fork在脏仓库里阻断 session 切换。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/agent-session-retry.test.tsat61babc2:auto-retry 行为的测试锚点。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/suite/agent-session-bash-persistence.test.tsat61babc2:bash 取消和 aborted assistant message 持久化的测试锚点。
学完本课后,你应该能做到:
- 解释 timeout、retry、abort、幂等、partial failure、恢复策略之间的区别。
- 判断一个错误是否适合自动重试,还是应该停止并要求用户介入。
- 在任务开始前设计脏工作区保护,避免覆盖用户未提交修改。
- 为工具失败设计 evidence schema,而不是只返回一段错误字符串。
- 说明为什么用户取消必须向 provider、tool、bash subprocess 和 retry sleep 传播。
- 为自己的
forge-code设计一个最小 checkpoint:task id、step、changed files、last event、failure class、resume action。
- 实践题:扩展
/Users/ryanchen/codespace/pi-agent-course-lab/src/20-reliability-scenarios.ts,把mitigation()改成返回RecoveryPlan,并为timeout、dirty-worktree、tool-failure、user-cancel各写一个示例输入输出。 - 思考题:如果一次任务已经完成
git commit,但在生成 PR 描述时网络失败,为什么不能简单从头重试整条任务链?