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

345 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 笔记: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` | 后台作业核心引擎(内存注册表) |