# 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 行**: ``` 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. ``` **机制总结**: 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 内容** | `` 标签 + `synthetic: true` | 大多数 LLM 对 `` 标签有特殊信任 | | **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)。