docs: 添加 opencode 子代理调度分发与合并机制调研笔记

This commit is contained in:
徐涛
2026-07-04 10:19:54 +08:00
parent fba78f5f33
commit 6315f2d008
+344
View File
@@ -0,0 +1,344 @@
# 笔记: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
```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
<task id="ses_xxxxx" state="completed">
<summary>任务简述</summary>
<task_result>
子 agent 输出的完整文本内容...
</task_result>
</task>
```
错误时:
```xml
<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
```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()` | 创建 jobfork 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. 取 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**(相同名称 + 相同输入)触发权限询问:
```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` agenthidden, 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` | 后台作业核心引擎(内存注册表) |