Planner 的价值不是生成一段看起来很完整的 TODO。它要把模糊工程任务拆成可执行、可验证、可恢复的 task graph。
本课的核心模型是:每个 step 都要有依赖、输入、执行者、验证方式和停止条件。执行过程中每完成一个 checkpoint 就记录状态;失败时根据失败类型选择 replan、retry、fallback 或 stop。这样 agent 才能处理跨文件、跨测试、跨审查的工程任务,而不是在长对话里凭感觉继续。
第 21 课已经把角色拆出来了。现在的问题是:主 agent 怎么知道下一步派谁?什么时候可以并行?什么时候必须等待测试?什么时候应该重做计划?
例如用户输入:
实现 trace replay,并补测试和文档。这不是一步任务。它至少包含源码调查、接口设计、实现、测试、文档、审查。Planner 要把这些动作变成图,而不是只输出“先看代码,再修改,再测试”。
本课从 /Users/ryanchen/codespace/pi-agent-course-lab/src/12-planner-task-graph.ts 出发,把 Step[] 扩展成带 checkpoint、replan 和 stop condition 的任务系统。
Planner 可以分成三层:
TaskGraph:描述任务之间的依赖关系,回答“哪些 step 可以执行”。ExecutionState:记录哪些 step 完成、失败、跳过、等待确认。ControlPolicy:决定失败后是 retry、replan、fallback 还是 stop。
最重要的不是 plan 文本,而是 step 的字段:
type Step = { id: string; title: string; dependsOn: string[]; owner: "researcher" | "planner" | "implementer" | "tester" | "reviewer"; verification: string; checkpoint: string; stopCondition?: string;};当 done 集合变化时,nextReadySteps(steps, done) 就能算出下一批可执行 step。失败时,planner 读 failedStep、evidence 和 remainingSteps,决定是否改图。
1. 从线性计划升级到 task graph
Section titled “1. 从线性计划升级到 task graph”starter 的 createPlan(feature) 返回四步:
inspect -> implement -> test -> review这是最小可用链。把它升级成 graph 后,可以表达并行:
inspect -> design -> implement -> test -> reviewdocs depends on designdocs 不一定要等 test 完成,但必须等 design 稳定。task graph 让 planner 明确这种关系。
2. Step planning 要包含验证方式
Section titled “2. Step planning 要包含验证方式”一个 step 如果没有验证方式,就不能自动判断是否完成。verification 不是泛泛写“检查通过”,而要写可收集的证据:
source anchors listeddiff touches only src/runtime and testsnpm test -- trace passesreview findings are empty or acknowledgeddocs page contains usage example
验证方式决定 tester/reviewer 后续要收什么证据。
3. Checkpoint 是恢复点,不是装饰字段
Section titled “3. Checkpoint 是恢复点,不是装饰字段”checkpoint 记录的是“完成到哪里可以恢复”。例如:
inspect.done: 已列出源码锚点implement.done: 已产生 difftest.failed: 测试失败日志已保存review.blocked: 需要用户确认 API 取舍有 checkpoint,长任务被中断后就不用从头读全部上下文。主 agent 可以从 checkpoint 恢复,或者把 checkpoint 交给另一个 subagent。
4. Replan 只在信息变化时发生
Section titled “4. Replan 只在信息变化时发生”不要每失败一次就重写全计划。replan 的触发条件应该明确:
- 源码调查发现原假设不成立。
- 实现 step 发现需要新增依赖或修改公共 API。
- 测试失败暴露缺少前置 step。
- reviewer 发现安全或架构问题。
- 用户改变范围。
replan 的输出也应该是 graph patch:新增 step、删除 step、修改依赖、修改验证方式,而不是重新输出一整段计划。
5. Stop condition 防止无限循环
Section titled “5. Stop condition 防止无限循环”Planner 必须知道什么时候停止:
- 所有 required steps 完成且验证通过。
- 同一 step 连续失败达到上限。
- 失败原因需要用户输入。
- 权限不足,例如需要写文件但当前是 readonly。
- 任务范围变大,超过当前执行预算。
stop condition 是 agent 可靠性的关键。没有 stop condition,replan 会变成无限“再试一次”。
6. 失败回退要保留证据
Section titled “6. 失败回退要保留证据”失败不是只有 retry。常见回退策略:
retry:同一步重跑,例如网络抖动或测试偶发失败。replan:改任务图,例如发现需要先改类型定义。fallback:换更小实现,例如先做只读 trace viewer,不做 replay。stop:需要用户确认或权限不足。
每次失败都要把命令、输出、diff、上下文摘要写进 evidence,供下一轮 planner 使用。
本课实践基于 /Users/ryanchen/codespace/pi-agent-course-lab/src/12-planner-task-graph.ts。starter 里已经有 createPlan(feature) 和 nextReadySteps(steps, done)。
先用最小图理解 ready step:
const plan = createPlan("trace replay");const done = new Set<string>();
nextReadySteps(plan, done); // inspect
done.add("inspect");nextReadySteps(plan, done); // implement然后扩展成可执行状态机:
type StepState = "pending" | "running" | "done" | "failed" | "blocked";
type ExecutionState = { done: Set<string>; failed: Map<string, string>; checkpoints: Record<string, string>;};
function completeStep(state, step, evidence) { state.done.add(step.id); state.checkpoints[step.id] = evidence;}变量流:steps 是静态任务图;done 是动态执行状态;nextReadySteps() 用依赖关系和 done 集合计算下一步;checkpoints 保存为什么可以认为某步完成。
课堂代码推演
Section titled “课堂代码推演”课堂上从 starter 的四步计划开始:
const steps = createPlan("trace replay");const done = new Set<string>();
while (true) { const ready = nextReadySteps(steps, done); if (ready.length === 0) break;
for (const step of ready) { const result = await runStep(step);
if (result.ok) { done.add(step.id); saveCheckpoint(step.id, result.evidence); continue; }
const decision = decideFailure(step, result); if (decision.type === "retry") await retry(step); if (decision.type === "replan") steps = applyGraphPatch(steps, decision.patch); if (decision.type === "stop") return summarizeBlocked(step, result); }}这段代码要让学生看到三个控制点:
nextReadySteps()是调度控制点,防止依赖没完成就执行。saveCheckpoint()是恢复控制点,防止中断后丢证据。decideFailure()是 replan/stop 控制点,防止失败后盲目继续。
再看一个 replan 例子:
// test 失败:缺少 trace fixtureconst patch = { add: [ { id: "fixture", title: "Add trace replay fixture", dependsOn: ["inspect"], verification: "fixture is loaded by test", }, ], update: [ { id: "test", dependsOn: ["implement", "fixture"] }, ],};这里 planner 没有推翻整个计划,只是在图里补了一个前置 step,并让 test 依赖它。学生要带走的模型是:replan 是对 task graph 的补丁,不是把历史抹掉重来。
/Users/ryanchen/codespace/pi-agent-course-lab/src/12-planner-task-graph.ts:本课 lab starter,展示Step、createPlan()和nextReadySteps()。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/plan-mode/README.mdat61babc2:Plan Mode 示例说明只读规划、计划提取、进度追踪、[DONE:n]标记和持久化。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/plan-mode/index.tsat61babc2:Plan Mode extension 的主实现,包含 plan/execution 模式切换、工具限制、上下文注入和完成追踪。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/plan-mode/utils.tsat61babc2:extractTodoItems()、extractDoneSteps()、markCompletedSteps()和isSafeCommand()等纯函数。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/plan-mode-utils.test.tsat61babc2:plan mode 工具函数的测试入口,用于验证计划提取和完成标记逻辑。/Users/ryanchen/codespace/external-sources/pi/packages/agent/src/agent-loop.tsat61babc2:低层 agent loop 的停止与继续边界,可对照 planner 的 step loop。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session.tsat61babc2:session 层处理 retry、abort、compaction 和事件,是把 step 执行结果落到长期会话状态的参考。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/subagent/index.tsat61babc2:subagent chain/parallel 模式可作为 task graph 执行器的参考。
学完本课后,你应该能做到:
- 把一个模糊 feature spec 拆成包含依赖关系的 task graph。
- 为每个 step 写出 owner、verification、checkpoint 和 stop condition。
- 用
done集合和依赖关系计算下一批 ready steps。 - 解释 replan、retry、fallback、stop 的区别,并知道什么时候触发。
- 设计失败证据格式,让后续 planner 能基于真实信息改图。
- 实践题:扩展
/Users/ryanchen/codespace/pi-agent-course-lab/src/12-planner-task-graph.ts,给Step增加owner、checkpoint、stopCondition,并实现一个applyGraphPatch()支持新增 step 和更新依赖。 - 思考题:为什么 planner 失败后不应该总是重新生成一整份计划?什么时候应该用 graph patch,什么时候应该停止等待用户确认?