DSH 子代理与多智能体编排:从 Spawn 到 Workflow
DSH 如何编排多个 Agent:命名 Provider、可续子代理、工作流脚本、Ralph 循环和后台作业系统完整拆解。
先说结论: DSH 的多 Agent 不是一种模式——而是一个可组合的接缝,包含六种命名 Provider(
spawn-in-process、fork-in-process、acp、codex、claude-code、dsh-sdk)、两个模型工具(subagent可续后台子代理、subagent_fork一次性前台子代理)、在worker_threads中运行模型编写 JS 的工作流引擎、用于有界迭代任务的 Ralph 循环,以及统一的后台作业系统。Provider 独立替换;模型看到的接口不变。
Subagent 接缝:为什么多个 Provider 并存
与 bash(每个上下文一个执行器)不同,subagent 接缝通过 ctx.subagents 支持多个命名 Provider 同时共存。这是 LLM 适配器注册表模式在子代理上的应用:每个 Provider 按名注册,模型工具选择用哪个。
| Provider | 功能 | 适用场景 |
|---|---|---|
spawn-in-process | 同一 Node 进程中创建全新子代理 | 默认本地委派 |
fork-in-process | 子代理继承父代理的对话前缀 | 带上下文的委派 |
acp | Agent Communication Protocol over JSON-RPC | 跨进程/跨机器 |
codex | 委派给 OpenAI Codex agent | 外部产品集成 |
claude-code | 委派给 Claude Code | 外部产品集成 |
dsh-sdk | 通过 SDK 调用另一个 DSH 实例 | 分布式 DSH 部署 |
Service Definition(dsh-subagent)声明合约。每个 Provider 包独立实现它。模型工具(dsh-tool-subagent、dsh-tool-subagent-control、dsh-tool-subagent-report)只依赖合约,不关心背后是哪个 Provider。
// 注册是 effect-scoped 且 HMR 安全的
const dispose = ctx.subagents.registerProvider(myProvider)
// 移除 Provider 阻止新启动,不撤销正在运行的子代理

两个模型工具:subagent vs subagent_fork
模型通过两个工具与子代理系统交互,分别绑定不同的 Provider 和运行模式:
| 工具 | 模式 | 默认执行方式 | Provider |
|---|---|---|---|
subagent | 可续(Continuable) | 后台 | spawn-in-process |
subagent_fork | 一次性(One-shot) | 前台 | fork-in-process |
subagent 创建的子代理在父代理当前 Turn 结束后继续存活。默认后台运行,拥有收件箱接收后续消息,通过 report 工具交付结果。
subagent_fork 是一次性的:子代理继承父代理的对话历史(一段平衡的已完成 Turn 前缀),在前台运行到完成,直接返回输出。没有收件箱、没有后续——携带上下文的”发射后不管”。
SubagentCapabilities:Provider 公示的能力
在一次性启动前,服务会检查 Provider 能力与请求的匹配。如果 Provider 做不到请求要求的能力,直接报错——绝不”接受后忽略”:
interface SubagentCapabilities {
readonly outputSchema: boolean; // 能否强制结构化输出
readonly depthLimit: boolean; // 能否限制递归深度
readonly toolFilter: boolean; // 能否限制子代理可用工具
readonly persona: boolean; // 能否应用不同 persona
}
这些标志只描述一次性 start() 路径。可续子代理通过 SubagentProvider.prepareContinuable 方法守门——方法存在本身就是能力声明,用 TypeScript 类型收窄作为发现机制。
可续子代理:Activation 生命周期
可续子代理是 DSH 长时间运行多 Agent 能力的核心。它们不只是”发射后不管”——会持久化,接受后续消息,还能从存储中冷恢复。
生命周期状态
可续子代理经历 Activation 生命周期:
Created → Running → Idle → (Cold Storage) → Resumed → Running → ...
关键特性:
- FIFO 收件箱:消息按序排列;每条被接受的消息成为一个 Turn
- 冷恢复:当前未加载的子代理可从持久化 Session 恢复
- 不丢消息:Agent inbox 是唯一队列,每条消息有一个可观测的顺序
// 启动一个可续子代理
const { childId, messageId } = await ctx.subagents.startContinuable({
provider: 'spawn-in-process',
request: { description: 'Research agent', prompt: 'Find papers on...' },
parent: currentAgent,
signal: abortController.signal,
})
// 之后发送后续消息(不同的 Turn,甚至不同的 session resume)
await ctx.subagents.followup(
currentAgent,
childId,
[{ type: 'text', text: 'Also check arxiv for...' }],
{ signal }
)
中断但不销毁
可以中断子代理的当前 Turn 而不销毁它:
// Fire-and-return:取消信号立即发出
// 但目标可能短暂继续运行直到观察到信号
await ctx.subagents.interrupt(targetSessionId, { signal })
// 未被认领的 inbox 工作保留;唤醒发送恢复 FIFO 队列

Report 工具:子到父通信
后台子代理如何告诉父代理有结果了?通过 report 工具——一个有特殊属性的专用通信通道:
| 属性 | 行为 |
|---|---|
| Per-child scoped | 按可续子代理注册,非全局 |
| 穿透 toolFilter | 即使父代理过滤了子代理的工具,report 仍然保留 |
| 投递模式 | wakeup(创建父代理 Turn)或 quiet(添加上下文不唤醒) |
// Report 投递行为配置
interface Config {
reportDelivery?: 'wakeup' | 'quiet'
}
wakeup(默认):report 创建一个普通的父代理 Turn——父代理立即被通知。quiet:向父代理添加上下文但不唤醒它。父代理只在其他事情触发 Turn 时才看到 report。
这就是后台 Agent 报告完成而不持续打断前台工作的方式。
控制工具:全局 Agent 管理
三个工具提供运行时对 Agent 群的控制:
send_message
向后台子代理发送后续消息,继续其对话。如果子代理正在工作,消息会等到当前 Turn 结束——无法重定向已在进行的工作。
interrupt_agent
请求取消子代理的当前 Turn。发射即返回:停止信号在返回前发出,但目标可能短暂继续运行。Agent 本身保持可用,可以接受后续消息。
list_agents
列出可续后台子代理及其状态:
| 状态 | 含义 |
|---|---|
running | 正在工作 |
idle | 已加载但在 Turn 之间(可能在等自己启动的子代理) |
ready | 仅存于存储中,可恢复 |
{
"scope": "children" // 或 "descendants" 遍历完整树
}
descendants 以稳定的前序遍历完整树,每个条目标注父 session ID 和深度。只能对 depth-1 条目用 send_message;更深的只能用 interrupt_agent。
工作流引擎:模型编写的编排脚本
对于超出简单委派的复杂多 Agent 协调,DSH 提供一个工作流引擎,在 worker_threads VM 中运行模型编写的 JavaScript 脚本:
interface WorkflowStartRequest {
script: string; // 纯 JS 体(允许顶层 await)
meta: WorkflowMeta; // 身份块(name、description、phases)
args?: unknown; // 作为 `args` 全局变量暴露给脚本
subagentProvider?: string; // 覆盖本次运行的子代理 Provider
maxTotalAgents?: number; // 本次运行的子代理上限
parent: Agent; // 脚本启动的每个子代理归属此 Agent
signal?: AbortSignal; // 取消运行
}
脚本内部用 agent() 启动子代理:
// 一个工作流脚本(模型生成,执行前验证)
const researcher = await agent({
description: 'Research papers on topic X',
prompt: 'Find the top 5 papers...',
})
const writer = await agent({
description: 'Write summary from research',
prompt: `Summarize these findings: ${researcher.result}`,
})
return { summary: writer.result }
关键约束:
- 每个运行一个 worker:通过
worker_threads隔离 meta执行前验证:引擎绝不通过执行脚本文本来提取元数据parent必填:每个子代理都归属到一个存活的 Agent- 结果是纯 JSON:脚本返回值作为宿主域数据物化
WorkflowResult:脚本如何结算
interface WorkflowResult {
value: unknown; // 脚本返回值(undefined 时为 null)
stopReason: 'completed' | 'cancelled' | 'error';
error?: string; // 仅 stopReason 非 completed 时存在
agentsSpawned: number; // 总共接受的 agent() 调用数
}
非 completed 结果映射为 isError 工具返回——部分输出绝不当成功上报。

Ralph 循环:有界迭代执行
Ralph 循环是由 workflow 和 subagent 原语构建的特定编排模式。它不是通用的 agent-loop 模式——而是一个固定的前台工作流,用于有界迭代任务:
| 属性 | Ralph 循环行为 |
|---|---|
| 结构 | 固定前台工作流 |
| 子代理 | 每轮一个全新子代理(无对话延续) |
| 状态传递 | 轮间有界结构化交接 |
| 不是 | 同会话目标、调度器或通用工作流功能 |
Ralph Round 与 Handoff
每个 Ralph round 是一个全新的子代理会话。子代理不接收父对话或前一轮子代理的对话种子。跨轮状态通过两个通道流动:
- 共享工作区:所有轮次可见的文件系统状态
- Ralph handoff:有界结构化报告,包含状态、摘要、证据、下一步和阻塞文本
// Handoff 补充工作区——不替代工作区作为权威来源
interface RalphHandoff {
status: string;
summary: string;
evidence: string;
nextSteps: string;
blockerText?: string;
}
循环持续启动新子代理直到目标达成或达到配置上限。每个子代理只看到自己的 handoff 和工作区——不积累对话历史。
后台作业:统一的异步工作
ctx.jobs 把所有后台工作统一到一个接口:
| 作业类型 | 来源 |
|---|---|
| 后台 bash 命令 | Shell 执行 |
| PTY 终端发送 | 终端会话 |
| 后台子代理 | Subagent 委派 |
三个模型工具管理它们:
job_list — 列举运行中/已完成的后台工作
job_output — 读取特定作业的输出
job_kill — 取消运行中的作业
模型不需要知道后台作业是 bash 命令、终端会话还是子代理——都通过同一个 job_* 接口呈现。这是 Capability Seams 模式 的实践:一个消费者,多种 Provider。
Provider 如何组合
出厂组合(packages/bundle/base/cordis.patch.yml)把 dsh-tool-subagent 加载了两次——每个后端一次:
- 一个实例:
toolName: 'subagent'、backgroundMode: 'continuable'、绑定spawn-in-process - 一个实例:
toolName: 'subagent_fork'、backgroundMode: 'one-shot'、绑定fork-in-process
每个实例有自己的描述和 run_in_background 行为。控制工具(send_message、interrupt_agent、list_agents)全局注册一次。Report 工具按子代理注册,作用域限于该子代理的上下文。
这只是插件配置——换掉 Provider 名称就能把委派重定向到外部产品,模型工具不变。
实战:研究 + 综合管线
一个用可续子代理的实际多 Agent 模式:
export function apply(ctx: Context) {
ctx.on('agent/pre-step', async (payload, next) => {
// 自定义编排逻辑
return next()
})
}
// 模型有机地驱动这个流程:
// 1. 模型调用 subagent "研究主题 A" → 后台子代理 A
// 2. 模型调用 subagent "研究主题 B" → 后台子代理 B
// 3. 两个子代理并行工作,通过 report 工具回报
// 4. 父代理被唤醒,接收两份报告,进行综合
// 5. 模型调用 subagent_fork 以完整上下文起草最终输出
不需要编排框架——模型自己决定何时委派、等待还是综合。工具和生命周期管理机制。
架构关联
- Capability Seams(能力接缝):subagent 接缝是多 Provider 的典范案例
- Agent Loop 内部机制:每个子代理运行自己的循环,同样的 Step/Turn/waterfall 机制
- DeepSeek Harness 开发者预览:完整架构概览
- 源码:github.com/deepseek-ai/deepseek-harness
常见问题
Q:子代理能启动自己的子代理吗?
A:能。深度通过谱系追踪,depthLimit(SubagentCapabilities 中)可以封顶递归。每个子代理运行完整的 agent loop——拥有同样的工具访问(减去 toolFilter 限制),可以自己调用 subagent。list_agents 工具用 scope: 'descendants' 能看到完整树。
Q:可续子代理的存储丢了会怎样?
A:变得不可恢复。list_agents 不会显示它(只呈现有 session 支撑的条目)。恢复路径是启动一个新的子代理做同样的任务——没有自动重试。持久化是可选的;没有持久化时,枚举仅限存活的。
Q:工作流引擎怎么防止脚本失控?
A:三个机制:maxTotalAgents 封顶 agent() 调用数;signal(AbortSignal)取消运行;引擎在执行前验证 meta,绝不通过执行脚本文本提取元数据。每次运行一个 worker 的隔离意味着崩溃的脚本不影响其他运行。
Q:一个 workflow 运行里能混用 Provider 吗?
A:一个 workflow 运行可以全局覆盖 subagentProvider,但脚本内单个 agent() 调用不能选 Provider——全部用同一个。想混用 Provider,直接从父代理用不同的 subagent 工具调用,而不是写在 workflow 脚本里。
Q:Ralph 循环和 workflow 脚本有什么区别?
A:workflow 脚本是通用的:模型写任意 JS 调用 agent()。Ralph 循环是固定策略——一个前台工作流,每轮启动全新子代理,轮间有界交接。它由 workflow 和 subagent 原语构建而成,但结构特定:轮间无对话延续,跨轮状态仅通过工作区 + handoff。通用编排用 workflow;有界迭代任务用 Ralph。


