Subagent Providers

一个接口,多个可互换的子代理后端。

委托契约不变,后端随便换。

60 行提供者抽象

问题

s06 的子代理只能"进程内新开一个循环"。但现在你想:

  • 让子代理跑在另一个产品里(Claude Code?Codex?一个 ACP 服务器?);
  • 让子代理继承父历史而不是从零开始;
  • 一个进程里同时存在多个不同后端的子代理。

把委托逻辑写死在 subagent 工具里?每换一个后端就要改工具。能力应该有一个稳定的接口,后端是可插拔的提供者。

解决方案

提供者抽象:

// 委托契约:start(request) -> run,run 返回 { runId, output }
const providers = new Map();

function registerProvider(name, provider) { providers.set(name, provider); }

// 进程内提供者:s06 的实现包一层
registerProvider("in-process", {
  async start({ prompt, system }) {
    return { runId: `run-${++seq}`, output: await agentLoop(prompt, { system }) };
  },
});

// 外部提供者:通过任意传输协议委托(示意)
registerProvider("acp", {
  async start({ prompt }) {
    const output = await acpCall("agent", { prompt }); // ACP 协议调用
    return { runId: `run-${++seq}`, output };
  },
});

// 工具只认接口,不认具体后端
define("subagent", {
  type: "object",
  properties: { prompt: { type: "string" }, provider: { type: "string" } },
  required: ["prompt"],
}, async ({ prompt, provider = "in-process" }) => {
  const p = providers.get(provider);
  if (!p) return `未知提供者:${provider}`;
  const run = await p.start({ prompt });
  return `[${run.runId}] ${run.output}`;
});

工作原理

第 1 步:契约三问——每个提供者都要回答:怎么启动(start)、怎么知道完成(run 解析)、怎么传结果(output)。

第 2 步:模型侧工具永不改变。换后端 = 换配置/换提供者注册,工具 schema 不变——这正是"能力缝"(Service Definition / Provider / Consumer 三角)的价值。

第 3 步:多提供者共存。同一次会话里,一个子代理跑在进程内、另一个委托给外部 Agent——父循环只看到统一的 [run-N] output 格式。

试一下

运行 s17:

  • 注册两个提供者(in-process + 模拟的 external),用 provider 参数切换
  • 观察:工具描述不变,模型是否感知得到后端差异?
  • 换一个"慢速提供者",看委托的阻塞特性是否依旧(s13 后台任务可以解决)

观察重点:接口稳定但后端可换——这带来的可测试性红利:测试时用 fake 提供者,生产用真实 Agent。

以下内容基于 packages/subagent 的核查。DSH 的子代理提供者族(s06 提过)就是本课的全部内容:

提供者 后端 场景
subagent-spawn-in-process 全新进程内子代 干净上下文(s06 的标准形态)
subagent-fork-in-process 从父历史 fork 需要父上下文延续的任务
subagent-acp ACP 服务器 自动化协议(s20 会再遇到 ACP)
subagent-codex 真实 Codex app-server 委托给 Codex
subagent-claude-code 真实 Claude Code(官方 SDK) 委托给 Claude Code
subagent-dsh-sdk 进程外 Harness 子进程 跨进程隔离

配套:subagent-in-process-driver 提供共享的进程内运行驱动;tool-subagent-control 提供对延续子代理的消息与列表控制。

DSH 的关键差异:提供者注册在 ctx.subagents 上,多个可共存(与你的 Map 一致);委托返回结构化运行结果(runId、状态、输出),支持可延续后台子代理(父代理稍后 send_message 继续同一子代理对话)。

一句话:本课没有任何新机制——只有"接口与实现分离"。它是整个 DSH 哲学(能力缝)在子代理上的缩影:模型侧契约稳定,后端世界开放