# 笔记:opencode 子代理调度、分发与合并及工作流推进 > 基于 `/Users/midnite/Samples/opencode` 源码调研,2026-07-04 --- ## 一、整体架构 ``` LLM(主 Agent) │ ├── 调用 Task tool(tool 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 标签返回 │ │ │ └── [后台] 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) ```typescript // 权限继承 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) ```xml 任务简述 子 agent 输出的完整文本内容... ``` 错误时: ```xml 任务失败 Error: 具体错误信息... ``` ### 3.3 传递给 LLM 的方式 **前台模式**: ``` TaskTool.execute() 返回 { output: "..." } ↓ AI SDK 将其转为 tool result,存入数据库 tool part ↓ 下一轮 LLM 调用时,tool result 作为消息历史的一部分传入 ↓ LLM 看到 XML,自行解析使用 ``` **后台模式**: ``` TaskTool.execute() 立即返回 ... ↓ 子 agent 完成后 → background.wait() 触发 → inject() ↓ 向父 session 注入合成 text part(synthetic: true) 携带 ... 结果 ↓ 父 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) ```typescript // 前台执行 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()` | 创建 job,fork run effect,返回 info | | `extend()` | 追加顺序执行的 run(通过 `Deferred` 链式等待前一个完成) | | `wait()` | `Deferred.await(done)`,可选 timeout | | `waitForPromotion()` | 等待 `promoted` Deferred 或检测 `background` 标记 | | `promote()` | 标记 `background = true`,触发 `onPromote` callback | | `cancel()` | 设置 `cancelled`,close 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. 取 tasks(subtask / 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 part(pending 状态) | | `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**(相同名称 + 相同输入)触发权限询问: ```typescript 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` agent(hidden, mode=primary, `*=deny`)执行。 压缩后的消息标记 `compacted: true`,后续通过 `MessageV2.filterCompactedEffect()` 过滤。 ### 5.5 重试机制(processor.ts lines 658-672) ```typescript 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 | 是 | 生成会话标题 | `*=deny`(step=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` | 后台作业核心引擎(内存注册表) |