8.2 KiB
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:
{ 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
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
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
// 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>
机制总结:
- 切换时不动 message history(之前所有 user/assistant/tool 消息完整保留)
- 通过比较
msg.info.agent字段判断上一条 assistant 用的哪个 agent - 在当前 user message 末尾追加一个
synthetic: true的 text part - 同时重新计算 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+ 考虑)
AgentSession应支持"切换 agent 但保留历史"——目前 Phase 4 v1 不做,但 trait 设计上要预留空间- System prompt 拆分为多层——
base_prompt + agent_prompt + env_context,便于将来按 agent 类型切换 - synthetic message 模式——切换 agent 时插入"状态变更通知"而非修改历史
❌ 不借鉴
- Tab 键循环切换(应用层 UI 概念)
.mdagent 定义文件(应用层文件加载)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)。