- Home
- Plugins
- Workflow & Automation
- dsh-agent-team-playbook
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-playbookREADME
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,实际是包元数据和安装步骤的问题。
验证它真的生效
这是提示注入插件,没有按钮可点,所以要从提示内容反查。三种方式:
-
查已注册的段落(不改运行时状态):
{ "platform": "host", "provider": "Config", "method": "listConfigs", "input": { "name": "dsh-agent-team-playbook" } } -
看插件行的
application字段:applied才算真正激活;overridden表示被更高优先级层覆盖,restart-required表示尚未生效。 -
在会话里观察:新开会话后,让 agent 复述「Agent Teams 有几条操作规则」, 应答出上表六条。这是端到端确认,比看配置更可信。
需要说明的限制:段落已注册 ≠ 用户一定看到了预期文本。渲染效果取决于 提示组装,本仓库不含自动化断言。
配置
| 键 | 类型 | 默认 | 说明 |
|---|---|---|---|
enabled | boolean | true | 置 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 再召人。做不到"边做边加"的弹性。
实战规则:
- 开干前写死成员清单:需要哪几个角色、各叫什么名字,一次性想清楚。
- 名字是永久资产,起名要能表达职责(
infix-researcher比worker1好,因为失败重试时你得换个名字)。 - spawn 失败不要盲目重试同名——
TEAM_MEMBER_NAME_TAKEN会立刻再失败,且失败条目已经占了名额。换名字重试。 - 用满后靠"复用闲置成员"而不是"加人":把新任务用
reassign或send_message派给已有的 inactive 成员。 - 预算内预留缓冲:8 个名额如果你规划了 8 个,那中途一次 spawn 失败就没救了。建议按 ≤7 规划。
3. 任务状态机增强(上游只有 3 态)
用户常需要更细的流程状态:规划中 → 实现中 → 测试中 → 已完成。但上游只有 3 态。
3.1 先说清上游到底有什么(以及为什么改不了)
任务状态枚举(types.ts:72):
export type TeamTaskStatus = 'pending' | 'in_progress' | 'completed' | 'deleted'
deleted 是删除墓碑,不是流程态。真正可用的流程态只有 3 个。
为什么不能直接加状态——三层都封死了:
-
工具层没有
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 无处可传。
-
服务层状态由 action 单向映射(
task-board.ts):action 目标 status 前置条件 claimin_progress必须 pending且依赖已满足releasepending必须 in_progress,清空 ownercompletecompleted必须 in_progressreopenpending必须 completed,清空 ownerreassignin_progress仅 Lead; pending/in_progressdeletedeleted无下游依赖 edit/set_dependencies不改 status owner 或 Lead -
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 落地建议
- 状态词表固定 4 个,写进角色文档:
planning/implementing/testing/completed completed用真实 status,不要用前缀——因为completed有真实语义(依赖解锁、taskReady判定基于它,task-view.ts:24-26)。其余 3 个前缀是对in_progress的细分- Lead 负责守规矩:既然机制不强制,就在 Lead 的 prompt 里写明"看到
[planning]的任务不应被他人 claim" - 接受它只是"给人和模型看的标签",别在它上面建自动化
一句话:软状态解决的是"人一眼看懂进度",不解决"流程控制"。 需要真控制就用
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,用于提示谁改哪些路径。
它不是锁。 三条证据:
-
类型注释直接写明(
types.ts,writeScope()的 docstring):Normalize one workspace-relative path prefix **without treating it as a lock**. -
重叠只产生 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 } } -
上游 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 配 8 | index.ts:42, cordis.patch.yml |
| lead 是否占名额 | 不占,8 = 8 teammate + 1 lead | roster.ts:257, projection.ts:344 |
| 名额能否回收 | 不能,名册只增不减 | journal 只有 team/member、team/task |
| interrupt 释放名额吗 | 不,完全不碰 roster | roster.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 或 Lead | task-board.ts:124-126 |
| write_scopes 是锁吗 | 不是,只产生 warning | task-view.ts:43-47 |
| blocked_by 强制执行吗 | 是,claim 时校验 | task-board.ts:133 |
| teammate 默认看得到我的历史吗 | 看不到,默认 fresh | tool-agent-team/index.ts:199 |
| 默认最大任务数 | 256 | index.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
webnovel-writer
lingfengqaq/webnovel-writer
loopx (dsh-loopx-plugin)
loopx-project/loopx
ouroboros (dsh-plugin)
q00/ouroboros
loopx (dsh-loopx-plugin)
huangruiteng/loopx