TodoWrite

显式计划让长任务可见、可纠正。

todo_add / todo_update / todo_list。

50 行3 个工具

问题

让 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 会话记忆的核心思想,在这里首次出现。