# dsh-taskforce [English](./README.md) | [简体中文](./README.zh-CN.md) 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 版本见[兼容性](#兼容性)): ```sh 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 会被拒绝,但往里装插件不会。 ```sh "\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、不需要网络): ```sh 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`),恢复会话可用(原文件自动备份): ```sh 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 说明](https://github.com/deepseek-ai/deepseek-harness); 保持单任务超时与尝试次数上限开启;把 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`(含渲染结果), 所以编排状态在会话日志里本来就是持久的。 - 插件注册一个 `orchestraGraph` Session 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 可以按任务**声明新类型**——每个声明"编程"出一个智能体规格: ```jsonc "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 进程后生效: 1. **直接改 `src/profiles.js`**:每个角色的 `role`(身份行)、`discipline`(永久约束)、 `effort`(`off|low|high|max`)、`toolDenyPatterns`(对宿主全局工具名做不区分大小写的 正则匹配,命中即对该角色硬禁用)。 2. **Config 覆盖(不改代码)**:在 profile 的 `cordis.patch.yml`(如 `~/.dsh/profiles/web/cordis.patch.yml`)的插件条目上加 `config:` 块: ```yaml - 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](./LICENSE)。