Files
agcore/docs/note-opencode-agent-switching.md
T

174 lines
8.2 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 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 2env/instructions/skills)和 Layer 3user.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 1agent 专属 prompt)。