子 Agent 与并行
一个 Agent 可以把任务委派给子 Agent。DSH 的 ctx.subagents 是一个能力缝:调用方只用一个 ctx.subagents API,具体 provider 决定子 Agent 跑在本进程、另一进程,还是通过未来的传输。这是"并行处理复杂任务"的机制:把一个多步骤任务拆给多个隔离的子 Agent 同时推进。
两种子 Agent
DSH 区分两种形态,职责不同:
| 形态 | 说明 | 关键 API |
|---|---|---|
| 一次性(one-shot) | 派一个子 Agent 干活并拿最终结果 | ctx.subagents.start(name, request) |
| 可续(continuable) | 建一个持久子会话,可多次发消息、可中断、可冷恢复 | startContinuable(spec) / followup / interrupt |
模型面对的是 tool-subagent(一次性、ctx.subagents.start)与 tool-subagent-control(send_message / interrupt_agent / list_agents),后者就是本篇讲的"可续子会话"的控制面。
一次性委派
// 请求可:选模型、要求结构化输出、限制委托深度、限制子 Agent 工具、设子 persona
const run = await ctx.subagents.start('spawn', {
prompt: '帮我分析这个仓库的 TODO',
outputSchema, // 可选:强约束最终结果结构
depthLimit, // 可选:封顶委派深度
toolFilter, // 可选:限制子 Agent 可见工具
signal, // 必填:发布前的取消通道
})
const result = await run.result // 一次性子 Agent 的最终结果
label是可选的持久展示标签signal是 once-shotstart的规范取消通道:发布前 abort → 回滚拒绝;发布后 abort → 取消剩余轮次但不隐藏返回的run- provider 通过
provider.capabilities(outputSchema / depthLimit / toolFilter)宣告支持,不支持的在建子会话前就拒绝
可续子会话(continuable)
const { childId, messageId } = await ctx.subagents.startContinuable({
label: 'research-assistant',
initialPrompt: '这是一个常驻研究助手,随时找我',
})
// 后续发消息(精确的直接父 agent 认证后才放行)
await ctx.subagents.followup(parent, childId, '再查一下 MCP 最新进展')
// 打断一个可续子 Agent 的当前轮次(保留其 inbox 与后代)
await ctx.subagents.interrupt(targetSessionId, authority)
// 列直接会话级子 Agent / 拉平整棵会话树
await ctx.subagents.listChildren(parentSessionId)
await ctx.subagents.listDescendants(rootSessionId)
关键性质(源码 README 原话要点):
- follow-up 权威来自子 Agent 持久 header 里记录的"确切直接父":父 Agent 在重组期间被注销/替换,无法授权投递
- 可续子会话要求
ctx.agents+ 会话持久化 + 一个具备prepareContinuable能力的 provider - 冷恢复:父 agent 不在时,从持久化 Session 重建(也需 session persistence)
reportFrom把子 Agent 的某条消息投回其直接父(quiet = 注入上下文;waking = 触发父的新一轮)
provider 家族
packages/subagent/ 下同一 seam 有多个实现。subagent-in-process-driver 是 spawn / fork 两个同进程 provider 的共享 run driver(不是单独注册的 provider 名),其余六个才是 getProvider(name) 能取到的 provider:
| provider | 子 Agent 跑哪 | 挂载 |
|---|---|---|
spawn | 当前进程里一个全新 Agent | 默认挂载(base) |
fork | 当前进程、带父已完成轮次 seed | 默认挂载(base) |
acp | 全新子进程,走 Agent Client Protocol | opt-in |
claude-code | 全新子进程,桥接 Claude Code CLI | provider 默认挂载,delegation 工具默认禁用 |
codex | 全新子进程,桥接 Codex CLI | provider 默认挂载,delegation 工具默认禁用 |
dsh-sdk | 全新子进程,完整 DSH runtime(JSON-RPC) | opt-in |
可以多个 provider 共存在一个 ctx.subagents 后面,getProvider(name) 按名取。
in-process-driver:共享 run driver
subagent-in-process-driver 是 spawn / fork 两个同进程 provider 的唯一实现——spawn 不传 session seed,fork 传父会话已完成轮次前缀,其余(深度校验、子 Agent 创建、persona/工具过滤/结构化输出安装、结果读取、取消、dispose)都在这一个实现里:
startInProcessRun只有子 Agent 在ctx.agents发布后才 fulfill;启动被拒时未发布的创建事务已 quiesce,调用方拿不到半建句柄- 深度读父的
delegationDepth(持久 header 权威,运行时只能加深不能降低),子深度 = 父 + 1 并写进子 header,所以持久化 + resume 都保住预算;超maxDepth报精确错误 - 结构化输出装整套契约:一个
structured_output工具 + 一段 order-190 系统提示段 + 一个只在该执行最终工具结果成功后 commit 的观察者 + 单次调用 guard +concludeTurn收尾 - 同一 driver 还给子 Agent 装 persona shadow、tool 过滤(删全局 wire schema / 可执行查找 / Code Mode SDK 绑定,但独立注册的指导段保留)、并应用 seam 的委派策略(父的显式沙箱覆盖 +
never批准钉)
spawn:同进程新 Agent
spawn 在当前进程里创建一个全新子 Agent:有自己的会话、看不到父对话历史,复用宿主的 agent factory 和 LLM/tool 服务。它广告 { outputSchema, depthLimit, toolFilter, persona } 全部 true,因为它在子 Agent 创建窗口内能强推这四个能力。子 Agent 继承父的工作目录/会话血统与模型(除非被覆盖),但从空对话开始。
fork:同进程 + 父已完成轮次
fork 与 spawn 共享全部运行机制,唯一差别是会话 seed:把父会话最后一个 turn/end 之前的连续前缀喂给子 Agent——父当前 in-flight 轮被排除(否则子会拿到不平衡的会话)。只传对话历史,不继承父的工具限制或权威。广告能力与 spawn 相同。因为 continuable 子会额外带 report 工具和提示段、破坏 fork 想复用的父前缀缓存,交付组合把 fork 绑定成 backgroundMode: one-shot。
acp:子进程里的 ACP client
acp 把每个子 Agent 跑在全新子进程,作为 Agent Client Protocol client 驱动:spawn → ACP initialize → newSession 全成功后 fulfill,表示远端会话已就绪、所有权已交给调用方。它不广告任何 start-time 能力(无法在远端进程里强推 depth / toolFilter / persona / 结构化输出),inheritsParentContext: false,唯一从父侧带过去的是工作目录。permission: reject|allow 自动应答子会话的权限请求(不向人弹窗)。每次 run 一个全新进程,无进程池。
claude-code / codex:桥接两个 CLI
这两个是固定 provider:
claude-code调官方 Claude Agent SDK 的query(),在父会话工作区解析原生claude可执行文件,提交一份自包含文本任务,只回传严格最终答案。SDK 读宿主的原生 Claude 设置与认证,persistSession: false、禁用AskUserQuestion(无人值守)。codex起官方codex app-server --stdio,建一个 ephemeral thread,同样只回传最终答案。无人值守下对命令/文件批准选 cancel/decline,权限请求给空集。contextWindowExceeded映射max-tokens,其余异常映射error。
两者都不广告可选能力、inheritsParentContext: false;子侧模型/工具/权限/认证全部来自原生产品安装。provider 行默认挂在 base 的 host plane(加载但不启动任何产品进程),但对应 delegation 工具行在 standard preset 里 disabled: true——复制 preset 并去掉 disabled 才把 subagent_claude_code / subagent_codex 暴露给由该副本组成的 agent。
dsh-sdk:子进程里的完整 harness runtime
dsh-sdk 把每个子 Agent 作为完整 DeepSeek Harness runtime 在全新子进程里跑,经 TypeScript SDK client 走 stdio JSON-RPC。与 acp 的区别在 wire 与子契约:子进程是一个完整 peer harness——自己的 cordis.yml 组合、会话持久化、模型路由与工具。同样不广告 start-time 能力、inheritsParentContext: false;其 delegation 工具设 maxDepth: provider-managed(子 harness 自持递归预算)。完整 runtime 每个 run 要启动整棵插件树,单次 spawn 成本高于 acp 的典型子进程。
什么时候用它
- 并行:把一个独立子任务(fetch + 摘要 + 写文件)拆给三个子 Agent 同时做
- 隔离:子 Agent 有自己的作用域与工具限制,不会污染父会话
- 可续对话:常驻助手、需要跨多轮持有状态(用 continuable)
- 一次性拿结果(用 one-shot
start)
验证
# 组合树里看可用的 subagent provider
dsh web --dump-config | grep -iE "subagent"
# 会话日志里看子 Agent 事件
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E "subagent/" | head
下一步
- 工作流与 Ralph:用脚本编排多个 agent 并行
- 写一个工具:给子 Agent 共享逻辑