# @xmoon76/dsh-subagent-router [English](README.md) | 中文 一个面向模型的 DSH 插件,用于在**模型选定的 LLM provider 与 model** 上启动子 agent(subagent)。模型选择路由(`provider`/`model`);部署方拥有子 agent 后端(`subagentProvider`,默认 `spawn`)、默认调度策略(`backgroundMode`)与路由白名单(`allowedProviders`)。单次调度的调度方式可用可选参数 `run_in_background` 覆盖,与官方 `@deepseek-ai/dsh-tool-subagent` 工具完全一致。continuable 子 agent 的后续轮次复用 `@deepseek-ai/dsh-tool-subagent-control` 的官方 `send_message` / `list_agents` / `interrupt_agent` 工具;后台 one-shot 任务用 `@deepseek-ai/dsh-tool-jobs` 的官方 `job_output` / `job_kill` 工具收集。 ## 安装(以 DSH profile bundle 方式) 本包以 DSH **profile bundle** 形式发布:其 `cordis.patch.yml`(`dsh.bundle.patch`)会在 profile 组合中自动插入**两个** router 行——`tool-subagent-router`(spawn + continuable,`subagent_route`)与 `tool-subagent-router-fork`(fork + one-shot,`subagent_fork_route`)。前提是 profile 的 bundles 包含 `@deepseek-ai/dsh-base`(所有官方 `web`/`headless` 模板都满足)——`subagents` 注册表及其 `spawn`/`fork` 后端来自该 base 层。 ```sh dsh plugin --profile add @xmoon76/dsh-subagent-router ``` 该命令把包安装进 profile,并把它加入 profile 的 `dsh.profile.bundles` 层列表;下次启动时其 patch 按下方默认配置插入两个 router 行(`tool-subagent-router` 与 `tool-subagent-router-fork`)。若要覆盖默认值,在 profile 自己的 `cordis.patch.yml` 里 patch 对应行 id(patch 会替换该行整个 `config`): ```yaml - id: tool-subagent-router config: allowedProviders: - deepseek-official ``` ## 使用指南(Usage walkthrough) 下面是一个面向模型的实际调用流程。派发前请确认 profile 已注册 router 工具(`subagent_route` / `subagent_fork_route`),且所选 provider/model 路由已经配置。 ### 启动可继续的子 agent(默认后台) `subagent_route` 的 `description` / `prompt` / `provider` / `model` 全部必填。模型只选择 LLM 路由;部署配置仍拥有后端与默认调度策略: ```jsonc { "description": "say hi", "prompt": "请简短地打个招呼,然后说明你正在使用哪个模型。", "provider": "codex", "model": "gpt-5.6-luna" } // continuable 结果:started subagent ``` continuable 结果只确认 inbox 已接受请求并返回持久 id,不包含子 agent 回复。请等待 DSH settlement notice,或按 id 查看子 agent transcript。 ### 同步等待全新子 agent 当下一步必须依赖子 agent 结果时,设置 `run_in_background: false`。该调用会以前台方式运行子 agent 并直接返回其最终输出,而不是返回 id: ```jsonc { "description": "review implementation", "prompt": "Review the diff and report concrete risks.", "provider": "codex", "model": "gpt-5.6-luna", "run_in_background": false } // foreground 结果:子 agent 的最终输出 ``` ### 多轮继续对话 子 agent 被接受后,使用官方 `send_message` 控制工具排队下一轮 FIFO 消息: ```jsonc { "subagent_id": "", "message": "你最擅长哪些工程任务?" } // message queued as the next turn for subagent ``` 后续轮次及 cold resume 都保持创建时的 `provider`/`model`;不支持会话中途切换路由。 ### 配套控制工具 以下控制工具不属于本包:继续控制必须从官方 `@deepseek-ai/dsh-tool-subagent-control` 插件单独挂载,后台任务控制来自官方 `@deepseek-ai/dsh-tool-jobs` 插件: | 工具 | 用途 | |---|---| | `send_message` | 向持久子 agent 排队下一轮(FIFO)。 | | `list_agents` | 列出或回忆已启动的子 agent。 | | `interrupt_agent` | 中断正在运行的子 agent 轮次。 | | `job_output` | 收集后台 one-shot 任务的输出。 | | `job_kill` | 停止后台 one-shot 任务。 | 官方委托工具(`subagent`、`subagent_fork`)是另一回事:它们是 `@deepseek-ai/dsh-tool-subagent` 的不同实例,绑定固定的部署路由。本 router **从不替换**它们——参见下方[官方工具共存](#官方工具共存)。 ### 启动动态 fork(one-shot) `subagent_fork_route` 由 bundle 默认挂载(fork + one-shot),因此**继承父会话已完成轮次**的 fork 子 agent 开箱即用。实例使用不冲突的名字(绝不用官方 `subagent_fork`): ```yaml # bundle 插入的默认值(可在 profile 里按 row id 覆盖) - id: tool-subagent-router-fork config: subagentProvider: fork toolName: subagent_fork_route backgroundMode: one-shot enableRunInBackground: true maxDepth: 3 ``` 模型调用(默认等待结果): ```jsonc { "description": "review prior design", "prompt": "Review the design discussed above and identify correctness or maintainability risks.", "provider": "openai", "model": "gpt-5.6" } // foreground 结果:子 agent 的最终输出 ``` Fork prompt 语义:子 agent 已经看到父会话的**已完成轮次**,`prompt` 只需写新增任务;当前 in-flight 父轮次**不在** fork seed 中。 ### 后台运行 fork 设置 `run_in_background: true` 注册一个后台 Task 并立即返回其 job id: ```jsonc { "description": "deep review", "prompt": "Perform a deep review of the design.", "provider": "codex", "model": "gpt-5.6-luna", "run_in_background": true } // background 结果:started background subagent job ``` 用 `job_output` 收集结果,用 `job_kill` 停止工作。后台 fork 是一次性 Task,**不是** continuable 子 agent:不能用 `send_message` 继续它。 ### 最佳实践 - 全新(`spawn`)子 agent 的 `prompt` 要自包含:它看不到父会话。 - fork(`fork`)子 agent 的 `prompt` 只需写增量:子 agent 继承父会话已完成轮次,只写新任务即可;当前 in-flight 轮次不在 fork seed 中。 - 派发前确认 provider/model 可用。没有模型发现工具,路由错误可能直到子 agent 首次解析路由时才出现。 - 把 continuable 启动结果当作确认而非子 agent 答案;实际结果通过 settlement notice 和 transcript 获取。 - 独立委派优先使用后台默认:在同一个 assistant turn 里一起启动多个子 agent,并在它们运行期间继续做其他有用工作;只有下一步依赖结果时才用 `run_in_background: false`。 - 如果部署策略需要限制路由,用 `allowedProviders` 配置;不要依赖 prompt 文案强制执行。 - 不要把凭证、endpoint、headers 放进 prompt 或工具参数;多实例挂载时为每个实例使用唯一的 `toolName`。 - 记住 `maxTokens` 不会在 activation 之间持久化。 ## 为什么需要这个包 官方 `@deepseek-ai/dsh-tool-subagent` 把一个实例绑定到一个固定的子 agent `agentOptions`(部署固定 provider/model)。本插件把 LLM 路由选择交给模型,同时让所有能力仍由 DSH seam 拥有:它只是 `ctx.subagents.startContinuable()` / `ctx.subagents.start()` 与 `ctx.jobs` 之上的薄 Consumer,不重新实现 continuation、session、持久化、权限、任务或队列。调度与生命周期语义其余部分与官方工具一致。 ## 契约(Contract) 面向模型的工具 `subagent_route` / `subagent_fork_route` 接受相同的参数: | 参数 | 必填 | 含义 | |---|---|---| | `description` | 是 | 委托任务的简短(3-5 词)标签。 | | `prompt` | 是 | 完整独立任务(全新子 agent)或基于已完成轮次的增量(fork 子 agent)。 | | `provider` | 是 | 子 agent 使用的已配置 DSH LLM provider 路由。 | | `model` | 是 | 子 agent 对话使用的 model id。 | | `run_in_background` | 否 | 调度覆盖。continuable 实例默认 `true`(返回持久 id);one-shot 实例默认 `false`(返回最终输出)。`enableRunInBackground: false` 时该参数不存在。 | 成功时根据实例的 `backgroundMode` 与本次调用的 `run_in_background` 返回三种规范结果之一: | 类型 | 何时 | 结构 | |---|---|---| | `continuable` | continuable 模式 + 后台(默认) | `{ kind: 'continuable', subagentId }` —— 持久 id,inbox 接受时解析 | | `foreground` | 任意模式 + `run_in_background: false`(或 one-shot 默认) | `{ kind: 'foreground', runId, output }` —— 子 agent 最终输出 | | `background` | one-shot 模式 + `run_in_background: true` | `{ kind: 'background', jobId }` —— 用 `job_output` 收集,`job_kill` 停止 | 凭证、endpoint、headers、`maxTokens`、`outputSchema` 与后端选择绝不暴露给模型。 ## 配置(Config) | 键 | 默认 | 含义 | |---|---|---| | `subagentProvider` | `spawn` | `ctx.subagents` provider 名。continuable 模式要求 `prepareContinuable`;one-shot 模式要求可 start 的 provider(fork 是受支持的 one-shot 后端)。 | | `backgroundMode` | `continuable` | 默认调度策略:`continuable` 调用 `startContinuable()` 并返回持久 subagent id;`one-shot` 调用 `start()` 并返回 run 的最终输出。`run_in_background` 可逐调用覆盖。绝不由模型选择。 | | `executionMode` | — | **已弃用**的 `backgroundMode` 旧别名。与 `backgroundMode` 同时配置时必须一致,否则插件启动时 loud 失败。 | | `enableRunInBackground` | `true` | 模型侧 `run_in_background` 参数是否存在并被采纳。`false` 时从 schema 移除该参数并强制所有调用走前台;伪造的 `run_in_background: true` 会在 `execute()` 中被拒绝。 | | `toolName` | `subagent_route` | 面向模型的工具名;每个已加载实例必须不同。 | | `maxDepth` | `3` | 绝对委派深度上限,或 `'provider-managed'` 表示不设上限。 | | `persona` | — | 覆盖 `deployment:persona` 的每子 agent persona。 | | `toolFilter` | — | 每子 agent 的全局工具限制;要求 `toolFilter` 能力。 | | `allowedProviders` | — | 部署侧 LLM provider 白名单,在任何子 agent 工作开始前于 `execute()` 中强制;显式 `[]` 拒绝所有。 | ## 路由策略(Routing policy) - 模型只选择 LLM 路由:`provider` 必须命名已注册的 DSH LLM adapter 路由,`model` 必须是其上的 model id。 - `allowedProviders` 是 executor 级强制,不是提示词暗示。`provider`/`model` 的有效性最终由子 agent 首次请求时的 DSH LLM/Agent 解析决定(不做 `listModels()` 硬白名单,保留动态 model 路由)。 - 子 agent 后端与默认调度策略是部署配置;模型从不选择它们。 ## 继续对话行为(Continuation behavior) - continuable 子 agent 是持久对话:`send_message`(官方控制工具)投递后续 FIFO 轮次,`list_agents` 列出它,`interrupt_agent` 中断它——全部走 `ctx.subagents` 的权威路径。 - cold resume 保持相同的 `agentProvider`/`agentModel`:持久 descriptor 保存它们,因此恢复后的 Activation 仍使用创建时的路由。 - `provider`/`model` 在创建时固定;不存在会话中途切换模型。 - 后台 one-shot 任务是 Task,不是 continuable 子 agent:`job_output` / `job_kill`(官方 `@deepseek-ai/dsh-tool-jobs`)是它的控制工具,`send_message` 不能继续它。 ## 官方工具共存(Official tool coexistence) 本插件**不替换**官方 `subagent` / `subagent_fork` 工具。两者在同一 composition 中同时挂载时: ```text subagent -> 官方 fresh child, 固定路由, continuable subagent_route -> router fresh child, 动态路由, continuable subagent_fork -> 官方 继承已完成轮次, 固定路由, one-shot subagent_fork_route -> router 继承已完成轮次, 动态路由, one-shot send_message -> 官方(@deepseek-ai/dsh-tool-subagent-control) interrupt_agent -> 官方(@deepseek-ai/dsh-tool-subagent-control) list_agents -> 官方(@deepseek-ai/dsh-tool-subagent-control) job_output -> 官方(@deepseek-ai/dsh-tool-jobs) job_kill -> 官方(@deepseek-ai/dsh-tool-jobs) job_list -> 官方(@deepseek-ai/dsh-tool-jobs) ``` 两个 router 工具与官方对应工具的唯一区别在 child route:官方实例使用部署固定的 provider/model,而 router 让模型在每次调用时选择 `provider`/`model`。其余一切——调度、`run_in_background` 语义、结果类型、system-prompt 引导——完全一致: | 工具 | Child | 路由 | 生命周期 | |---|---|---|---| | `subagent` | fresh | 固定 | continuable(`send_message`) | | `subagent_route` | fresh | **动态** | continuable(`send_message`) | | `subagent_fork` | 继承已完成轮次 | 固定 | one-shot(`job_output` / `job_kill`) | | `subagent_fork_route` | 继承已完成轮次 | **动态** | one-shot(`job_output` / `job_kill`) | router 从不 shadow、替换或修改官方工具定义:它只注册自己的工具名,官方 schema 与行为保持原样(由共存测试套件锁定)。 ## 支持矩阵(Support matrix) | 后端 | backgroundMode | `run_in_background` 省略 / `false` | `run_in_background: true` | 状态 | |---|---|---|---|---| | `spawn` | `continuable` | 前台(等待输出) | 持久 continuable 子 agent | ✅ 推荐 | | `fork` | `one-shot` | 前台(等待输出) | 后台 Task(`job_output` / `job_kill`) | ✅ 推荐 | | `spawn` | `one-shot` | 前台(等待输出) | 后台 Task | ⚪ 兼容 | | `fork` | `continuable` | 前台(等待输出) | 持久 continuable 子 agent | ⚠️ 非推荐 | router 是通用 provider Consumer,因此当 provider 暴露 `prepareContinuable()` 时并不硬拒绝 `fork + continuable`——但产品文档推荐 `fork + one-shot`。 ## 工具名冲突规则(Tool name collision rules) - 每个已加载 router 实例需要**唯一**的 `toolName`。名字已被工具注册表占用时,mount 会在注册任何东西之前 loud 失败。 - DSH 官方 subagent/control 名字(`subagent`、`subagent_fork`、`send_message`、`interrupt_agent`、`list_agents`)被配置为 router `toolName` 时给出专用诊断。 - 永远不要把 router 的 `toolName` 配成 `subagent` 或 `subagent_fork`。随 bundle 提供的是 `subagent_route`(spawn + continuable)与 `subagent_fork_route`(fork + one-shot);更多实例须自行选用唯一名字。 ## 模型体验(Model Experience) ### 工具 schema #### 模型看到什么 注册的 router schema(`subagent_route` / `subagent_fork_route`):`description`、`prompt`、`provider`、`model`(全部必填)加可选的 `run_in_background` 覆盖。`description`/`prompt` 文案跟随后端 provider 的 `inheritsParentContext`:全新子 agent 被告知要提供完整独立 prompt;fork 子 agent 被告知它已看到已完成轮次。continuable 实例说明 `run_in_background` 的 `true` 默认值、settlement notice 与显式前台覆盖;one-shot 实例说明 `false` 默认值与用 `job_output` / `job_kill` 收集的 job id。不存在 `api_key`、`base_url`、`max_tokens` 或后端/模式参数。 #### Token 影响 工具可见的每个请求有固定 schema 成本;本包除 continuable 实例的 `tool:` 引导 section(见下)外不贡献 system-prompt section。 #### KV Cache 影响 注册的工具 schema 不变时前缀稳定;provider 注册生命周期可能在首个变化的工具定义处使复用失效。 ### System-prompt 引导 `enableRunInBackground: true` 的 continuable 实例贡献一个 `tool:` system-prompt section(order 116.5),告诉模型默认后台委派、在同一个 assistant turn 里一起启动独立委派、在子 agent 运行期间继续工作,并且只有下一步依赖结果时才用 `run_in_background: false`。工具缺席(provider 尚未注册或已被移除)时该 section 渲染为空,因此 HMR 不会残留过期引导。one-shot 实例不贡献 section。 ### 工具结果 #### 模型看到什么 `started subagent `(continuable)、子 agent 的最终文本(foreground)或 `started background subagent job `(background)。continuable 结果不携带子 agent 回复;子 agent 按 id 的 transcript 是其行为的来源,settlement notice 独立到达。 #### Token 影响 每次被接受的创建追加一条短结果(continuable)、每次任务注册追加一条(background),或子 agent 输出(foreground)。 #### KV Cache 影响 在可复用请求前缀之后仅追加。 ## 已知限制与延后工作(Known Limitations and Deferred Work) - **不支持会话中途切换模型** —— `provider`/`model` 在创建时固定;持久 descriptor 保存它们,因此恢复后的 Activation 仍使用创建时的路由。 - **没有模型发现工具** —— 模型必须已经知道已配置的 provider/model id;只读发现工具延后。 - **后台启动的 continuable 子 agent 无法被发起调用的工具同步收集** —— 它的 settlement 通过 continuation notice 机制到达,按 id 的 transcript 仍然可用;下一步依赖结果时请用 `run_in_background: false`。 - **`maxTokens` 不可持久化** —— 每次 activation 的预算不保存在 DSH continuable descriptor 中,因此工具不暴露它。 - **只能使用已配置的 LLM adapter/route** —— 子 agent 路由必须在请求时解析;`provider`/`model` 有效性可能直到子 agent 路由解析时才失败(按设计不做 `listModels()` 硬白名单)。 - **继续控制需要官方 control 工具** —— `send_message` / `list_agents` / `interrupt_agent` 来自 `@deepseek-ai/dsh-tool-subagent-control`,需单独挂载。 - **后台 one-shot 任务需要官方 jobs 栈** —— `ctx.jobs`(`@deepseek-ai/dsh-jobs` + 一个注册表实现,如 `@deepseek-ai/dsh-jobs-local`)与 `job_output` / `job_kill` 工具(`@deepseek-ai/dsh-tool-jobs`);没有它们时后台调用 loud 失败。 - **one-shot 输出不流式** —— run 的最终输出在子 agent settle 后一次返回;中间步骤留在子 agent 的 transcript 中。 - **输出 schema 使用 DSH tools 的 value-schema 方言** —— 规范 foreground `output` 是 `{ type: 'array', items: { type: 'json' } }`,其中 `'json'` 是 `@deepseek-ai/dsh-tools` 基于 Schemastery 的 value 类型(官方 `tool-subagent` 使用的同一方言),不是裸 JSON-Schema 关键字;只有 DSH 的 tool registry 消费它。 ## 开发(Development) ### 前置条件 Node.js ≥ 22 与 npm。所有 DSH peer 依赖都从 npm registry 解析(`@deepseek-ai/dsh-*` `0.1.0-rc.x`),因此不需要 `deepseek-harness` checkout。 ### 门禁(Gates) ```sh npm run typecheck # 对 src + tests 跑 tsc npm run lint # oxlint npm run test # vitest(包级集成 + Loader composition) npm run test:coverage # src/ 每文件 100% npm run build # tsc 输出到 lib/ npm pack # tarball smoke(结构、内容、独立安装) ```