TodoWrite
显式计划让长任务可见、可纠正。
todo_add / todo_update / todo_list。
问题
让 Agent 做一个 10 步任务:"重构这个模块,跑测试,提交 PR。"
模型在上下文里默默规划,然后一头扎进去。半小时后你发现:它把第 2 步和第 4 步的顺序搞反了,第 5 步干脆忘了。你无法看到它"打算做什么",只能在它做错之后才纠正。
如果计划是模型私有的思考,用户就失去了方向盘。计划必须外化成可见、可改的状态。
解决方案
一个会话级的 todo 列表 + 三个工具:todo_add(加条目)、todo_update(改状态)、todo_list(看全貌)。计划写在列表里,而不是模型脑子里。
const todos = []; // [{ id, content, status: "pending" | "in_progress" | "completed" }]
define("todo_add", { type: "object", properties: { content: { type: "string" } }, required: ["content"] },
({ content }) => { const t = { id: todos.length + 1, content, status: "pending" }; todos.push(t); return JSON.stringify(t); });
define("todo_update", { type: "object", properties: { id: { type: "number" }, status: { type: "string", enum: ["pending", "in_progress", "completed"] } }, required: ["id", "status"] },
({ id, status }) => { const t = todos.find((x) => x.id === id); if (!t) return `no todo #${id}`; t.status = status; return JSON.stringify(t); });
define("todo_list", { type: "object", properties: {}, required: [] },
() => todos.length ? todos.map((t) => `[${t.status}] #${t.id} ${t.content}`).join("\n") : "(empty)");
工作原理
第 1 步:让 todo 出现在系统提示词里,这样模型每次 step 都能看到当前计划——这是"可见"的关键。把列表渲染成固定格式追加到 system 消息:
function renderPlan() {
return todos.length
? "当前计划:\n" + todos.map((t) => `[${t.status}] #${t.id} ${t.content}`).join("\n")
: "(暂无计划。对于多步任务,请先用 todo_add 制定计划。)";
}
第 2 步:在系统提示词中写下使用规则——计划是给谁看的?给用户看的。
const SYSTEM = `
你是编程助手。对于多步任务:
1. 先 todo_add 制定计划;
2. 每完成一步,todo_update 标记 completed;
3. 计划变化时更新条目,不要新建重复条目。
${renderPlan()}
`;
第 3 步:循环不变。工具执行照旧,但模型现在有了一面"计划墙",用户随时能打断纠正:"第 3 步不用做,直接跳到第 4 步。"
todo 列表本身只是一个数据数组——机制的价值在于它被模型和用户共同看见、共同修改。
试一下
运行 s05,给一个多步任务:
- "制定计划:1) 读 README 2) 跑测试 3) 总结结果。按计划执行"
观察重点:模型是否真的先建计划再动手?中途打断它(输入"跳过第 2 步")能否生效?注意 todo 状态在每一轮都随系统提示词刷新——如果它不刷新,模型就看不见自己改过的计划。
以下内容基于 packages/todo 的核查。DSH 的 todo 是单一产品包(一个会话拥有一份列表,无可替换的提供者契约):
- 模型工具:
packages/todo/tool-todo—— 存储并展示会话的 todo 列表,注册在ctx.tools上。 - 持久化:todo 更新作为 session 事件写入会话日志(事件载荷见 docs/subsystems/session.md)——不是内存数组。
关键差异:
| 你的最小实现 | DSH 的真实实现 |
|---|---|
const todos = [] 内存数组 |
todo 写入会话事件日志,崩溃/恢复/回放后计划仍在 |
| 手动拼进 system 消息 | 通过系统提示词组装器的命名变量/区块注入(s10) |
| 规则写在 SYSTEM 字符串里 | 工具 schema + 模型体验描述是产品的正式契约 |
一句话:todo 是最简单的"外化状态"示例。它的生产形态要求状态持久化进事件日志——这正是 s09 会话记忆的核心思想,在这里首次出现。