dsh-subagent-codebuddy:把子智能体委派给本地 CodeBuddy CLI 的 DSH bundle

前言

写 DSH profile 的人迟早会遇到一个具体问题:父智能体要把一件活交给子智能体,而子智能体跑在一个独立的 CLI 进程里,不是进程内的队友,也不是可以继续对话的本地孩子。这时用通用委派工具得到的只有一句 started subagent <id>,模型不知道它已经开跑、也等不到任何进度,只能反复去问 wait_agent / list_agents,而这两个工具只跟踪进程内队友和可继续的本地孩子,对进程外一次性子智能体永远回答 “no subagent running”。

下面介绍的 lloydzhou/dsh-subagent-codebuddy 就是针对这个场景的 profile bundle:它把每次委派落在本地 CodeBuddy CLI(codebuddy --acp)里,模型通过配置钉住,并在工具描述与结果文案上明确告诉模型「子智能体已经在跑、你不要等它」。

这是什么

一句话定位:一个独立的 DeepSeek Harness profile bundle,提供一个一次性(one-shot)子智能体 provider,每次子任务在本地 CodeBuddy CLI 中以 ACP 会话运行,子模型由配置钉住(默认 deepseek-v4.1-flash,以 --model <id> 传入)。

  • 维护者:lloydzhou
  • 包名:dsh-subagent-codebuddy,npm 上同名发布
  • 版本:0.2.1-rc.5(见 package.json)
  • 许可证:MIT
  • 语言:TypeScript
  • 分类:工作流
  • 来源归属:社区目录页在 skillhub.cn,属独立社区站点;插件本身是开源仓库。目录站与 DeepSeek / 幻方没有官方从属关系。
  • 星标:0(截至写作时,来自 GitHub 仓库页)

它解决的是三个连在一起的问题:委派要能真正发起、结果要能在子智能体 settle 时回到父会话、模型不要因为一句含糊的结果去轮询一个永远查不到的运行。

运行时行为:一次性委派与结果回传

provider 写在 harness 暴露的公开 subagent 接口上,具体是 @deepseek-ai/dsh-subagent 里的 NO_START_CAPABILITIES、settleRunResult、subprocessRunHandle 三个 helper。它不带 prepareContinuable,所以两侧都没有 inbox:CodeBuddy 子智能体是独立 CLI 进程,没有 dsh 工具,无法回调父会话。

结果回传方式因此很有限:子智能体只在 settle 时报告一次,harness 注入一条 Background subagent <id> finished,带着子智能体的收尾消息,这条 notice 会启动父智能体的下一轮。运行中途没有进度通道——README 强调这是后端特性,不是插件缺陷。

作为对照,README 解释了 Team message from <child>: … 这类 notice 属于可继续的本地子智能体,它们带 dsh 的 send_message 工具和父会话收件箱。CodeBuddy 子智能体不属于这一类,因此不会出现这种中途消息。

于是工具要做的事变成:阻止父智能体在原地空等。它的结果文案说明轮询无效、父智能体应继续做别的事或结束本轮,让 settle notice 能在最早的 step 边界到达。

模型侧的两个工具

bundle 挂载的两个工具都来自这个包自己的 ./tool 入口,而不是通用的 @deepseek-ai/dsh-tool-subagent:

  • subagent_codebuddy:启动一次运行
  • codebuddy_steer({ childId, message }):给仍在运行的一次运行追加消息

两个工具走的是同一条运行时路径 ctx.subagents.startActivation({ delivery: 'parent' });差别在文案。通用工具的 started subagent <id> 没有措辞旋钮,模型没有理由停止寻找这次运行;这个包的描述和结果明确说子智能体已经在跑、没有任何工具能观测它、结果会自动到达。

codebuddy_steer 包的是 CLI 的 ACP steer 扩展,这是运行中唯一能进消息的通道。README 给了一个对照表:

second session/prompt    →  CLI 当作竞争轮次,约 33ms 结束第一轮,输出为空
--input-format stream-json →  启动的是另一个 session,正在运行的那个看不到
session/steer            →  {steered: true},消息在子智能体下一个 step 边界投递,
                           正在进行的工具调用不被中断

childId 是启动 notice 里那个 id。provider 在 session/new 返回时于 src/steer.ts 发布 live handle,运行 settle 时撤销,所以已经结束的 id 会被拒绝,工具改为列出仍然 live 的 id,而不是猜。子智能体返回 {steered:false} 时按拒绝上报并附原因,不重试,常见原因是子智能体正处在两轮之间。

聊天卡片展示什么

每次委派在 transcript 中生成一张卡片(conversation.chat.node,kind 为 codebuddy-run),锚定在自己的 tool/call 事件上。卡片折叠三样东西:交接结果、settle notice、以及中途发进去的消息。外部子智能体只产生这三个时刻,所以卡片能诚实表达的范围也就是这些:

状态 触发来源 卡片展示
运行中 tool/call 计时从调用开始
后台运行中 tool/result(activation notice) 计时继续;正文点名运行时与日志,随子智能体动作填充
已完成 settle finished 收尾语句,附在任务与时间线之下,CLI 的 cost 行提到头部
已停止 settle was stopped 完整保留该 notice
被拒绝 settle declined the task 同上
失败 settle failed before it finished / ended abnormally,或运行抛错 诊断信息,外加所有 - [done] … 步骤

原则是「一次运行一张卡片」。codebuddy_steer 调用在 transcript 里也是普通工具调用,同样匹配 /codebuddy/,若只看名字会另开一张永远没人 settle 的卡片,停在「后台运行中」挨着它只是发消息的那次运行。所以定义按调用做了什么来判:arguments 里带 childId 的是「对某次运行说话」而不是「启动一次运行」,会折进启动那个孩子的卡片(正文里显示为 ↪ 插话 …),若当前页面从未见过那张卡片则折入空。它自己的结果在任何情况下都被忽略——运行状态只在子智能体 settle 时改变。普通工具行照常上报这次调用。

卡片正文是记录而非替换:按运行发生的顺序读——孩子拿到的任务(完整 prompt,头部只放调用方的 3–5 词标签,正文折叠四行、本地可展开)、中途消息、孩子做过的事,最后才是它怎么结束。settle 是追加收尾块,不覆盖已有内容。为了让重挂载(重连、从 session log 重新渲染 transcript)后仍成立,src/client/live.ts 里有一个按运行的小缓存:轮询在 settle 后停止,没有它,已完成的卡片重新渲染时历史会丢失。

两条需要直说的后果:

  • 计时衡量的是等待,不是生命。两个事件之间卡片从 transcript 什么也听不到,浏览器读不到运行的 beacon;所以头部只计数并明说这一点,hover 文本指向 codebuddy-runs,那里确实能回答这个问题。卡片真正能看到的子智能体动作,取决于宿主是否应答(见下一节)。
  • 随宿主一起死掉的子智能体会永远停在「后台运行中」,因为没有事件可折叠,codebuddy-runs 称之为失联。宿主活着而孩子死掉是另一种情况:ACP 管道关闭,运行以 error settle,卡片转「失败」,该场景由 tests/mock-acp-child.mjs 的 crash 固定。

实时进度:transcript 之外唯一的通道

只折叠 transcript 事件的卡片,在一次十分钟的运行里只能展示两个时刻。子智能体的动作根本不在那份 transcript 里——它们是 ACP session/update 通知,到达宿主进程,provider 本来已把它们镜像进运行日志。src/progress.ts 每个运行在内存里保留最近 60 条,键是运行时采用的 run id(即 child id),src/progress-route.ts 在宿主自己的监听上提供服务:

GET /subagent-codebuddy/progress?ids=<childId>[,<childId>…]
    → { runs: { <childId>: { steps, updatedAt, endedAt? } } }

卡片在运行未结束时轮询这个路径(src/client/live.ts,每 1.5s),在 settle 或连续三次失败后停止。README 列了四个设计决定,其中一条是使用相对 URL 而非端口,因为 web UI 由同一个 listener 提供服务。卡片能看到的动作行随宿主应答而出现,最后动作 12s 前 会随时间变旧,折叠头部的 N 步 计数会变化;没有这个 feed 时退回纯计时。

本包还带一个 codebuddy-runs 命令行入口(package.json 的 bin),供人工查看运行状态。

安装与启用

先说清楚一件事:我手上的 README 没有给出一条可以直接复制的一键安装命令,因此这里不替它拼一条 dsh plugin add ...。从 package.json 看,这个包声明了 bundle 结构:

{
  "dsh": {
    "bundle": { "patch": "./cordis.patch.yml" },
    "client": {
      "inject": ["@deepseek-ai/dsh-client-ui-conversation"],
      "platform": "web"
    }
  },
  "engines": { "node": ">=20", "dsh": ">=0.2.1-rc.3" },
  "bin": { "codebuddy-runs": "./bin/codebuddy-runs.mjs" },
  "license": "MIT"
}

也就是:安装这个包之后,harness 读取 dsh.bundle.patch 指向的 cordis.patch.yml,在其中插入两行——provider 行与挂载本包 ./tool 入口的 tool 行。cordis.patch.yml 的原文如下:

- insert:
    - id: subagent-codebuddy
      name: 'dsh-subagent-codebuddy'
      config:
        permission: allow
        startupTimeoutMs: 60000
        envFile: ~/.dsh/secrets/codebuddy-api-key
        env:
          CODEBUDDY_INTERNET_ENVIRONMENT: internal
    - id: tool-subagent-codebuddy
      name: 'dsh-subagent-codebuddy/tool'
      config:
        provider: codebuddy
        toolName: subagent_codebuddy
        maxDepth: provider-managed

几处配置的意思需要讲明,它们都来自补丁原文的注释:

1、permission: allow 让子智能体的工具提示自动批准,真实任务才能动手;需要严格只读的孩子时改成 reject。
2、envFile: ~/.dsh/secrets/codebuddy-api-key 指定承载 API key 的文件;运行消耗的是登录账号的订阅额度,或这个 envFile 提供的 key。
3、CODEBUDDY_INTERNET_ENVIRONMENT: internal 显式指定验证 key 时使用的 CodeBuddy 站点。国内站(copilot.tencent.com)与国际站(codebuddy.ai)不互相接受对方的 key,而 CLI 按机器上缓存的产品配置选站;从未装过桌面端的机器会退回国际站,导致一个完全有效的 key 报 401。这里显式写名字就绕开了这个依赖。
4、maxDepth: provider-managed 是必填项,且必须显式写这个值。省略 key 时 harness 会退回自己的数字 depth 默认值,而 CodeBuddy provider 没有 advertise 出 depthLimit capability,无法强制,该行会直接激活失败并报 provider "codebuddy" cannot enforce maxDepth ...。对进程外子智能体来说 provider-managed 是唯一合法值。

启用前需要满足的对端依赖(来自 peerDependencies):@deepseek-ai/cordis ~4.0.5,以及 @deepseek-ai/dsh-session、@deepseek-ai/dsh-subagent、@deepseek-ai/dsh-subprocess、@deepseek-ai/dsh-timeout 均为 ^0.2.1-rc.3;Node 要求 >=20。

还有一个位置约束写在 cordis.patch.yml 注释里:两行都必须放在 bundle 顶层。如果把 tool 挂进某个 agent preset(例如 preset-cordis),它就被绑死在那个 preset 上,任何不带这个确切 preset 行的 profile 都无法挂载该工具,provider 也就没有让模型触达它的路径。

适用场景与注意点

适合谁:已经在 DSH 上写 profile、希望把子任务放进本地 CodeBuddy CLI 的开发者;需要进程外一次性委派,且能接受「没有中途进度、结果只在 settle 时回来一次」这一后端特性的人。如果需要子智能体中途双向收发消息,这个插件不满足,它只提供从父到子的单向 steer。

需要注意的几点:

  • 插件以当前 DSH 进程的权限运行,会读取 envFile 指向的密钥文件、会拉起本地 codebuddy --acp 子进程。安装前先读源码与 LICENSE(MIT),确认这个权限面你能接受。
  • 开支走 CodeBuddy 账号的订阅额度或 envFile 提供的 key,不是 dsh 侧的额度。
  • 没有中途进度通道,参数上不要指望模型在等待期间看到什么。工具已经通过文案尽力阻止父智能体空等,但 settle 之前父会话拿不到子智能体的动作,除非宿主提供上一节的 progress feed。
  • 子智能体随宿主一起挂掉时会永久停在「后台运行中」,因为没有事件可折叠;这是设计边界,不是可修的 bug。
  • 星标为 0,仓库近几天内创建,生态层面属于很新的小项目。采用前值得自行检查一次仓库活跃度与依赖版本。
  • 社区目录站是独立站点,插件不是 DeepSeek / 幻方的官方发布。

结尾

这个包的价值不在于做了一件别人做不到的事,而在于把一个边界情况说清楚:进程外的一次性子智能体没有 mid-run 进度,工具文案、steer 通道、聊天卡片、progress feed 这四层都围绕「让父智能体不空等、让结果诚实地到达」这个目标设计。如果你的 profile 正需要把委派发给本地 CodeBuddy CLI,它是一个可以直接放进 bundle 的实现。

  • 目录页:https://www.skillhub.cn/plugins/lloydzhou/dsh-subagent-codebuddy
  • GitHub:https://github.com/lloydzhou/dsh-subagent-codebuddy
羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜