opencode 整体架构文档
opencode 是一个开源的 AI 编程 Agent(对标 Claude Code):终端 / 桌面 / Web 多端 UI + 本地后台服务 + 多 Provider LLM 接入 + 插件 / MCP / Skill 扩展生态。
本文从结构、流程、上下文、通讯、特色工具五个维度说明其整体设计,并重点拆解子代理、skill/MCP、三层记忆与上下文压缩的内部实现。
一、整体结构
1.1 仓库布局
Bun monorepo(Turborepo 管理),约 60 个包,分为三大板块:
| 板块 | 包 | 职责 |
|---|---|---|
| 引擎 | schema | 纯类型与事件定义(零逻辑) |
protocol | RPC 协议分组(对外的 HTTP API 契约) | |
core | 引擎本体:会话、Agent、指令、工具、MCP、skill、权限、事件总线 | |
ai | LLM 客户端抽象(多 provider、压缩、tool-history、图像等模态) | |
plugin | 插件公共 API(给第三方写插件用) | |
util/effect-drizzle-sqlite/http-recorder | 共享工具、DB 封装、录制回放 | |
| 服务与接入 | server | HTTP API 服务端,持有引擎与数据库 |
client | 生成的前端客户端(代码生成产物,禁手改) | |
sdk | 对外 SDK,组合 Client/Core/Server | |
function/enterprise | FaaS / 企业部署形态 | |
codemode | Effect 原生的沙箱代码执行 | |
| 界面 | cli | 命令入口(启动 TUI / 无头 / 后台服务) |
tui | 终端 UI(opentui + Solid) | |
app | Web / 桌面渲染前端 | |
desktop | 桌面壳(Electron) | |
session-ui/ui/theme | 共享组件、组件库、主题 token | |
gui-extensions | 编辑器扩展(GUI 集成) | |
console/stats | 管理后台 / 统计数据 |
另有services/(官网、更新服务)与sdks/(VS Code 扩展)。
1.2 分层依赖(严格单向)
schema(纯类型,零逻辑) ↑ ↑ protocol core ──→ ai ↑ ↑ client ←──────── server ↑ cli / tui / app(只准依赖 client + schema + protocol,禁止触碰 core/server)依赖纪律(AGENTS.md 强制约束):
- UI 层永远通过协议访问引擎,不做进程内直连;
client是生成代码:协议或 ServerHttpApi变更后bun run generate重新生成,不允许手改;- 运行时依赖方向:
Schema → Protocol → Server单向,Client 可依赖 Schema/Protocol,sdk组合三者; - 默认分支
v2,新分支/worktree/PR 默认对齐v2。
1.3 核心领域概念
| 概念 | 说明 |
|---|---|
| Session | 一次会话的聚合根(Root),所有消息/事件/状态以 Session 为边界,跨进程按 ID 定位 |
| Agent | 行为配置:系统提示、可用工具、权限、模型、步数上限。分primary(独立会话的主代理)与subagent(子代理)两种模式 |
| Location | 工作目录作用域。工具、权限、文件系统、MCP、技能、虚拟环境均按 Location 隔离——同一进程可同时服务多个项目目录互不串扰 |
| Project / Workspace | 项目与工作区的持久化身份,管理目录归属与配置层级 |
| Job | 后台任务注册表,承载子代理、后台 shell 的启动与崩溃恢复 |
| Bus | 事件总线:durable 事件先落库再广播,事件溯源(Event Sourcing)入口 |
| State/Transform | 可重放的注册表模式:工具 / 技能 / MCP / Agent 的"注册-覆盖-卸载"全走同一机制——后注册覆盖先注册,卸载自动回退到上一个 |
二、核心流程
2.1 一个回合(Turn)的生命周期
用户输入 │ ▼ ① 准入(Admission):prompt 先作为 durable 事件写入收件箱(session inbox) │ —— 先落库,再调度,崩溃不丢 ▼ ② 唤醒(Wake):执行协调器被 advisory 唤醒 │ 同 Session 的重复唤醒合并;不同 Session 并发执行(有写前 claim) ▼ ③ Drain:进入对应 Location,加载历史、Agent、模型、指令、工具 ▼ ④ Step 循环(一个 Step = 一次逻辑 LLM 调用) │ ├─ 每步前重新加载历史 + 估算 token,超预算先压缩 │ ├─ 组装 system + messages + tools,发起流式调用 │ ├─ 工具调用:多个可并发,但每个结果 durable 落库后才继续 │ └─ 工具结果回填 → 下一个 Step(编排从不交给内存态 tool loop) ▼ ⑤ 结算:步数用尽 / 无更多工具调用 / 用户打断 → Session 空闲,释放写前 claim两个关键机制:
- 一个 Step 多次物理尝试(physical attempt):限流重试、流中断续传、超长触发压缩重建,都不消耗新的逻辑步额度;assistant 消息 ID 在泛化重试时保持不变。
- 写前执行声明(claim):进程开始忙一个 Session 时落一条恢复标记;正常结束(完成/失败/用户打断)释放,进程死亡则保留。启动时据此恢复中断的顶层会话,带每条执行的持久化 attempt 计数。
2.2 输入交付词汇(steer vs queue)
新输入有两种交付模式,在安全步边界生效:
- steer(默认):插队转向,优先于队列项,最多插到第一次转向控制之前;在空闲边界 steer 也优先;
- queue:排队,Session 空闲边界逐条投递(空闲边界若无 steer 则每轮投递一条再让运行器决定是否续跑)。
手动/compact、会话搬迁等控制指令也走同一收件箱,构成交付边界。收件箱项在"排队与转向之间"允许取消/变更;新用户输入表态会重置所选 Agent 的步数额度。
2.3 子代理流程(subagent)
主 Agent 调用subagent工具时,内部实现如下(详见第五节):
- 校验嵌套深度:沿 parentID 向上数祖先,
depth ≥ 配置上限(默认 1,即默认不允许嵌套)即失败; - 创建独立子 Session(记 parentID),模型选择优先级:显式指定 model > Agent 配置的 model > 继承父会话 model;新子会话注入开场白"You are a subagent spawned by another session.",不带父上下文——调用方需自带全部背景;
- 前台:阻塞(
jobs.block)等子会话跑完,取子会话最后一条 assistant 文本返回,包装进<subagent sessionID=... state="completed">;若父被中断则同时中断并取消子代理; - 后台:立即返回
status="running"+ sessionID(批注:不要轮询);子会话完成后,向父会话投递一条synthetic 通知消息(<subagent sessionID=... state="...">,带 source/meta),唤醒父级继续;去重键childSessionID:startedAt; - 续聊同一子代理:传入
sessionID且该会话的 parentID 等于当前会话即复用,仅追加新 prompt(不加开场白);期间模型/Agent 可被切换; - 动态清单:每次请求前把"当前可用的子 Agent 清单 + 描述"拼进工具描述里(过滤掉 primary/hidden/被权限拒绝的),提示里明确"用户点名的陌生名字多半是模型名,应传入模型参数"。
2.4 配置发现流程
启动时从全局目录(~/.config/opencode,兼容~/.claude、~/.agents)和项目目录(.opencode、.agents、.claude及opencode.json/.jsonc)向上查找配置文件与指令文件(AGENTS.md 等),自带文件 watcher 热重载。配置系统由大约 20 个"配置插件"组成,每个领域(Agent / MCP / Skill / Provider …)一个,用与第三方插件完全相同的机制喂数据。
三、上下文(Context)管理
3.1 一次 LLM 请求的上下文构成
┌─────────────────────────────────────────┐ │ System 提示 = 内置指令(日期/环境) │ │ + 发现的指令(AGENTS.md…) │ │ + Agent 自定义 prompt │ │ + 技能清单(<available_skills>)│ │ + MCP server 指令 │ │ (以上均为 Instructions 组的渲染)│ ├─────────────────────────────────────────┤ │ Messages = 指令 epoch 基线(渲染的初始值) │ │ + 历史的 System 更新消息(delta) │ │ + 会话历史(用户/助手/工具,文本或工具结果)│ │ + (若压缩过)<conversation-checkpoint> 占首条 │ ├─────────────────────────────────────────┤ │ Tools = 内置工具 + 插件工具 + MCP 工具 │ │ (请求时快照,注册变化不影响在途请求) │ └─────────────────────────────────────────┘3.2 记忆分层(重点)
全部记忆共用同一个 SQLite(drizzle)后端,三层差异在作用域与持久性:
| 层 | 机制 | 生命周期 | 实现落点 |
|---|---|---|---|
| 短期 | 每次请求对指令源做一次并发read()后拼装上下文(内置core/date、core/environment就是典型"每次现读"的源),外加会话消息投影与收件箱待发项 | 单次请求 | instructions源的实时read() |
| 中期 | 指令值增量(value delta):每类指令源算 SHA-256 哈希,只持久化"变更的 delta 与新增 blob",正文按内容寻址去重存储(AGENTS.md 改一个字不复制整文件);投影到instruction_state/instruction_entry;指令 epoch以压缩为边界推进,epoch 基线作为每次请求首条渲染值 | 单会话内跨压缩 | instruction-state/instruction-entry投影 +instruction_blob表 |
| 长期 | ①durable 事件日志(事件溯源:所有状态可折叠重放,UI 崩溃恢复靠它);②KV 存储(sqlite 键值库,插件storage域的底座,get/set/remove/scan);③指令文件(AGENTS.md / 全局配置,跨会话永久注入);④ Agent/Skill/MCP 配置本身 | 跨会话永久 | bus事件日志 +kv表 + 全局/项目指令发现 |
中期记忆的关键设计是"值增量":每次模型调用前读全部指令源、哈希、只持久化变更的 delta。epoch 只在压缩结束时推进——epoch_start前移、initial_values基线滑动到当前值;会话搬迁也保留 epoch(让目标会话的指令变更保持时序),提交回滚则清除。
3.3 上下文压缩(Compaction)
触发:
- auto:每步前对完整请求估算 token(
estimateContext),达到模型窗口上限(默认 10% 余量)即压缩;紧邻已完成的压缩之后、或 checkpoint 后的首个实测 prompt 之前不压缩; - overflow:provider 明确拒绝"上下文超长"(且输出尚未开始),允许一次机会压缩后重建同一逻辑 Step,第二次溢出则失败终止;
- manual:用户
/compact命令进收件箱,投递时触发。
两条策略(按模型compaction设置选择):
- 结构化总结(默认):用专门的内置
compactionAgent把旧历史总结进固定模板,模板分节为Objective / Requirements / Decisions / Work State / Next Move / Relevant Files / Important Context,
采用滚动式合并旧 checkpoint;同时保留最近约 15k token 的原文(recent)。之后每次请求开头加一条<conversation-checkpoint>(含<summary>与<recent-context>)。 - 原生压缩:provider 自带的上下文续接机制(trigger / endpoint 两种机制),由 AI 客户端层(
LLMClient.compact/canCompact)承载;引擎只管路由、重试、用量与 checkpoint 持久化,需要稳定的 route provenance。
溢出重试预算:SHRINK_STEPS = [0.7, 0.5, 0.35]——首次溢出前目标就已压到估算值的 70%,每次"too long" 拒绝按上次 × step再次缩小,35% 用尽仍失败则终止。
EPOCH 联动:压缩结束Compaction.Ended写 durable 消息行,同时调用advanceEpoch推进指令 epoch。
不可变性:完整转写永不删除——压缩只改写"模型可见的活跃历史",所有消息持久化在session_message表,历史加载以最近一次完成的压缩为第一条消息,但存储记录完整保留。
四、通讯
4.1 客户端-服务端架构
┌──────┐ HTTP RPC ┌──────────┐ 进程内 ┌───────┐ │ TUI │ ──────────→ │ Server │ ──────────→ │ Core │ → LLM Provider │ Web │ (生成客户端)│ (HttpApi)│ (Effect 服务)│(引擎) │ → MCP / FS / Shell │ 桌面 │ ←────────── │ │ │ │ └──────┘ 事件流(SSE) └──────────┘ └───────┘- 后台服务持有引擎与数据库;多端 UI 只是协议消费者;
- 协议按领域分组(session / message / mcp / skill / tool / config …约 30 组),与引擎领域一一对应。
4.2 事件体系(双通道)
- durable 事件:先写 sqlite 再广播,带聚合内单调序列号;投影器在发布事务内同步执行(事件与投影原子提交);支持
log重放(断线后按 watermark 续传),UI 刷新 / 崩溃恢复都靠它; - live 事件:流式文本、reasoning、工具进度等瞬态增量,只在本实例内广播、不保证重放。
4.3 MCP 接入
- 传输两种:
- stdio(自研 transport):帧上限 16 MiB;stderr 排空保留 1000 字符尾;关闭时先
SIGTERM、2 秒后升级SIGKILL;出站帧走有界队列(cap 64); - Streamable HTTP:
MCP-Session-Id会话 + terminate 清理。
- stdio(自研 transport):帧上限 16 MiB;stderr 排空保留 1000 字符尾;关闭时先
- 工具注册:server 名做净化(非
[A-Za-z0-9_-]替换为_),工具全名<server>_<tool>,注册进统一工具注册表,与本地工具同构(同一 schema / 权限 assert / 输出截断管道); - MCP 发布
ToolsChanged时 debounce 100ms 后整表 reload,保持插件覆盖优先级; - MCP prompts:可调用模板(带 server 标记 + PromptsChanged);
- MCP resources:暴露为
opencode_list_mcp_resources/opencode_read_mcp_resource两个内置工具,底层把resources与resourceTemplates合并成资源目录; - OAuth / elicitation:远程 server 注册为 OAuth 集成(按
sha1(name\0url)键控),凭据变化自动重连;elicitation 表单把 JSON-schema 属性映射为表单字段,支持外链 URL 模式。
4.4 插件体系
- 插件 =
define({ id, effect }),拿到约 20 个 domain 的上下文(tool / skill / session / mcp / permission / storage / vcs / worktree / websearch …); - 内置功能本身也是插件:subagent、skill、bash 与第三方工具走完全相同的注册路径,没有特权代码;
- 三种插件源:本地 Bun 模块、Node 模块、npm 包;
- 工具 / 技能 / MCP / Agent 注册全部走可重放 Transform 机制,支持热插拔与覆盖回退。
五、特色工具(专项实现)
5.1 subagent —— 子代理派发
详见 2.3。要点:独立子会话 + 模型优先级(显式 > Agent 配置 > 继承父)、前台阻塞 / 后台 synthetic 通知、sessionID续聊可切模型、嵌套深度默认 1、动态子 Agent 清单注入工具描述。会话分类:general/explore→ subagent 模式,build/compaction/title/summary→ primary 模式。
5.2 skill —— 技能加载
本质是指令注入,不是代码执行——技能目录里的脚本由模型用普通 shell 工具自己跑。
- 发现:扫描配置目录里
{*.md, **/SKILL.md},解析 frontmatter + Markdown 正文;技能 id 为目录名(SKILL.md)或文件名;autoinvoke由元数据opencode/autoinvoke决定(disable-model-invocation强制为关); - 来源:本地目录(含
watch监听、100ms debounce 重扫)与远程下载(读远端index.json,缓存到~/.cache/skills/<hash>,带.opencode-version版本化原子更新); - 注入:读取
SKILL.md内容 +采样文件清单(前 10 个文件),渲染成<skill_content name=...>块(含# Skill:、正文、基目录、采样提示、<skill_files>),作为技能的结果返回或直接被调用时注入; - 运行清单:系统提示里始终维护
<available_skills>清单,变更用 diff 增量下发——新增渲染"New skills are available…“,移除渲染"no longer available and must not be used”;有变更就把整表作废重发。
5.3 question —— 结构化提问
向用户发起带选项的表单提问(支持多问、多选、推荐项标注),用于执行中获取确认/澄清,代替模型自问自答或胡乱猜。
5.4 patch / file-diff —— 代码修改与差异
patch:对文件做受控修改(含 BOM 处理、格式化器联动、写后校验);file-diff:生成文件差异视图,模型与 UI 双方共用统一 diff 语义。
5.5 opencode 工具族 —— 自省与自管理
models(列出/筛选可用模型)、session_rename/session_move(改名与搬迁)、worktree_create / list / remove(Git worktree 管理,配合隔离实验)。
5.6 mcp-resource 工具族
opencode_list_mcp_resources/opencode_read_mcp_resource:枚举与读取 MCP server 暴露的资源(自定义 URI scheme 的文档、记录等),适合"用户提到一个非本地文件的东西"的场景。
5.7 基础工具箱
- 文件:read / write / edit / glob / grep(Location 作用域内,走权限系统);
- shell:后台进程管理、输出尾部保留 + 全量输出落盘引用(超长输出不爆上下文);
- webfetch / websearch:网页抓取(渲染为 Markdown,外部内容视为数据)与网络搜索;
- 所有工具输出统一做模型侧截断(默认头尾保留、中间省略标记),超长内容进托管存储。
六、安全与权限
- 统一权限系统:每个工具调用前
assert(action + resources + 可记忆的允许规则),用户可放行一次或永久保存; - Location 作用域的文件系统访问,工具拿不到作用域外的路径;
- UI 层与引擎层严格隔离,客户端无进程内直连引擎的通道;
- MCP 输入做路径穿越 / URL 校验;技能下载有版本化原子更新(staging + backup 回滚)。
七、小结
opencode 的架构可浓缩为四句话:
- 事件溯源 + 投影:一切先落 durable 事件、投影原子提交,状态靠折叠;崩溃可恢复、UI 可重放。
- Location 作用域:一个进程服务多目录,工具 / 权限 / 上下文按目录隔离。
- 一切皆插件:配置、内置工具、MCP、技能走同一套可重放注册机制,无特权路径。
- 上下文即预算:指令是值增量、记忆分三层、压缩双策略 + 阶梯重试——引擎对"模型能看什么"的每个字节都有明确账本。