收尾复盘

全书要点串联:一条消息穿过的各层、二十条值得记住的设计、与另外三本的四方对照,以及可迁移的工程经验和这份源码的短板。

作者 David更新于 第 38 篇(共 38 篇)

读到这里,OpenCode 的 V1 现役链路与 V2 迁移方向都已拆完。这一篇把散落在各章的要点收拢起来。

全景回顾

默认 opencode 命令下,一条消息从终端到模型再回到屏幕,经过的层:

TUI(packages/tui:OpenTUI + SolidJS,主线程)
  ↓ SDK 请求:默认经 RPC 隧道进 Worker;带 --port 时走真实 HTTP
服务端(Effect HttpApi:路由分组 → 实例定位 → handler)
  ↓
SessionPrompt(createUserMessage → loop → runLoop,一个会话一个 Runner)
  ├── system     ← 模型家族模板 + 环境信息 + 指令文件 + Skills + MCP 说明
  ├── LLM.stream ← AI SDK(默认)或 @opencode-ai/llm(实验开关)
  └── SessionProcessor ← LLMEvent → Part(text / reasoning / tool / step / patch)
        └── 工具 execute ← 插件钩子 ← Permission.ask(Deferred 挂起等用户)
  ↓
Session.updatePart → durable 事件:SQLite 事务里分配序号、跑投影
  ↓ GlobalBus → global.event(RPC)或 SSE
TUI 的 sync store(16ms 攒批)→ 渲染

V2 的链路已在旁边成形:/api/session → session_input 收件箱 → SessionRunCoordinator → SessionRunner(每轮一次 llm.stream、工具急切执行)→ session.next.* 持久事件。两代共用 session 表,由同一个服务端进程同时提供。

二十条值得记住的设计

边界与契约

  1. 连 TUI 也是客户端。服务端跑在 Worker 里,TUI 只发 SDK 请求、订阅事件;同一棵路由树既能进程内调用,也能对外监听(启动链路)。
  2. handler 只认 Request 与 Response。Worker 隧道、opencode run 的本地模式、sdk-next 都只是把网络换成一次函数调用(SDK、协议与客户端)。
  3. 依赖方向写进规则。schema → core 与 protocol → server,client 永不依赖 core;protocol 决定中间件放在哪,server 注入具体的键(SDK、协议与客户端)。
  4. 界面动作也是事件。/tui/append-prompt 这类端点只发布事件,由正在运行的 TUI 接收,VS Code 扩展因此不需要自己的界面(ACP、GitHub 与其他入口)。

会话引擎

  1. 一个会话一个 Runner。连发消息不需要队列:新消息先落库,循环下一步重读历史自然看到(主循环)。
  2. @ 文件伪装成已发生的 Read 调用。模型看到的是它熟悉的工具结果,而不是一段来路不明的文本(主循环)。
  3. 压缩请求本身是一条消息。它可见、可回看,还充当历史视图的锚点;数据库里的旧消息一条不删,只是模型看不到(上下文压缩)。
  4. 撤销先暂存、后清理。revert 只写边界,删除推迟到下一条消息,/redo 与 unrevert 几乎零成本(快照与撤销)。
  5. 影子 git 仓库借用对象库。经 objects/info/alternates 复用用户仓库的对象,巨型仓库也不必重算哈希(快照与撤销)。
  6. 快照要在流开始前拍。AI SDK 可能先于 start-step 执行工具,等事件到了再拍就晚了(流处理器)。

工具与权限

  1. 模糊编辑配一道比例闸。九级匹配级联省掉大量“空白不一致”的重试,isDisproportionateMatch 防止锚点匹配吞掉半个文件(文件工具)。
  2. 按命令元数记住“总是允许”。tree-sitter 解析出每条子命令,记下 git checkout * 这样的有效前缀,而不是整条命令或整个工具(Shell 工具)。
  3. 看不见的工具。某个权限的最后一条相关规则若是模式为 * 的 deny,对应工具就从请求里整个消失(权限系统)。
  4. 工具 ID 即权限键。所以在 PowerShell 上 Shell 工具也叫 bash,改名要等 2.0(Shell 工具)。

模型与扩展

  1. 模型目录不进仓库。models.dev 按磁盘缓存、编译期快照、网络三级获取,新增提供方原则上只需给目录提 PR(提供方目录与 models.dev)。
  2. 把“取凭据的函数”交给 loader。自定义 fetch 每次请求都重读凭据,token 刷新对 AI SDK 透明(认证与提供方插件)。
  3. 插件串行、共享可变的 output。加载顺序就是语义,内置插件总在外部插件之前(插件系统)。

V2 的方向

  1. 受理与执行分离。提示先落进持久收件箱拿到回执,模型失败、进程重启都不丢输入(V2 会话内核)。
  2. 事件、序号与投影同在一个事务。读模型永远与事件一致;“可回放”与“实时渲染”拆成两个通道(存储:SQLite 与事件投影、事件与同步)。
  3. 单一执行体与陈旧调用拒绝。V2 工具的名字属于注册而非工具本身,本轮开始后被替换的注册只会得到 Stale tool call;输出只在结算边界截断一次(V2 工具与权限)。

四方对照

维度llm-to-agentGrok BuildKimi CodeOpenCode
定位教学最小实现生产实现生产实现生产实现
语言教学用RustTypeScriptTypeScript + Effect
规模小约 85 万行(非测试)/ 96 crate38 万行 / 19 个单元(引擎约 13.6 万行)近 50 万行 / 36 包(agent 相关约 17 万行)
前端1TUI 为主,另有 headless 与 ACP7(TUI、-p、Web、Remote Control、VS Code、ACP、SDK)TUI、Web、桌面、VS Code、ACP、GitHub、run、SDK
依赖组织无显式传参VS Code 式 DI × ScopeEffect 的 Service、Layer 与 LayerNode
TUI—自研pi-tui(vendored 自 pi-mono)OpenTUI + SolidJS
存储——wire.jsonl 追加日志 + minidb 索引SQLite + Drizzle + 事件投影
模型接入单一分层 catalog引擎自带 human/llm(协议基座 + trait)models.dev + AI SDK,另有自研 packages/llm
引擎代数112 代 → 1 代(v1 已删除)2 代并存(V1 现役、V2 迁移中)

最大的结构差异来自一个选择:把服务端当成产品的中心。Grok Build 以 TUI 为中心,agent 核心与界面可以贴得很近;Kimi Code 要喂七种形态,于是有了 klient 契约层与独立的 transcript 包;OpenCode 走得更远,连默认的 TUI 都是 HTTP 客户端,服务端、协议、SDK、事件同步因此全都成了一等公民。代价是哪怕只在终端里用,也要跑一套完整的服务端(藏在 Worker 线程里);回报是任何能发 HTTP 请求的东西都能驱动它。

从 OpenCode v1.14.19 分叉出来的 MiMo Code 保留了这套服务端架构,改动集中在会话引擎之上,见《MiMo Code 源码解读》。

这不是谁更好,是约束不同。

可迁移的经验

给 AI 助手的文档要和代码一起评审。 根 AGENTS.md 写清了依赖方向、Effect 用法与测试约定,是现成的读者指南;但本书也记下了几处文档与代码不符的地方——原生 LLM 开关、TUI 抽包后仍依赖 core、CONTRIBUTING.md 里的旧路径。文档一旦被当成指令,漂移的代价就更高。

工具说明是给模型的契约,写了就要兑现。 edit.txt、write.txt 说“没先读就会失败”,webfetch.txt 说会自动升级 HTTPS、会摘要大页面,代码里都没有。模型会照着说明行事,说明与实现不一致,等于给了它错误的前提。

把数字从说明里拿出来。 Shell 工具的说明按 shell 种类渲染,默认超时与截断阈值在渲染时插入,说明与代码不会漂移——恰好是上一条的解药。

用最小 API 守住不变量。 V2 的 Tool.make 只有一个执行体,AGENTS.md 明令禁止第二种入口。与其事后审查,不如让错误的写法根本写不出来。

规格里标清完成度。 V2 会话规格的对齐清单逐项标注 complete、partial、missing(21 项里分别是 3、8、10),迁移到了哪一步一目了然,本书第 6 部分的许多判断都靠它。

兼容负担要关进边界。 插件工具仍用 Zod 描述参数,注册表只在边界上转换;V1 读懂部分 V2 配置写法时,产出 invalid、unsupported、conflict 三类诊断,而不是悄悄吞掉。

这份源码的短板

  • 两代并存的认知成本:一个 opencode serve 进程同时提供 /session/... 与 /api/...,读代码时得时刻分清走的是哪条链路。
  • 声明了却没接上:插件的 permission.ask 钩子没有触发点,SessionContextEpoch.reset 没有调用方,snapshot、retry 两种 Part 不再产生,copilot-gpt-5.txt 等提示词文件无人引用,effect-sqlite-node 只出现在依赖清单里。
  • 默认宽松:内置 agent 以“全部放行”打底(V1 与 V2 相同),编辑与 Shell 都不询问,只对越界目录、.env 读取(V1 还有疑似死循环)提问;想收紧得自己写规则,而 Shell 从来不是沙箱。
  • Effect 的门槛:Context.Service、Layer、Effect.fn、InstanceState 贯穿全仓,不熟 Effect 的贡献者起步要先补一课。
  • 大文件不少:provider.ts 与 transform.ts 各约两千行,会话视图 routes/session/index.tsx 超过 2700 行。

继续读什么

回到专栏首页可查任意章节。源码基准 3104c142(v1.18.30,2026-09-09)——仓库更新很快,读时以当前 HEAD 为准;V2 迁移完成后,第 6 部分的“迁移中”会变成“现役”,但本专栏梳理的结构与契约一般稳定。

本页目录