DeepSeek Harness · 编排与子 Agent

workflow / schedule / plan / todo:编排原语的取舍

四种编排原语各管什么,为什么没做成一个大而全。核心文档:docs/subsystems/workflow.zh.md 等四篇。

课程目标读完你能说清三件事:DSH 把多步编排拆成的四个原语各自的适用边界,workflow 管执行、schedule 管时间、plan 管协作姿态、todo 管进度展示;模型写脚本与框架状态机这两种编排思路在四个原语上怎么分工;以及为什么这四样东西刻意没有合成一个统一的任务系统。
交互演示 · 原语选择器

四个真实场景,每个场景选一个原语。选错也没关系,判词会讲清楚为什么。点「播放」可以看自动讲解,自己点卡片可以随时抢答。

场景 1 / 4
等待第一个场景。
演示为教学化归纳,各原语的边界事实分别出自 docs/subsystems/workflow.zh.mdschedule.zh.mdplan.zh.mdpackages/todo/tool-todo/README.zh.md,核对日期 2026-08-13。
逻辑拆解 · 四个原语各管一摊

先给结论:这四个原语没有共享一个任务引擎,它们连持久化形态都不一样。workflow 是一次性的:模型写一段 JS 脚本,引擎在 node:worker_threads 的 vm 里执行,脚本里的 agent() 打回宿主起子 Agent,跑完只留结果与展示记录,逻辑本身不落成持久状态机。schedule 是持久的:create、dispatch、delete 都是 schedule/change 会话事件,回放日志就能重建全部提醒状态。plan 更轻,就是一个 plan/mode 布尔事件的日志折叠。todo 是快照:每次 todo_write 整表替换,UI 靠投影渲染最新一份。

大纲里那个问题「多步编排应该是模型写脚本还是框架状态机」,DSH 的回答是两个都要,但分工明确。执行编排交给模型写脚本,因为编排逻辑千变万化,框架预设不完;时间、姿态、展示交给框架状态机,因为这三样需要跨轮次甚至跨重启的确定性,模型的脚本给不了。workflow 文档自己说了,它的 meta 字段词汇与 Claude Code 的 dynamic workflows 对齐(workflow.zh.md 第 41、49 行),思路同源,落点不同。

还有一条容易忽略的纪律。workflow 脚本里拼错一个 agent() 选项,抛的是 fatal: true 的 WorkflowError,parallel() 组合器对它直接重抛、终止整个脚本;只有子 Agent 真实的运行失败才映射成逐项的 null(workflow.zh.md 第 116 行)。写错代码和运行失败是两类错误,混在一起脚本就没法调了。

workflow · 执行编排 模型写 JS 脚本 · worker + vm 执行 agent() 回宿主起子 Agent · 一次性,不留状态机 谁拿方向盘:模型 schedule · 时间 schedule/change 事件持久化 · 只在本会话内交付 错过的间隔合并成一次 · followup 不打断当前轮 谁拿方向盘:框架状态机 plan · 协作姿态 plan/mode 日志事件的折叠 · 激活时注入指引段落 软性指引,硬限制归沙箱与审批 · 退出过人机评审 谁拿方向盘:框架状态机 todo · 进度展示 todo_write 整表替换 · todo/write 事件 + 投影渲染 给人看的,不驱动执行 · 单一所有者,子 Agent 不共享 谁拿方向盘:框架状态机 四个原语 · 四种持久化形态 · 都是可选能力,agent loop 不依赖任何一个 合成一个大而全的任务系统,四种生命周期就得强行共享一套状态,谁都说不清自己是什么
教学化结构图:节点与连线用于解释源码关系,内容经过课程化整理。
plan 不是权限

plan mode 是软性指引:激活时往系统提示词里加一段 plan:policy,工具目录一个不变(为了请求缓存稳定)。真正拦住写操作的是沙箱和审批,两者都不读 plan 状态,要分别配。

schedule 不出会话

提醒只以 followup 轮次回到原会话,没有推送、没有外部通知通道,冷会话不干活。交付语义是至少一次:准入后、落 dispatch 前崩溃,恢复会重复一次提醒。

todo 不驱动执行

todo_write 是纯展示状态:整表替换、落日志、投影给 UI。没有部分更新、没有回读工具、没有稳定 id。把它当任务引擎用,是对这个原语最常见的误读。

关键证据 · 错过合并与 pending 切换

先看 schedule 的固定速率决策,这是本课唯一值得整段看的代码:会话离线错过了 N 个到期时点,恢复后不逐个补发,一次除法直接算出最新一次到期,再把记录推进到未来。不枚举、不回放、不积压:

packages/schedule/schedule/src/domain.ts第 536 至 543 行节选
  const steps = Math.floor((acceptedAt - target) / interval)
  const occurrence = target + steps * interval
  /* v8 ignore next -- bounded operands and a quotient-derived product stay safe. */
  if (!Number.isSafeInteger(occurrence) || occurrence < target || occurrence > acceptedAt) {
    throw new ScheduleLogError('every occurrence arithmetic must stay within the accepted interval')
  }
  const occurrenceAt = new Date(occurrence).toISOString()
  const next = occurrence + interval
源码快照说明:依据本地仓库 deepseek-harness-master,核对文件 packages/schedule/schedule/src/domain.ts,核对日期 2026-08-13。代码块保留源码原文。

第二条边界是 plan mode 的生效时机,逻辑用文字讲。用户在模型流式输出时点了切换,插件不立刻写日志,选择先挂在进程内存的 pending 里,等下一个轮内 pre-step 边界才动手。顺序讲究得很:监听器先 await next() 问下游这一步收不收,下游拒绝、信号已取消或者没有 pending,都原样放行;三关都过了才把选择追加进日志。追加万一失败,只记一条 warn 日志然后放行这一步,绝不因为一次姿态切换失败就阻塞整个轮次。这也回答了崩溃语义:pending 只活在进程内存,切换还没落日志时崩溃,重启后 plan mode 维持切换前的状态。

出处:packages/plan/plan-mode/src/index.ts 第 205 至 218 行的 agent/pre-step 监听器,核对日期 2026-08-13。

todo 那条最有态度的设计不用贴代码:allowParallelInProgress 是必填配置,schema 里写的是 z.boolean().required(),没有默认值(packages/todo/tool-todo/src/index.ts 第 41 至 43 行)。允不允许多个任务同时进行中,取决于这个部署跑不跑并发子 Agent,工具自己观测不到,所以强制部署方表态。设成 false 后,模型多标一个进行中就吃 Error: invalid todos: at most one task may be in_progress(第 107 至 109 行)。

横向对比 · 统一 Task 框架 vs 四个独立原语

Claude Code 走的是聚合路线:七种异步工作(shell 命令、本地子 Agent、远程 Agent、Teammate、工作流、MCP 监控、记忆整合)统一挂在一个 Task 框架下,共享 registerTask、updateTaskState、kill 一套生命周期(书稿 study/chapters/06-task-system.md 第 27 至 47 行引 tasks/types.ts)。DSH 相反,subagent 文档明确写着可继续路径「不会创建 Task,也不会创建承载中间结果的包装层」,四个编排原语更是各有各的持久化形态。聚合换来统一的进度 UI 和管理入口,拆分换来每个原语能把自己的语义说到底,比如 schedule 的错过合并、plan 的 pending 切换,塞进统一框架里都得妥协。

todo 这个小工具上的分歧最能看出两家的脾气。Claude Code 的 TodoWrite 在提示词里硬编码了纪律:「Exactly ONE task must be in_progress at any time (not less, not more)」,条目还要求 content 加 activeForm 双形态,执行中显示进行时文案(书稿 study/chapters/14-all-prompts.md 第 1243 至 1293 行引 TodoWriteTool/prompt.ts 原文)。DSH 把同一条纪律做成了必填的部署配置:跑并发子 Agent 的组合选 true,单线程纪律选 false,选了 false 就由代码拒绝而非提示词劝告;条目形状刻意最小,只有 content 和三态 status。一个用提示词约束模型,一个用 schema 约束部署,然后让代码执行。

课堂练习
01

推演两条边界

其一:模型正在流式输出一大段方案,用户此刻点了「进入 plan mode」,这个选择什么时候真正写进日志、什么时候开始影响模型请求?如果这一轮结束前进程崩了,重启后 plan mode 是开还是关?(提示:pending 只存在于进程内存。)其二:一条 every_seconds: 3600 的提醒,会话离线 5 小时后恢复,恢复瞬间会触发几次提醒、下一次目标定在哪?用本课第一段源码里的 steps 算式手推一遍。

Takeaway:执行编排交给模型写脚本,时间、姿态、展示交给框架状态机,四个原语四种持久化形态,谁也不冒充谁。挑原语时先问一句:这件事的状态需要活多久?活一次 run 的用 workflow,活到会话重启之后的用 schedule 和 plan,只是给人看的用 todo。