OpenCode 的 package 划分更像一条依赖漏斗:越靠下越稳定、越靠上越接近产品宿主。
flowchart BT
S["schema: 可编码领域形状"]
P["protocol: API / error / transport contract"]
L["llm: canonical message / provider event"]
C["core: Effect services + DB + Session V2"]
SV["server: middleware / HTTP / location"]
OC["opencode: CLI + product orchestration"]
CL["client: generated promise API"]
SDK["sdk-next: client + core + server composition"]
UI["tui / app / web / desktop"]
S --> P
S --> L
P --> SV
L --> C
S --> C
C --> SV
C --> OC
SV --> OC
P --> CL
CL --> UI
C --> SDK
SV --> SDK
SDK --> UI
仓库 AGENTS.md 给出的原则可以压缩成一句话:Schema → Core / Protocol → Server;Client 只能依赖 Schema / Protocol,不能反向依赖 Core / Server;sdk-next 才组合 Client、Core、Server。
packages/schema/src 定义 Session、Message、Part、Prompt、Event、Permission、Project、Workspace 等数据。它通过 Effect Schema 建立编码与解码关系,ID 也有前缀和格式约束。
Schema 的价值不只是类型检查:
- HTTP body 和 query 可以由同一份定义解码;
- SQLite projector 可以把事件转成同样的结构;
- SDK codegen 可以从 API 定义生成客户端;
- Event Manifest 可以把事件变成可订阅的联合类型。
packages/protocol/src 放跨 server/client 的错误和 API 描述。它不应该知道具体的 SessionRunner,但可以描述 SessionNotFoundError 这样的稳定错误边界。
packages/llm/src/schema/messages.ts 把消息、文本、reasoning、tool-call、tool-result、ToolDefinition 和 LLMRequest 标准化。provider 适配器只需要把 canonical request 翻译成自己的 wire format,再把流翻译回 LLMEvent。
Core 是真正的“骨骼”:
packages/core/src/session:Session V2、History、Input、Runner、Compaction;packages/core/src/system-context:上下文 Source、Registry、Baseline;packages/core/src/tool:跨宿主可用的工具与输出治理;packages/core/src/database:Effect + Drizzle + SQLite;packages/core/src/project、location、workspace:作用域与持久化;packages/core/src/provider、catalog:模型和 provider 的领域表示。
packages/server 里是通用 HTTP / Location 中间件;packages/opencode 里则是具体的 CLI、旧版服务、配置发现、插件加载、MCP、LSP、路由 handlers 和应用 runtime。这样核心服务可被不同宿主使用,但产品入口仍然集中管理。
specs/tui-package.md 把 TUI 抽取的目标写得很清楚:TUI 通过 @opencode-ai/sdk 获取 Session、Message、File、Provider、Agent、Permission 等数据,缺少的能力要先加到 server API 和 generated SDK,而不是直接 import packages/opencode 内部实现。
这是一个很实用的架构测试:如果一个 UI 功能必须直接读取后端 service,说明 API 边界还没有表达完整的产品能力。
AppLayer 不是“把很多服务放进数组”。每一个 LayerNode 表示一个服务的构造函数和依赖,Layer.provideMerge 让共享的 Node / observability / runtime 被合并。效果是:
数据库、FS、配置、认证
→ Project / Provider / Agent
→ Session / Context / LLM / Tools
→ HTTP handlers / CLI / TUI host
当测试要替换 Database、LLM 或 Provider 时,不需要改业务函数签名,只需要提供另一层实现。这也是 Effect.Service 比全局单例更适合 OpenCode 的原因。
当前仓库不是一次性重写,而是在迁移:
- V1 的
packages/opencode/src/session/*仍承担大量产品功能和兼容入口; - V2 的
packages/core/src/session/*把 durable inbox、Location-scoped runner、Context Epoch 和 Event V2 作为新的规范化方向; packages/opencode/src/event-v2-bridge.ts、projector 和 API 层负责把两边逐步接起来。
这解释了为什么阅读时会遇到 SessionV1、SessionV2、MessageV2 和两套 event schema。正确做法不是把重复代码马上合并,而是先确认每个类型属于哪个状态模型。
让底层只依赖稳定数据和小接口,让上层决定宿主、显示和网络。越靠近 UI,依赖越多;越靠近 Schema,依赖越少。
SessionRunner、SystemContextRegistry、ToolRegistry 都不是单个文件,而是由 schema、store、service、event 和测试共同组成的边界。看源码要按语义组,而不是只看文件名。
从 SessionApi 的路由反向看:如果 API 要支持 prompt、abort、compact、revert、permission、find file、share,那么后端必然需要对应的 Session、Input、Compaction、Revert、Permission、File 和 Share 服务。
OpenCode 的分层不是“为了好看”,而是为了解决三个变化维度:模型供应商会变、客户端宿主会变、Session 执行语义会迁移。Schema / Protocol 稳定跨边界,Core 承载领域状态,Server 提供运行时,UI 只消费契约。