Files
agcore/design/note/note-opencode-subagent-dispatch.md
T
徐涛 28ca43ccb2 chore(docs): 将设计文档从 docs 移至 design 目录
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
2026-07-23 05:45:53 +08:00

13 KiB
Raw Blame History

笔记:opencode 子代理调度、分发与合并及工作流推进

基于 /Users/midnite/Samples/opencode 源码调研,2026-07-04


一、整体架构

LLM(主 Agent
    │
    ├── 调用 Task tooltool call
    │       ↓
    │   TaskTool.execute()              ← packages/opencode/src/tool/task.ts
    │       │
    │       ├── agent.get()             ← 查找 Agent 定义(agent.ts
    │       ├── deriveSubagentPermission() ← 权限合并(subagent-permissions.ts
    │       ├── sessions.create()       ← 创建子 session
    │       │
    │       ├── [前台] background.wait() + background.waitForPromotion() race
    │       │        ↓ 完成
    │       │    renderOutput() → XML <task> 标签返回
    │       │
    │       └── [后台] background.start() → notify() 异步注入结果
    │
    └── 会话循环(runLoop)              ← prompt.ts
            │
            ├── 检测 subtask type part → handleSubtask()
            ├── 检测 compaction → compaction.process()
            └── 正常流程 → LLM.stream() → processor.handleEvent()

二、子代理调度(Dispatch

2.1 三种触发入口

入口 触发方式 调用链路
A — LLM 自主 LLM 调用 task tool 系统提示词中注入了 Task tool 描述 + describeTask() 输出子代理列表 → LLM 决策
B — subtask part 消息中有 type: "subtask" 的 part handleSubtask() 直接执行 TaskTool,不走 LLM
C — agent part 消息中有 type: "agent" 的 part 转为"调用 task tool 带 subagent: XXX"的提示词,引导 LLM

2.2 TaskTool.execute() 完整流程(task.ts

execute(params, ctx):
  1. background 开关检查(需 experimental flag
  2. ctx.ask() 权限询问
  3. agent.get(subagent_type) 查找子代理定义
  4. task_id 存在 → sessions.get(task_id) 恢复已有子 session
     task_id 不存在 → sessions.create() 创建新子 session
  5. deriveSubagentSessionPermission() 合并权限
  6. 添加默认 deny 规则(todowrite / task
  7. 确定 model(继承或子代理自定义)
  8. 执行 runTask() → ops.resolvePromptParts() + ops.prompt()
  9. 结果格式化为 XML ← renderOutput()

2.3 关键:子 session 创建(task.ts lines 121-158

// 权限继承
const childPermission = deriveSubagentSessionPermission({
  parentSessionPermission: parent.permission ?? [],
  subagent: next,
})

// 默认 deny 规则
const childToolDenies = [
  // 子代理自己的 permission 没允许 todowrite → 默认 deny
  ...(next.permission.some(r => r.permission === "todowrite") ? [] 
     : [{ permission: "todowrite", pattern: "*", action: "deny" }]),
  // 子代理自己的 permission 没允许 task → 默认 deny(防嵌套)
  ...(next.permission.some(r => r.permission === "task") ? []
     : [{ permission: "task", pattern: "*", action: "deny" }]),
  // 主 agent 专有工具也不给子代理
  ...(cfg.experimental?.primary_tools?.map(p => ({ permission: p, ... })) ?? []),
]

三、通信格式:Tool Call / Tool Result

3.1 父→子:Task tool 参数

{
  subagent_type: "explore" | "general" | ...,
  description: "简短描述(3-5词)",
  prompt: "子代理的完整任务描述",
  task_id?: "恢复已有子 session 时使用",
  command?: "触发该调用的 CLI 命令(可选)",
  background?: true  // 后台模式(需 experimental flag
}

3.2 子→父:XML 包装的纯文本(renderOutput

<task id="ses_xxxxx" state="completed">
  <summary>任务简述</summary>
  <task_result>
    子 agent 输出的完整文本内容...
  </task_result>
</task>

错误时:

<task id="ses_xxxxx" state="error">
  <summary>任务失败</summary>
  <task_error>
    Error: 具体错误信息...
  </task_error>
</task>

3.3 传递给 LLM 的方式

前台模式

TaskTool.execute() 返回 { output: "<task>...</task>" }
    ↓
AI SDK 将其转为 tool result,存入数据库 tool part
    ↓
下一轮 LLM 调用时,tool result 作为消息历史的一部分传入
    ↓
LLM 看到 XML,自行解析使用

后台模式

TaskTool.execute() 立即返回 <task state="running">...
    ↓
子 agent 完成后 → background.wait() 触发 → inject()
    ↓
向父 session 注入合成 text partsynthetic: true
    携带 <task state="completed">... 结果
    ↓
父 LLM 在下一轮循环中看到该消息

四、分发与合并(Distribution & Merge

4.1 并行分发

  • 无专用分发层。依赖 LLM 在单条消息中发出多个 tool call
  • task.txt 引导 LLM"Launch multiple agents concurrently whenever possible"
  • 底层通过 Effect.ts 的 Effect.forkIn(scope, { startImmediately: true }) 实现同一消息内多 tool call 并发
  • 子 agent 之间完全隔离,无直接通信

4.2 结果合并

无专用合并逻辑。 合并完全通过 LLM 的上下文理解完成:

  • 前台:tool result 自然进入消息历史,LLM 下一轮读取
  • CLI 命令:额外注入 "Summarize the task tool output above and continue with your task." 引导 LLM 总结
  • LLM 自主调用:无额外引导,LLM 自行决定如何使用

4.3 前台/后台切换机制(task.ts lines 303-333

// 前台执行
return yield* Effect.raceFirst(
  background.wait({ id: nextSession.id }),      // 等完成
  background.waitForPromotion(nextSession.id),   // 等 promote 到后台
)

当用户将前台任务 promote 到后台时,waitForPromotion 先返回(标记 metadata.background = true),TaskTool 转而返回后台模式的输出。

4.4 后台作业引擎(core/background-job.ts

纯内存、非持久化注册表。使用 Effect.ts 的 SynchronizedRef 做并发控制。

操作 行为
start() 创建 jobfork run effect,返回 info
extend() 追加顺序执行的 run(通过 Deferred 链式等待前一个完成)
wait() Deferred.await(done),可选 timeout
waitForPromotion() 等待 promoted Deferred 或检测 background 标记
promote() 标记 background = true,触发 onPromote callback
cancel() 设置 cancelledclose scope(中断所有子 fork

五、工作流推进(Workflow Progression

5.1 核心循环(prompt.ts → runLoop

runLoop(sessionID):
  while true:
    1. MessageV2.filterCompactedEffect() 获取消息
    2. MessageV2.latest() 取最近 user/assistant/tasks
    3. 检查 finish 状态
       - 不是 tool-calls 且有 finish → break(退出循环)
    4. 取 taskssubtask / compaction 队列)
       - subtask → handleSubtask() → continue
       - compaction → compaction.process() → continue/break
    5. 检查 overflow → 自动创建 compaction task → continue
    6. 构建 assistant message
    7. SessionProcessor.create() 创建 handle
    8. SessionTools.resolve() 解析所有工具
    9. 构建 system prompt(环境信息 + skills + MCP + instructions
    10. handle.process() — 启动 LLM stream
    11. 检查 result:
        - "compact" → 返回给外层触发 compaction
        - "stop" → break
        - "continue" → 继续循环

5.2 SessionProcessor 事件处理(processor.ts

Stream 事件 处理逻辑
reasoning-start/delta/end 创建 reasoning part → 增量追加 → 最终持久化
tool-input-start/delta/end 创建/更新 tool partpending 状态)
tool-call 标记 running → 设置 input → doom loop 检测
tool-result completeToolCall() → 持久化结果 + 附件
tool-error failToolCall() → 标记错误
provider-error 抛出异常 → 触发重试
text-start/delta/end 流式文本 → updatePartDelta() 增量持久化
step-start 创建快照(snapshot
step-finish 生成 patch diff → 更新 usage/tokens → overflow 检测 → 触发 summary
finish stream 结束

5.3 Doom Loop 检测(processor.ts lines 351-377

连续 3 次完全相同的 tool call(相同名称 + 相同输入)触发权限询问:

const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD)  // DOOM_LOOP_THRESHOLD = 3
if (recentParts.length === DOOM_LOOP_THRESHOLD &&
    recentParts.every(part => 
      part.type === "tool" &&
      part.tool === value.name &&
      part.state.status !== "pending" &&
      JSON.stringify(part.state.input) === JSON.stringify(input)
    )) {
  yield* permission.ask({ permission: "doom_loop", ... })
}

5.4 Compaction 工作流

两种触发方式:

触发条件 行为
step-finish 检测到 isOverflow() + auto: true 创建 compaction task → 下一轮循环执行 → 压缩后 continue
step-finish 检测到 isOverflow() + auto: false 标记 assistantMessage.error → idle 等待用户干预

Compaction 使用专门的 compaction agenthidden, mode=primary, *=deny)执行。 压缩后的消息标记 compacted: true,后续通过 MessageV2.filterCompactedEffect() 过滤。

5.5 重试机制(processor.ts lines 658-672

Effect.retry(
  SessionRetry.policy({
    provider: input.model.providerID,
    parse,        // 错误解析(区分可重试/不可重试)
    set: (info) => status.set(sessionID, { type: "retry", ... }),
  }),
)

遇 provider 错误自动重试,LLM stream 完成后 Effect.ensuring(cleanup) 保证资源释放。


六、六种内置 Agent

名称 Mode Hidden 用途 核心权限特征
build primary 默认 agent,全部工具 question/plan_enter=allow
plan primary 计划模式,禁用编辑 edit=deny(除 plans), task(general)=deny
general subagent 通用子代理 todowrite=deny(默认禁止改 todo
explore subagent 只读代码探索 *=deny,仅 read/grep/glob/bash/webfetch/websearch
compaction primary 会话压缩(自动) *=deny
title primary 生成会话标题 *=denystep=1 时异步 fork
summary primary 生成消息摘要 *=deny(每个 step-finish 时异步 fork

用户可通过 config.agent 自定义 agent(支持 mode: "all"),也可通过 agent.generate 让 LLM 辅助生成。


七、权限模型总结

父 session permission
    │
    ├── 仅继承 deny 规则 + external_directory 规则  ← subagent-permissions.ts
    │   (父 agent 的 allow 规则不传播到子代理)
    │
    ├── 子代理自身 permission(来自 agent 定义)
    │
    ├── 默认 deny
    │   - todowrite(除非子代理明确允许)
    │   - task(除非子代理明确允许,默认防嵌套)
    │
    └── 主 agent 专有工具 deny(来自 config.experimental.primary_tools

子代理的 session 权限 = 父 deny + 父 external_directory + 自身 permission - 默认 deny - primary_tools deny


八、关键设计决策

决策 意图 效果/局限
结果以 XML 纯文本嵌入上下文 简单、LLM 可直接理解 LLM 自行解析 XML;大结果可能被截断
无专用 merge 逻辑 简洁,不引入额外抽象 依赖 LLM 的理解能力处理返回结果
默认禁止子代理嵌套 task 防止无限递归 限制了多级分解场景
同一消息多 tool call 并发 利用 LLM 并行能力 子 agent 隔离,无法协作
Effect.ts 贯穿全程 类型安全、结构化并发 学习曲线陡峭
session 作为隔离边界 天然权限/消息隔离 每个子 session 独立数据库记录,开销较大
后台引擎纯内存 有意识取舍(注释说明) 进程重启丢失状态

九、参考源码路径

文件 角色
packages/opencode/src/tool/task.ts Task tool 核心实现(调度入口)
packages/opencode/src/tool/task.txt Task tool 的 LLM 使用说明
packages/opencode/src/agent/agent.ts Agent 定义注册中心
packages/opencode/src/agent/subagent-permissions.ts 子代理权限推导
packages/opencode/src/tool/registry.ts 工具注册 + describeTask() 列出可用子代理
packages/opencode/src/session/prompt.ts 会话循环 + handleSubtask() + 提示词构建
packages/opencode/src/session/processor.ts LLM stream 事件处理器
packages/opencode/src/session/tools.ts Tool ↔ AI SDK 桥接
packages/opencode/src/session/system.ts 系统提示词生成(含 Task tool 说明)
packages/opencode/src/background/job.ts 后台作业包装层
packages/core/src/background-job.ts 后台作业核心引擎(内存注册表)