dsh-agents
Verified@banbolee/dsh-agents · v0.3.0 · MIT · Web UI
Agent team runtime layer for DeepSeek Harness: named subagents, per-agent personas and tool surfaces, explicit delegation graph, and absolute depth/width budgets.
Install
dsh plugin add @banbolee/dsh-agents Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
@banbolee/dsh-agents
English | 中文
用 YAML 定义你自己的 Agent 团队,并在 DSH 里以具名工具(agent_<id>)显式委派。本插件把自带的 preset 作为 @deepseek-ai/dsh-agent-preset 行声明的在 profile 的 @deepseek-ai/dsh-agent-preset-registry 上,并在 Web 端的 Plugin Configuration 里提供一张配置页。
设计与决策的完整依据见 docs/agents-plugin-plan.md。
当前状态
方案 §16 的 Stage 1–4 已完成,Gate A–E 平台探针全绿:
- Host 装配:读取内置 catalog(
catalog/*.yaml,7 个 Agent)与用户 catalog($DSH_HOME/banbo-agents/agents/*.yaml),受约束合并与全量校验;发布不可变 ABI generation,用原子current指针替换激活;写入 ABI manifest,保护已发布的toolName/presetId/ main-child 形态 /child.continuation。 - Persona:用户
prompts/优先、包内prompts/兜底;严格 UTF-8、双重尺寸上限、末尾单换行归一化。 - 委派 runtime:
agent_<id>、delegate_batch、授权图与 absolute depth 校验、root Session 并发预算、one-shot/continuable 生命周期、HolderRegistry 与结构化隐私安全日志。 - CatalogRemote:只读、启动期固定的 catalog 视图(无 persona、路径、composition、settings);由官方 Typert generator 生成 Host/客户端 descriptor。
- Web 设置卡:占用插件页自己的
plugins.bundle.config座位(按包名作 key),经严格 Typert Remote 读 catalog,用官方共享配置表单ctx.configForms.get('banbo-agents')读写 enabled/model,而这张表单就是本插件自己 Loader row 的Config。
安装
dsh plugin --profile <profile> add @banbolee/dsh-agents
从源码安装:
dsh plugin --profile <profile> add -w ./plugins/agents
安装后重启 profile:catalog 与 generation 在 Host ready 之前完成,坏配置会让启动直接失败并在报错里给出文件路径。
Preset 声明与自定义预设
本插件把自带的两个 preset(banbo 与 planner)声明为 cordis.patch.yml 里的两条 @deepseek-ai/dsh-agent-preset 行。preset 是声明而不是目录:config.id 是 Session 保存的 preset 身份,config.plugins 是它挂载的 composition。roster 本身属于 profile 的 agent-preset-registry 行(config { default, selectedDefault }),DSH Web 0.2.0-rc.1 以 default: standard 插入它;本插件刻意既不声明也不 patch 这一行,所以别的 Bundle patch 它也无法拿走这里的 preset。
要加自己的 preset,把同样形状的行写进你自己的 profile patch —— $DSH_HOME/profiles/<profile>/cordis.patch.yml —— 或安装一个自带该行的 Bundle:
- insert:
- id: preset-review
name: '@deepseek-ai/dsh-agent-preset'
config:
id: review
name: Review
description: Reviews changes with the shell only.
order: 30
plugins:
- id: persona
name: '@deepseek-ai/dsh-persona'
config:
prefix: You review software changes.
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'
要跑本插件某个主 Agent 的 composition 还必须带上两条 runtime 行——@banbolee/dsh-agents/main-runtime 与 @banbolee/dsh-agents/delegation——每条都把 config.agentId 设成那个 Agent。照抄 cordis.patch.yml 里 preset-banbo 的 plugins: 列表并改掉两个 agentId 即可;这个 composition 已经没有生成器替你产出了。
两条要清楚的结果:
- 用户主 Agent 的 YAML 里写了
main.presetId: my-lead,就必须有一条config.id为my-lead的 preset 行;没有它该 Agent 仍在 catalog(与 ABI)里,但它的 runtime 会拒绝激活并以preset mapping mismatchfail-loud; - profile 若没有组装任何
agent-preset-registry行,这些 preset 行会一直 pending,本插件也会一直等agentPresets服务——Web profile0.2.0-rc.1是组装了它的。
dsh-tui
dsh-tui 0.11.2——本插件实际验证过、也是本机正在运行的那一版——已经在同一套 0.1.7 模型上。它自己的 bundle patch 声明了自己的 roster 行 dsh-tui-agent-preset-registry(包名 @deepseek-ai/dsh-agent-preset-registry,config { default: standard }),并在该包可解析时让 0.1.2 时代的 dsh-tui-agent-presets roster 退场(cordis.patch.yml 原话:"0.1.7 replaces directory discovery with declarations in a registry")。由此两条结论:
- 本插件挂在 profile 组装的哪一条 registry 行之下都能工作——Web profile 的
agent-preset-registry或 TUI 的dsh-tui-agent-preset-registry——因此完全不需要 TUI 专属 patch 行,0.1.5 的dsh-tui-agent-presets座位已彻底移除; - TUI 的 roster 默认仍是
standard,所以要显式选中我们的 preset:DSH_TUI_PRESET=banbo dsh-tui,或 TUI 自己的/preset banbo(持久化的选择会盖过默认值,环境变量又盖过两者)。
dsh-tui 0.10.2 及更早是 0.1.5 世代:它们的 @deepseek-ai/dsh-agent peer 上限低于 0.2.0-rc.1,而且唯一 roster 是扫目录的 dsh-tui-agent-presets 行、压在已退役的 @deepseek-ai/dsh-agent-presets 包上。本插件不支持它们——下限是 0.11.2:0.1.7 的 registry 模型自 0.11.0 起就有,但 0.11.1 的 @deepseek-ai/dsh-agent peer 上限止于 0.1.7-rc.2,所以 0.11.2 才是本机验证过、声明了本插件 ^0.2.0-rc.1 依赖族所需 0.2.0-rc.1 peer 的那一版。
团队一览
内置 7 个 Agent。main 形态出现在官方 Session 的 preset picker 里;child 形态只能通过具名工具委派。
| Agent | 形态 | continuation | 可委派给 | maxDepth |
|---|---|---|---|---|
banbo |
main | — | planner, research, explorer, implement, review, executor | 2 |
planner |
main + child | optional | research, explorer, review | 1 |
executor |
child | optional | research, explorer, implement, review | — |
implement |
child | optional | research, explorer | — |
review |
child | optional | research, explorer | — |
research |
child | one-shot | — | — |
explorer |
child | one-shot | — | — |
maxDepth 是绝对深度上限,不是相对层数。banbo 的 2 意味着 Banbo → Executor → Implement 合法,而 Implement 不能再委派第三层。
委派语义
- 前台(默认):下一步依赖子结果、或会改同一文件时,前台等待。
- 后台:仅当存在明确无依赖、无冲突的其他工作时才用
run_in_background: true。 - batch:多个互不依赖的 one-shot 结果都返回后才能继续时用
delegate_batch。batch 不接受需要保留对话的 Agent。 - one-shot:
research/explorer是一次性任务,结算后不保留可继续会话;给它们发复用型指令不会有第二次送达。 - continuable:
optionalcontinuation 的 child 在前台调用时仍是 one-shot,只有后台调用才保留可继续会话。需要同一专家继续上下文时,先list_agents找 idle 的 continuable child,再用send_message复用。
子 Agent 可能通过 direct message 和 settlement notice 两次送达同一份结论;按 childId 视为同一次完成,不重复行动。
部分状态与并发
deadline到期拿到的是partial_timeout:基于部分结果继续。cancel_requested与cleanup_deferred都不代表任务已完成。- 并发超限 fail-fast:root Session 并发预算用尽时新委派直接失败并说明原因与修复路径,不排队、不静默降级。
- 并发计数不跨重启:重启后额度从零开始,历史占用不会被重新计入。
Persona 与覆盖
内置 persona 按运行形态拆分,固定包含 Role、Responsibilities、Non-goals、Tool Policy、Delegation Policy、Collaboration Protocol、Output Contract、Failure Policy 八节。lint 会机械校验:节标题唯一、顺序固定、每节非空,且不出现未授权的工具名或通用委派入口。
不要编辑发布包里的 prompts/。覆盖有两条路径:
- 同名自动覆盖:把与内置文件同名的文件放进
$DSH_HOME/banbo-agents/prompts/,例如prompts/planner-child.md,它会自动覆盖包内的同名 persona,无需改 YAML。 - 自定义文件名:先放文件,再在用户 Agent YAML 里显式引用:
main:
persona: prompts/my-lead-main.md
覆盖文件遵守同一 UTF-8、64 KiB 单文件与总量限制;配置修改在下次 profile 启动时生效。
设置
Web 端在 Settings → Plugins 里选中 @banbolee/dsh-agents bundle:它的详情页会在 Plugin Configuration 区块里渲染这张卡(插件页自己的 plugins.bundle.config 座位,按包名作 key)。表单的命名空间就是本插件的 Loader row id banbo-agents;一次写入落在 profile patch 中该行的 user 层,并被提交进运行中的插件实例而不需要重挂载。
可编辑项:
includeDefaults:是否启用全部内置 Agent(默认开)。用户自定义 Agent 永远启用。- 每个 Agent 的
enabled:停用可逆,不删除定义。 - child 形态的
model:{ provider, model, reasoningEffort? }或{ default: true }。
不可编辑项(会明确说明原因):
- main 形态的 model:主 Agent 的模型只由官方 Session model selector 决定。给 main-only Agent 写 model 会以
model-on-main-only拒绝。 - 已退役(retired)Agent:只读展示,不能重新启用。
设置卡采用暂存编辑 + 一次提交:改动先进入草稿,点提交后用当前 revision 走一次 mutate;如果 revision 已过期,会保留草稿并提示冲突,不会覆盖别人的改动。清空 model 使用路径级 unset,不会连带删除同 Agent 的其他字段。
YAML/JSON 形状示例:
includeDefaults: true
agents:
planner:
enabled: false
implement:
model:
provider: default-provider
model: default-model
live 状态 vs 冻结 descriptor:enabled / model 是 live 的,改完对新建的子 Agent 生效(main 的模型仍归官方选择器)。而子 Agent 创建时冻结的 persona / toolFilter(continuable descriptor)不会因为后来改设置而回写;已经存在的 continuable child 继续用创建时的组合。这也是为什么推荐用 enabled: false 而不是删除定义,以及为什么退役 Agent 会保留同名空壳工具:冷恢复时冻结的 toolFilter 里必须仍有那个名字,否则官方 tools.restrict() 会直接抛错。
数据布局
$DSH_HOME/banbo-agents/
├── agents/*.yaml 你写的 Agent 定义(唯一需要手工编辑的目录)
├── prompts/*.md 你写的 persona;同名文件覆盖包内版本
├── .generated/ Host 生成的 ABI 产物,勿手工编辑
│ ├── generations/<hash>/ 不可变的一代:abi.json + complete
│ └── current -> generations/<hash> 原子替换的指针
└── .children/<childId>.json continuable 子 Agent 的身份 sidecar
preset 不在这里:preset 是插件自带 cordis.patch.yml 或你自己 profile patch 里的一行,永远不是本插件扫描的目录。
.generated/ 只由本插件写入和清理,且只清理带自己 complete 标记的目录;你放进去的其它内容不会被跟随、也不会被删除。指针替换失败时旧指针原样保留,编译直接失败并报错,不会出现半激活的一代。
Sidecar 可移植性
每个 continuable child 一个不可变 JSON 文件,路径是 .children/<childId>.json,以 childId 为唯一键。写入先把内容落到临时文件,再用 link 原子创建(create-if-absent)发布:已存在的同名记录不会被覆盖——内容完全相同视为幂等重试,内容不同则报 already-exists。不支持硬链接的文件系统回退为「先检查再 rename」,拒绝覆盖的语义不变。多 child 并发写不同路径互不冲突。文件内容不含绝对路径,因此:
- 备份/迁移整个
$DSH_HOME/banbo-agents/目录即可保留子 Agent 身份; - 单独移动
DSH_HOME而带上该目录同样有效; - 删掉某个 sidecar 只会让对应 child 无法按身份复用,不影响其它 child。
卸载与数据保留
dsh plugin --profile <profile> remove @banbolee/dsh-agents
普通卸载保留 $DSH_HOME/banbo-agents/ 全部内容(YAML、persona、ABI manifest、身份 sidecar)。重装后 catalog、退役空壳和旧 continuable 子 Agent 的身份都能恢复。卸载只意味着本插件不再挂载到运行时,它的 preset 行随之消失,官方 roster 回到默认状态。
破坏性清除(不可逆)
只有手工删除才会真正清除:
rm -rf "$DSH_HOME/banbo-agents"
这会同时删除 YAML、persona、ABI manifest 与退役空壳记录。删除 ABI manifest 的后果:旧 continuable 子 Agent 冻结的 toolFilter 里那些 agent_<id> 名字会消失,冷恢复将无法进行。
三条明确的非承诺
send_message的复用不精确计数:它是尽力而为的会话复用,不保证同一个子 Session 被复用几次。- 并发计数不跨重启:重启后并发额度从零开始,历史占用不会被重新计入。
- 删除一个有
main形态的主 Agent 会连带整棵子树失效(连带其下全部 continuable 子 Agent,因为冷恢复需要活着的直接父 Session)。这是期望行为,不是缺陷。
因此推荐用 enabled: false 停用 Agent,而不是删除定义文件:停用随时可撤销;删除会让该 Agent 退役(有 main 形态时连带整棵子树失效),虽然把同名文件放回去可以复活,但要改语义就换新 id。
删除后放回同名 YAML 会复活,但用的是新 composition
删掉定义文件后,那个 id 会退役:已经没有东西再声明它的 preset,旧主 Session 走官方 agent-preset/not-found。把同名文件放回去、并在删过 preset 行时重新声明那一行,再重启,这个 id 会复活——旧 Session 的 preset 解析重新成功,于是按 current-policy resume 语义继续运行。
这条边界要清楚:
- 好处:误删之后放回原文件即可恢复,是最自然的救回路径;
- 风险:如果放回的是改写过的定义,旧 Session 会在一个新 composition 下继续,历史里可能出现新 composition 做不了的工具调用;
- 推荐:要改定义语义就换一个新 id,不要复用旧 id 改含义。复活的定义仍受 ABI 收窄检查约束——不能删掉已发布的 main/child 形态、不能改 preset id、不能切换
continuation,否则启动会明确失败。
Web 设置卡与构建
Host Remote、Typert descriptor 与 Web client 都有构建产物,源码改动后必须重新构建:
pnpm --filter @banbolee/dsh-agents build # node scripts/build.mjs
构建会:
- 先清空
lib/; - 在临时 workspace 适配层里调用官方 Typert generator,产出
lib/typert.host.*与lib/typert.remote-client.*; - 用
tsc产出 Host Remote 与客户端类型(lib/types/**); - 用
tsdown产出单文件经典客户端 bundlelib/client.js(window.__ModuleLoader__工厂、无代码分割)。
排障:
- 设置卡不出现:确认构建已跑过且 profile 里存在
lib/client.js;再次刷新页面。客户端只在 Remote mount 与list()成功后才注册 locale 与 slot,任一步失败都会整体回滚。 - preset picker 里没有 Banbo:确认 profile 组装了
agent-preset-registry行,且我们的preset-banbo/preset-planner行都在(dsh --dump-config)。没有 registry 时这些 preset 行会一直 pending,本插件也不会激活。 - 改了 persona/YAML 没生效:配置在下次 profile 启动时生效;改动的是已有 continuable child 的冻结 descriptor 时,需要新建 child。
- 启动直接失败并给出文件路径:这是期望的 fail-loud。按报错里的文件与字段修正 YAML;旧 generation 与
current指针保持原样。
开发
env NODE_ENV=development pnpm test # 整仓
env NODE_ENV=development npx vitest run --dir plugins/agents # 只跑本插件
node scripts/build.mjs # 重新构建产物
Gate 探针位于 tests/gates/,与产品单测分开:
env NODE_ENV=development npx vitest run --dir plugins/agents # 只跑本插件普通 lane
env NODE_ENV=development pnpm test:agents:gates # Gate + packed lane
tests/packed.spec.ts 会跑真实 npm pack 并断言发布面与纯 ESM 导入,需要先执行一次构建。