28ca43ccb2
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
345 lines
13 KiB
Markdown
345 lines
13 KiB
Markdown
# 笔记: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)
|
||
|
||
```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 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)
|
||
|
||
```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` | 后台作业核心引擎(内存注册表) |
|