DeepSeek Harness · 編排與子 Agent

workflow / schedule / plan / todo:編排原語的取捨

四種編排原語各管什麼,為什麼沒做成一個大而全。核心文件:docs/subsystems/workflow.zh.md 等四篇。

課程目標讀完你能說清三件事:DSH 把多步編排拆成的四個原語各自的適用邊界,workflow 管執行、schedule 管時間、plan 管協作姿態、todo 管進度展示;模型寫腳本與框架狀態機這兩種編排思路在四個原語上怎麼分工;以及為什麼這四樣東西刻意沒有合成一個統一的任務系統。
互動演示 · 原語選擇器

四個真實場景,每個場景選一個原語。選錯也沒關係,判詞會講清楚為什麼。點「播放」可以看自動講解,自己點卡片可以隨時搶答。

場景 1 / 4
…
等待第一個場景。
演示為教學化歸納,各原語的邊界事實分別出自 docs/subsystems/workflow.zh.md、schedule.zh.md、plan.zh.md 與 packages/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。