用 TOML 文件给 DeepSeek Harness 声明具名子代理的插件。它不修改 DSH 源码,作为普通 bundle 通过 dsh plugin add 安装。
- 一个工具 +
agent_type参数:模型只看到subagent_custom(名字可配)这一个委派工具,用agent_type选择委派给谁,而不是每个定义多一份工具 schema。 - 两层目录:
$DSH_HOME/agents/*.toml(全局,始终加载)与<projectRoot>/.dsh/agents/*.toml(项目级,默认关闭,需显式信任)。同名时项目定义覆盖全局定义。 - 失败互不牵连:某个定义写错或用了当前 provider 不支持的能力时,只有该定义不可用(日志 + 工具描述说明原因),其余照常,且绝不会导致 Agent 创建失败。
- 热生效:定义文件在每次委派时重新读取;目录 watcher 让模型看到的
agent_type列表同步刷新。
文档导航:安装 · 不支持与智能体团队组合 · 插件设置项 · 用自然语言创建定义 · 插件行配置 · TOML 字段参考 · 常见误解 · AI 代写提示词注意 · DSH 升级时看这里 · 示例定义 · 技术文档 · 开关场景讲解
dsh plugin --profile web add <path-to-this-checkout>dsh plugin --profile web add @heluojiang/dsh-agents-toml包已发布到 npm(scoped 包
publishConfig.access: public,因此公开可装)。从 npm 装到的是构建产物(lib/、assets/skill/、cordis.patch.yml、文档);prepare在发布时已构建宿主与客户端两个面。
npm 安装注意两件事:
-
刚发布的版本不会被立刻装上 —— 要装最新版就显式写版本号。 pnpm 11 默认启用"最小发布年龄"(
minimumReleaseAge):dsh plugin add @heluojiang/dsh-agents-toml会解析到上一个够老的版本(实测:0.2.2 发布约 20 分钟后,裸包名装到的仍是 0.2.1),并把解析到的那个版本追加进 profile 的pnpm-workspace.yaml→minimumReleaseAgeExclude。因此:- 立刻装最新版:显式写版本,例如
dsh plugin --profile web add @heluojiang/dsh-agents-toml@0.2.2; - 或等过了发布年龄窗口再装(那时裸包名自然取到最新);
minimumReleaseAgeExclude记的是"包@版本",所以每个新版本首次安装都要显式指定一次。
- 立刻装最新版:显式写版本,例如
-
判断"是否发布成功"要看注册表,不要看网页。 npmjs.com 对不存在的包也会渲染一个页面,容易误判;权威判据是:
npm view @heluojiang/dsh-agents-toml version # 成功时输出最新已发布版本号 curl -s -o /dev/null -w '%{http_code}\n' https://registry.npmjs.org/@heluojiang%2Fdsh-agents-toml # 成功时 200
另外 npm 不允许覆盖已发布版本:改动后必须升版本(
npm version patch)再npm publish;直接重发同一版本会报You cannot publish over the previously published versions—— 那说明该版本早已发布成功,不是失败。
安装后,web profile 会自动加载本插件的客户端半边(dsh.client 清单 + ./client 导出,产物 lib/client.js),无需额外步骤;headless / sdk / acp 等没有 GUI 的 profile 会忽略它。
dsh plugin --profile web add github:Heluojiang/dsh-agents-tomlgit 安装拿到的是源码,包内自带 prepare 脚本。pnpm ≥10 默认拦截依赖的构建脚本,第一次 add 会失败并打印放行所需的完整 key:
ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED The git-hosted package "dsh-agents-toml@<版本>" needs to execute build scripts but is not in the "allowBuilds" allowlist.
allowBuilds:
dsh-agents-toml@https://codeload.github.com/<you>/dsh-agents-toml/tar.gz/<sha>: true
把 allowBuilds: 下面那一整行追加到 $DSH_HOME/profiles/<profile>/pnpm-workspace.yaml(该文件已存在,追加,不要覆盖),然后重跑 add。两个坑:只写裸包名 dsh-agents-toml: true 不足以放行(pnpm 按完整 spec 匹配);key 里的 SHA 随提交变化,更新到新提交后要把新打印的 key 也加进去。
放行构建脚本等于允许该包以你的权限执行代码:只放行你信任的仓库,并用 github:<you>/dsh-agents-toml#<sha> 固定提交。
用 --patch 覆盖层,行名写成绝对路径(裸包名只在已安装到 profile 时才能解析):
# dev.patch.yml
- insert:
- id: dsh-agents-toml
name: '/absolute/path/to/dsh-agents-toml/lib/index.js'dsh web --patch ./dev.patch.yml| 动作 | 开启 HMR 的 profile(web 默认) |
关闭 HMR 的 profile(headless / sdk / acp / sdk-minimal) |
|---|---|---|
| 安装/卸载本插件 bundle | 不需要重启(DSH 监听 profile 的 package.json 与 patch 文件) |
需要重启 |
新增/修改 *.toml 定义 |
不需要重启:每次委派都重读文件,下一个模型请求就能用新定义(监听开着时 agent_type 列表也在此时刷新) |
同左 |
| 在插件设置项里改配置 | 不需要重启:四个键保存后立即作用于运行中的会话,见下表 | 不适用(无 GUI) |
手改 cordis.patch.yml |
不需要重启(补丁文件被监听,该行重载) | 需要重启 |
| 升级本插件版本(含客户端半边) | Host 模块不热替换,建议重启;刷新浏览器会重新拉取 lib/client.js |
需要重启 |
四个设置项各自的生效方式(这是最容易误判的一处):
| 设置项 | 写入后什么时候生效 |
|---|---|
信任项目级定义 trustProjectAgents |
立即:重新安装每个运行中 Agent 的工具,agent_type 列表当场更新,新出现的目录也当场开始监听 |
监听定义目录 watchDefinitions |
立即:关掉会关闭已打开的目录监听,打开会马上重新打开 |
工具名 toolName |
立即:每个运行中 Agent 用新名字重新注册(旧注册先释放) |
列出不可用定义 reportFailuresToModel |
立即:工具描述重新生成 |
换句话说:四项都在保存后立即作用于运行中的会话,不需要重启、不需要新会话。保存本身会写入 profile 补丁,因此改动不会丢;刷新浏览器不影响其中任何一项(工具表是 Host 侧按 Agent 安装的,浏览器只影响界面)。
技术细节:只改这四个开关时,Loader 不会重挂本插件行,而是原地更新取值并通知插件(
loader/volatile-update)。本插件收到通知后对每个运行中 Agent 重新执行一次"发现 + 安装"——因为四项都改变已安装工具的内容(列表、名字、描述),只更新 watcher 不足以让运行中的会话看到。代价是保存设置时每个 Agent 的工具会被替换一次,这与"改一个.toml文件"走的是同一条路径。
本插件按"具名子代理"设计,与官方的「智能体团队」组合包不支持组合使用。请先在「插件 → 官方 → 智能体团队」把它关掉,再使用本插件。
官方对该组合包的说明就是它的设计意图:Ordinary subagent delegation and overlapping global child controls are disabled(@deepseek-ai/dsh-experimental-agent-team-profile 的 README)。其 cordis.patch.yml 会禁用四行——tool-subagent-control、tool-subagent-list-agents、tool-subagent、tool-subagent-fork——并改挂 tool-agent-team。于是:
| 影响 | 具体表现 |
|---|---|
| continuable 子代理无法主动追问 | 续聊只能靠官方 dsh-tool-subagent-control 的 send_message(agent_id);它被禁用后,拿子代理 id 去调同名工具只会得到 active teammate "<id>" not found。(子代理收尾的最终文本仍会回传:那条结算通知由 dsh-subagent 的续接管理器发出,团队组合包只禁用四个工具行,不触及它) |
| 同名工具不同含义 | 团队工具的 send_message 参数是 target(队友)、list_agents 列的是队友;拿子代理 id 去调只会得到 active teammate "<id>" not found,模型与人都容易误用 |
| 官方委派工具消失 | 官方 subagent / subagent_fork(标准预设里默认 backgroundMode: continuable)被换成队友工具 —— 这不是本插件的工具,但会改变模型的默认选择 |
| 模型可能不再选你的定义 | 两套工具同处一个作用域,多智能体任务上模型可能优先用团队工具 |
仍然照常工作:agent_type 委派、TOML 解析与全部校验、能力位判定、max_depth、[tools]、output_schema、设置页四个开关、内置 Skill、定义热更新 —— 这些都不经过团队工具。
自查与关闭:
- 会话工具列表里出现
spawn_teammate,即团队已启用; - 关闭团队:插件页「官方」栏关掉「智能体团队」的开关(等价于把它从 profile 的
dsh.profile.bundles移除),或dsh plugin --profile <profile> remove @deepseek-ai/dsh-experimental-agent-team-profile; - 本插件的设置页会自动检测:一旦发现团队已启用,就在配置区顶部显示红色提示,建议你去关掉团队。这个提示框本身可以关闭(右上角
×):关掉后不再显示,浏览器里记住该选择,等团队真的关闭后再启用时会重新提示。 - 提示只做提醒:本插件不会替你关闭团队,也不会改动你的 profile —— 关闭动作始终由你在插件页执行(上一条)。
安装后在插件页里可以看到本插件自己的配置区:打开「插件」→「已安装」→ 点击 dsh-agents-toml。
| 设置项 | 配置键 | 作用(一句话) |
|---|---|---|
| 信任项目级定义 | trustProjectAgents |
是否加载 <项目根>/.dsh/agents/*.toml(安全开关,默认关) |
| 工具名 | toolName |
模型调用的那把工具叫什么(默认 subagent_custom) |
| 监听定义目录 | watchDefinitions |
定义文件变化后是否立刻刷新模型看到的 agent_type 列表 |
| 在工具描述里列出不可用定义 | reportFailuresToModel |
是否把失败定义及原因写给模型看 |
每个开关的具体场景(含"开着/关掉分别是什么现象"、以及"模型看到什么"与"调用时读什么"的区别)见 guide/settings-explained.md。保存会写入当前 profile 的 Cordis 补丁,无需重启。
注意区分两个名字:工具名是模型调用的工具(
subagent_custom);agent_type是委派给哪个定义(reviewer、explorer,由 TOML 的name决定)。改工具名不需要动任何 TOML。
项目级定义默认关闭,因为 <projectRoot>/.dsh/agents/*.toml 会随 git clone 一起到来(忽略时日志会记一行说明)。
方式一(推荐):上面的设置项里打开「信任项目级定义」。
方式二(无 GUI / 脚本化):把下面这段追加到 $DSH_HOME/profiles/<profile>/cordis.patch.yml。脚本是幂等的,并会处理"补丁还是默认空序列 []"的情况:
$profileName = 'web' # 你的 profile 名
$dshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }
$patch = Join-Path $dshHome "profiles\$profileName\cordis.patch.yml"
$manifest = Join-Path $dshHome "profiles\$profileName\package.json"
$block = @(
''
'# 允许 <projectRoot>/.dsh/agents/*.toml 的定义(dsh-agents-toml)'
'- id: dsh-agents-toml'
' config:'
' trustProjectAgents: true'
)
if (-not (Test-Path $manifest)) {
"profile 不存在:$manifest —— 先用 dsh --profile $profileName ... 初始化它"
} elseif (-not ((Get-Content $manifest -Raw | ConvertFrom-Json).dsh.profile.bundles -contains '@heluojiang/dsh-agents-toml')) {
"该 profile 还没安装本插件,先运行:dsh plugin --profile $profileName add github:<you>/dsh-agents-toml"
} elseif ((Get-Content $patch -Raw) -match 'trustProjectAgents') {
"已存在 trustProjectAgents 配置,未修改:$patch"
} else {
$lines = @(Get-Content $patch)
$code = @($lines | Where-Object { $_.Trim() -ne '' -and -not $_.TrimStart().StartsWith('#') })
if ($code.Count -eq 1 -and $code[0].Trim() -eq '[]') {
# 默认补丁就是一个流式空序列 [],必须替换它:在它后面追加块序列会让 YAML 解析失败
Set-Content -Path $patch -Encoding utf8 -Value (@($lines | Where-Object { $_.Trim() -ne '[]' }) + $block)
} else {
Add-Content -Path $patch -Encoding utf8 -Value $block
}
"已写入 $patch"
}验证与其余方式(--patch 临时启用、让 agent 代改)见 技术文档 · 插件行配置。
装完插件后,会话的技能目录里会多出一条本插件自带的技能 dsh-agents-toml。你不需要记字段,直接说需求即可:
帮我加一个只读的 SQL 审查子代理,不许改文件。
再给我一个便宜的探索助手,能反复追问的那种。
模型会加载该技能,然后:
- 问清三件事:用途与边界、能不能改文件/跑命令、定义放在哪个目录;
- 按规范写文件:把它写到
$DSH_HOME/agents/(默认,始终加载),或在你明确要求"随仓库分发"时写到<项目根>/.dsh/agents/; - 提醒你信任开关:写入项目目录时它会告诉你,需要先在插件设置里打开「信任项目级定义」,否则该文件不会加载;
- 告诉你结果:定义的
name就是调用时的agent_type,写完下一次委派即生效(模型看到的列表在下一次安装或设置写入时刷新)。
技能正文里带着字段规范与常见坑(max_depth = 0 必然失败、output_schema 只能配 one-shot、[tools] 工具名是部署相关的、哪些 provider 支持模型路由/persona),所以模型不需要你解释这些;需要更深的细节时它会读包内的 guide/technical.md 与 guide/*.toml 模板。
边界:技能是指令而不是强制——模型仍然需要 write 权限才能落盘;如果某个精简组合没有挂载技能服务,插件照常工作,只是没有这条技能。技能与实现的细节见 技术文档 · 内置 Skill。
本插件的 bundle 自带 cordis.patch.yml,安装时已经自动插入了这一行;它的 7 个配置键全部有默认值,所以:
- 一个键都不写也能正常工作——
config整段可以完全不存在; - 需要改时,优先用上面的插件设置项(4 个键有 UI,保存会替你写进 profile 补丁);
- 另外 3 个键是"部署布局",没有 UI,需要时手写进
$DSH_HOME/profiles/<profile>/cordis.patch.yml。
| 键 | 默认值 | 作用 | 有设置项? |
|---|---|---|---|
trustProjectAgents |
false |
是否加载项目目录的定义 | ✅ |
toolName |
subagent_custom |
模型看到的工具名 | ✅ |
watchDefinitions |
true |
定义目录变化后重装工具、刷新 agent_type 列表 |
✅ |
reportFailuresToModel |
true |
在工具描述里列出不可用定义及原因 | ✅ |
defaultProvider |
spawn |
定义未写 provider 时使用的传输 |
❌ 手改 |
projectAgentsDir |
.dsh/agents |
项目内的相对目录 | ❌ 手改 |
userAgentsDir |
未设置($DSH_HOME/agents) |
覆盖用户级定义目录(绝对路径);仅在该行加载时读取,改后要重载该行 | ❌ 手改 |
手改时的完整写法(patch 是整段替换 config,但没写的键会由 schema 默认值补齐,所以只写要改的项即可):
- id: dsh-agents-toml
config:
trustProjectAgents: true # 打开项目级定义
defaultProvider: spawn # 例:部署级默认传输
projectAgentsDir: .dsh/agents # 例:项目内目录改名
# userAgentsDir: 'D:/my/agents' # 例:把用户目录放到别处$DSH_HOME 解析与 Harness 一致:环境变量 DSH_HOME(去空白后非空)优先,否则 ~/.dsh;开头的 ~、~/、~\ 按操作系统主目录展开,最终解析为绝对路径。
把文件放进 $DSH_HOME/agents/(或已信任的项目 <项目根>/.dsh/agents/),文件名随意,定义名由 name 决定。下面这份带全部键的注释说明,可直接作为模板:
name = "reviewer" # 必填:agent_type 取值,须匹配 [A-Za-z0-9][A-Za-z0-9_-]{0,63}
description = "只读代码审查" # 必填:模型据此选择该子代理(非空)
enabled = true # 可选,默认 true;false 时不出现在 agent_type 列表里
mode = "one-shot" # 可选,默认 "one-shot";"continuable" 会立即返回子代理 id
provider = "spawn" # 可选,默认取插件行 defaultProvider(默认 spawn)
# 以下四项是"子代理走哪个模型"的路由覆盖,需要 provider 支持 agentOptions
llm_provider = "deepseek-official" # 可选:子代理使用的 LLM provider
model = "deepseek-v4-flash" # 可选:子代理使用的模型
reasoning_effort = "high" # 可选:推理档位
max_tokens = 4096 # 可选:正整数
persona = """ # 可选:只作用于该子代理,遮蔽部署 persona
你是资深代码审查者,只报告问题与依据,不修改文件。
"""
max_depth = 1 # 可选,默认取 Host 的 subagent.maxDepth(默认 1);最小 1
output_schema = { type = "object", properties = { summary = { type = "string" } }, required = ["summary"] }
# 可选:对象根 JSON Schema;只能配 mode = "one-shot"
[tools] # 可选:从提示词移除并拒绝执行这些工具(只能做减法)
deny = ["write", "edit"] # 名字必须是本部署真实注册的工具完整可运行的两个例子:guide/explorer.toml(continuable + 子代理模型路由)、guide/reviewer.toml(one-shot + 工具限制)。两者都逐键标注了必填/可选与省略时的行为。
| 键 | 必填 | 类型 / 可选项 | 默认 | 作用与约束 |
|---|---|---|---|---|
name |
是 | string,[A-Za-z0-9][A-Za-z0-9_-]{0,63} |
— | agent_type 的取值。同一目录内重名 → 两个定义都失败;跨目录同名时项目覆盖用户 |
description |
是 | 非空 string | — | 模型选择该子代理的依据:会以 <name> — <description> 的形式写进 agent_type 参数说明(枚举本身只带名字),所以写清"什么时候该用它" |
enabled |
否 | true / false |
true |
false:不进 agent_type 列表与工具描述;显式调用报 subagent "x" is disabled in <file> |
mode |
否 | "one-shot" / "continuable" |
"one-shot" |
one-shot:等待子代理完成并返回文本;continuable:立即返回 started subagent <childId>——本次调用不带结论,子代理收尾时会把它的最终文本作为结算通知投递给父会话(前提是结论写在最后一条消息里,见 继续一个 continuable 子代理) |
provider |
否 | 已注册的传输名(随 profile 而定,如 spawn/fork/acp/codex/claude-code/dsh-sdk) |
插件行 defaultProvider(spawn) |
未注册时调用报错并列出已注册的 provider 名 |
llm_provider |
否 | string | 不覆盖(继承父级路由) | 子代理的 LLM 路由 provider;需要 agentOptions |
model |
否 | string | 同上 | 子代理使用的模型;需要 agentOptions |
reasoning_effort |
否 | string | 同上 | 推理档位;需要 agentOptions |
max_tokens |
否 | 正整数 | 同上 | 生成长度上限;需要 agentOptions |
persona |
否 | 非空 string(多行用 """) |
部署 persona | 只作用于该子代理;需要 persona |
max_depth |
否 | 整数 ≥ 1 | Host subagent.maxDepth(默认 1) |
本定义创建出的子代理的绝对深度上限;需要 depthLimit。不写时:provider 支持 depthLimit 才下发 Host 深度,不支持则本次委派不下发上限(见下) |
output_schema |
否 | TOML 表(对象根 JSON Schema) | — | 子代理返回结构化结果;需要 outputSchema,且只能配 one-shot。成功时父会话收到的是该 schema 的 JSON 文本 |
tools |
否 | 表,子键 allow / deny(非空字符串数组) |
不限制 | 从子代理提示词移除且拒绝执行;需要 toolFilter。名字必须是本部署真实注册的工具 |
键名允许用 - 代替 _(llm-provider ≡ llm_provider、max-depth ≡ max_depth)。未知键与类型错误一律报错,不会被静默忽略。
| 字段 | 需要的 provider 能力 | 支持的 provider |
|---|---|---|
llm_provider / model / reasoning_effort / max_tokens |
agentOptions |
spawn、fork、dsh-sdk |
persona |
persona |
spawn、fork |
tools.allow / tools.deny |
toolFilter |
spawn、fork |
max_depth |
depthLimit |
spawn、fork |
output_schema |
outputSchema |
spawn、fork(且仅 one-shot) |
mode = "continuable" |
prepareContinuable |
spawn、fork |
acp / codex / claude-code 不声明任何启动能力。只要定义里没写上表任何能力字段,这些 provider 照常可用;一旦写了,失败时机分两类:
- 解析期失败(文件写错):必填缺失、类型错误、未知键、
max_depth = 0、[tools]出现非allow/deny的键 —— 定义直接判失败并给出原因。 - 调用期失败(能力不匹配):provider 是否具备某项能力只有在委派那一刻才能确定,因此报错形如
subagent "x" cannot run on provider "codex": child LLM routing is unsupported by this provider。
Host 的深度是绝对深度上限,而 Harness 规定:请求里带 maxDepth 就必须由具备 depthLimit 的 provider 执行,否则整次调用被拒(官方工具为此提供了 maxDepth: 'provider-managed')。因此本插件的规则是:
| 定义 | provider 有 depthLimit |
结果 |
|---|---|---|
写了 max_depth |
有 | 按该值下发 |
写了 max_depth |
无 | 调用期报错(an explicit depth cap is unsupported by this provider),与上表一致 |
没写 max_depth |
有 | 下发 Host 的 subagent.maxDepth(默认 1) |
没写 max_depth |
无 | 不下发上限,本次委派没有深度限制;Host 日志会为该 provider 记一条 warn |
也就是说:想让某个 provider 上一定有上限,就在定义里写 max_depth(代价是该 provider 必须支持 depthLimit)。
mode = "continuable" 是 Harness 自带能力(ctx.subagents.startContinuable()),本插件只把 TOML 字段映射过去,不自己实现回传与续聊。于是有四条必须分清的事实:
-
工具返回值只是"已启动":
started subagent <childId>(与官方subagent工具在 continuable 下的措辞逐字相同)。初始 prompt 在入队被接受时就返回,这就是这次调用的全部返回值 —— 结论不在里面。 -
收尾会回传,但只回传最终文本:子代理那一轮结束时,
dsh-subagent的续接管理器会向父会话投递一条结算通知(官方措辞:you are notified when the run settles):Background subagent <id> finished and will do no further work unless you send it more. Its closing message: <子代理最后的文本>父会话空闲时会被它唤醒;挂掉/超限/拒绝/失败的收尾也各有对应句式(
was stopped before it finished/ran out of room/declined the task/failed before it finished),没有文本时写It left no closing message.。不回传的是中间过程:工具输出、推理、以及中途说过但不在收尾文本里的话 —— 这些要靠消息往返。两种投递形态不要混判:子代理运行中主动
send_message(agent_id = 父代理 id)时,父会话看到的形态是Agent <子代理id> sent a message:+ 正文 —— 它只表示"还在跑,先报一件事";只有这一轮结束后由宿主自动投递的Background subagent <id> finished …+Its closing message:才表示"已收尾,以下是结论"。发送方会同步拿到投递确认(message delivered to agent <父会话id>),失败时返回错误、可见且可重发;这条确认是送达而非答复。 -
父会话必须在它收尾时仍然存在:结算通知由续接管理器投递给当时仍活着的直接父 Agent —— 服务没有持久化父邮箱,父会话若已结束(典型是
dsh … "任务"这类一次性 headless 运行,父轮次先turn/end),通知无处投递、静默丢弃且不重放。注意这与模型侧send_message不同:那条失败会返回错误,而结算通知这一路不会报错。因此需要当场拿到结论的一次性任务请用one-shot定义(one-shot的结论就是工具返回值,不经过这条路);continuable留给父会话能持续在线的场景(GUI 会话)。 -
续聊要靠官方的
dsh-tool-subagent-control,不是任意叫send_message的工具:
| 工具 | 参数 | 语义 |
|---|---|---|
send_message |
agent_id、message |
只授权直接父子之间(父 → 直接 continuable 子,或驻留的 continuable 子 → 直接父);返回 {messageId} = 送达确认,不是答复。目标在跑就在最近步骤插入;空闲则唤醒;已结算则冷启动一个新 Activation 再投递 |
interrupt_agent |
agent_id |
要求它停止当前工作(不等它停下);之后仍可用 send_message 继续 |
写
persona的要点:既然回传的只是"最后一条消息的文本",continuable定义的 persona 必须明确要求把结论汇总在收尾消息里(含证据、结论、未解决问题),而不是散落在中途的工具调用中。中途出现影响父代理决策的发现时,再显式要求它用send_message(agent_id = 父代理 id)提前发一条。
两条硬限制:
one-shot子代理永远无法续聊;兄弟、隔代祖先、自身也都不被授权。- 启用 Agent Teams 后"续聊"这条会被它替换掉。
dsh-experimental-agent-team-profile会禁用tool-subagent-control(连同tool-subagent、tool-subagent-fork、list-agents),改挂tool-agent-team:那个send_message的参数是target(队友),对子代理 id 只会报active teammate "<id>" not found。此时agent_type委派本身照常可用,结算通知也照常投递,只是父代理无法再主动追问该子代理 —— 这是该实验性 profile 的既定取舍,不是本插件的问题。
误解 1:mode = "continuable" 会等子代理跑完,答复稍后自动送达。
前半句错、后半句对。started subagent <childId> 就是这次工具调用的全部返回值(官方语义:初始 prompt 入队被接受即 resolve),结论不在返回值里;但子代理收尾时宿主会投递一条带它最终文本的结算通知(见上一节),父会话空闲时会被唤醒。要注意这个回传是"最后一条消息的文本",中途的工具输出、推理都不在其中 —— 所以结论必须写在收尾消息里,persona 要这样要求它。
误解 2:任何叫 send_message 的工具都能继续子代理。
只有官方 dsh-tool-subagent-control 的那个可以:参数是 agent_id,只授权直接父子之间,返回的是送达确认而不是答复。Agent Teams 的同名工具参数是 target、寻址的是队友:拿子代理 id 去调只会得到 active teammate "<id>" not found。
误解 3:装了 Agent Teams 就不能用本插件了,或者装了就收不到子代理结论。
本插件的 agent_type 委派直接调用 ctx.subagents.start() / startContinuable(),与官方 tool-subagent* 无关,所以委派本身照常可用;结算通知也照常投递(它由续接管理器发出,团队组合包只禁用四个工具行)。但官方不支持这个组合使用方式:被 Agent Teams 替换掉的正是"官方直连委派"那一路 —— 父代理续聊 continuable 子代理的控制工具、list_agents(变成列队友)、以及官方 subagent / subagent_fork 工具本身。因此本插件不推荐、也不支持与它同时启用;设置页检测到团队开启时会显示红色提示(提示框自身可关闭;关闭团队仍需你自己在插件页操作,本插件不会代为改动 profile),完整说明见与智能体团队(Agent Teams)不支持组合使用。
误解 4:continuable 保证父会话一定收得到结论。
收得到的前提是父会话在子代理收尾时仍然存在。结算通知没有持久化邮箱,只在父会话自己的 turn 流里追加;父会话先结束就静默丢失,也不会重放。一次性运行(headless / -p 单轮 / 脚本化)要用 one-shot 定义,它的结论就是工具返回值。
配了 output_schema 的 one-shot 定义,父会话收到的是符合该 schema 的 JSON 文本(缩进两格)。这一点值得单独说明,因为 Harness 会给这类子代理下一条硬指令(When you have your final answer, you MUST report it by calling the structured_output tool… Do not finish with a plain text answer):子代理的最终文本通常是空的,真正的结论在结构化值里。所以:
- 成功时:工具返回值就是那段 JSON,不会再出现"(the subagent finished without a text answer)"这种占位句;
- 如果子代理同时留了文本,文本会附在 JSON 之后(中间空一行),两条信息都不丢;
- 子代理跑完却没有产出符合 schema 的值时,错误里会明确写出
the child did not produce a value for output_schema,而不是让它看起来像模型崩了; - 想要 JSON 之外的散文结论,就在
persona里另外要求它交一段总结文本(上面那条指令并不禁止追加文本)。
persona 是子代理的系统提示,父会话最终收到什么,由它决定。让 AI 代写定义时,最容易在这里出问题:
你选的 mode |
父会话拿到什么 | persona 必须写清的 |
|---|---|---|
one-shot |
工具调用的返回值 = 子代理最终文本(配了 output_schema 时改为那段 JSON) |
报告什么、什么顺序、什么算证据 |
continuable |
先拿到子代理 id;收尾时收到结算通知,里面只有子代理最后一条消息的文本 | 要求它把结论写在收尾消息里(研究发现 + 证据 + 未解决问题),不要只回"完成";中途必须让父会话知道的发现,再要求它用 send_message(agent_id = 父代理 id) 提前发出,并明确只发那一条增量、完整结论仍只放收尾消息 —— 漏写后半句时,子代理会把整份结论中途也发一遍,父会话收到两份重叠内容(先是一条 Agent <id> sent a message:,随后收尾通知里又是同一份) |
两种模式都不要把本次任务写进 persona(任务由调用方的 prompt 传入),也不要重复调用方已有的规则;persona 只写角色、边界和汇报格式。
它约束的是本定义创建出来的子代理的绝对深度(父级为 0,直接子代理为 1),而不是"这个子代理还能不能再往下委派":
max_depth = 1(推荐):允许本次委派;子代理若再想委派,其深度 2 > 1 会被拒绝 —— 这才是"它不能再往下委派"。max_depth = 0永远无法成立(子代理深度至少为 1),因此本插件在解析阶段就判该定义失败并说明原因,而不是让模型在运行时撞到subagent depth 1 exceeds maxDepth 0。
tools.allow / tools.deny 的名字必须来自该部署实际注册的工具:Windows headless profile 只有 pwsh,web profile 还可能有 bash/terminal,其他平台是 bash。写错不会静默忽略,而是在委派时明确报错并列出已知工具名:
Error: tools.restrict() names unknown global tools "bash", "terminal";
known global tools: create_goal, edit, …, pwsh, read, write
所以示例只 deny 每个部署都有的 write/edit,shell 工具的 deny 行以注释保留,按你的部署取消注释。
- 权限预设 / 沙箱 / 审批策略:由父会话快照继承,"只读子代理"只能用
[tools] deny近似(并一并 deny shell 类工具)。 - provider 实例级设置(如
claude-code的permissionMode、acp的command/args/env):属于 profile 里的插件行,定义只能按名选择已注册的 provider。 - 子代理工作目录:继承父会话 cwd。
- 新增工具:
[tools]只能做减法。 run_in_background:未暴露;后台语义由mode决定。
完整清单与原因见 技术文档 · 不支持的能力。
- 日志:每个失败文件一行
dsh-agents-toml: <file>: <原因>(同一文件 + 原因只报一次)。 - 工具描述:列出不可用定义及原因(可关,见设置项)。
- 调用报错:名字不存在 → 列出当前可用名字;命中失败定义 → 给出原因与文件路径。
- 任何定义问题都不会导致 Agent 创建失败。
| 文档 | 内容 |
|---|---|
guide/technical.md |
技术文档:实现结构、插件行配置语义、解析与校验规则、能力位规则与报错、生命周期、热更新、客户端半边、真机验证结论、开发与测试、限制 |
guide/settings-explained.md |
四个设置项的场景讲解 |
guide/explorer.toml / guide/reviewer.toml |
逐键注释的完整示例 |
npm install
npm run check # 两个编译面类型检查 + 文档链接检查 + 单测
npm run check:harness # 用已安装的 DSH 校验本插件依赖的声明是否还在(见下)
npm run check:e2e # 用真实 Harness 复验"声明如何变成模型可见的工具"(见下)
npm run build # 产出 lib/*.js、lib/types/*.d.ts 与 lib/client.js单测完全不依赖 DSH 安装,也不读写真实 $DSH_HOME。细节与覆盖范围见 技术文档 · 测试。
单测里的 tools.register 是替身,因此"设置写入后正在运行的 Agent 会不会重新安装工具"这条路径在单测里无法验证——它取决于 Loader 的 volatile 语义。scripts/e2e.mjs 用真实组件复验它,分三档,按环境变量逐档启用:
| 档位 | 需要 | 验证内容 |
|---|---|---|
| Loader(默认跑) | 已安装的 DSH | 用真实 cordis-plugin-loader 建行、建 Agent、写设置:项目定义即时安装、volatile 写入重装而不重挂、关闭信任释放工具、重名双向剔除、关监听后文件不再触发重装 |
委派(E2E_PROJECT + E2E_API_KEY) |
模型凭据 | 真跑一次 dsh --profile … --json,从会话日志核对 subagent_custom 的 agent_type 枚举、子代理工具表里 deny 是否生效、AGENTS.md 是否注入子代理 |
浏览器(E2E_GUI) |
一个运行中的 Web 实例 + 带调试端口的浏览器 | 在已打开的会话里改设置/改定义文件,从该会话后续请求的日志核对枚举当场变化;continuable 子代理的收尾文本是否作为结算通知回投父会话 |
| 变量 | 默认 | 用途 |
|---|---|---|
E2E_DSH_ROOT |
解析已安装的 @deepseek-ai/dsh,再退回 node.exe 旁边的全局安装 |
被测 Harness 的安装目录;Loader 档要它的 cordis 与 cordis-plugin-loader |
E2E_PLUGIN_ROOT |
本仓库根 | 被测插件;Loader 档加载它的 lib/index.js,所以先 npm run build |
E2E_HOME |
无 | 一次性 DSH_HOME。委派档与浏览器档必需(要有 profile 与会话目录) |
E2E_PROFILE |
plugin-dev |
E2E_HOME 里给委派档用的 profile 名 |
E2E_GUI_PROFILE |
plugin-gui |
浏览器实例所用 profile 名;浏览器档通过它找到 cordis.patch.yml 来读回设置写入 |
E2E_PROJECT |
无 | 被检查的真实项目(要有 .dsh/agents/*.toml)。脚本只读它:跑前跑后逐个 SHA-256 比对并核对 git status --porcelain |
E2E_API_KEY |
无 | provider key。不给就跳过委派档而不是判失败 |
E2E_BASE_URL |
无 | provider 的 base URL;key 不是发给默认端点时设置 |
E2E_GUI |
无 | 运行中的 Web 实例 URL(含 token)。给了才跑浏览器档 |
E2E_CDP_PORT |
9222 |
那个浏览器的远程调试端口 |
① Loader 档——不需要凭据,几秒钟。先构建,因为 Loader 档加载的是 lib/ 而不是 src/:
npm install && npm run build
npm run check:e2e # 或 node scripts/e2e.mjs② 委派档——先造一个一次性 home 和一个打开信任的 profile,再把 key 从环境变量传进去:
export E2E_HOME=/tmp/dsh-e2e
export E2E_PROJECT=/path/to/your/project # 有 .dsh/agents/*.toml 的项目
export E2E_API_KEY=sk-...
export E2E_BASE_URL=https://your-endpoint/v1 # 可选
dsh --profile plugin-dev --from-default-profile headless
dsh plugin --profile plugin-dev add "$PWD"
# 在 profile 补丁里打开 trustProjectAgents(见「插件设置项」),或让项目定义先不生效看反例
node scripts/e2e.mjs这一档会真跑一次模型,消耗真实额度;断言全部读会话日志,因此不依赖模型说了什么。
③ 浏览器档——需要三样东西同时活着:一个 Web 实例、一个带远程调试端口的浏览器、以及它们所属的一次性 home。缺任何一样这一档都跑不起来,所以下面按顺序给出全部命令。
# 1) 隔离 home 与 profile(不要用你日常那个 profile)
export E2E_HOME=/tmp/dsh-e2e
dsh --profile plugin-gui --from-default-profile web
dsh plugin --profile plugin-gui add "$PWD"
# 2) 起 Web 实例(用私有端口,避免和日常实例抢);控制台会打印带 token 的 URL
dsh --profile plugin-gui --port 3931 --no-open
# 记下形如 http://127.0.0.1:3931/?token=<token> 的地址
# 3) 起浏览器并开远程调试(用独立 user-data-dir:Chrome 136+ 禁止在默认
# user-data-dir 上开远程调试,用默认目录会静默失败)
# Chrome/Edge 均可,端口与下一步的 E2E_CDP_PORT 必须一致
chrome --remote-debugging-port=9222 --user-data-dir=/tmp/cdp-profile \
--no-first-run --no-default-browser-check about:blank
# 4) 跑这一档
export E2E_PROJECT=/path/to/your/project
export E2E_GUI='http://127.0.0.1:3931/?token=<token>'
node scripts/e2e.mjs在 Windows PowerShell 里把 export X=… 换成 $env:X = '…',$PWD 换成 (Get-Location).Path,第 3 步的浏览器命令换成:
Start-Process chrome -ArgumentList '--remote-debugging-port=9222',
'--user-data-dir=D:\temp\cdp-profile','--no-first-run','--no-default-browser-check','about:blank'浏览器档检查的是会话日志(谁在什么时刻被注册、agent_type 枚举是什么),不是画面,因此无头启动也可以;但 Chrome 在窗口被最小化或完全遮挡时可能不响应截图——这一档不截图,所以不受影响。
约定:E2E_HOME 用一次性 home;E2E_PROJECT 用真实项目,脚本在跑之前后对 AGENTS.md 与 .dsh/agents/*.toml 逐个算 SHA-256 并比对 git status --porcelain,跑完必须一字不差;凭据只从环境变量读。浏览器档连的是调用者自己起的实例(E2E_CDP_PORT,默认 9222),不会去碰别的端口。它不在 npm run check 里,也不在 prepublishOnly 里:三档都需要真实环境,且委派档与浏览器档会消耗真实额度。
本插件不 import 任何 @deepseek-ai/dsh-* 包:它只通过 src/host.ts 里的结构性类型调用 Harness 的服务,因此发布包的版本落后于运行时也不会把它锁死。代价是"声明被改名"这类变化编译器不会发现,所以有一个专门的检查:
npm run check:harness # 自动解析已安装的 @deepseek-ai/dsh,解析不到就找 node.exe 旁边的全局安装
DSH_SHAPE_ROOT=<node_modules> npm run check:harness # 指定别的安装位置它会逐条读取已安装包的声明文件,核对本插件用到的每个名字(subagents.start / startContinuable / getProvider / resolveMaxDepth、SubagentResult.structured、depthLimit、inheritsParentContext、loader/volatile-update、skills.registerProvider、plugin-manager/changed、remote.<namespace>、listBundles 等),任何一条消失就打印 DRIFT 并以非零码退出,指明要改哪个模块(src/host.ts、src/harness.ts、客户端半边)。本机根本没有 DSH 时它单独报一句 no DeepSeek Harness installation found 并以退出码 1 结束,而不是把 16 条声明全判成 DRIFT——"没装"与"被改名"必须区分开,否则第一眼的结论正好是反的。它不在 npm test 里,因为单测刻意不需要 DSH 环境。
src/harness.ts 是全项目唯一做能力探测的地方:composition 缺少某个可选服务时(Agent 注册表、共享深度策略、技能注册表),它会在加载时明确告知缺什么、会少哪个功能,而不是在用到时才静默降级。
Harness 会检查插件的 @deepseek-ai/dsh 与 @deepseek-ai/dsh-* peer 范围,不满足就拒绝安装(并提示用 dsh plugin allow-version 逐版本放行)。本包故意不声明这类 peer:
- 它不 import 任何 DSH 包,所以不存在"版本对不上就加载失败"的机制;权威检查是
npm run check:harness,它针对当前真实安装逐条核对声明,比一个写在清单里的范围更准确; - 写死范围会把"向上兼容"变成"范围之外一律拒绝":DSH 现在是
0.2.0-rc.2这类预发布版本,^0.2.0这种范围不匹配预发布,反而会挡住能正常工作的运行时(本机实测:声明^0.2.0时安装被拒,改成^0.2.0-rc.2才能装); - 代价是升级 DSH 后可能出现签名漂移,由
check:harness与docs/technical.md的宿主契约清单兜住。