DeepSeek Harness 的具名子代理模板:模板固定起始路由(或它从中解析出路由的路由池)、智能体预设和可选人设,委派智能体按名字挑选即可。
30.7/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/18 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/windwhiterain/dsh-subagent-templates.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 14 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装仓库 package.json 标记 private,未发布到 npm,需从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-subagent-templates(未发布到 npm,仅可源码安装)
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/15 09:45:40
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件
查看扫描证据(共 7 处,展示前 7 条)
- 删除本地文件递归probe/store.probe.mjs:34
rmSync(ROOT, { recursive: true, force: true }) - 写入本地文件probe/store.probe.mjs:35
mkdirSync(ROOT, { recursive: true }) - 读取本地文件probe/store.probe.mjs:102
if (!existsSync(LIVE_STORE)) { - 写入本地文件probe/store.probe.mjs:107
mkdirSync(copy, { recursive: true }) - 写入本地文件probe/store.probe.mjs:108
copyFileSync(LIVE_STORE, join(copy, `${MAPPING_DOMAIN.name}.json`)) - 读取本地文件probe/store.probe.mjs:182
const onDisk = JSON.parse(readFileSync(join(ROOT, `${MAPPING_DOMAIN.name}.json`), 'utf8')) - 删除本地文件递归probe/store.probe.mjs:198
if (failed === 0) rmSync(ROOT, { recursive: true, force: true })
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 具名子代理模板
- 固定起始路由
- 按名挑选委派
README
由 DeepSeek 最新模型翻译生成dsh-subagent-templates
DeepSeek Harness 的具名子代理模板。每个模板固定一条起始路由——一个服务商/模型对,或一个路由池,按子代逐个解析出路由——外加一个智能体预设,以及可选的人设。这样,委派的智能体按名称和描述挑模板,不必每次调用都指定服务商、模型和推理强度。
这是一个树外包。它只导入一个包:schema 库 @deepseek-ai/schemastery,行的 Config schema 由它构建;Loader 用这个 schema 校验行,Settings 界面编辑的也是它。它不导入任何 Harness 运行时包:除此之外,它只是跑在 ctx 服务上的纯 JavaScript,lib/harness.js 存放它复刻的那一小部分 Harness 行为。Harness 检出目录里没有任何改动。
模板调用创建的是普通会话
一次委派不是 Harness 子代理。
展开完整文档(README 剩余 1 节)
dsh-subagent-templates
DeepSeek Harness 的具名子代理模板。每个模板固定一条起始路由——一个服务商/模型对,或一个路由池,按子代逐个解析出路由——外加一个智能体预设,以及可选的人设。这样,委派的智能体按名称和描述挑模板,不必每次调用都指定服务商、模型和推理强度。
这是一个树外包。它只导入一个包:schema 库 @deepseek-ai/schemastery,行的 Config schema 由它构建;Loader 用这个 schema 校验行,Settings 界面编辑的也是它。它不导入任何 Harness 运行时包:除此之外,它只是跑在 ctx 服务上的纯 JavaScript,lib/harness.js 存放它复刻的那一小部分 Harness 行为。Harness 检出目录里没有任何改动。
模板调用创建的是普通会话
一次委派不是 Harness 子代理。它创建的是根会话,和人手动打开会话完全一样:
workspaceRegistry.create(cwd) → a workspace in the parent's directory
agents.create({ sessionId }) → a top-level Agent on the template's route
workspace.attachSession(id) → it appears in the normal session list
agent.followup(prompt) → it starts working
这是已发布的 webhook 会话创建路径(packages/webhook/webhook/src/session.ts),不是新发明。由此带来的结果,正是这套设计的要点:
- 模型选择器始终可用。 对客户端认定为子代理的会话,Client 会隐藏模型选择——@deepseek-ai/dsh-client-ui-model-selection 拒绝任何带 subagentAddress 的会话,因为那些 RPC 会在直接父级续接路径之外激活持久化历史。委派的任务和用户打开的会话是同一类东西,所以它们得到同一套界面:选择器、预设选择器、权限预设,以及用户可以输入内容的输入框。
- 用户的选择优先于模板。 模板的路由是子代的初始选择,只在子代拿到第一个持久化请求头之前生效。用户一旦在选择器里改了模型,子代就跑在改后的模型上。
- 子代可以再次委派。 子代是普通会话,跑同样的预设组合,因此持有同样的 subagent 工具,能打开自己的模板会话。嵌套不需要递归预算,因为这一切都不经过 ctx.subagents。
子代的请求头刻意不带 parentSession,也不带 origin,所以 Client 永远不会把它归类为子代理。因此父子关系不能放在请求头里——这两个字段恰恰会让选择器重新隐藏——它放在本插件自己的存储域(subagent_templates,表 children)里。
父级给每个子代命名
每次委派都带一个 name,这个名字是子代唯一的句柄:
| 名称出现的位置 | 作用 |
| 子会话的 Session 标题 | 侧边栏标签、会话列表和面板行都显示同一个词 |
| subagent 工具结果 | 后台调用返回 { kind, name };如果调用方还有其他子会话,还会返回 siblings;Session id 永远不会传给模型 |
| 结算通知 | subagent "explorer" finished. —— 父会话用这个词指代子会话 |
| delete_subagent | 接收 names —— 一个或多个 —— 而不是 Session id |
| 子会话自己的提示词 | 告诉子会话它叫什么,这样 ask_parent 就能自报身份 |
名称原样存储:不限制字符集,不做大小写折叠,不做规范化。名称不能为空,因为它要当标题和句柄用。名称在同一个父会话的存活子会话之间唯一 —— 同一父会话的两个子会话不能共用同一个词,否则之后对任一子会话的引用都会有歧义。重名会在创建任何东西之前被拒绝,所以失败的调用不产生任何开销;进行中的占位覆盖同一轮里的两次调用,这两次调用并发执行。删除子会话会重新释放它的名称。
两个不同的父会话可以各有一个叫 reviewer 的子会话。唯一性按父会话划分,因为任何引用都在这个范围内解析。
一个委派世界,不是两个
模板子会话是普通的根 Session。正因如此,它保留了模型选择器和完整的 Session 界面,也正因如此,它刻意不出现在 ctx.subagents 里。原生委派工具都读那个注册表,所以它们都无法服务模板子会话:
| 原生工具 | 为什么无法服务模板子会话 |
| list_agents | 读 ctx.subagents,其中没有模板子会话 —— 它只会报告空列表 |
| send_message | 只能触达常驻可继续的子会话,而 Harness 对不归类为子代理的会话拒绝这条路径 |
| interrupt_agent | 依据子代理服务中目标记录的血缘关系做授权 |
| subagent_fork | 创建原生子会话:第二种对象,第二种生命周期 |
这不是配置疏漏。把子会话注册到 ctx.subagents,恰恰是让 Client 把它当作子代理、隐藏其模型选择器的原因,所以对同一个子会话来说,这两者互斥。
因此本插件在自己的映射和命名之上,提供了真正重要的三项能力:
| 本插件 | 替代 | 作用 |
| list_subagents | list_agents | 按名称列出本会话的子会话,附带各自运行的模板,以及状态是 working、idle 还是 ended |
| message_subagent | send_message | 按名称向子会话发消息:工作中的子会话在下一步接收,空闲的子会话用它开启新一轮 —— 这一轮产生的输出会像第一次那样回报回来 |
| interrupt_subagent | interrupt_agent | 停止子代理当前的工作,并保留它 —— 会话、记录和名称都还在,之后发一条消息就能让它改做别的事 |
interrupt_subagent 和 delete_subagent 是同一个决定的两端:interrupt 停止工作,子代理仍可用;delete 则彻底结束它。被中断的子代理不会为被停止的那一轮报告任何内容,因为它没有完成,也就没有结果可报;委派本身仍然开着,子代理下一轮产出什么,就报告什么(见「结果」)。
有两样东西是刻意不复现的:
- subagent_fork。 继承上下文的子代理是另一种对象,生命周期也不同,重新加回来,等于把这个设计要消除的那道分裂重新打开。用自包含提示词做委派,才是模板的用途。
- 子代理树。 list_subagents 只报告直接子代理,因为其他工具能作用的就是这个范围:再次委派的子代理有自己的子代理,从那个子代理出发才能到达。孙代理是真实对象,但本会话没有可用的名字指代它。
想改用原生工具,就重新启用 tool-subagent-control、tool-subagent-control/list-agents 和 tool-subagent-fork。这样会话就同时有两套界面,而这正是本插件要避免的状态。
关于预设注册表有一点要注意:部署方决定暴露哪些预设、哪个是默认预设。在默认预设里禁用这三行,只覆盖新会话;如果某个会话显式选择了仍挂载它们的预设,原生界面就会回来。要覆盖这样的预设,就得把它的整个 config 重述一遍,这比让用户自己看得见预设选择更不划算。
子代理可以向父代理提问
委派出去的任务,通常有些东西父代理知道、子代理不知道:已经做过的决定、指的是哪个文件、手里握着的答案。模板子代理是普通 Session,因此继承部署的 send_message —— 它只能到达常驻、可继续的子代理,而 Harness 对不归类为子代理的会话会拒绝这条路径。这里补的就是这条缺失的路径。
在子代理的创建窗口里,插件在子代理自己的作用域上注册以下内容,其他 Session 永远看不到:
- ask_parent —— 向委派它的会话发一个问题或一条发现。父代理在下一步收到,空闲的父代理会被它唤醒。它带有同样的 agent-message 归属信息,所以记录里能看出是谁问的 —— 标题开头是 subagent "<name>" sent a message:,写的是子代理的名字而不是它的 Session id,归属信息自带的 senderSessionId 仍能标识发送方 Session。子代理这一轮里没有回复,所以工具自己的描述会要求提一个父代理能一次答完的问题。
- 一份运行时上下文,写明父代理 Session 并解释这条通道:子代理是一项委派任务,看不到父级的对话(父级也看不到它的),而它的最终消息本来就会自动送达父级——所以 ask_parent 是用来传最终消息带不了的内容,不是再发一份副本。
子代理还保留部署里的 send_message,上下文会明确说明这条路径到不了发起委派的会话。这里刻意不收回它:模板子代理持有 subagent_fork(在 personal 预设里),它会创建一个可续接的子代理,而 send_message 正是通往那个子代理的有效路径。因为不相干的理由禁掉它,等于砍掉一项本来能用的能力。
两处注册都在子代理的创建作用域里,所以它们属于创建时生成的那个智能体。宿主之后恢复的会话——重启之后,或在重新加载的插件行下——到来时是另一个智能体,两者都没有,只剩映射记录还记着是哪个会话发起的委派。插件看到智能体出现时会再教一遍这样的子代理;作用域里已经有 ask_parent 的会拒绝第二次注册,这个拒绝就是「已经教过」的答复,而不是失败。
每条消息都走接收方自身状态要求的那条路径
ask_parent、message_subagent 和结算通知都走同一个通道(lib/delivery.js),由它根据接收会话此刻在做什么,在两条路径之间选一条。选哪条不是偏好问题:一条路径在客户端里可读,另一条不可读。
接收方空闲时,消息作为独立的一轮排队。 消息保留自己的来源——提问是 agent-message,通知是 subagent-settled——因为开启一轮的投递,本来就会在该来源下画成可见的轮次触发器,带图标和标题,标明是谁发的。
接收方正在工作时,消息在下一步被接收,这次投递会拼进已经在跑的那一轮。Web Chat 把来源不是 user 的消息画成注入的上下文,而上下文行不属于它的可见行——于是消息被模型读到,却没人看得见。所以轮次中途的投递会占用 user,这样它才成为可见的轮内行。office 插件出于同样的理由,遵循的也是这条规则。
伪装的只是来源的 kind。消息的其他部分一概不变:模型可见的内容、持久的 MessageId,以及来源上的其他所有字段,包括记录发送者的 senderSessionId。不写 rpcId——浏览器侧的提示词准入是那个字段唯一的写入方,没有它的消息在下游任何环节都不会被当成人发出的指令。
失败的一轮不会吞掉消息
每个拟议步骤之前,Harness 会把接收方整个收件箱批次声明为持久删除,批次会写回日志,作为只有在准备该步骤的请求时,才会写入 user/message。中间任何环节失败——前置步骤监听器、无法解析的路由、磁盘写满——都会结束这一轮,而批次已被认领、从未写入日志。于是这条消息既不在日志里,也不在收件箱里,收件人下一轮运行时根本不知道它曾被发送过:继续下去就会漏掉这个发现。
通道记住自己交出了什么,并盯着收件人自己的日志,找出模型实际看到的那条消息。当某一轮以 error 或 blocked 结束——一次拒绝,丢弃了它已认领的批次——它仍持有、既未写入日志也未排队的消息会被放回并重新投递。放回发生在收件人进入空闲之后,所以替换消息是下一轮最先认领的东西,而不是驱动已经决定不为它开启一轮的那条。失败的那一轮从未认领的消息仍在队列里,保持原样。
取消是刻意不修复的。 停止是有人决定这项工作不该继续;把消息放回去等于推翻这个决定,而不是修复故障。一条消息最多放回三次,之后丢弃并给出警告,而不是永远重试。
挂载了什么
| 注册项 | 作用域 | 用途 |
| subagent(名称是 toolName) | Host 平面 | 按父级选定的 name 委派给一个模板,并声明 run_in_background;前台返回子级的输出,后台返回该名称 |
| list_subagent_templates | Host 平面 | 报告每个模板的模型和智能体预设 |
| list_subagents | 委派会话自己的作用域 | 按名称列出本会话的子级,含模板和工作状态 |
| message_subagent | 委派会话自己的作用域 | 向调用者自己的某个子级发送消息 |
| interrupt_subagent | 委派会话自己的作用域 | 停止某个子级当前的工作,保留该子级 |
| delete_subagent | 委派会话自己的作用域 | 委派智能体按名称删除自己的子级——一次调用可传多个名称,或传 "" 删除全部 |
| ask_parent + 父级身份上下文 | 每个子会话自己的作用域 | 让被委派的子级向父级提问 |
| subagentTemplates 会话投影 | host | 把父级的子级发布到 Client 面板 |
| Subagents 右侧栏标签页 | 浏览器 | 列出本会话的子级,并打开或删除其中一个 |
会话只携带它能用的工具
只有 subagent 和 list_subagent_templates 注册在 Host 平面。四个作用于子级的工具——list_subagents、message_subagent、interrupt_subagent、delete_subagent——安装到会话自己的智能体作用域,时机是该会话第一次委派成功的那一刻。
在第一次委派之前,它们每一个都只能回答「你没有以「子代理」命名的工具,从不委派的会话永远用不到它们:四个工具白白占着提示词,还成了四个诱人的死胡同。按智能体作用域注册还意味着,这些工具属于某个会话,而不属于整个部署——委派的子会话会为自己的子会话拿到自己的一套,无关会话永远看不到它们。
有两点后果值得直说:
- 它们出现在会话的下一次模型请求里。 委派已经发生;工具无法出现在已经在途的请求中。
- 插件运行期间,它们永远不会被撤回。 已经委派过的会话随时可能再次委派;最后一个子会话消失后,它们会如实回答((no subagents)、unknown),而不必让会话再挣一次。这也是 list_subagents 能报告「什么都没有」的唯一方式。
热重载会替换这个插件,而它记录的子会话仍在存储中,所以激活时会接管那些已经有子会话的活跃会话——通过 Agent 注册表和映射存储找到它们。没有这一步,重载后的会话会继续工作,却悄悄失去管理自己委派内容的手段。这种接管自身覆盖不了的是重启:重启不会带回任何 Agent,直到用户打开一个;见下文「重启改变了什么」。
这四个工具位于 Agent 的作用域,而不是本插件的作用域,所以它们的寿命长于注册它们的实例,结束实例的拆卸必须自己撤回它们。一个作用域拒绝同名注册两次,所以留下过期注册的实例会让它的替代者什么都接管不到——并让会话调用绑定到已关闭映射域的工具。因此接管会保留注册自己的 disposer,并在卸载时运行它;作用域拒绝的注册会让会话保持未标记,这样之后的委派可以接管,而不是让会话永久失去这些工具。
重启改变了什么,以及它绝不能改变什么
按智能体作用域注册和结算监视都属于同一个 Agent,所以两者都随它消亡。热重载保留 Agent,这就是重载一直可存活的原因;重启不保留,会话会作为不同的* Agent 回来,一次一个,在用户打开它们时。Host 在加载插件之前不会恢复任何会话,所以本插件只在加载时做的事,在冷启动时什么都看不到。
于是工作改为按 Agent 重新推导,从 agent/created 出发,按该会话扮演的角色:
- 委派过的会话拿回它的四个工具,依据映射存储对「这个会话有子会话吗」的回答;
- 被委派的会话拿回它的 ask_parent,并在它的轮次上武装一个结算监视。
每个监视都从计数恢复,而不是从零。重新武装的监视对父级已有内容的记忆有两半:子会话自己的日志说明有多少轮次已经结束,映射记录里的 reportedTurns 说明其中有多少轮送达了父级。「子级日志比记录更靠前」正是「有一轮已结束,但父级还没收到通知」,因此,Host 停机期间结束的轮次会在父级恢复时恰好投递一次,既不会丢失,也不会重复。计数器出现之前写下的记录不带这个值,读取时按「日志里的全部内容」处理,而不是按零处理,所以旧的委派永远不会被重放。
同一套等待逻辑也决定了父级关闭时的行为:通知没有可投递的智能体时会被暂存,而不是丢弃,等该会话恢复后再投递。只有已结束的委派才会放弃这条通知。
因此,重启不会让会话失去任何它还能处理的东西。真正的代价在途中的工作:子级里正在运行的轮次会随进程一起终止,和其他任何地方一样;正在等待它的前台委派会丢失工具结果,改为通过通知通道上报。
面板
浏览器端(client.js)通过任何外部标签页类型都会用的两个公开阶段,在右侧边栏加一种标签页:sidebarRightTabs.register 定义这个类型是什么,再在该定义自己的 id 下注册带键的 sidebar.right.pane.tab 来提供主体。再加一条指南条目,标签页才能被发现。
主体按会话挂载,只读框架数据:模板信息来自 Host 的 subagentTemplates 投影,子级是否还存在来自会话目录,状态点来自各会话的运行状态。它自己不持有订阅,也从不碰存储域。
每行显示子级的名字(和父级用的是同一个词)、它的模板、存在时长和状态。点击名字会通过内置的工作区导航打开子级,于是它作为普通会话出现,带自己的选择器。删除会在行内询问,然后执行内置的「归档并停止」;插件不发明任何会话生命周期。会话目录丢弃子级时,该行就从列表里消失,这是归档自身的后果;所以归档被拒绝时,行会保留,并把原因显示在行上。
子级永远不会被自动回收
被委派的子级是普通会话,所以它会一直存在,直到有东西显式结束它:
- 用户从面板或会话列表里删除它,或者
- 父智能体用子级的名字调用 delete_subagent。
除此之外没有别的途径。特别是:父会话关闭时,被委派的子级不会被停止;本插件卸载时也不会停止——代码重载绝不能销毁进行中的工作。delete_subagent 会停止子级、释放句柄并归档其会话(从列表移除,日志保留),与面板执行的是同一操作。它的作用范围限于调用者自己的子级:调用者从未给过的名字会被拒绝,返回结果会说明这一点,而不是叫别人家孩子的名字。要停止子代理的工作、又不结束它,用 interrupt_subagent。
一次调用删多个,"" 删全部
delete_subagent 接收 names,是个数组,不是每次调用只传一个名字。父级往往同时处理完多个子级——一批分发都答完了,外加一个放弃的范围——一条规则的成本是一次调用,不是每个子级一次调用。它返回 { deleted, notFound }:结束掉的名字,和从未有过的名字,这样一批里部分失败时,每个名字都能如实汇报,而不是遇到第一个缺失就停下。
"" 是唯一一个表示本会话委派出去的所有子级的名字。它受的权限约束和其他名字一样——通配符在调用方自己的映射里解析——所以它只能触达该会话自己的子级,永远碰不到别人的。值得说清它的代价: 是对本会话仍在运行的一切做破坏性操作,包括答案还没到的子级。批次已知时,传明确的名字。因为通配符按名字解析,一个真名叫 的子级会被任何通配符调用删掉。
一次 spawn 会告诉父级它已有哪些子级
调用方已有其他子级时,委派会在工具结果里返回 list_subagents 本会返回的那些行——名字、工作状态、模板——外加一句话:子代理永远不会因为父级而被停止:
started background subagent "reviewer"
You already delegated: explorer [idle]: medium, scout [working]: high. A
subagent is never stopped for you, so when one is no longer needed — its answer is in, or its work is off track — delete it with delete_subagent, which takes several names in one call and "" for every subagent you delegated.
这两条信息只在那一刻需要,别处都不需要:父级的工具列表不说它持有哪些子级,对话里也没说删除只是它自己的事。第一次委派没有其他子级,所以结果里两条都不带,而两条路径的汇报方式相同——前台调用的输出后面跟这段提示,后台调用的名字后面也一样。
这个生命周期需要两个持有者,且都不是发起委派的那次调用。
- 创建信号是第一个。Agent 的整个生命都熔接在它创建时传入的信号上(agent-loop 从 createAgent 到销毁一直挂着监听器),所以用工具调用自己的 exec.signal 创建的子级,会在那次调用返回的瞬间被拆掉——后台委派正是这样。子级改用本插件持有的 controller 创建。
- Agent 创建时所处的上下文是第二个,也是容易丢的那个。ctx.get() 返回的是被追踪的服务,AgentRegistry.create() 会把新 Agent 的生命周期绑到读取调用时所用的上下文上:agent-loop 把 Agent 的整个 teardown 注册为该上下文 fiber 的 effect,fiber 卸载时用 disposed 取消它的 machine。如果通过本插件自己的上下文创建子代理,子代理会随本插件的 loader entry 一起消亡——被监视文件发生 HMR 编辑时如此,任何 profile 重组导致该 entry 的组合行变化时也如此,而 Plugin Manager 的每个操作都会引起这种重组。因此,每个子代理都改为通过应用根上下文(ctx.root)创建,它的 fiber 只在进程退出时卸载。正因如此,「本插件卸载时子代理不会被停止」才真正成立,而不只是愿望。
读取子代理的最后一行日志。 运行到完成的子代理以 turn/end reason={"kind":"completed"} 结束。被主动删除的子代理(delete_subagent,或用户操作)以 turn/end reason={"kind":"aborted","reason":{"kind":"disposed"}} 结束——这与缺陷导致子代理在没有任何删除调用的情况下死亡时产生的记录形状相同。区分依据不是形状,而是有没有谁请求了这次删除。子代理自行以 disposed 死亡,就是上面说的生命周期绑定缺陷。
配置
- id: subagent-templates
name: 'dsh-subagent-templates'
config:
toolName: subagent # optional; the model-facing tool name
maxDepth: 1 # optional; how deep delegation may nest (default 1)
maxActiveSubagents: 4 # optional; how many children may work at once (default 4)
templates: # required; at least one
- id: medium # required; lowercase letters, digits, hyphens; the template argument
name: Medium # required; display name
description: >- # required; when to pick this template, in the model's vocabulary
Executes and explores: reads code, runs commands, and reports what it found.
provider: command-code-goat # one route: a registered LLM provider id …
model: deepseek/deepseek-v4.1-flash # … and a model that provider serves pool: medium # … OR the name of a route pool owned by dsh-llm-quota-retry preset: personal # optional; agent preset the child joins (default: the deployment default) reasoningEffort: high # optional; the child's starting thinking effort maxTokens: 64000 # optional persona: You are… # optional; the child's own persona toolFilter: # optional; the child's narrowed tool set
deny: [write]
模板只固定子代理是什么。它不说明一次委派是否等待;等待与否由调用上的 run_in_background 决定。
在浏览器中编辑运行中的行
模板列表和两个上限可以在 Plugins 设置界面上、该行自己的页面里编辑:添加、删除、重排、改写模板,用开关选择 route 或 pool,然后保存。该行不会重启——编辑后的值会提交到运行中的配置,委派工具会依据这些内容重建,因此下一次委派看到的是新列表,调用工具的模型看到的是新描述。页面会写入配置档补丁(cordis.patch.yml),所以这次编辑也会成为下次启动的新默认值。
页面背后的 schema 规定每个模板只能有一种路由形式,其他形式一律拒绝,因此同时写了 pool 和 provider/model 的模板无法保存。normalizeConfig 仍会对行里保存的内容运行,正是它捕获 schema 无法表达的问题——重复的 id、格式错误的 toolFilter、未知字段。
toolName 在这里刻意不可编辑:重命名模型当前正在调用的工具需要该行重启,所以它留在补丁文档里。
保存有两点后果,第一次保存前值得了解。行的 config 块会作为数据重写,所以其中的注释不会保留——把说明写在行上方,那里能存活。把三个字段都恢复为出厂值会整体移除该块,此时该行回退到本 bundle 自带的示例模板,而不是保留你原有的列表。
一条路由,或从池中解析出一条
模板固定恰好一个:
- provider + model —— 一条固定路由。两者都必填,且都不能为空。
- pool —— 由 dsh-llm-quota-retry 拥有的路由池名称。路由在每个子代理上解析,就在该子代理存在之前:池会跳过服务商额度耗尽的那些路由,再按各自剩余额度比例从其余路由中抽取一条。因此同一模板的两个子代理会从不同服务商启动,模板就是会话表达「这些都行,把工作分摊到它们上面,挑一个能用的」的方式。
两者都声明或都不声明,该行在激活时失败。pool 模板对应的服务未挂载,或该服务未定义该池,委派会失败,并给出指明模板和池的消息——让子代理跑在部署默认模型上,等于一次没人选择的委派。
模板上的 reasoningEffort 是回退值:解析出的路由可能自带 effort,那个值对子代理优先。maxTokens、preset、persona 和 toolFilter 不受池影响。
list_subagent_templates 会报告每个模板使用哪种形式,模型无需二次委派就能区分固定路由和池。
toolFilter
模板可以收窄子代理的工具集:allow 列出保留的工具,deny 列出失去的工具,两者都不声明的模板保留全部。
空的 allow 是锁定,不是空操作:子代理除了本插件自己安装的工具(ask_parent)之外不保留任何工具。既无 allow 也无 deny 的过滤器会让该行失败,而页面会写入该行接受的任何内容——所以 {"allow": []} 是有效且刻意的锁定,字段提示也说明了它的作用。
schema 不会填入任何内容。缺失的 toolFilter,以及其中缺失的 allow,都必须保持缺失:schemastery 会把缺失的数组变成 [],所以只声明 deny 的模板,到插件手里会变成 { deny: [...], allow: [] },产出的子代理一个工具都没有。
生效的模板列表来自配置档补丁
This package's own cordis.patch.yml is a bundle layer: the host re-reads it
on every profile recomposition but never watches it, so an edit there applies at
the next recomposition rather than when it is saved. A profile override replaces
the whole config object, so the effective list is the last layer that declares
one.
The only layers the host watches are the profile's own cordis.patch.yml and
$DSH_HOME/cordis.patch.yml. Put your templates in the profile's:
- id: subagent-templates
name: 'dsh-subagent-templates'
disabled: false
config:
toolName: subagent
templates:
- id: medium
name: Medium
description: >-
coding, implement, explore, investigate.
provider: opencode-go
model: deepseek-v4.1-flash
preset: personal
reasoningEffort: high
An id-targeted override must restate every key the row needs — toolName and templates here, with maxDepth and maxActiveSubagents falling back to their defaults. Saving that file applies the new list without restarting the host, by restarting this row: apply() re-runs and the tools re-register, while live children are untouched (they are root Sessions created through the application root), and a settlement watch or a waiting foreground call keeps working across the restart — both read the Agent registry through that same root context.
A template's provider, model, and reasoningEffort are the child's starting
route: they seed its first request, and the child's own model picker can change
them afterwards. A pool template's route is chosen the same way, from the pool,
and the child's picker can change that too — and once the route it moved to falls outside the pool, dsh-llm-quota-retry stops managing it.
persona gives the child a persona of its own. It is registered as a
deployment:persona-prefix section on the child's scope, so it shadows the
deployment persona for that one child and is invisible to the parent and to its
siblings. A template without one leaves the deployment persona alone.
toolFilter narrows what the child may call, as allow, deny, or both. It is
applied as a restrict() on the child's scope, so it intersects with whatever the
child's preset already admits and never widens it.
maxDepth
maxDepth bounds how deep delegation may nest, and defaults to 1: a session
may delegate, and a child of that delegation may not delegate again. A session is
counted at depth 0 when it has delegated nothing, so its child is depth 1. Set it
higher, or to provider-managed to state that the bound belongs to the delegation
provider — this plugin delegates to ordinary root Sessions and mounts no provider, so it enforces nothing for that value.
A session the cap has closed is not given subagent or
list_subagent_templates in the first place. Withholding them is the shape this
takes because a refusal is not: a session carrying a tool it can only fail with
pays for it on every turn, in a tool error the model has to interpret each time,
for a fact that cannot change. The two tools are denied on that session's own
scope and nowhere else, so a top-level session under the same row still
delegates, and a session that has children keeps
list_subagents / message_subagent / interrupt_subagent / delete_subagent
— it may not open more, but it can still manage the ones it has.
The depth is decided when the child is created, inside its own creation window,
because the record naming it a child is not written until the delegating call
returns; a session that reaches the cap some other way — one the host resumed after a restart — is judged from the recorded chain instead. maxDepth is editable live, so the restriction is reversible: raising the cap hands the tools back to every session it had closed.
A template child carries no parentSession header — that omission is what keeps its model picker — so the Harness cannot answer its depth. The plugin counts it by walking its own mapping store instead. With no storage domain form mounted there is no recorded chain to walk, so the cap cannot be enforced and delegations proceed.
maxActiveSubagents
maxActiveSubagents bounds how many of one session's children may be working at
once, and defaults to 4. A child that has finished stops counting, so it
frees its slot immediately — the same rule the Harness's own subagent activation pool follows. A child that finished but was never deleted therefore does not block new work; only children still running do.
The cap is per delegating session, not per host: each session gets its own, exactly
as each session has its own child names. It reads the same mapping store as
maxDepth, so with no storage domain form mounted the count is unanswerable and
delegations proceed.
Configuration is validated at activation: an unknown field, a malformed id, a
duplicate id, an unusable toolFilter, a maxDepth that is neither a
non-negative integer nor provider-managed, or a maxActiveSubagents that is not
a positive integer fails the row instead of the first delegation. A background
field from an older patch is an unknown field, so it fails the row rather than
being ignored.
Results
A delegated child reports every turn it finishes on its own terms, not one
result per delegation. What it produces is a conversation the parent can
continue: message_subagent sends it more work, it runs another turn, and that
turn's closing text reaches the parent the same way the first one did.
A foreground call waits for the child's first turn to settle and returns its
closing text as the tool result. That one turn is the call's result and not also
a notice, because the parent is reading it there. A later turn of the same child
is a result nobody is reading anywhere else, so it is reported as a notice.
A background call returns the child's name immediately, and every turn the
child finishes afterwards arrives as a subagent-settled notice. No collection
call is involved.
An interrupted turn is not an outcome. A user who stops a delegated child is
redirecting it rather than ending the delegation: the child keeps its session and
everything it had done, so a foreground call keeps waiting for it and the watch
stays armed. Whichever turn the child ends naturally is what reports — the foreground call's tool result, or a subagent-settled notice — and the interruption itself never fails a delegating call and never answers one. A child nothing wakes again leaves the wait pending on purpose; delete_subagent, or cancelling the delegating call, is what ends it.
Only a natural end is reported: completed, max-tokens, or the child's own
failure. A turn the host refused admission to (blocked) and a turn that never
ended are stops rather than results, so the watch reports nothing for either and
stays armed for the turn that comes next; a deleted child reports nothing at all.
Install
From the directory that contains your checkout of this repository:
dsh plugin --profile <profile> add ./dsh-subagent-templates
or the Plugin Manager's install action with this directory as the target. The
bundle patch inserts the row at the Host plane.
The row's schema is schemastery, so the package carries one dependency. A link:
install does not install a linked package's dependencies, so install them in the
checkout once:
npm install
Without it the row fails to activate with ERR_MODULE_NOT_FOUND. Adding the
settings page to dsh.client.inject also means the first install — or any later change to that block — needs the host to restart: the client-module scan caches a package's manifest for the life of the process.
A profile that mounts dsh-base without dsh-web-app (the shipped headless
profile) also has the base bundle's own host-plane tool-subagent row, which
registers the same subagent name; one tool layer rejects two registrations of
one name, so disable that row first:
- id: tool-subagent
disabled: true
The web profile ships it disabled already.
Use it in a preset
The shipped presets mount @deepseek-ai/dsh-tool-subagent themselves, and a
nearer scope wins a tool name. To make a preset delegate through templates
instead:
1. Drop the preset's tool-subagent row (in the profile's own
cordis.patch.yml) so the Host-plane template tool owns the subagent name.
2. Disable tool-subagent-control, tool-subagent-control/list-agents, and
tool-subagent-fork in the same preset. They read ctx.subagents, which holds
no template child, so they would report nothing and refuse every id.
Leave tool-subagent-codex and tool-subagent-claude-code alone; they are
disabled by default and delegate to external CLIs, which is a separate capability
rather than a second route to the same children.
Known Limitations and Deferred Work
- No parent/child grouping in the shipped sidebar. The child is a normal
session, so the official workspace tree lists it as its own session rather than
nested under the parent. This plugin's own panel is where the grouping shows;
the tree is not extended.
- Deleting from the panel leaves the mapping record behind. The panel performs
the shipped archive-with-stop, so the child Session goes and the panel row goes
with it, but this plugin's own mapping row survives until the parent agent calls
delete_subagent for that name. A later call re-archives an already-archived
Session, which is a no-op. A Host-side delete reachable from the panel would
remove both in one step; it is a client-to-Host call this plugin does not make.
- A background child is stopped by interrupting or deleting it, not by
interrupt_agent.* That tool authorizes against a target's recorded lineage
in the subagent service, which a root-session child has none of. Use
interrupt_subagent, or — since the child is a normal session — its own stop button.
- The plugin owns the child Agent's lifetime. A root Session created here is
not registered with ctx.subagents, so send_message, list_agents, and the
team panel do not see it. Open the child's own tab to talk to it.
- A child that outlived this plugin keeps running with no handle in memory. A
reloaded instance rebuilds the mapping, the parent's tools, and the child's
watch, but the AgentHandle that created the child died with the previous
instance, so delete_subagent stops such a child through the shipped
archive-with-stop path (the Agent registry answers workspace/session-stop by
cancelling its turn) rather than by disposing a handle it no longer holds. The
Agent object itself stays registered, idle and archived, until the process
exits — the residue the shipped panel already leaves for any archived Session.
- No per-call model override. A call names a template and nothing else, by
design; a pool template's route is resolved by the pool, and changing the
child's model afterwards is the user's action in its picker.
- A pool template depends on dsh-llm-quota-retry. The pool, its balance
scripts, and the allowance marks belong to that plugin: this one only asks it
which route a child should start on, and fails the delegation loudly when the
service is absent or the pool is undefined. A template with a fixed
provider/model needs nothing from it.
- No structured output. A template call returns the child's closing text.
- The row's schema owns the shape. toolName cannot be edited from the
settings page because a rename needs the row to restart, and a template that
states neither a route nor a pool is refused wherever it is written. Everything
else about a template is free to change while the row runs.
- Replicated Harness internals. lib/harness.js mirrors shipped files that a
bundle cannot import. A Harness upgrade that changes the final-output
selection rule or the turn-end vocabulary must be reflected there. The file
names its sources.
- The user a mid-turn message claims is a rendering decision, not an
identity one. A consumer that tells a human's own words from another agent's
by source.kind alone now reads a child's mid-turn message as the former. What
keeps that bounded today is that those consumers also require an rpcId, and
only browser prompt admission writes one — a Harness change that dropped that requirement would turn this into a real authority question rather than a naming one. The durable senderSessionId on the same source is unaffected.
- The client half has no bundler. client.js is served as one classic script,
so it imports nothing, reaches React and the shared primitives through the
module system's require, and injects its own stylesheet from --dsw- tokens
instead of shipping a CSS file. The settings page it registers reads and writes
the row's config through the configForms service rather than talking to this
plugin, so it needs no host service of its own. A panel that grew real styling
would want the same treatment the shipped packages get.
Durable storage
The mapping lives in the subagent_templates storage domain, one record per
delegated child, keyed by the child's Session id. The record names its parent, the
name the parent gave it, the template it ran under, the time, the working
directory, and reportedTurns — how many of that child's ended turns have already reached its parent. That last one is what makes a settlement survive a restart, and it is written only after a notice has actually been handed over, so it never runs ahead of what the parent has.
The record's identity field changed from label to name when the naming
requirement arrived, and the unit version did not move. That is deliberate: a
record written before the requirement held a task description in label, which
was then the only word that record had for its child, so the validator reads it
as the name and the record is rewritten in the new form on the next write. A new
reader that accepts both records and always writes the current one is a
backward-compatible change, not a migration — so there is no version to declare compatible, no record to back up, and no unit to move to another layout. The unit's version can be raised later, once no old record remains. reportedTurns was added the same way: a record that has none is read without it, and the watch that resumes from it falls back to the child's own log rather than to zero, so nothing already delivered is ever repeated.
Records are indexed by parent as they are read and written. The question asked
most often — does this Session have children — is asked of every Agent the Host creates, and it must not cost a scan of the whole domain; this plugin is the only writer of that table, so the index cannot fall behind the medium.
probe/store.probe.mjs opens the profile's real subagent_templates.json with
this plugin's own spec, through the Harness's own JSON storage backend, and
fails if any record in it would be refused. That is the check a format change
actually needs, because a record the Host cannot read is a row that fails to
activate.
Developing against a live host
This plugin runs as a link:ed bundle, so the host executes whatever is on disk.
Source hot reload is opt-in per file through the hmr row's root list in
the profile patch, and that list must be kept in step with the plugin's actual
files: a root naming a deleted file is dead weight, and a module missing from
the list silently never reloads.
That failure is quiet and expensive. An edit to an unwatched module leaves the
host running the old code while every other module reloads, so the host runs a
mixed build: a fix appears not to work, disappears intermittently, and looks
like a race or a lifecycle bug rather than a missing watch entry. When a change
seems not to take effect, compare each source file's mtime against the last
cordis.patch.yml write before suspecting the code, and add any module the
list does not name.
A reload restarts this plugin's loader entry, and it is not the only thing that
does: this bundle's own cordis.patch.yml is re-read on every profile
recomposition rather than watched, so an edit to it applies at the next one — and a recomposition that changes this entry's composed config restarts the entry. Live children survive all of that, because they are created through the application root context rather than this plugin's (see "A child is never reaped automatically"); a child that dies disposed anyway means that rule has been broken again. So does everything that has to outlive the entry: the settlement delivery and the child's ask_parent read the Agent registry through that same root context. A watch does not outlive the entry — its teardown stops it — because the replacement arms one of its own from the durable reportedTurns count, and two watches on one child would report every turn twice. What that costs is only the waiting: a template edit mid-delegation reports through the replacement's watch instead of stranding the child.
The host also has no way to load a plugin's client half unless its package.json
declares the ./client export and the dsh.client manifest; a client file that
is present but undeclared is simply never loaded, and a declared one that throws
takes the whole browser plugin batch down with it.
node probe/probe.mjs
node probe/client.probe.mjs
cd /path/to/deepseek-harness
DSH_HARNESS_ROOT=$PWD node --import tsx/esm /path/to/dsh-subagent-templates/probe/store.probe.mjs
probe/probe.mjs and probe/client.probe.mjs need no Harness. probe/store.probe.mjs
drives the Harness's own storage code, so it runs from a Harness checkout with
DSH_HARNESS_ROOT set to it.
probe/probe.mjs runs the delegation path — session creation, the name that titles it, name uniqueness, the required background flag, preset join, the model default the user can override, a pool template resolving a different route for each child and failing loudly without its service, foreground settlement, background delivery, a stopped turn reporting nothing and being waited through, the two owners that keep a child alive (the creation signal, and the app root context it is created through), the three tools that stand in for the native ones, the scope they are installed into, adoption after a reload, adoption after a restart — no Agent live at load, then the user's sessions coming back one at a time — a settlement that survives one, a turn that ended while the parent Session was closed, a legacy record with no reportedTurns, ask_parent restored on a resumed child and installed only once, one watch per child whoever created it, the reload handoff, a deleted child that this instance never created, delete_subagent by name — several names in one call, and the wildcard for every child at once — the note a spawn adds about the children the caller already delegated, the projection rows, the mapping store, and configuration rejection — against in-memory fakes, with no Harness and no model. Its delegation helper takes the background flag as a separate argument and refuses to run without it, so no probe call can quietly stop testing the call a model actually makes, and it reads the child-management tools out of the parent agent's own scope rather than the Host plane, which is the boundary under test.
probe/client.probe.mjs runs the browser half the way the module system runs it:
the file is loaded with a window.__ModuleLoader__ and a require, and the tab
body it registers is driven against a jsdom document and the real React taken from
the Harness checkout's pnpm store. It checks the two-stage tab registration, that
both dictionaries carry the same keys, the row the panel draws, opening a child,
the delete question, the archive it performs, a refused archive, and the two ways
a child stops being listed. For the settings page it checks that the page
registers only while the Host serves the row, the summary line, that an edit
reaching the page stages and then saves as one whole-value mutation, that an
invalid toolFilter and a blank id block the save, that restoring the shipped
defaults unsets the three fields, and that a refused write keeps the draft. It does
not prove the panel or the page inside a real browser.
probe/store.probe.mjs runs the mapping store against the Harness's own storage
code over a copy of the real store, which is where a record-format change is
decided. It is the one probe that needs the Harness checkout, for its TypeScript
sources and tsx.
probe/isolated-e2e.md is the keyless end-to-end run
that proves the same delegation path through a real Loader tree and app in an
isolated profile, and records the assertions to read out of the session logs.
License
MIT预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
windwhiterain/dsh-subagent-templates
DeepSeek Harness 的具名子代理模板:模板固定起始路由(或它从中解析出路由的路由池)、智能体预设和可选人设,委派智能体按名字挑选即可。
相关插件 · 技能 / 提示词
人设统管ZiChuanLan/agent-purge
一份系统提示词,喂给多个 coding…
★ 931已汉化DSH
73.8
人物画像蒸馏titanwings/distilly
Distilly — 把他们的思考方式提炼成可复用的技能,供任何智能体或机器人使用。原名 Colleague Skill(原同事 Skill)。
★ 25,659已汉化DSH
71.3
推理工作空间套件Tiger3807861189/J-Space-Cognition-Suite
J-Space 是面向复杂推理、大型仓库工程、子代理协作与授权安全分析的推理时工作空间和控制套件。
★ 2,957已汉化DSH
70.5
两阶段工具锚定预设xiaobright/dsh-anchored-standard
实验性 DeepSeek Harness agent preset…
★ 3,742已汉化DSH
70.1
智能体工作流编排nrslib/takt
不再日夜盯着 AI 编码智能体。
★ 1,402已汉化DSH
70
Agent 运行时手册Electricitysheep/dsh-handbook
DeepSeek Harness dsh 从 0 到 1 深度手册:安装/插件开发/性能调优/实测案例/同模型多…
★ 827已汉化DSH
68.7