DeepSeek Harness 源码解析 · 第 27 讲
DeepSeek Harness 源码解析系列 第 27 讲:Todo 与交互审批(interaction) 基于 DeepSeek Harness 源码 · 2026-09-10 💡 本讲一句话: Agent 的"任务清单"不是存在某个数据库里的状态,而是 session 事件日志上的一次次整表快照;而"要不要让人类拍板"则走一条 fail-closed 的审批链——没人应答时宁可拒绝也不放行。读完你能带走三件事:todo_write 为什么每次都要发完整列表、approval/request 为什么必须包在 turn 里、ask_user_question 为什么只允许根 agent 调用。 一、todo_write:整表替换,而不是增量更新 第 26 讲我们看了 Goal 与 Plan——那是"目标层"的规划。本讲的 todo_write 是更细粒度的"任务清单":模型每做完一步就更新一次,UI 实时渲染进度条。源码在 packages/todo/tool-todo/src/index.ts (共 226 行)。最反直觉的设计是:每次调用都发送 完整列表 ,直接替换上一版——没有局部更新、没有按条目编辑。先看校验函数: 📄 packages/todo/tool-todo/src/index.ts (第 91-111 行) function toTodoList(raw: { content: string; status: string }[], allowParallel: boolean): TodoItem[] { // 校验函数:把模型给的原始列表规范化成合法 TodoItem[],任何脏数据在这里 fail loud const todos: TodoItem[] = [] // 输出容器——逐条 push 规范化后的任务项 const seen = new Set<string>() // 去重集合:以 trim 后的 content 为键,模型常把同一任务写两遍 let active = 0 // in_progress 计数器——循环结束后按并发策略裁决是否超限 for (const item of raw) { // 逐条处理原始输入(schema 已保证字段存在) const content = item.content.trim() // 先 trim:首尾空格在这里归一,空白串在下一步被拦下 if (content.length === 0) { // 空内容分支——日志里不允许出现无意义条目 throw new Error('invalid todo: `content` must be a non-empty string') // fail loud:拒绝整次写入而不是静默丢弃该条 } if (seen.has(content)) { // 重复分支——同一任务出现两次会让 UI 进度条和计数失真 throw new Error(`invalid todos: duplicate content ${JSON.stringify(content)}`) // 报错信息带上具体内容,方便模型自我修正后重试 } seen.add(content) // 登记去重键——后续同内容条目会命中上面的重复分支 if (item.status === 'in_progress') active++ // 只统计 in_progress;pending/completed 不受并发策略约束 todos.push({ content, status: item.status as TodoItem['status'] }) // schema 已保证 status 合法,cast 只是把该契约记录在类型上 } if (!allowParallel && active > 1) { // 单活跃纪律分支:非并发部署只允许一个 in_progress throw new Error(`invalid todos: at most one task may be in_progress (got ${ac…