docs: 添加 OpenCode Agent 切换机制调研笔记
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# OpenCode Agent 切换机制调研笔记
|
||||
|
||||
> 调研日期:2026-06-09
|
||||
> 调研方式:直接读 `sst/opencode` 源码(本地路径 `/Users/midnite/Samples/opencode`)
|
||||
> 调研目标:OpenCode 在切换 agent 时,整个上下文(系统提示词 + agent 提示词)是如何注入的
|
||||
> 关联:
|
||||
> - `docs/note-agent-harness-references.md` — 之前的参考项目调研
|
||||
> - `docs/note-agent-runtime-design.md` — AG Core Phase 4 设计决策
|
||||
> - `docs/7-agent-runtime.md` — AG Core Phase 4 方案文档
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
| 维度 | 详情 |
|
||||
|------|------|
|
||||
| 项目 | `sst/opencode`(GitHub),不是 npm `opencode-ai`(npm 只发编译产物) |
|
||||
| 规模 | 160k+ stars、900+ contributors、13k+ commits |
|
||||
| 语言 | TypeScript + Bun 运行时 + Effect(依赖注入 / Layer) |
|
||||
| 定位 | 开源 AI 编程代理(TUI / Desktop / IDE 插件),与 Claude Code / Cursor 对标 |
|
||||
| Agent 切换 | 终端按 **Tab 键** 在 primary agent 之间循环(build / plan) |
|
||||
|
||||
## 2. Agent 分类
|
||||
|
||||
OpenCode 把 agent 严格分为三类:
|
||||
|
||||
| 类型 | 内置 | 触发方式 | 用途 |
|
||||
|------|------|---------|------|
|
||||
| **Primary(主代理)** | build / plan | Tab 键循环切换 | 用户直接交互 |
|
||||
| **Subagent(子代理)** | general / explore / scout | 主代理自动调 / 用户 `@提及` | 专门任务 |
|
||||
| **Hidden system(隐藏)** | compaction / title / summary | 框架自动调,用户不可见 | 系统级 |
|
||||
|
||||
源码定义在 `packages/opencode/src/agent/agent.ts` 的 `Agent.Info` schema:
|
||||
|
||||
```typescript
|
||||
{ name, description, mode, native, hidden,
|
||||
topP, temperature, color,
|
||||
permission: PermissionV1.Ruleset,
|
||||
model, variant, prompt, options, steps }
|
||||
```
|
||||
|
||||
每个 agent 是**纯配置对象**,可由用户 `opencode.json` 覆盖或自定义 `.md` 文件定义。
|
||||
|
||||
## 3. 核心:System Prompt 完整拼接机制
|
||||
|
||||
OpenCode 把 system prompt 分为 **3 层**,每次 LLM 调用时**完整重新计算**(不缓存)。
|
||||
|
||||
### 3.1 拼接顺序
|
||||
|
||||
源码:`packages/opencode/src/session/llm/request.ts:58-66`
|
||||
|
||||
```typescript
|
||||
const system = [
|
||||
// Layer1:主 agent 提示词
|
||||
...(input.agent.prompt
|
||||
? [input.agent.prompt] // ① agent 自带 prompt(如 PROMPT_EXPLORE)
|
||||
: SystemPrompt.provider(input.model)), // ② 或按 model 选择 provider prompt
|
||||
|
||||
// Layer2:动态上下文(prompt.ts:1408-1414)
|
||||
...input.system, // ③ env + instructions + skills
|
||||
|
||||
// Layer3:用户自定义 system
|
||||
...(input.user.system ? [input.user.system] : []), // ④ 单次 user 消息的 system 字段
|
||||
]
|
||||
.filter(x => x)
|
||||
.join("\n")
|
||||
```
|
||||
|
||||
### 3.2 Layer 2 的内部构成
|
||||
|
||||
源码:`packages/opencode/src/session/prompt.ts:1408-1414`
|
||||
|
||||
```typescript
|
||||
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
|
||||
sys.skills(agent), // 当前 agent 可用的 skills 描述
|
||||
sys.environment(model), // 工作目录、日期、平台、git 状态
|
||||
instruction.system(), // 自动读取 AGENTS.md / CLAUDE.md / CONTEXT.md
|
||||
MessageV2.toModelMessagesEffect(msgs, model),
|
||||
])
|
||||
const system = [...env, ...instructions, ...(skills ? [skills] : [])]
|
||||
```
|
||||
|
||||
**关键发现**:
|
||||
- **AGENTS.md / CLAUDE.md 是 instruction 自动注入**,不是用户手动 @引用
|
||||
- `system.ts` 的 `provider()` 函数**根据 model ID 选择不同 .txt 模板**(如 `PROMPT_ANTHROPIC` / `PROMPT_GEMINI` / `PROMPT_CODEX`)
|
||||
- `environment()` 注入**运行时环境信息**(cwd、平台、日期)
|
||||
|
||||
### 3.3 Agent 配置中的 `prompt` 字段
|
||||
|
||||
源码:`packages/opencode/src/agent/agent.ts`
|
||||
|
||||
```typescript
|
||||
// build / plan 没有 prompt 字段 → 走 SystemPrompt.provider(model)
|
||||
build: { name: "build", mode: "primary", permission: ... },
|
||||
plan: { name: "plan", mode: "primary", permission: ... },
|
||||
|
||||
// explore / compaction / title / summary 有显式 prompt
|
||||
explore: { ..., prompt: PROMPT_EXPLORE, mode: "subagent" },
|
||||
compaction: { ..., prompt: PROMPT_COMPACTION, mode: "primary", hidden: true },
|
||||
title: { ..., prompt: PROMPT_TITLE, mode: "primary", hidden: true },
|
||||
summary: { ..., prompt: PROMPT_SUMMARY, mode: "primary", hidden: true },
|
||||
```
|
||||
|
||||
**结论**:agent 的 `prompt` 字段**只决定 Layer 1 的内容**。Layer 2(env/instructions/skills)和 Layer 3(user.system)始终拼上。
|
||||
|
||||
## 4. 核心:Agent 切换时的 4 个动作
|
||||
|
||||
源码:`packages/opencode/src/session/reminders.ts`(**整个文件 92 行就是答案**)
|
||||
|
||||
OpenCode 用 **`synthetic: true` 的 text part 注入到 user message**,而不是修改 system prompt。
|
||||
|
||||
| 切换方向 | 动作 | 模板文件 | 大小 |
|
||||
|---------|------|---------|------|
|
||||
| 任意 → **plan** | user message 追加 `PROMPT_PLAN` | `session/prompt/plan.txt` | 26 行 |
|
||||
| **plan → build** | user message 追加 `BUILD_SWITCH` | `session/prompt/build-switch.txt` | **5 行** |
|
||||
| build → **plan** (experimental) | user message 追加 `PLAN_MODE` | `session/prompt/plan-mode.txt` | 70 行 |
|
||||
| 任意切换 | system prompt **完全重算** | `request.ts:58-66` | — |
|
||||
|
||||
**最关键的发现——`build-switch.txt` 全文只有 5 行**:
|
||||
|
||||
```
|
||||
<system-reminder>
|
||||
Your operational mode has changed from plan to build.
|
||||
You are no longer in read-only mode.
|
||||
You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
|
||||
</system-reminder>
|
||||
```
|
||||
|
||||
**机制总结**:
|
||||
1. 切换时**不动 message history**(之前所有 user/assistant/tool 消息完整保留)
|
||||
2. 通过**比较 `msg.info.agent` 字段**判断上一条 assistant 用的哪个 agent
|
||||
3. 在**当前 user message 末尾追加**一个 `synthetic: true` 的 text part
|
||||
4. 同时**重新计算 system prompt**(Layer 1 根据新 agent 的 `prompt` 字段切换)
|
||||
|
||||
## 5. 关键设计决策
|
||||
|
||||
| 决策 | 做法 | 原因推断 |
|
||||
|------|------|---------|
|
||||
| **system prompt 重算而非缓存** | 每次 LLM 调用都重新拼接 | agent / model / instructions 都可能动态变化 |
|
||||
| **历史消息不重置** | 切换 = 追加 synthetic part | 保持上下文连贯,避免"切换即失忆" |
|
||||
| **切换提醒伪装成 user 内容** | `<system-reminder>` 标签 + `synthetic: true` | 大多数 LLM 对 `<system-reminder>` 标签有特殊信任 |
|
||||
| **agent prompt 与 model prompt 二选一** | `agent.prompt ?? SystemPrompt.provider(model)` | build/plan 共享 model prompt,自定义 agent 可独立 prompt |
|
||||
| **AGENTS.md 自动注入** | instruction.service 每轮扫描 | Claude Code 兼容,提升跨工具体验 |
|
||||
|
||||
## 6. 与 AG Core Phase 4 的对应关系
|
||||
|
||||
| OpenCode 机制 | AG Core 对应 | 借鉴价值 |
|
||||
|--------------|-------------|---------|
|
||||
| 3 层 system prompt 拼接 | `AgentSession::submit_turn` 中组装 `LlmCycle.with_system_prompt()` | **高**——可拆分为「base prompt + agent prompt + env context」 |
|
||||
| agent 切换时追加 synthetic part | Phase 4 v1 **不做**(仅保留单 agent 角色) | 中——v0.2+ 才考虑 |
|
||||
| AGENTS.md 自动注入 | `prompt::PromptTemplate` + 文件加载(应用层) | 低——文件 I/O 是应用层职责 |
|
||||
| 权限矩阵三态 (allow/ask/deny) | `tools::PermissionChecker`(Phase 2 已实现) | 已有 |
|
||||
| Hidden system agent (Compaction) | `llm::compact`(Phase 0 已实现) | 已有 |
|
||||
| Tab 键循环切换 | 应用层 UI 概念 | 不在 core 库范围 |
|
||||
|
||||
## 7. 借鉴 / 不借鉴清单
|
||||
|
||||
### ✅ 值得借鉴(v0.2+ 考虑)
|
||||
|
||||
1. **`AgentSession` 应支持"切换 agent 但保留历史"**——目前 Phase 4 v1 不做,但 trait 设计上要预留空间
|
||||
2. **System prompt 拆分为多层**——`base_prompt + agent_prompt + env_context`,便于将来按 agent 类型切换
|
||||
3. **synthetic message 模式**——切换 agent 时插入"状态变更通知"而非修改历史
|
||||
|
||||
### ❌ 不借鉴
|
||||
|
||||
- Tab 键循环切换(应用层 UI 概念)
|
||||
- `.md` agent 定义文件(应用层文件加载)
|
||||
- `mode: primary/subagent` 区分(AG Core 是 lib 不做 UI 角色区分)
|
||||
- Hidden system agent 字段(AG Core 已在 L0/L1 实现等价能力)
|
||||
- AGENTS.md 自动注入(应用层职责)
|
||||
|
||||
## 8. 一句话总结
|
||||
|
||||
> **OpenCode 的 agent 切换机制 = "system prompt 完全重算" + "user message 追加 5 行 synthetic 提醒"。** Agent 切换**不**重置消息历史,**不**改写之前内容,只在末尾追加一条"状态变更通知",并按新 agent 重新组装 system prompt 的 Layer 1(agent 专属 prompt)。
|
||||
Reference in New Issue
Block a user