dsh-taskforce
English | 简体中文
DeepSeek Harness(dsh)的多智能体编排插件。
把长任务拆给多个目标明确的智能体,避免任何单个智能体的上下文累积整个任务:规划者把工作分解为任务图,
每个 worker 在全新的一次性上下文中运行,只拿到自己那块任务需要的信息,并以结构化报告回传结果。
核心闭环:计划 → 派发 → 结构化报告 → 审核 → 通过 | 重做。
状态:闭环 + 跨重启恢复 + Web 面板 + 自动触发提示段——计划 → 派发 → 结构化报告 → 审核(通过/失败)→
回退 → 再派发 → 收尾,全部跑通;跨重启恢复(重放宿主日志重建任务图)与会话输入框上方的
Web 编排面板 都已真机验收。另有让流水线被自动选中的系统提示段:已实测该段确实进入模型上下文、
且落在 workflow 段与 subagent 段之后,但行为层的另一半——不点名时 agent 是否真的会去调
orchestrate_plan——尚未验收(那次实验被无效的 API Key 挡在 401)。
离线回归 16 组全绿(CI 每次 push / PR 都会跑)。设计文档见(本地)../DESIGN.zh.md。
> 命名:仓库与本地目录沿用历史名 dsh-orchestra,而 GitHub 仓库与包名是 dsh-taskforce——照路径操作没问题。
> 工具名 orchestrate_*、服务名 orchestra、投影键 orchestraGraph 是刻意冻结的:它们出现在宿主自己的
> 工具调用日志里,改名会让改动之前的会话恢复不出来,所以不要"顺手统一"。
安装
从本地目录安装(克隆后在本目录内执行 pnpm install——本插件有一个 npm 依赖;验证过哪些
dsh 版本见兼容性):
git clone https://github.com/LiSheng5/dsh-taskforce.git
pnpm install
dsh plugin --profile web add <本插件所在绝对路径>
<本插件所在绝对路径> 必须是绝对路径。dsh plugin add 也直接接受 git 形式
(github:LiSheng5/dsh-taskforce)——依赖由 pnpm 在安装时解析。
用的是桌面版而不是 CLI? 桌面版自带启动器(Windows 上是
resources\runtime\cli\bin\dsh.cmd),并且独占管理 desktop profile:用 CLI 去
boot / dump 这个 profile 会被拒绝,但往里装插件不会。
"\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add <本插件所在绝对路径>
应用自己的侧边栏 Plugins 页做的是同一件事——接受 registry 包名、git 地址、tarball
或本地绝对路径——装完默认就是启用状态。重启应用即生效。
兼容性
| dsh 版本 | 状态 |
|---|---|
0.1.7-rc.2 |
开发与验证基准:全闭环、跨重启恢复、Web 面板均真机验收过 |
0.2.0-rc.2 |
桌面版自带运行时;安装、挂载、提示词组装已 headless 实测(提示段渲染在预期位置、工具文案确实到达模型),编排全闭环与 Web 面板尚未在此版本重跑 |
dsh 目前是开发者预览版,官方明示会出现破坏性变更。升级 dsh 后先跑一次离线回归
(不需要 dsh、不需要 API Key、不需要网络):
node test/local-loop.mjs
同一套回归在 CI 里每次 push / PR 都会自动跑(.github/workflows/offline-regression.yml,
Node 20 / 22 / 24 三档)——所以"升级 dsh 后本地跑一次"和"CI 挡住回归"用的是同一条命令。
没有 @deepseek-ai/dsh-system-prompt 的组装只少一样东西:让流水线被自动选中的那段
提示。点名调用时,所有工具照常工作。
升级自早于本次修复的版本
早期版本会向会话日志写入自定义事件类型,导致这些会话在 dsh 重启后无法加载
(contains event type "orchestra/graph" … refusing to interpret the log)。修复脚本会
给这些行补上官方兼容标记(ignorable),恢复会话可用(原文件自动备份):
node scripts/repair-session-logs.mjs --dry-run # 先看影响范围
node scripts/repair-session-logs.mjs # 执行修复(含自检与备份)
node scripts/repair-session-logs.mjs --restore # 回滚
安全
本插件会 spawn 有真实权限的子智能体:在会话工作区内读写文件、执行 shell,受各 worker
profile 的工具限制约束。在重要工作区运行编排前,先阅读
dsh 官方 SAFETY 说明;
保持单任务超时与尝试次数上限开启;把 worker 的报告当作"声称"对待——这正是审核者存在的意义。
worker 默认还会被摘掉宿主的 agent-plane 控制面工具(send_message、interrupt_agent、
list_agents、subagent、subagent_fork):其中 send_message 能把 worker 的文字直接塞进
编排者(A)的会话,正是本插件要避免的事;其余几个让 worker 能打扰父代理或再拉代理。
这是实测校准出来的(真实 researcher worker 曾报出这些工具),嫌太严可以
denyControlPlane: false 关掉这一组。
目录结构
dsh-taskforce/
├── package.json # bundle 清单:dsh.bundle.patch + dsh.client(web 面板)
├── cordis.patch.yml # 组装补丁:把本插件插入 profile
├── index.js # 插件入口:Config schema + apply()
├── client.js # 浏览器半:会话输入框上方的编排面板(__ModuleLoader__ 懒工厂)
├── src/
│ ├── protocol.js # Task / Report / Verdict JSON Schema(宿主子集)
│ ├── graph.js # DAG 校验、就绪计算、状态迁移(CAS)
│ ├── service.js # 编排状态(按会话分键,ctx.provide('orchestra'))
│ ├── events.js # 跨重启恢复:折叠宿主 tool/call + tool/result 重建任务图
│ ├── router.js # 上下文路由:worker 任务书与审核包的确定性组装
│ ├── profiles.js # agent_type → role/discipline/effort/toolDenyPatterns
│ ├── prompts.js # 所有面向模型的文案:自动触发提示段 + 拆解指导
│ ├── json-schema.js # 入参校验(子集语义)
│ ├── retry.js # 工具 JSON 流式截断(MALFORMED_RESPONSE)的自动重试
│ └── tools/ # 7 个编排工具
├── scripts/
│ └── repair-session-logs.mjs # 修复历史版本写坏会话日志的维护脚本
└── test/local-loop.mjs # 离线回归测试(假宿主,无需网络)
跨重启恢复
编排状态在内存里按会话分键,进程重启即丢。恢复靠重放宿主自己的日志:
- dsh 会把每次工具调用写成
tool/call(含完整参数)与tool/result(含渲染结果),
所以编排状态在会话日志里本来就是持久的。 - 插件注册一个
orchestraGraphSession projection(ctx.sessionProjections.register,
在ctx.inject([...])下可选注册),纯函数折叠这些事件重建任务图;重启后第一次触达
该会话时装载,于是orchestrate_status能报出重启前的任务图。 - 效果在 result 结算时才落地:宿主在工具执行前就写
tool/call,急切应用会把"正在
执行的那一次调用"应用两次;而被中断的派发(写日志的进程已死)因此不会污染恢复结果,
对应任务保持可再次派发。 - 拿不到 projection 能力时静默降级为纯内存态(与早期版本行为一致),不影响插件加载。
- 渲染契约(
✓ —)之前录下的会话能恢复计划、attempts、判定与 finalized
标记,但恢复不出派发结果:旧摘要没有成功/失败标记,编造一个比"让任务停在原处"更糟。
这类结果仍会消费掉自己的 pending 记录,因此不会被误报成"被重启打断"。
> 插件不写自定义会话事件:dsh 的持久化读路径拒绝加载含"本构建不认识且未标
> ignorable"事件类型的日志,而 Session.append() 无法写该标记——仓库外插件写自定义
> 事件会让整个会话在重启后无法加载。详见 src/events.js 顶部注释。
Web UI 面板
会话输入框上方会显示当前会话的编排面板(只在真的提交过计划后出现,普通会话完全无感):
▾ ▣ orchestra #1 · 1/2 · review 1 PASS
─────────────────────────────────────
✓ t1 researcher completed att 1
读 package.json 得到 name/version …
· t2 coder pending → ready
- 数据来自同一个
orchestraGraph投影的 wire 视图(client.js经useProjection('orchestraGraph')
读取),所以它和orchestrate_status看到的是同一份状态,且随事件自动刷新;重启后显示的是恢复出来的图。 - 面板只看:任务 id、agent_type、状态、attempts、是否可派发、报告摘要 / 诊断 / 问题条数;
worker 的 persona 等细节留在宿主侧不下发。 - 浏览器半是免构建的纯脚本(
client.js,dsh.client.platform=web+immediately);
面板容器继承宿主主题(currentColor),不引入任何样式文件或第三方依赖。 - 想关掉它:卸载插件即可(client 半随 loader 行一起消失,会话界面不受影响)。
工具(7 个)
插件还会注册一段系统提示段(tool:orchestrate_plan,order 2810),让宿主 agent 在
用户没有点名插件的大需求上也会主动走流水线——在此之前实测到的行为是"列一堆
todo_write 然后自己开写"。这段遵循宿主自己的惯例(workflow 占 order 2600、subagent 占
2800),用 ctx.inject(['systemPrompt'], …) 可选注册(没有该服务的组装只损失这段倡导),
并且对看不到 orchestrate_plan 的 agent 渲染成空串——也就是每一个 worker:spawn 子代理
的 persona 只是对部署 persona 的作用域覆盖,全局提示段是会进到子代理里的。所有面向模型的
文案集中在 src/prompts.js。
| 工具 | 作用 |
|---|---|
orchestrate_plan |
提交任务图(mission_brief + tasks + 可选 workers 声明);fail-loud 校验并返回就绪集。拆解规则就写在它的 description 里:工作流只是找切口的手段、必须落成文件所有权,任务数按请求规模伸缩,并行写者冲突,两个拆解骨架,反模式 |
orchestrate_status |
每任务一行的紧凑视图 + 当前可派发集合;无报告而失败的任务附带诊断原因 |
orchestrate_dispatch |
把就绪任务 spawn 成一次性隔离 worker,阻塞等全部结算,按任务返回一行报告摘要(≤4 并发) |
orchestrate_review |
对全部报告跑审核者,返回结构化判定(pass/fail + 定位到任务的问题清单) |
orchestrate_reroute |
把审核问题带回 worker:任务回 pending、guidance 注入下次任务书、返回受影响下游 |
orchestrate_finalize |
记录交付摘要并锁定任务图 |
orchestra_ping |
存活探针 |
计划时编程智能体
规划者不限于四个内置 worker 类型。orchestrate_plan 支持 workers 字段,
A 可以按任务声明新类型——每个声明"编程"出一个智能体规格:
"workers": [
{
"type": "ui-builder",
"role": "你是界面构建师,专注像素级还原。", // 身份(新类型必填)
"discipline": "不得改动 src/api/ 下的文件。", // 可选;缺省用通用兜底
"effort": "high",
"model": "deepseek-flash", // 可选默认值
"toolDenyPatterns": ["bash"]
}
]
- 任务里直接写
"agent_type": "ui-builder"引用。覆盖内置类型(如"type": "coder"
且不给 role)只替换给出的部分。 - 分层保持不变:任务级
persona仍叠加在声明的 role 之上,声明的discipline
永远追加——A 写不丢它。 - 审核环节不可被 plan 重新编程:审核者永远使用运营方配置的规格。
- 引擎级保证不受影响:编排工具对 worker 恒拒绝、委托深度阻止孙代理、任务是一次性隔离会话。
配置(worker 提示词、模型、限制)
规划者自己写任务级提示词:计划里的每个任务可带 persona 字段——A 为该任务定制的角色定义
(如"你是数据库迁移专家,只改 SQL 文件")。它成为 worker 的主系统提示词,叠加在 profile 的
discipline(A 不可覆盖的非协商约束:researcher 不写文件、reviewer 只出判定……)之上。
model 与 reasoning_effort 同样是 A 在计划时逐任务设置的字段。
另外两种调整方式——重启 dsh 进程后生效:
-
直接改
src/profiles.js:每个角色的role(身份行)、discipline(永久约束)、
effort(off|low|high|max)、toolDenyPatterns(对宿主全局工具名做不区分大小写的
正则匹配,命中即对该角色硬禁用)。 -
Config 覆盖(不改代码):在 profile 的
cordis.patch.yml(如
~/.dsh/profiles/web/cordis.patch.yml)的插件条目上加config:块:
- id: dsh-taskforce
name: dsh-taskforce
config:
taskTimeoutMs: 1200000
maxConcurrentWorkers: 6
profiles:
researcher:
role: |
你是资深代码考古学家……
discipline: |
只读,不修改任何文件。
effort: off
toolDenyPatterns: ["write", "bash"]
coder:
model: deepseek-flash
Config 的 role/discipline/model/effort 替换内置值;toolDenyPatterns
与内置的拼接。引擎旋钮:provider(默认 spawn)、maxConcurrentWorkers(4)、
maxAttempts(3)、maxReviewRounds(3)、taskTimeoutMs(600000)。
设计说明
- 手写工具定义、近零依赖:工具定义与
defineTool编译产物同形;唯一依赖是 npm
上的@deepseek-ai/schemastery(用于 Config schema),pnpm 本地路径安装保持零解析负担。 - 上下文隔离:worker 用 spawn(绝不用 fork)的一次性子智能体;上游信息只走结构化
Report 通道,绝不携带历史。 - 提示词分层且冻结:系统提示段管何时进流水线,
orchestrate_plan的 description 管
怎么拆,参数 description 管逐字段规则——全部集中在src/prompts.js的模块常量里。
段与工具 schema 每次请求都会重发,字节稳定的文案才保得住 provider 的前缀缓存。两段文案
只进 A 的请求:worker 拿到空串、也看不到规划工具的 schema,所以TASK_ITEM_SCHEMA的措辞
在 worker 轮次上零成本。离线测试对两段各设了字符预算,每请求成本不会悄悄膨胀。 - 按交付物拆,绝不按职能拆:这段规划文案是横向调研四家 agent 系统后定稿的——Claude Code
的 subagents、dsh 自己的 workflow/subagent 提示段、Anthropic 的多智能体研究系统、ZCode。
没有一家按职能(UI/美术/玩法,或前端/后端)切分:都要求「一个自包含的交付物」或「按子系统
归组」,Anthropic 还把子任务数量按请求规模伸缩而不是给死区间。原因是机制性的,不是文风问题:
职能不拥有文件,按职能切出来的任务要么在同一个工作区里互相踩,要么只交回一段散文。所以文案把
工作流教成找切口的方法,切口必须重新表述成文件所有权;同时明确禁止规划任何 worker 产不出来
的交付物——这个宿主没有图像/音频/视频生成工具。 - 按会话分键的状态:任务图以调用方 agent 的 session id 为键——并发会话互不串扰。
- 分层 worker 限制:编排工具对 worker 恒拒绝;profile 的 deny 模式在派发时对宿主
全局工具视图求交(绝不传入不存在的名字,配置错误只会静默降级而非抛错)。孙代理被
委托深度天然阻止。控制面工具只能按模式(pattern)拒绝——它们不是本插件的工具,
不同组装里未必存在;resolveToolFilter一律先枚举再匹配,所以缺哪个都不会报错。 - 只用宿主的词汇落日志:编排状态走宿主对工具调用的记录(
tool/call+tool/result),
插件侧只额外写宿主日志(orchestra/graph等审计行),因此不引入任何宿主不认识的会话
事件类型,会话日志始终可加载。 - 恢复即重放同一套服务方法:折叠器不重写状态机,而是用记录的参数调用
submitPlan/markDispatched/settleReport… 同一批方法,恢复出的图与在线语义不会漂移。
License
MIT — 见 LICENSE。