终端 Agent 项目 opencode 仓库结构导读:32 个包里哪些是主干,改功能从哪进
本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。
你打开 anomalyco/opencode(下称 opencode,一个跑在终端里的开源 AI 编码 Agent 项目,不是泛指「开源代码」)这个仓库,要判断的第一件事不是「代码在哪」,而是「你要动的东西属于哪一圈」——因为这个仓库里的 32 个包不是 32 个平级模块,它们被一条明确的依赖方向切成了几层,走错一层,改动会被类型检查和代码生成挡回来。
仓库根目录的 package.json 里写着 "license": "MIT",repository.url 指向 https://github.com/anomalyco/opencode,name 就是 opencode。整个仓库是一个 Bun workspace,workspaces.packages 声明了 packages/*、packages/console/*、packages/stats/*、packages/sdk/js、packages/slack 这几组成员。packages/ 下正好 32 个目录,全仓 6358 个受版本控制的文件——这个体量下,靠 grep 猜路径是最慢的读法。
站内已经写过几篇同类的仓库导读:ecc 的仓库结构拆的是技能与上下文的分层组织,Hermes Agent 的仓库结构拆的是常驻自托管服务的模块边界,pi 的仓库结构拆的是可嵌入式运行时的扩展点。这篇只管一件事:opencode 的包依赖方向,以及由它推导出的「改动入口地图」。
一、32 个包先分四圈,别一个个读
按 ls packages/ 的实际结果,32 个包是:app、cli、client、codemode、console、containers、core、desktop、docs、effect-drizzle-sqlite、effect-sqlite-node、enterprise、function、http-recorder、httpapi-codegen、identity、llm、opencode、plugin、protocol、schema、script、sdk、sdk-next、server、session-ui、slack、stats、storybook、tui、ui、web。
一次性读完是浪费时间。按职责分四圈更实用。
第一圈是主干:opencode 和 core。这两个包放着会话运行、工具执行、模型调度、权限判定这些真正决定 Agent 行为的东西。CONTRIBUTING.md 里对 packages/opencode 的定位是 OpenCode 的核心业务逻辑与服务端。
第二圈是契约:schema、protocol、server、client、sdk、sdk-next。它们定义「数据长什么样、端点怎么走、错误怎么表达」,是主干对外的那层皮。
第三圈是外壳:tui、app、desktop、web、session-ui、ui、storybook。终端界面、浏览器界面、Electron 桌面壳、文档站、共享组件库。根 package.json 的脚本里能看出各自的启动方式——dev:desktop 进 packages/desktop,dev:web 进 packages/app,dev:storybook 进 packages/storybook。
第四圈是周边:llm、codemode、http-recorder、httpapi-codegen、effect-drizzle-sqlite、effect-sqlite-node、plugin、script、cli、console、stats、slack、function、enterprise、identity、containers、docs。这里面有几个是被主干依赖的能力包(llm、codemode),有几个是工具链(httpapi-codegen、script),剩下的是官网、统计站、集成这类跟你改 Agent 行为八竿子打不着的东西。
判断一个包在哪一圈,有条捷径但不总是走得通:少数包在自己的 package.json 里写了 description,一句话就够定性——codemode 写的是 Effect-native confined code execution over schema-described tools,http-recorder 写的是 Record and replay Effect HTTP client traffic with deterministic cassettes,读完就知道要不要读下去。问题是仓库里绝大多数包压根没填这个字段,core、schema、protocol、server、llm、tui 全都没有。
捷径断了就退一档:先看包内有没有 AGENTS.md,没有再看 README.md,再没有就 ls 一下 src/ 的第一层。仓库里的 AGENTS.md 一共十来份,散在 packages/app/、packages/codemode/、packages/core/src/tool/、packages/desktop/、packages/effect-drizzle-sqlite/、packages/llm/、packages/opencode/、packages/schema/、packages/stats/ 这些位置。经验上,有这份文件的地方,就是有人认真划过边界、也最容易踩到别人定的规矩的地方——它比包名可靠得多。
二、主干里 opencode 和 core 各管什么
这是最容易走错的一步。两个包都有 session/,都有 tool/,名字撞得厉害。
packages/core/src/ 下有 session/、tool/、system-context/、permission/、database/、filesystem/、provider.ts、catalog.ts、skill/、pty/ 等。它的 package.json 明确导出了几个入口:./session/runner、./system-context、./effect/layer-node、./effect/app-node,再加通配的 ./*。这些导出名基本就是它的职责说明书——会话运行器、系统上下文、Effect 层装配。
packages/opencode/src/ 下则有 cli/、session/、tool/、provider/、mcp/、lsp/、ide/、acp/、worktree/、share/、installation/ 等。packages/opencode/package.json 里 bin.opencode 指向 ./bin/opencode,也就是说它才是那个装完之后你敲的命令。它的 dependencies 里同时列着 @opencode-ai/server、@opencode-ai/tui、@opencode-ai/llm、@opencode-ai/codemode、@opencode-ai/plugin——它是把这些拼起来的那一层。
两个 session/ 的差别落在具体文件上。packages/core/src/session/ 有 context-epoch.ts、input.ts、projector.ts、run-coordinator.ts、store.ts、execution/、runner/;packages/opencode/src/session/ 有 prompt/、llm/、reminders.ts、retry.ts、overflow.ts、summary.ts、system.ts。前者是耐久化的会话机制,后者更贴近「这一轮跟模型怎么说话」。
packages/opencode/src/session/prompt/ 下是 14 份 .txt——anthropic.txt、gpt.txt、gemini.txt、codex.txt、kimi.txt、beast.txt、trinity.txt、default.txt、meta.txt、plan.txt、plan-mode.txt、plan-reminder-anthropic.txt、build-switch.txt、copilot-gpt-5.txt。提示词按模型家族分文件放,不是拼在 TypeScript 字符串里。这意味着你想调某个模型下的行为,改的是一个纯文本文件,不需要碰逻辑代码。
工具那边同理。packages/opencode/src/tool/ 有 25 个 .ts 和 15 个 .txt——.ts 是实现(read.ts、write.ts、edit.ts、glob.ts、grep.ts、shell.ts、task.ts、skill.ts、webfetch.ts、websearch.ts、apply_patch.ts、plan.ts、lsp.ts、code-mode.ts、registry.ts、truncate.ts 等),.txt 是对应的工具描述文本。这种「实现与描述分文件」的做法,让改工具描述这件事不必重新走一遍编译期的心智负担。关于工具描述该怎么写才让模型用对,可以看工具描述的写法。
packages/core/src/tool/ 里还专门放了一份 AGENTS.md,把这一层的规矩写得很直白:tool.ts 定义规范的 Tool.make({ description, input, output, execute, toModelOutput });application-tools.ts 存进程级注册;tools.ts 暴露只管注册的 Tools.Service 视图;registry.ts 只存规范工具,把 Location 级注册覆盖在应用级注册之上。它还写了一句很关键的边界:注册表本身不依赖 PermissionV2.Service,不做执行授权——定义过滤是目录可见性,不是执行授权。这两件事在很多 Agent 项目里是混在一起的。
三、契约链:schema → protocol → server,箭头不能反
根目录 AGENTS.md 第三条把依赖方向写死了:
Keep runtime dependencies directed from Schema to Core and Protocol, then from
Core and Protocol to Server. Client runtime code may depend on Schema and
Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
packages/schema/AGENTS.md 把它复述成一行箭头:@opencode-ai/schema <- @opencode-ai/protocol <- @opencode-ai/server,并且要求 schema 里放的是可序列化的契约定义,不是服务实现和运行时注册表。三段各自负责什么,CONTEXT.md 的「Client contract architecture」一节讲得更细:protocol 把 schema 的值组合成路径、载荷、信封、错误、游标、流;server 同时引入两者,托管 protocol 定义的那批分组,并负责协议与领域之间的适配。翻成人话就是,schema 是最轻的叶子,越往下游包袱越重,反过来引就是把重的塞进轻的。
这条链有个直接的操作后果,AGENTS.md 第二条写了:改完公开的 Protocol 或 Server 的 HttpApi 之后,要从 packages/client 跑 bun run generate,不要直接编辑 src/generated 或 src/generated-effect。packages/client/package.json 里还配了 check:generated,做法是先重新生成再 git diff --exit-code 比对这两个目录。你手改生成物,这一步就会红。
packages/client 对外只暴露两个入口:. 和 ./effect。CONTEXT.md 里对这个分割解释得很清楚——根入口没有到 Effect 的运行时路径,/effect 只依赖 Effect、Schema 和 Protocol。也就是说不用 Effect 的消费者不会被拖进 Effect 的运行时。
CONTEXT.md 这份文件本身值得单独读一遍。它不是架构图,是一份词表:System Context、Session History、Context Source、Context Epoch、Mid-Conversation System Message、Safe Provider-Turn Boundary、Admitted Prompt、Prompt Promotion、Session Drain、Model Tool Output、Managed Tool Output File……每个词都给了定义,还专门标了 _Avoid_,比如 System Context 后面跟着 Avoid: System prompt,Session History 后面跟着 Avoid: Session Context。读代码之前先把这份词表过一遍,能省掉大量「这个名字是不是那个意思」的来回。
四、改一处功能,从哪个包进去
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 终端界面 | 按键、主题、输入框、组件渲染 | packages/tui/src/(keymap.tsx、theme/、prompt/、component/) | 改交互、改显示、加快捷键 |
| CLI 命令入口 | 子命令解析、启动 TUI 或服务端 | packages/opencode/src/cli/cmd/(serve.ts、run/、tui.ts、mcp.ts) | 加一个新子命令 |
| 会话机制 | 输入准入、上下文纪元、运行协调、投影 | packages/core/src/session/(input.ts、context-epoch.ts、run-coordinator.ts、runner/) | 改会话生命周期与恢复 |
| 单轮对话编排 | 提示词、重试、压缩、溢出处理 | packages/opencode/src/session/(prompt/、retry.ts、compaction.ts、overflow.ts) | 改 Agent 的说话方式 |
| 工具注册与结算 | 规范工具类型、注册覆盖、输出限界 | packages/core/src/tool/(tool.ts、registry.ts、tools.ts、application-tools.ts) | 改工具的通用机制 |
| 具体工具实现 | 读写、检索、执行、抓取 | packages/opencode/src/tool/(read.ts、edit.ts、shell/、grep.ts) | 加一个新工具 |
| 系统上下文 | 组装并增量更新注入模型的环境事实 | packages/core/src/system-context/(builtins.ts、registry.ts) | 改往上下文里塞什么 |
| 模型接入 | 协议适配、流式事件、路由 | packages/llm/src/(protocols/、providers/、route/、schema/) | 接一种新的协议形态 |
| 网络契约 | 数据形状、端点、错误、游标 | packages/schema/src/、packages/protocol/src/、packages/server/src/ | 改公开 API |
| 生成式客户端 | Promise 与 Effect 两套调用面 | packages/client/src/(generated/、generated-effect/) | 只跑生成,别手改 |
| 插件面 | 对外扩展入口 | packages/plugin/src/(tool.ts、tui.ts、v2/) | 写第三方插件 |
| 文档站 | 官方文档与站点 | packages/web/src/content/docs/(36 份 mdx 加各语言子目录) | 改文档 |
几条典型路径可以直接套用。
想加一个内置工具:先读 packages/core/src/tool/AGENTS.md,再看 packages/opencode/src/tool/ 里一个结构最简单的现成工具(比如 glob.ts 配 glob.txt),照着 Tool.make 的形状写。注意 AGENTS.md 里那句「edit、write、apply_patch 声明共享的 edit 动作」——权限动作不是自动按工具名一一对应的。
想改某个模型下的行为:直奔 packages/opencode/src/session/prompt/,找对应的 .txt。不确定当前走的是哪条路径,packages/llm/AGENTS.md 里写明了 packages/opencode/src/session/llm.ts 是决定请求走 AI SDK 还是走 llm 包原生路由的那一层,native-request.ts 是下沉适配器,ai-sdk.ts 负责把 AI SDK 的流片段转成共享事件。
想改服务端接口:packages/server/src/ 只有 api.ts、routes.ts、handlers/、handlers.ts、middleware/、location.ts、auth.ts、cors.ts、pty-environment.ts 这么几项,很快能扫完。改完记得跑客户端生成。
想理解多项目会话怎么组织:读 specs/project.md。这份规格开头一句话就是目标——让单个 OpenCode 实例为多个项目、每个项目的不同 worktree 运行会话,后面直接列了 GET /project、POST /project/init、POST /project/:projectID/session 这些路由草案。它还诚实地在几个查询接口上面标了一行 // These are awkward。
五、边界与代价:这套结构放弃了什么
放弃了「随便在哪写都行」的自由。 那条 schema→protocol→server 的箭头意味着你想在 protocol 里顺手引一个 core 的工具函数是不行的。CONTEXT.md 在客户端契约那一节还追加了一条更硬的:额外的公开 schema 放 Schema、额外的网络分组放 Protocol,而这两个包都不得传递性地加载数据库、Drizzle、会话执行、providers、watchers、原生模块或 WASM。注意「传递性」三个字——不是你没直接 import 就算过关,间接拖进来一样算违规。这条约束的收益是浏览器安全的客户端包,代价是你偶尔要为一个小函数在两处各写一遍。
放弃了扁平化的心智模型。 主干被拆成 opencode 和 core 两个包之后,一个功能常常横跨两处。读 packages/opencode/src/session/prompt.ts 的时候,很可能得同时开着 packages/core/src/session/prompt.ts。这不是设计缺陷,是分层的必然账单,但对第一次读的人不友好。
它明确不管的事:packages/codemode/AGENTS.md 直接写了「不要加投机性的通用权限或审批策略」,这个包只管在明确的、有 schema 描述的工具上做受限执行,授权、持久化、外部权限归宿主应用。同样,packages/core/src/tool/AGENTS.md 说注册表不做执行授权。授权这件事在这个仓库里是分散在叶子上的,不存在一个「全局权限中心」文件让你一改改全部。
贡献路径也有边界。 CONTRIBUTING.md 列了容易被合入的改动类型(bug 修复、追加 LSP 与 formatter、提升 LLM 表现、新增 provider 支持、环境相关修复、文档改进),同时写明任何 UI 或核心产品功能必须先经过核心团队的设计评审。它还提到新增 provider 基本不需要改代码,要先去 models.dev 那个仓库提 PR。你花两周写一个大功能再提 PR,很可能不是技术问题挡住你。
这类工具本身的风险不能回避。 opencode 会在你的机器上执行 shell 命令、直接改你的代码文件、把代码内容发给模型服务商。packages/opencode/src/tool/ 里 shell/、write.ts、edit.ts、apply_patch.ts 都是有真实破坏力的入口。文档站的 permissions.mdx 给了三档取值——"allow" 直接跑、"ask" 每次问、"deny" 直接拦:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"*": "ask",
"bash": "allow",
"edit": "deny"
}
}
同一份文档还写了 --auto 会自动批准那些本来要问的请求(显式的 "deny" 规则仍然强制执行)。在自己的实验目录里开 --auto 是省事,在有生产配置、有密钥文件、有未提交改动的仓库里开,等于把误删误改和敏感文件外泄的概率一次性拉满。至于把边界收到多紧才够用,最小权限怎么设计那篇讲得更细。
另外,packages/opencode/src/tool/ 里有 webfetch.ts、websearch.ts、mcp-websearch.ts,src/mcp/ 是完整的 MCP 接入目录。每接一个外部 MCP server,就是多开一个既能读你本地内容又能对外说话的通道,这个面必须自己盘清楚。第三方插件同理:packages/core/src/tool/AGENTS.md 写明应用级工具通过 opencode.tools.register(...) 注册,与内置工具共用同一个 Tool.make 类型和同一个注册表,Location 级注册还会覆盖应用级注册。换句话说,插件塞进来的工具跟 edit、shell 站在同一层,没有另一层沙箱把它们隔开——装之前先看它注册了什么。涉及模型服务商侧的策略,各家规则不同且会调整,以官方最新说明为准。
六、上手与避坑清单
别在仓库根目录跑测试。 根 package.json 的 test 脚本是这样写的:
"test": "echo 'do not run tests from root' && exit 1"
AGENTS.md 里也点了名,这个守卫叫 do-not-run-tests-from-root。为什么会踩:monorepo 里习惯性在根上敲 bun test。怎么避:进包目录跑,比如 packages/opencode、packages/core。
别直接敲 tsc。 AGENTS.md 要求始终从包目录跑 bun typecheck。为什么会踩:仓库配了 tsgo(各包的 typecheck 脚本是 tsgo --noEmit),根上还有 bun turbo typecheck 做编排。怎么避:包内 bun typecheck,全量用根上的 turbo 脚本。
别以为默认分支是 main。 AGENTS.md 第四、五条写了:本仓库默认分支是 dev,本地可能根本没有 main 这个 ref,做 diff 要用 dev 或 origin/dev。为什么会踩:肌肉记忆敲 git diff main。怎么避:先确认远端默认分支再对比。
别手改生成目录。 前面说过的 src/generated 与 src/generated-effect。为什么会踩:报错栈指到生成文件里,顺手就改了。怎么避:改上游的 Protocol 或 Server 定义,再从 packages/client 跑 bun run generate。
别照着 CONTRIBUTING.md 找 TUI。 那份文档里写 TUI 代码在 packages/opencode/src/cli/cmd/tui/,但当前代码里这个目录不存在——TUI 主体在独立的 packages/tui/(app.tsx、keymap.tsx、theme/、component/),packages/opencode/src/cli/tui/ 下只有 layer.ts、validate-session.ts、worker.ts 三个文件。为什么会踩:文档滞后于重构,而这类导读文档没人跑 CI 校验。怎么避:任何文档里给的路径,先 ls 一下再当真。这条规则对整个仓库都成立。
别忽略提交与分支的格式约束。 AGENTS.md 要求分支名最多三个词、用连字符、不要斜杠和 feat/ 这类前缀(示例给的是 session-recovery、fix-scroll-state、regenerate-sdk),提交与 PR 标题用 type(scope): summary,合法的 type 只有 feat、fix、docs、chore、refactor、test。为什么会踩:各家规范不同,习惯带过来就错。怎么避:提交前扫一眼这一节。
别按自己的口味写代码。 AGENTS.md 的 Style Guide 篇幅很长且很具体:尽量避免 try/catch,避免 any,能用 Bun API 就用(比如 Bun.file()),依赖类型推断而非显式标注,优先函数式数组方法而非 for 循环,禁止别名导入和星号导入,优先 const 并用三元或早返回替代重新赋值,避免 else,Drizzle schema 字段用 snake_case 以免重复写列名。为什么会踩:这些不是通用 TypeScript 惯例,是这个仓库的选择。怎么避:动手前读完 Style Guide,另外仓库配了 oxlint 和 husky,有些会被自动挡住,有些不会。
别把周边包当主干读。 console、stats、slack、function、identity、containers 这些跟你改 Agent 行为基本无关。为什么会踩:包名看起来都挺重要。怎么避:先 ls 一眼第一层就够了。packages/identity/ 里躺着的是 mark.svg、mark-light.svg、mark-96x96.png、mark-192x192.png、mark-512x512.png、mark-512x512-light.png 六个图标文件,没有一行代码;packages/console/、packages/stats/、packages/containers/、packages/docs/ 连自己的根 package.json 都没有(前两个是被根 workspaces 用 packages/console/*、packages/stats/* 展开的嵌套组)。一眼就能排除的东西,别花时间读。选开源项目、判断哪些代码值得投入时间,开源项目选型方法那篇提到的几个判据在这里同样适用。
收尾:三份文件的阅读顺序
如果只给你半小时,按这个顺序读:
第一,根目录 AGENTS.md。它同时是依赖方向的宪法、代码风格的强约束、以及测试与类型检查的操作手册。读完你就知道哪些改动会被机制挡回来。
第二,CONTEXT.md 的 Language 一节。二十来个术语的定义,读完再看代码,Context Epoch、Safe Provider-Turn Boundary 这些名字才不会是噪音。
第三,你目标模块下的那份 AGENTS.md。仓库里散落着多份——packages/core/src/tool/、packages/schema/、packages/llm/、packages/codemode/、packages/app/、packages/desktop/、packages/opencode/、packages/opencode/src/session/llm/ 等都各有一份。这些比顶层文档新,而且往往写了「当前的缺口是什么」,比如 packages/core/src/tool/AGENTS.md 结尾那节 Current Gaps 就明说插件启动还没按规范工具重新设计过。
一个自检问题:动手前先问自己「我这次改的是主干、契约、外壳还是周边」。答不上来,说明还没到写代码的时候。
本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 opencode 开源终端编码 Agent 拆解:服务端分离、双主智能体与权限规则 和 opencode 会话全流程:消息组织、单轮步骤与中断重试的落点。