28ca43ccb2
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
13 KiB
13 KiB
笔记: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 <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 part(synthetic: 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() |
创建 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(相同名称 + 相同输入)触发权限询问:
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)
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 |
后台作业核心引擎(内存注册表) |