Skip to main content
E

dsh-agent-team-playbook

elfgzp/dsh-agent-team-playbook

Injects the non-obvious operating rules of Agent Teams into the Lead session through one ordered system-prompt section: plan the roster before spawning, roster slots are never released, write scopes are advisory, and wait_agent short-circuits when no peer is running.

Install

dsh plugin --profile web add github:elfgzp/dsh-agent-team-playbook

README

Agent Teams Playbook

一个 DeepSeek Harness 插件(bundle):把 Agent Teams 的硬性操作规则注入每个会话, 让接管 Team 的 agent 在动手前就知道哪些路走不通。

解决什么问题:Agent Teams 有一批"官方 README 没写、踩了才知道"的约束——名额不可回收、 interrupt 不释放成员、写作用域只是提示不是锁、队友默认看不到你的历史、 无活跃队友时 wait_agent 会立刻返回而不是等待。这些约束出错时没有报错, 只有行为异常。本插件把它们变成注入到系统提示里的守则。

非官方项目,与 deepseek-ai 无隶属关系。事实依据上游 tag dsh-v0.2.1-rc.3 源码核验, 详见文末「设计依据」。


它做了什么

注册一个有序的系统提示段落(team-playbook:rules,order 650), 为每个 agent 注入六条规则:

规则约束内容
先规划 roster 再召人名册只增不减:inactive 成员、失败 spawn 都持续占名额;名字全会话唯一
写作用域是提示不是锁重叠只报 warning,不阻止写入;并发必须按目录切分
派活必须自带完整上下文队友默认 fresh context,看不到你的历史
用依赖表达顺序,别用标签blocked_by 是唯一强制的顺序机制;文本状态标签不可强制/不可筛选
正确解读队友状态inactive ≠ 完成;send_message 成功 ≠ 对方已处理
只报告已验证的结果不能因为队友说成功就认为成功

650 这个位置紧接上游 TEAM_POLICY(600) 之后、PTC_ONLY(800) 之前, 读起来是 Team 策略的延续,不会割裂后续工具段落。

它不做什么

明确边界,避免误期待:

  • 不新增任何工具。这是纯提示注入,不改 Team 的运行时行为。
  • 不强制任何流程。软状态标签、写作用域重叠,仍然只是提示。
  • 不做 UI。没有 client 半边,不加面板/按钮。
  • 不修改上游源码。依赖上游 @deepseek-ai/dsh-experimental-agent-team 原样工作。

安装

要求:DeepSeek Harness >=0.2.1-rc.3 <0.3.0(走 systemPrompt + agents 两个服务)。 已实测:0.2.1-rc.3、0.2.1-rc.5。

版本约束写在 package.json 的 peerDependencies 里,DSH 安装时会据此校验。 不要写死精确版本:早期版本写成 "0.2.1-rc.3",Harness 升级到 rc.5 后插件直接 报 incompatible-version、rows: [],一行都不加载。区间写法对 0.2.1-rc.x 与 0.2.x 正式版都成立;0.2.2-alpha.x 这类预发布版按 semver 默认不匹配, 遇到时需自行确认 API 未变再放宽。

# ⚠️ 务必 clone 到持久目录(不要用 /tmp —— 见下方「源目录不能删」)
cd ~/Work                                    # 或任何你长期保留的位置
git clone https://github.com/elfgzp/dsh-agent-team-playbook.git
cd dsh-agent-team-playbook

# ⚠️ 关键步骤,不能跳过。若上层目录有 pnpm-workspace.yaml 见「安装陷阱」
pnpm install --prod

# ✅ 装完立刻验证依赖真的装上了,再去 install_bundle
node --input-type=module -e "const m=await import('./lib/index.js'); \
  console.log('deps OK', Object.keys(m), m.inject)"
# 期望: deps OK [ 'Config', 'apply', 'inject' ] [ 'systemPrompt', 'agents' ]

这三步顺序不要调换:pnpm install → 自检 → install_bundle。 把自检放在 install_bundle 之前,能让"依赖没装上"当场暴露; 若跳过自检直接装,症状会变成 application: failed + ERR_MODULE_NOT_FOUND, 而这个报错不提示真实原因,容易误判成代码 bug(见「故障排查」)。

⚠️ 安装陷阱:上层目录的 pnpm-workspace.yaml

这是实测踩到的静默失败,也是最难排查的一类:

如果你的某个上层目录(如 ~、~/Work)存在 pnpm-workspace.yaml, pnpm 会把那个目录认作 workspace 根,在子目录里执行 pnpm install 会输出:

No projects found in "/Users/you"

然后什么都不装,而且退出码是 0。因为 exit code 正常,&& 链、 CI 脚本、set -e 都不会报错,你以为装好了,直到 install_bundle 才失败。

验证:ls node_modules/ 不存在或为空即命中此陷阱。

解法(任选其一,推荐第一个):

# 1. 显式忽略 workspace —— 最直接,实测有效
pnpm install --prod --ignore-workspace

# 2. 把仓库 clone 到不受 workspace 配置影响的位置
#    (确保从仓库往上到 / 都没有 pnpm-workspace.yaml)

# 3. 临时移走上层配置
mv ~/pnpm-workspace.yaml /tmp/ && pnpm install --prod && mv /tmp/pnpm-workspace.yaml ~/

注意 --ignore-workspace-root-check 不是这个场景的选项, pnpm 会直接报 Unknown option。

🔴 源目录不能删

link: 协议安装不会复制文件,只在 profile 里登记一条指向该目录的链接。 因此 clone 出来的目录是插件的一部分:

  • 删掉它 → 下次启动 Harness 时插件加载失败,插件行变为 application: failed
  • 移动它 → 同样失效(链接仍指向旧路径)

典型踩法:把仓库 clone 到 /tmp 做试验。macOS/Linux 会定期清理 /tmp, 插件就在某次重启后随机失效,而报错信息不会提示"源目录已消失", 排查成本很高(本插件开发过程中就踩过一次)。

请 clone 到 ~/Work/、~/src/ 之类的持久位置。

为什么必须 pnpm install:plugin_manager 用 link: 协议安装本地目录, 此时依赖不会从 profile 的 node_modules 解析,而是从插件自己的目录解析。 跳过这一步会在启动时报:

failed to import: Error [ERR_MODULE_NOT_FOUND]:
  Cannot find package '@deepseek-ai/schemastery' imported from .../lib/index.js

这个错误发生在"安装成功但 application: failed"时,容易被误判为代码问题。

它藏的第二个坑:link: 安装也不会帮你装 peerDependencies。 本包把 @deepseek-ai/dsh-* 声明为 peerDependencies(它们由宿主提供), 但 pnpm 的 autoInstallPeers 会在本地 node_modules/ 里也放一份, 所以 pnpm install --prod 是唯一能让 lib/index.js 找到它们的步骤。 pnpm-lock.yaml 已提交,锁定到具体版本,保证不同机器结果一致。

装完可以用一行确认依赖解析正常(不依 DS 运行时):

node --input-type=module -e "const m=await import('./lib/index.js'); \
  console.log(Object.keys(m), m.inject)"
# 期望输出: [ 'Config', 'apply', 'inject' ] [ 'systemPrompt', 'agents' ]

然后用 plugin_manager 从本地目录安装:

{
  "action": "install_bundle",
  "target": "/absolute/path/to/dsh-agent-team-playbook"
}

安装后:

  • 在 插件详情 里应看到标题「Agent Teams 操作守则」与对应描述(来自 locale/*.json)
  • 已有会话的 agent 会立即生效;新创建的 teammate 也会自动获得该段落
  • 卸载用 remove_bundle,或在 set_bundle 里禁用

目录安装是软链接到 checkout:好处是改完 lib/index.js 重装即生效, 代价是 checkout 必须一直存在(见上文「源目录不能删」)。 替换已安装包需要重启,因为要加载新的 JS 模块代际。

想彻底摆脱"目录必须保留"的约束,可以把插件打包成 tarball 再安装 (pnpm pack),或用固定 tag 发布 —— 但那样每次改动都要重新打包。

故障排查:装上了但没生效

install_bundle 返回成功,不代表插件真的在跑。按这个顺序查:

现象含义处置
application: applied已激活正常
application: failed + ERR_MODULE_NOT_FOUND,且 node_modules/ 不存在或为空依赖从未装上:多因上层目录有 pnpm-workspace.yaml(见「安装陷阱」)pnpm install --prod --ignore-workspace
application: failed + ERR_MODULE_NOT_FOUND,但 node_modules/ 有包装过但被删/不完整回插件目录重跑 install
application: overridden被更高优先级的层覆盖检查是否有同名 patch 行
application: restart-required代码未重新加载重启 Harness(替换已装包必须重启)
上次能用、重启后突然 failed,且未改过任何代码源目录被删/移动(link: 的目标消失了)确认目录还在;不在就 clone 到持久目录重装
list_bundles 里本插件 error.code: incompatible-version,rows: []Harness 升级后不满足 peerDependencies 的版本约束确认新版本 API 兼容后放宽 peerDependencies 区间,remove_bundle + install_bundle
条目不存在patch 没插入确认 package.json 的 dsh.bundle.patch 指向正确

ERR_MODULE_NOT_FOUND 是最容易误判的一个 —— 它发生在"安装成功但激活失败"时, 很容易被当成代码 bug 去查 lib/index.js,实际是包元数据和安装步骤的问题。

验证它真的生效

这是提示注入插件,没有按钮可点,所以要从提示内容反查。三种方式:

  1. 查已注册的段落(不改运行时状态):

    { "platform": "host", "provider": "Config", "method": "listConfigs",
      "input": { "name": "dsh-agent-team-playbook" } }
    
  2. 看插件行的 application 字段:applied 才算真正激活; overridden 表示被更高优先级层覆盖,restart-required 表示尚未生效。

  3. 在会话里观察:新开会话后,让 agent 复述「Agent Teams 有几条操作规则」, 应答出上表六条。这是端到端确认,比看配置更可信。

需要说明的限制:段落已注册 ≠ 用户一定看到了预期文本。渲染效果取决于 提示组装,本仓库不含自动化断言。

配置

键类型默认说明
enabledbooleantrue置 false 时不注册任何段落(等价于禁用)

仓库结构

package.json          bundle 清单:dsh.bundle.patch 指向 cordis.patch.yml
cordis.patch.yml      Loader 补丁:插入 agent-team-playbook 这一行
lib/index.js          插件实现:为每个 agent 注册一个 system prompt 段落
locale/{zh,en}.json   插件管理器显示的标题与描述
icon.svg              插件图标
docs/                 设计说明与实战细节(非插件运行时文件)
examples/             可复制的任务模板

files 只包含运行必需的 4 项(lib/index.js、cordis.patch.yml、locale/*.json、icon.svg), docs/ 与 examples/ 是随仓库分发的文档,不参与打包。


设计依据与实战细节

以下内容解释为什么是这六条规则。每条都可在上游源码中核验, 命令见 §10 获取源码与核验本文。

1. 什么时候该用 Team,什么时候不该

这是本文最想先讲清楚的。Agent Teams 有固定成本(名额不可回收、消息开销、共享 cwd 的写冲突风险),不是免费的并行。

决策树

任务能否拆成 2+ 个互不依赖、可并行的子问题?
├─ 否 → 单 agent 直接做。不要为了"显得专业"建 Team。
└─ 是 → 子问题之间是否需要共享决策 / 中途对齐?
        ├─ 否 → 各自独立跑完再汇总 → ✅ Team 是好选择
        └─ 是 → 取决于对齐频率:
              ├─ 高频对齐(每步都要同步)→ ❌ Team 不合适,
              │     消息开销 > 并行收益;改用单 agent 串行
              └─ 低频对齐(只在关键节点)→ ✅ Team 可行,
                    用共享任务板表达依赖,而非消息轰炸

额外的否决条件(任一命中就不要用 Team):
  ✗ 子任务会写同一批文件(写作用域无法切分)→ 文件冲突,见 §5
  ✗ 子任务需要跑同一份构建/测试且互相干扰
  ✗ 任务总量小到协调成本占主导(比如 3 个小改动)
  ✗ 你已经用满名额且无可复用的闲置成员(见 §2,名额不可回收)

适合 / 不适合的典型

✅ 适合理由
多角度调研,最后汇总成一份文档天然并行、写作用域互相隔离(各写各的文件)
大规模机械改造(几十个文件同一种改法)按目录切分,依赖少
一边调研一边实现(调研完 pm 出需求、dev 实现)用任务板 blocked_by 表达阶段依赖
对抗式验证(一个实现、另一个找 bug)视角不同,交叉验证价值高
❌ 不适合理由
单个 bug 的诊断修复串行推理,并行只会让信息碎片化
需要频繁对齐的设计工作消息开销超过并行收益
多个 agent 改同一个文件无文件锁,必然冲突(§5)
需要精确顺序的数据库迁移顺序性 >> 并行性

2. 名额与角色规划(最容易踩的坑)

这一节纠正网上流传的两个错误说法。先看结论:

默认上限 16 个 teammate;当前 profile 配置为 8 个 teammate。 lead 不计入该上限。 state.members 数组里只有 teammate。 名册没有删除机制,名额不可回收。

2.1 上限到底是多少

三个数字,别搞混(源码 packages/experimental/agent-team/src/index.ts):

const DEFAULT_MAX_MEMBERS = 16            // L42 代码默认值
const DEFAULT_MAX_TASKS = 256             // L43
const DEFAULT_MAX_MESSAGE_BYTES = 65_536  // L44
maxMembers: z.number().step(1).min(1).default(DEFAULT_MAX_MEMBERS),   // L60

而当前生效的 8 来自 profile 的显式配置,不是代码默认值 (packages/experimental/agent-team-profile/cordis.patch.yml):

- insert:
    - id: agent-team
      name: '@deepseek-ai/dsh-experimental-agent-team'
      config:
        maxMembers: 8          # ← 这里把默认的 16 覆盖成 8
        maxTasks: 256
        maxMessageBytes: 65536
        disposalTimeoutMs: 5000

2.2 lead 到底算不算名额

不算。 证据链:

roster.ts:257 的上限检查只针对 state.members:

if (state.members.length >= this.maxMembers) {
  throw new TeamError(`Team member limit ${this.maxMembers} reached`, 'TEAM_MEMBER_LIMIT')
}

而 state.members 里只有 spawn 出来的 teammate——lead 是在投影阶段被单独 prepend 进去的 (projection.ts:344):

const members: TeamMemberProjection[] = [{ id: rootId, name: 'lead', role: 'lead', phase: 'active' }]
for (const member of state.members) {        // ← 真正的 teammate 在这里
  members.push({ id: member.id, name: member.name, role: 'teammate', ... })
}

所以:maxMembers: 8 = 8 个 teammate,加上 lead 一共 9 个 agent。 看到"上限 8(含 lead)"的说法是错的。

2.3 名额为什么不可回收

因为名册是只增不减的。journal 只有两种事件类型(全仓 grep appendAndFlush(root, '):

team/member      ← 只有新增
team/task

没有 member-removed / member-deleted。 三个直接后果:

行为是否释放名额源码依据
teammate 变 inactive(没事干)❌ 否只是 status 变化,roster 不变
interrupt_agent 打断❌ 否roster.ts:187-197 只调 subagents.interrupt(),完全不碰 roster
spawn 失败❌ 否以 phase: 'failed' 留在名册里照样占位(roster.ts:283-295)
进程退出❌ 否—
名字复用❌ 禁止TEAM_MEMBER_NAME_TAKEN 查的是全历史(roster.ts:254)

上游 README README.zh.md §已知限制 也确认了这点:

"扁平且不可变的 roster——只有 Lead 可以创建直接 teammate;不支持嵌套 Team、重命名、删除或名字复用。" "不会自动释放 owner——成员不活动、interrupt、进程退出与工作失败都不会释放任务 owner。"

2.4 由此推出的操作纪律

必须先规划 roster 再召人。做不到"边做边加"的弹性。

实战规则:

  1. 开干前写死成员清单:需要哪几个角色、各叫什么名字,一次性想清楚。
  2. 名字是永久资产,起名要能表达职责(infix-researcher 比 worker1 好,因为失败重试时你得换个名字)。
  3. spawn 失败不要盲目重试同名——TEAM_MEMBER_NAME_TAKEN 会立刻再失败,且失败条目已经占了名额。换名字重试。
  4. 用满后靠"复用闲置成员"而不是"加人":把新任务用 reassign 或 send_message 派给已有的 inactive 成员。
  5. 预算内预留缓冲:8 个名额如果你规划了 8 个,那中途一次 spawn 失败就没救了。建议按 ≤7 规划。

3. 任务状态机增强(上游只有 3 态)

用户常需要更细的流程状态:规划中 → 实现中 → 测试中 → 已完成。但上游只有 3 态。

3.1 先说清上游到底有什么(以及为什么改不了)

任务状态枚举(types.ts:72):

export type TeamTaskStatus = 'pending' | 'in_progress' | 'completed' | 'deleted'

deleted 是删除墓碑,不是流程态。真正可用的流程态只有 3 个。

为什么不能直接加状态——三层都封死了:

  1. 工具层没有 status 参数。team_task_update 的入参只有固定的 action 枚举 (tool-agent-team/src/index.ts:375-380):

    action: {
      type: 'string', required: true,
      enum: ['claim', 'release', 'edit', 'set_dependencies', 'complete', 'reopen', 'reassign', 'delete'],
    }
    

    传自定义 status 无处可传。

  2. 服务层状态由 action 单向映射(task-board.ts):

    action目标 status前置条件
    claimin_progress必须 pending 且依赖已满足
    releasepending必须 in_progress,清空 owner
    completecompleted必须 in_progress
    reopenpending必须 completed,清空 owner
    reassignin_progress仅 Lead;pending/in_progress
    deletedeleted无下游依赖
    edit / set_dependencies不改 statusowner 或 Lead
  3. UI 是封闭 switch(client-ui-agent-team/src/client/TeamAction.tsx:33-36):

    function statusKey(status: TeamTask['status']): TeamKey {
      switch (status) {
        case 'pending': return 'status.pending'
        case 'in_progress': return 'status.in_progress'
        ...
    

    locale 里也只有 3 条(locales.ts:30-32:待处理 / 进行中 / 已完成)。

3.2 三个候选方案与实测结论

方案 A:subject 前缀([planning] ...)
[implementing] 实现 bigram 倒排表
[testing] 验证误命中与精排
维度结论
可行性✅ 完全可行。subject 上限 200 字符(task-board.ts:59),前缀占 12 字符无压力
UI 可见性✅ 优秀。UI 直接渲染 task.subject(TeamAction.tsx:148),前缀在卡片标题最显眼位置
机器可读⚠️ 需要自己解析前缀;上游不理解它
阻止误改❌ 完全不能。前缀只是文本
方案 B:description 里的 YAML front-matter
---
phase: implementing
owner_role: dev
evidence: pending
---
正文:要做什么、验收标准...
维度结论
可行性✅ 可行。description 上限 16384 字符(task-board.ts:60),且 requiredText() 只 trim() 首尾空白、保留内部换行(validation.ts)——front-matter 不会被破坏
UI 可见性⚠️ 很差。description 在卡片里被 -webkit-line-clamp: 2 截断成 2 行(TeamAction.module.css:273-277),front-matter 会吃掉全部可见行,正文完全看不见
机器可读✅ 最佳。结构化、可解析、可放任意多字段
阻止误改❌ 完全不能

B 有个容易忽略的副作用:front-matter 占 3~5 行,而预览只有 2 行 → teammate 在 UI 上看不到任务内容,必须点"展开"。这在实践中很烦。若要采用 B,把 front-matter 压缩到 1 行(phase: implementing | role: dev)无 --- 包裹。

方案 C:组合(推荐)
subject:      [implementing] 实现 bigram 倒排表     ← 人眼在 UI 一眼看到
description:  phase=implementing role=dev
              (正文另起,空行分隔)
  • subject 前缀给人看(UI 标题位,无截断)
  • description 首行给机器读(单行 key=value,不占满 2 行预览)
  • 两者都由同一个常量映射生成,避免不一致

3.3 能力边界:软状态做不到什么(必读)

不要对软状态有不切实际的期待。它全部是约定,不是机制:

期望现实
强制 teammate 按流程流转❌ 无法强制。teammate 可以跳过 [planning] 直接 complete
让 UI 显示"测试中"并配色❌ 做不到。UI 只认 3 态 + 固定 locale key
状态变更可被审计/追溯⚠️ 只能看 revision 数字变化,看不到历史值(改了就覆盖,journal 事件是整条快照)
防止并发两个 agent 同时改❌ 软状态与 CAS 无关;CAS 只保护 expected_revision
按状态筛选任务❌ team_task_list 只支持按 status(3 态)和 owner 过滤,不支持按 subject 内容过滤

另一个硬约束:team_task_update 的 edit 要求 owner 或 Lead 权限 (task-board.ts:124-126):

const authorizeOwner = (): void => {
  if (!lead && !owner) throw new TeamError('task mutation requires its owner or Team Lead', 'TEAM_TASK_UNAUTHORIZED')
}

所以 teammate 只能用 [implementing] 改自己名下的任务;跨任务统一改状态得 Lead 来。

3.4 落地建议

  1. 状态词表固定 4 个,写进角色文档:planning / implementing / testing / completed
  2. completed 用真实 status,不要用前缀——因为 completed 有真实语义(依赖解锁、taskReady 判定基于它,task-view.ts:24-26)。其余 3 个前缀是对 in_progress 的细分
  3. Lead 负责守规矩:既然机制不强制,就在 Lead 的 prompt 里写明"看到 [planning] 的任务不应被他人 claim"
  4. 接受它只是"给人和模型看的标签",别在它上面建自动化

一句话:软状态解决的是"人一眼看懂进度",不解决"流程控制"。 需要真控制就用 blocked_by 依赖图——那个是上游真实强制执行的(taskReady(),task-board.ts:133)。


4. 角色定义

角色契约见 docs/roles.md。它提供 pm / arch / dev / sdet / qa 五个角色的 输入、产物、红线、验收证据结构,可直接套用到任意项目。

核心思想:角色不越界,越界的产物不予验收。 每个角色必须产出可复现的验证证据, "写完了"不算完成。

起步用四个角色足够:pm(要做什么)→ arch(怎么做)→ dev(实现)→ sdet/qa(验证)。 角色越多,名额越紧张(§2),协调成本越高。


5. 派活:fresh context 意味着什么

spawn_teammate 有 context 参数(tool-agent-team/src/index.ts:199-203):

context: {
  type: 'string',
  enum: ['fresh', 'fork'],
  description: 'fresh starts without Lead history; fork inherits completed Lead turns. Defaults to fresh.',
}

5.1 fresh 的后果

默认是 fresh——teammate 看不到你的任何历史。 它不知道:

  • 你为什么做这件事(背景)
  • 你之前试过什么、什么失败了
  • 文件在哪、命名规范是什么
  • "写完了"在这个项目里意味着什么

所以:任务描述必须自带完整上下文,把 teammate 当成一个刚入职、能力很强、但对你项目一无所知的同事。

5.2 可复制的任务模板

## 背景
[为什么做这件事。2~4 句。包含:业务动机、当前现状、触发原因]

## 你的职责
[一句话定位这个角色要干什么]

## 具体任务
1. [可执行的步骤,越具体越好]
2. [写明产出文件路径]

## 约束
- 工作目录:/abs/path(不要碰其他目录)
- 不要修改:/abs/path/a、/abs/path/b
- 语言/风格:[如"中文回复,代码注释中文"]
- 禁止:[如"不要 git commit / 不要装新依赖"]

## 已有信息(省去你自己找)
- 关键文件:/abs/path/x.go:120-180 是主逻辑
- 已知结论:[别人已经验证过的事实,避免重复调研]
- 已知坑:[踩过的坑]

## 交付标准(验收证据)
- [ ] 产出文件写在哪
- [ ] 必须包含哪些内容
- [ ] 验证方式:[跑什么命令 / 看什么输出]
- [ ] 完成后用 send_message 汇报:[汇报要点]

## 边界
遇到以下情况停下来问我,不要自行决定:
- [列举需要 escalation 的情况]

5.3 fork 什么时候用

fork 继承 Lead 已完成的所有 turn。适合:

  • 需要 Lead 全部上下文的延续性工作(比如"接着我上面的调研再挖一层")
  • 但注意:fork 只继承"已完成"的 turn,看不到当前进行中的 turn(这一点在工具描述里写明)

默认用 fresh + 好模板,比 fork 更可控——fork 出来的 agent 继承了你的全部历史,token 成本高,且容易"记得太多无关的东西"。


6. 写作用域(write_scopes)只是提示

team_task_create / update 支持 write_scopes,用于提示谁改哪些路径。

它不是锁。 三条证据:

  1. 类型注释直接写明(types.ts,writeScope() 的 docstring):

    Normalize one workspace-relative path prefix **without treating it as a lock**.

  2. 重叠只产生 warning,不阻止(task-view.ts:43-47):

    for (const other of state.tasks) {
      if (other.id === task.id || other.status !== 'in_progress') continue
      if (task.writeScopes.some(left => other.writeScopes.some(right => scopesOverlap(left, right)))) {
        warnings.add(`write scopes overlap with ${other.id}`)     // ← 只加一条 warning
      }
    }
    
  3. 上游 README 明确承认:

    "write scope 仅作提示——Bash、formatter、代码生成器与直接外部写入可以绕过文件版本检查;Lead 必须协调 owner 并检查最终 diff。"

实践做法

  • 切分原则:按目录切,不要按文件切(tmp/research/ 比 tmp/research/a.md 更好,粒度合理)
  • 每个并发任务给独立的文件/目录,让 warning 天然为空
  • Lead 必须在最后跑一次 git diff / 全量验证——这是唯一可靠的冲突兜底
  • 格式化和代码生成也算写:如果两个 member 都跑 gofmt/prettier,即使源码不重合也可能互相覆盖

7. 真实案例:并行调研 → 串行实现

场景

给一个 Go 后端项目(TDSQL 不支持全文索引)调研"中文任意位置子串搜索"的可行方案。 需要:现状审计 + 数据库实测 + 外部方案调研 + 前端影响分析,最后汇总成决策文档。

怎么拆

阶段成员任务写作用域
并行调研backend-auditor审计现有搜索代码路径tmp/research/backend-*.md
db-prober连库实测数据量/查询耗时tmp/research/db-*.md
web-researcher调研外部方案tmp/research/external-*.md
infix-researcher深挖 CJK bigram 索引实现tmp/research/infix-*.md
frontend-auditor前端搜索交互影响tmp/research/frontend-*.md
汇总Lead合并 5 份产出、做决策—

关键设计:

  • 5 个成员,1 lead + 5 = 6 个 agent,在 8 个名额内(§2),留了 2 个缓冲
  • 写作用域按文件名前缀完全隔离,互不重叠 → 并行零冲突
  • 每人只写自己那一个文件,汇总由 Lead 做 → 避免了"多人改同一文档"
  • 任务描述里给了共同的背景段落(fresh context 必须自带上下文,§5)

换到实现阶段

调研完成后转串行,用 blocked_by 表达阶段依赖:

task-1 [pm]      出需求规格与验收标准
task-2 [arch]    技术方案设计        blocked_by: task-1
task-3 [dev]     实现                blocked_by: task-2
task-4 [sdet]    补充/执行测试       blocked_by: task-3
task-5 [qa]      验收与报告          blocked_by: task-4

blocked_by 是上游真实强制的:claim 时 taskReady() 检查所有 blocker 是否 completed (task-board.ts:133),不满足直接抛 TEAM_TASK_BLOCKED。这是唯一"硬"的流程控制机制—— 需要真控制时用它,不要指望 §3 的软状态。

可从 examples/task-templates.md 复制这些任务的完整写法。


8. 协调机制的两个陷阱

这一节的结论直接来自上游内置的 Team policy prompt 与工具实现——它们是运行时真正在执行的行为, 不是建议。优先读原文,比读任何二手总结可靠。

8.1 inactive 不等于"做完了"

内置 policy prompt 原文(tool-agent-team/src/index.ts:41):

"inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents."

list_agents 里的 inactive 只表示"当前没有 turn 在执行"。一个 teammate 可能是:

  • ✅ 真的做完了,在等你验收
  • ⏳ 正在等你的消息(它上一轮问了你问题)
  • ⚠️ 中途失败退出了,但 roster 里还挂着
  • 💤 你派给它的任务它根本没读到

不要看到 inactive 就认为任务完成。 判断完成看两处:共享任务板上 status === 'completed', 或者它 send_message 给你的汇报内容。

其他状态词:「provisioning means member creation is in progress; failed means member creation failed」(同一行)。

8.2 wait_agent 会立刻返回,不是"等到有进展"

wait_agent 有个硬短路(tool-agent-team/src/index.ts:266-282):

const hasActivePeer = ctx.agentTeams.listMembers(caller).some(member =>
  member.id !== caller.id && ACTIVE_WAIT_STATUSES.has(member.status))
if (!hasActivePeer) {
  return { timedOut: false, noProgress: { reason: 'no-active-peer', message: NO_ACTIVE_PEER_MESSAGE } }
}

ACTIVE_WAIT_STATUSES 只有 running 和 provisioning(index.ts:44)。

所以:当你所有 teammate 都是 inactive 时,wait_agent 会立刻返回 noProgress,不会等满 timeout。

上游提示的应对(NO_ACTIVE_PEER_MESSAGE,index.ts:45):

"No other Team member is running or provisioning. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake each required inactive teammate before waiting again."

正确姿势:

list_agents  →  发现队友 inactive
  →  它手上还有没做完的活?
        ├─ 是 → send_message 叫醒它(会 resume 它的 turn)
        └─ 否 → 不要 wait,直接推进你自己的部分
  →  确认有 running/provisioning 的队友了,再 wait_agent

其他 wait_agent 细节(index.ts:250-253):

  • timeout 范围 10000 ~ 3600000 ms,默认 30000
  • "never wakes inactive members"——它不能唤醒人,只能观察
  • "Re-list after wakeup or timeout instead of polling"——超时后重新 list,不要盲目轮询

8.3 发出去 ≠ 对方处理了

"A successful send does not mean the recipient has processed it."(index.ts:41)

send_message 成功只代表投递成功。目标可能:

  • 正在跑,要到下一个 step boundary 才看到
  • 处于 inactive,需要被启动/恢复才会处理
  • 处理了但没回复你

所以发完不要假设对方已经知道;需要确认就等它的回复,或查任务板状态。


9. 速查表

事项事实依据
teammate 上限默认 16,当前 profile 配 8index.ts:42, cordis.patch.yml
lead 是否占名额不占,8 = 8 teammate + 1 leadroster.ts:257, projection.ts:344
名额能否回收不能,名册只增不减journal 只有 team/member、team/task
interrupt 释放名额吗不,完全不碰 rosterroster.ts:187-197
spawn 失败占名额吗占,以 phase:'failed' 留下roster.ts:283-295
名字能复用吗不能,全历史唯一roster.ts:254
任务状态4 值但只有 3 个流程态types.ts:72
能自定义状态吗不能,工具层无 status 参数tool-agent-team/index.ts:375
任务的创建者是谁Lead 创建,owner 可为空task-board.ts:52
谁能改任务owner 或 Leadtask-board.ts:124-126
write_scopes 是锁吗不是,只产生 warningtask-view.ts:43-47
blocked_by 强制执行吗是,claim 时校验task-board.ts:133
teammate 默认看得到我的历史吗看不到,默认 freshtool-agent-team/index.ts:199
默认最大任务数256index.ts:43
单条消息上限65536 字节index.ts:44
subject / description 上限200 / 16384 字符task-board.ts:59-60

10. 获取源码与核验本文

本文所有结论都可用下面的方式自行核验(需要 gh / git):

# 稀疏 clone 精确 tag(只拉这几个包,很快)
git clone --depth 1 --branch dsh-v0.2.1-rc.3 \
  --filter=blob:none --sparse \
  https://github.com/deepseek-ai/deepseek-harness.git dsh-src
cd dsh-src
git sparse-checkout set packages/experimental/agent-team \
  packages/experimental/tool-agent-team \
  packages/experimental/client-ui-agent-team \
  packages/experimental/agent-team-profile

# 核验本文的关键结论(括号内是应看到的行号)
grep -n "DEFAULT_MAX_MEMBERS\|DEFAULT_MAX_TASKS" \
  packages/experimental/agent-team/src/index.ts                    # 42, 43, 44
grep -n "members.length >= this.maxMembers" \
  packages/experimental/agent-team/src/roster.ts                   # 257
grep -n "member.name === name" \
  packages/experimental/agent-team/src/roster.ts                   # 254(名字永久占用)
grep -n "role: 'lead'" \
  packages/experimental/agent-team/src/projection.ts               # 344(lead 单独投影)
grep -n "TeamTaskStatus =" \
  packages/experimental/agent-team/src/types.ts                    # 72(只有 3 个流程态)
grep -n "function statusKey" \
  packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx  # 33(UI 封闭 switch)
grep -rn "appendAndFlush(root, '" packages/experimental/agent-team/src/ | \
  sed "s/.*'\([^']*\)'.*/\1/" | sort -u
  # 输出应恰好两行: team/member 与 team/task —— 证明名册无删除事件
cat packages/experimental/agent-team-profile/cordis.patch.yml
  # 看到 maxMembers: 8 覆盖了代码默认的 16

本地安装产物(node_modules)与源码的关系:npm 包只含 lib/(编译产物), 不含 src/。经比对,dsh-v0.2.1-rc.3 tag 的 README/locale 与本地安装产物逐字节一致, package.json 仅差 monorepo 的 workspace:* 协议。所以要读源码必须去上游取,不要试图从 node_modules 里还原(那里没有 sourcemap,也没有 src/)。


附:与官方文档的分工

文档定位读者
上游 README.zh.md规范说明:包的能力、配置、持久化模型、设计理念想了解"它是什么"
本文实战决策:什么时候用、名额怎么规划、任务怎么写、坑在哪想了解"怎么用不出事"

两者互补。遇到冲突以上游源码为准,并欢迎提 issue 修正本文。

License

本文档以 MIT 发布。上游 deepseek-harness 亦为 MIT;本文不包含上游源码拷贝。

Related plugins