diff --git a/docs/note-opencode-agent-switching.md b/docs/note-opencode-agent-switching.md new file mode 100644 index 0000000..1fcfc4c --- /dev/null +++ b/docs/note-opencode-agent-switching.md @@ -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 行**: + +``` + +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)。 \ No newline at end of file