21 · Todo List:任务清单与进度面
§1 · TL;DR
Section titled “§1 · TL;DR”§2 · 四家任务进度面
Section titled “§2 · 四家任务进度面”| 维度 | Codex | Claude Code | OpenClaw | Hermes |
|---|---|---|---|---|
| 核心抽象 | `UpdatePlanArgs`:`explanation` 加 `plan[]`,每项只有 `step` 和 `status` | `TodoWrite` 的 `TodoItem`;Tasks V2 进一步变成 `TaskSchema` 文件任务 | ACP `tool_call` / `tool_call_update` 事件,外加 parent stream 进度转发 | `TodoStore`:session 内存任务表,`todo` tool 读写 |
| 状态集 | `pending` / `in_progress` / `completed` | `pending` / `in_progress` / `completed`,Tasks V2 另有 owner、blocker、metadata | `in_progress` / `completed` / `failed` 来自运行时事件,不是模型自填 checklist | `pending` / `in_progress` / `completed` / `cancelled` |
| 写入方式 | 模型调用 `update_plan` 整体提交当前 checklist | `TodoWrite` 整体写入;Tasks V2 用 `TaskCreate` / `TaskUpdate` 做增量文件任务 | 运行时根据工具和子 agent 执行自然产生事件 | `merge=false` 整表替换;`merge=true` 按 id 更新 |
| 恢复方式 | 事件进入 TUI / app-server 通知;不是长期计划文件 | 旧 `TodoWrite` 可从 transcript 恢复;Tasks V2 存 `~/.claude/tasks/.../*.json` | session event、parent relay、compaction summary 保留 active task status | 从历史里的最近 `todo` tool 响应 hydrate;压缩后只注入未完成项 |
| 与计划关系 | `update_plan` 在 Plan Mode 里直接报错,避免 approval plan 与执行清单混用 | `plans.ts` 管计划文件,`TodoWrite` / Tasks 管执行进度 | 计划和进度通过事件投影分开,todo 不由模型集中持有 | 没有单独 approval plan;`todo` 是轻量执行焦点 |
| 主要风险 | 状态太粗,适合单 agent 执行,不适合多人 claim/block | 能力最强但路径分裂:TodoWrite、Tasks V2、Plan Mode 要讲清边界 | 没有模型托管 checklist,复杂目标可能散在对话和摘要里 | 内存优先,若历史或压缩链路断掉会丢任务面 |
§3 · 四家源码怎么落地
Section titled “§3 · 四家源码怎么落地”Codex · update_plan 是 checklist,不是 Plan Mode
Section titled “Codex · update_plan 是 checklist,不是 Plan Mode”Codex 的命名容易误导:update_plan 听起来像「进入计划模式」,源码里却明确把它定位成 TODO/checklist tool。协议层的数据结构非常小:
Codex codex/codex-rs/protocol/src/plan_tool.rs:9-28 — `update_plan` 的状态机只有三态,item 只有 step 与 status。
pub enum StepStatus { Pending, InProgress, Completed,}
pub struct PlanItemArg { pub step: String, pub status: StepStatus,}
pub struct UpdatePlanArgs { pub explanation: Option<String>, pub plan: Vec<PlanItemArg>,}真正关键的是 handler 里那条硬边界:如果当前 turn 处在 Plan Mode,update_plan 直接拒绝。也就是说 Codex 把「给用户看的 proposal / approval plan」和「执行中的 checklist」拆成两套 surface。
Codex codex/codex-rs/core/src/tools/handlers/plan.rs:75-89 — Plan Mode 中禁用 `update_plan`,成功时发送 `EventMsg::PlanUpdate`。
if turn.collaboration_mode.mode == ModeKind::Plan { return Err(FunctionCallError::RespondToModel( "update_plan is a TODO/checklist tool and is not allowed in Plan mode".to_string(), ));}
session .send_event(turn.as_ref(), EventMsg::PlanUpdate(args)) .await;前端再把这个事件变成可见进度。TUI 的 on_plan_update 会统计 completed / total,刷新状态面,然后把 new_plan_update 写入 history cell。history_cell.rs 里 Completed 用打勾加划线,InProgress 和 Pending 用不同样式的方框。这说明 todo list 不只是 prompt 纪律,而是协议事件、UI 标题、历史记录三层一起工作的产品机制。
Codex 还有一条容易漏掉的链路:Plan Mode 输出的 proposed plan 会被 stream parser 拆成 PlanDelta,最终作为 app-server v2 的 item/plan/delta 推给前端;而 update_plan 成功后走的是 turn/plan/updated。这说明 Codex 同时支持「模型流式提出计划」和「工具提交执行 checklist」两条协议通道,二者不要混成一个状态表。
Claude Code · TodoWrite 管 session,Tasks V2 管团队任务
Section titled “Claude Code · TodoWrite 管 session,Tasks V2 管团队任务”Claude Code 的 TodoWrite prompt 比 Codex 更强势:复杂多步任务、用户显式要求、发现新任务、收到新指令,都应更新 todo;开始某项前先标 in_progress,完成后马上标 completed;状态同时要求 content 和 activeForm,后者是 UI 上显示的现在进行时。
claude-code/src/tools/TodoWriteTool/prompt.ts:147-166 — TodoWrite prompt 把状态、activeForm、单一 in_progress 和完成条件写成硬规则。
- pending: Task not yet started- in_progress: Currently working on (limit to ONE task at a time)- completed: Task finished successfully
Task structure:- content: Imperative form describing what needs to be done- activeForm: The present continuous form shown during execution
- Exactly ONE task must be in_progress at any time- ONLY mark a task as completed when you have FULLY accomplished it旧 TodoWrite 的存储位置是 AppState:按 agentId 或 sessionId 存一份 todo。全部完成时清空,避免下一轮继续携带一张已经结束的任务表。它还会在一次性关闭 3 个以上任务、但没有验证任务时返回 verification nudge,提醒模型别把「写完」误当「完成」。
claude-code/src/tools/TodoWriteTool/TodoWriteTool.ts:53-88 — TodoWrite 在 Todo V2 关闭时启用,写 AppState,并在 allDone 时清空。
isEnabled() { return !isTodoV2Enabled()}
const todoKey = context.agentId ?? getSessionId()const oldTodos = appState.todos[todoKey] ?? []const allDone = todos.every(_ => _.status === 'completed')
context.setAppState({ todos: { ...appState.todos, [todoKey]: allDone ? [] : todos, },})Tasks V2 是另一层:utils/tasks.ts 里 TaskSchema 有 id、subject、description、activeForm、owner、blocks、blockedBy 和 metadata,存到 ~/.claude/tasks/<taskListId>/<id>.json,claim 时还会检查完成态、blocker 和 busy owner。这个设计适合 IDE、多 agent、团队任务,不适合替代轻量 session todo。
Tasks V2 还不是简单的「大号 TodoWrite」。TaskUpdateTool 只有 Todo V2 开启时才启用,输入 schema 能改 status、owner、activeForm、blocks、blockedBy 和 metadata;attachments.ts 还分别对 TodoWrite 和 TaskCreate / TaskUpdate 做「太久没更新」的 reminder 统计。这是两套提醒和恢复纪律:session todo 提醒当前焦点,TaskUpdate 提醒团队任务板别失真。
Hermes · 轻量 todo,压缩后只带未完成项
Section titled “Hermes · 轻量 todo,压缩后只带未完成项”Hermes 的 todo tool 是最小但很实用的一版。它是每个 AIAgent / session 一份内存 TodoStore,支持整表替换和按 id merge,状态多了一个 cancelled。它最值得抄的是压缩恢复策略:format_for_injection() 只注入 pending 和 in_progress,不注入 completed / cancelled,避免压缩后模型把已经做完的事又做一遍。
Hermes hermes-agent/tools/todo_tool.py:90-118 — Hermes 压缩后只注入未完成 todo,已完成或取消的项不会重新进入 active context。
def format_for_injection(self) -> Optional[str]: visible = [ item for item in self._items if item["status"] in ("pending", "in_progress") ] if not visible: return None
lines = ["[Todo List - Continue from here]"]run_agent.py 还会在新 agent 对象拿到历史时,从最近一次 todo tool 响应里 hydrate 回 TodoStore。这比「把 todo 当 memory 写死」更克制:todo 是当前任务面的状态,不是长期偏好;它应该跟 session 走,过期就消失。
Hermes 的 ACP adapter 还补了一层 runtime progress:make_tool_progress_cb() 把 tool.started 变成 ACP ToolCallStart,用每个 tool name 一条 FIFO 队列追踪并发同名工具;make_step_cb() 再从 prev_tools 里取出对应 id,发完成更新。这不是模型 todo,而是工具事实流,适合 UI 展示「现在到底跑到哪一步」。
OpenClaw · 不托管 todo,而是投影进度事件
Section titled “OpenClaw · 不托管 todo,而是投影进度事件”OpenClaw 没有一个等价的模型自管 todo list。它的重点是把工具调用、子 agent 运行和压缩连续性投影成可见事件:ACP translator 在工具开始时发 tool_call、状态 in_progress;工具结束时发 tool_call_update,状态 completed 或 failed。auto-reply projector 再把这些事件转成 channel 里的工具摘要,并对 terminal status 做去重。子 agent relay 也会把 child session 的 progress updates 流到 parent session。
这个选择很合理:OpenClaw 更像多渠道运行时和 operator surface,真实进度往往来自工具、channel、子 agent,而不一定来自模型维护的一张 checklist。代价是复杂用户目标的「还剩什么没做」需要依赖摘要、事件投影和 latest user request,而不是一份统一 todo 表。
OpenClaw 也给了一个反面提醒:事件流不是越多越好,必须能识别「无进展」。tool-loop-detection.ts 会把相同参数且结果不变的 polling、ping-pong 调用和全局重复调用分别标成 warning / critical;父子 session relay 还有 no-output watcher,子 session 长时间没输出时会给 parent session 发 stall 提示。也就是说 runtime progress surface 至少要有 no-progress detector,否则 UI 只是在展示卡死过程。
§4 · 共同点
Section titled “§4 · 共同点”第一,共同点不是「都有 todo list」,而是都把进度从 prose 里拿出来。Codex 用协议事件,Claude Code 用工具状态和任务文件,OpenClaw 用 ACP runtime event,Hermes 用 tool JSON。只在最终回复里写「我接下来会做 A/B/C」不算任务管理,因为 UI、恢复、提醒、压缩都读不到它。
第二,状态机都非常小。pending / in_progress / completed 是最稳定的三态。Hermes 多一个 cancelled,OpenClaw 多一个 failed,Claude Tasks V2 多 owner 和 blocker,但核心还是「未开始、正在做、已结束」。
第三,必须有唯一当前焦点。Codex tool spec 要求最多一个 in_progress;Claude prompt 要求 exactly one;Hermes schema 也写了 only ONE item in_progress。没有这个约束,todo list 会退化成愿望清单,模型不知道下一步该推进哪个。
第四,完成态要谨慎处理。Claude Code 要求只在 fully accomplished 时标完成,还加 verification nudge;Hermes 干脆不把 completed 注回 active context;Codex TUI 把 completed 划掉。完成项是审计痕迹,不应该继续抢注意力。
第五,todo list 要能跨压缩或恢复保留「未完成」。Claude Code 从 transcript 恢复 TodoWrite,Tasks V2 文件持久化;Hermes 从历史 tool response hydrate 并在压缩后重注入未完成项;OpenClaw 的 changelog 专门强调 compaction continuity 要保留 active task status、batch progress、latest user request 和 follow-up commitments。
§5 · 关键差异
Section titled “§5 · 关键差异”模型托管 checklist
- 最适合单 agent coding CLI
- 模型知道目标拆分和下一步焦点
- 实现简单,UI 可直接渲染
- 容易被模型过早标完成
- 不天然适合多人 claim/block
- 需要提醒和恢复机制
文件化 Task 系统
- 适合团队任务、IDE、长任务
- owner / blocker / metadata 可审计
- 进程重启后仍可恢复
- 复杂度明显上升
- 要处理锁、迁移、并发 claim
- 不应拿来替代轻量当前焦点
运行时事件流
- 真实反映工具和子 agent 执行
- 适合多渠道 operator UI
- 失败和完成来自外部事实
- 模型没有统一剩余任务表
- 需要 compaction summary 兜住语义目标
- 事件多时要去重和聚合
压缩注入焦点
- 长上下文 agent 最实用
- 只带未完成项,避免重复做完工
- 实现成本低
- 依赖历史或压缩链路
- 缺少持久审计
- 任务 id 质量取决于模型
最容易误做的是把三层合并成一个「全局计划」:
- Approval plan:用户批准前的设计或执行计划,可能不会被执行。
- Execution todo:当前 turn / session 正在推进的 checklist,必须随着事实更新。
- Durable task:后台、团队、跨进程任务,需要 owner、锁、blocker、重试和持久化。
Codex 和 Claude Code 都在源码里把这三层拆开了。自己做 agent 时如果只做一张巨大表,会同时背上审批、执行、恢复、审计、团队协作五种责任,最后每一种都不可靠。
§6 · 评分
Section titled “§6 · 评分”| 系统 | 价值 | 理由 | 风险 |
|---|---|---|---|
| Codex | 最干净的最小模型 | `update_plan` schema 小,Plan Mode 中禁用,事件进入 TUI 和 app-server;非常适合 coding CLI 当前任务进度。 | 状态粗、无 owner/blocker,不适合团队任务或长后台任务。 |
| Claude Code | 最完整的 IDE 任务面 | `TodoWrite` 有使用纪律、activeForm、提醒、恢复;Tasks V2 又补上文件化、owner、blocks、claim。 | TodoWrite、Tasks V2、Plan Mode 三套概念必须解释清楚,否则用户和模型都会混用。 |
| OpenClaw | 最适合 operator 进度投影 | 工具调用、工具更新、子 agent relay 都变成可投影事件,适合多渠道 UI 和父子 session。 | 没有模型自管 checklist,复杂目标的剩余工作需要靠摘要和 active task continuity 兜底。 |
| Hermes | 最轻的长上下文恢复 | 一个内存 TodoStore 加压缩后只注入未完成项,就解决了很多重复做工和丢焦点问题。 | 内存优先,若历史恢复和压缩注入不稳定,todo 状态也会丢。 |
§7 · 复刻方案
Section titled “§7 · 复刻方案”Todo List / Progress Surface 复刻方案
最小可行
- 定义一个小 schema:`id?`、`content`、`status`、可选 `activeForm`;状态先只做 `pending` / `in_progress` / `completed`。
- 暴露一个 `update_todo` 或 `update_plan` 工具;工具结果同时写入事件流和当前 session state。
- 强制最多一个 `in_progress`;完成前必须有外部验证或明确的执行证据。
- UI 渲染当前 checklist,并在标题或状态栏显示 completed / total。
- 上下文压缩后只重新注入 pending / in_progress,不注入 completed。
进阶
- 加 transcript restore:从最近一次 todo tool call 或事件恢复当前任务表。
- 加 reminder:复杂任务连续 N 个 assistant turn 没更新 todo,就插入内部提醒。
- 加 verification nudge:一次关闭多个任务但没有验证项时,提醒模型先跑检查。
- 加 file-backed tasks:当需要 owner、blocks、claim、跨进程协作时,再引入持久任务文件和锁。
- 加 runtime progress events:工具 start / update / terminal status 单独投影给 operator UI。
- 加 no-progress detector:相同结果的 polling、ping-pong 或子 session 无输出要告警,不要只显示进度。
一开始别做
- 不要把 Plan Mode 和 todo list 混成一张表;一个是批准前方案,一个是执行中状态。
- 不要把 todo 写进长期 memory;当前任务结束后它应该自然过期。
- 不要靠最终回复里的自然语言当进度源;压缩、恢复、UI 都读不到。
- 不要让多个任务同时 `in_progress`;这会让下一步焦点失效。
- 不要把 completed 项反复注入 active context;模型会重复做已经结束的工作。
§8 · 架构图
Section titled “§8 · 架构图”§9 · 源码索引
Section titled “§9 · 源码索引”§10 · 小练习
Section titled “§10 · 小练习”- 给自己的 agent 加一个
update_task_progresstool,schema 限定为content、status、activeForm,并验证多个in_progress会被拒绝。 - 把 todo 更新写入事件流,再在 CLI 或 Web UI 上渲染 completed / total,而不是只让模型在文字里汇报。
- 做一次上下文压缩:确认 completed 项不会重新注入,pending / in_progress 项会带着当前顺序回来。
- 模拟用户先要求「给我计划,不要改代码」,再要求「开始执行」:确认 Plan Mode 输出和 execution todo 是两套数据。
- 加一个提醒规则:复杂任务连续 10 个 assistant turn 没更新 todo 时,插入内部 reminder,但不要把 reminder 泄露给用户。
§11 · 面试题:10 道带答案的高频考点
Section titled “§11 · 面试题:10 道带答案的高频考点”Q1 · 概念:为什么 todo list 不是 Plan Mode?
Plan Mode 的核心是 approval:在用户允许之前,agent 输出的是候选方案,可能会被修改、拒绝或完全不执行。Todo list 的核心是 execution:任务已经开始,状态必须随着真实行动更新。把二者混成一张表,会让「还没批准的计划」看起来像「正在执行的事实」。
Codex 在源码里给了最清晰的边界:update_plan handler 在 Plan Mode 中直接返回错误,错误文本明确说它是 TODO/checklist tool,不允许在 Plan Mode 里用。Claude Code 也类似,plans.ts 管计划文件,TodoWrite 和 Tasks 管执行任务。追问:用户说「先列 todo」怎么办?答:如果只是让 agent 展示计划,那是 proposal;只有开始执行后,才把它变成 execution todo。
Q2 · 设计:为什么要限制最多一个 in_progress?
Todo list 的第一职责是保留当前焦点。如果允许多个 in_progress,模型每次恢复上下文时都不知道下一步该推进哪一个,UI 也无法显示「当前正在做什么」。这会把 checklist 变成愿望清单。
Codex tool spec 写的是 at most one,Claude prompt 写得更强:exactly ONE。Hermes schema 也强调 only ONE item in_progress。实际实现可以选择「最多一个」或「复杂任务执行中必须一个」,但不能允许无限多个。追问:并行工具调用怎么办?答:并行工具调用属于某一个当前任务内部的实现细节,不等于同时推进多个 top-level todo。
Q3 · UI:Claude Code 的 activeForm 有什么价值?
content 通常是祈使句,例如「Run tests」;activeForm 是现在进行时,例如「Running tests」。后者直接服务 UI:spinner、状态栏、任务面板显示的是「正在做什么」,而不是「要做什么」。
这看起来像小字段,其实是在 schema 层把执行态和待办态分开。没有 activeForm,UI 只能把待办标题硬改成进行时,或者显示很生硬的文本。追问:所有 agent 都要加吗?答:纯 CLI 可以先不加;有 IDE、Web UI、移动通知时值得加。
Q4 · 恢复:为什么 completed 项不应该反复注入上下文?
Completed 项已经失去执行价值,只剩审计价值。把它反复注入 active context,会消耗 token,也会诱导模型重复解释或重复执行已经完成的工作。Hermes 的 format_for_injection() 只注入 pending / in_progress,就是这个原因。
Claude Code 旧 TodoWrite 在 allDone 时把 session todo 清空。Codex TUI 把 completed 划掉,也是视觉上降低注意力。追问:那审计怎么办?答:审计应该在 transcript、event log、task file 或 history cell 里,不应该塞进当前 prompt 的高优先级区域。
Q5 · 恢复:TodoWrite 为什么要从 transcript 恢复?
非交互或 SDK 场景下,进程状态不一定稳定,AppState 可能没有旧 todo。Claude Code 的 sessionRestore.ts 扫 transcript 里最后一次 TodoWrite tool_use,把 todo list 恢复出来,保证 resume 后仍知道未完成工作。
这说明 todo list 虽然不是长期 memory,但也不能只存在模型脑子里。最小实现至少要让它进入可重放事件或 transcript。追问:恢复哪个版本?答:恢复最近一次成功提交的 todo 状态,不要从多次零散自然语言里推断。
Q6 · 取舍:什么时候需要 file-backed Tasks,而不是轻量 TodoWrite?
当任务需要跨进程、跨 IDE 窗口、跨 agent 或多人协作时,轻量 session todo 不够。你需要 task id、文件持久化、owner、blocks / blockedBy、claim、busy check 和迁移逻辑。Claude Code Tasks V2 就是这个方向。
反过来,单 agent coding CLI 不应该一开始就引入 file-backed task 系统。Codex 的三态 checklist 足够表达当前执行焦点,复杂度低很多。追问:后台 cron 算哪种?答:cron / background task 更接近 durable task,要有持久化和重试;当前 turn 的 checklist 只负责「我现在在做哪一步」。
Q7 · OpenClaw:为什么 OpenClaw 更偏进度事件,而不是模型托管 todo?
OpenClaw 的场景是多渠道运行时:Telegram、Discord、gateway、子 agent、ACP 都可能参与。真实进度常常来自工具开始、工具结束、子 session relay、delivery 状态,而不是模型手写的一张表。把这些 runtime facts 投影成事件,比让模型复述进度可靠。
代价是「目标还剩什么」需要靠 compaction continuity、latest user request、batch progress 和 follow-up commitments 这类摘要机制保存。追问:能不能也加 todo?答:可以,但应该作为 execution todo 的补充,而不是替代 runtime event stream。
Q8 · API:replace 和 merge 两种 todo 写入方式怎么选?
Replace 简单:每次提交完整当前表,适合 Codex 式小 checklist,避免局部更新导致旧项残留。Merge 灵活:按 id 更新已有任务,适合 Hermes 这种允许模型只补充或改动部分项的工具,也适合长任务逐步发现新分支。
风险也相反。Replace 容易误删未完成项,所以模型每次要带全量;Merge 容易留下陈旧项,所以必须有 id、去重和清理规则。追问:MVP 用哪个?答:单 agent MVP 用 replace;需要长时间任务或外部系统修改同一张表时再加 merge。
Q9 · 验证:为什么完成 todo 前要有 verification nudge?
Agent 最常见的错觉是「我改完了」等于「任务完成了」。Claude Code 在一次关闭多个 todo、但没有验证任务时返回 verificationNudgeNeeded,本质上是把测试、lint、HTTP smoke 或人工可见证据纳入完成条件。
这条规则比看起来重要:todo list 如果能被模型随手标 completed,却不要求证据,它会变成自我安慰列表。追问:所有完成都要跑测试吗?答:不一定;但至少要有与任务风险匹配的验证证据,比如命令输出、页面截图、HTTP 响应或明确说明未验证。
Q10 · 总结:一个 production agent 的 todo/progress surface 至少要有哪些层?
最小五层:一是小 schema,三态加内容;二是单一当前焦点,限制 in_progress;三是事件化,让 UI 和 transcript 读得到;四是恢复和压缩,只保留未完成工作;五是完成验证,不让模型无证据地关闭任务。
再往上做三层:reminder,长时间不更新时提醒;durable task,跨进程/团队协作用;runtime progress event,工具和子 agent 的真实执行状态单独投影。追问:最忌讳什么?答:把计划、todo、memory、cron、审计全塞进一张万能表。那不是任务管理,是边界丢失。