# 怎么读这份源码

> 读 OpenCode 前要掌握的 Effect 惯例（Service、Layer、Effect.fn、InstanceState）、LayerNode 依赖图、实验开关表、代码风格与测试约定，以及一条推荐的阅读路线。

- 作者：David（道雾轩）
- 专栏：OpenCode 源码解读（https://daiw.net/manual/opencode.md）
- 最后更新：2026-10-01
- 原文：https://daiw.net/manual/opencode/reading-the-source
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

OpenCode 的难点不在算法而在写法：几乎每个模块都是 Effect 的 Service。

## 先看哪些文档

写给编码 Agent 的 `AGENTS.md` 也是现成的读者指南：根 `AGENTS.md`（依赖方向、风格、V2 会话内核的硬约束）、`packages/opencode/AGENTS.md`（模块形状、Effect 用法）、`packages/opencode/test/AGENTS.md`（测试写法）。根目录的 `CONTEXT.md` 是 V2 会话运行时的术语表，`specs/` 下是设计规格。

文档不总跟得上代码，下面几处在 v1.18.30 里已过时：

- `CONTRIBUTING.md` 仍说 TUI 在 `packages/opencode/src/cli/cmd/tui/`，实际已拆到 `packages/tui`；它提到的 `bun dev spawn` 在 `src/index.ts` 里没有对应命令。
- `packages/opencode/AGENTS.md` 让人参考的 `specs/effect/migration.md` 不在这个版本里。
- `src/session/llm/AGENTS.md` 说总开关 `OPENCODE_EXPERIMENTAL=true` 也能启用原生 LLM，但 `runtime-flags.ts` 里的 `experimentalNativeLlm` 只认 `OPENCODE_EXPERIMENTAL_NATIVE_LLM`。

## 模块形状：自我重导出

`packages/opencode/AGENTS.md` 禁止用 `export namespace` 组织模块：

> It is not standard ESM, it prevents tree-shaking, and it breaks Node's native TypeScript runner.

替代方案是“平铺的顶层导出 + 文件末尾一行自我重导出”，以 `packages/opencode/src/agent/agent.ts` 为例（删减后）：

```ts
export interface Interface {
  readonly get: (agent: string) => Effect.Effect<Info>
  readonly list: () => Effect.Effect<Info[]>
  // ...
}

export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}

export const use = serviceUse(Service)

const layer = Layer.effect(
  Service,
  Effect.gen(function* () {
    const config = yield* Config.Service
    // ...
  }),
)

export const node = LayerNode.make({
  service: Service,
  layer: layer,
  deps: [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node, locationServiceMapNode],
})

export * as Agent from "./agent"
```

消费方写 `import { Agent } from "@/agent/agent"`，再用 `Agent.Service`、`Agent.node`。`serviceUse`（`packages/core/src/effect/service-use.ts`）用 Proxy 生成访问器，`Agent.use.get("build")` 直接得到一个依赖 `Agent.Service` 的 Effect。多文件目录（如 `src/session/`）不设 barrel `index.ts`，免得一次 import 把所有兄弟模块都求值。

## Effect.fn 命名

对外方法用 `Effect.fn("Domain.method")`，内部辅助用 `Effect.fnUntraced`。名字会成为追踪 span，也是最好用的**搜索锚点**：搜 `"SessionPrompt.` 能列出 `SessionPrompt.prompt`、`SessionPrompt.loop`、`SessionPrompt.run`（即 `runLoop`）等方法。

## InstanceState：按目录隔离

一个服务端进程可以同时服务多个项目目录，“每个目录一份”的状态放进 `InstanceState`（`packages/opencode/src/effect/instance-state.ts:26`，删减后）：

```ts
export const make = <A, E = never, R = never>(
  init: (ctx: InstanceContext) => Effect.Effect<A, E, R | Scope.Scope>,
): Effect.Effect<InstanceState<A, E, Exclude<R, Scope.Scope>>, never, R | Scope.Scope> =>
  Effect.gen(function* () {
    const cache = yield* ScopedCache.make<string, A, E, R>({
      capacity: Number.POSITIVE_INFINITY,
      lookup: () =>
        Effect.gen(function* () {
          return yield* init(yield* context)
        }),
    })
    // ...
  })
```

`ScopedCache` 以目录为键：`init` 只在某目录第一次被访问时执行，实例释放时连同作用域清理。当前目录来自 `InstanceRef`，服务端按查询参数 `directory` 或请求头 `x-opencode-directory` 定位实例（`packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts:87`）。[OpenCode 是什么](https://daiw.net/manual/opencode/what-is-opencode)里的七个 Agent 就构造在这样的闭包里，所以各项目的配置互不干扰。

## LayerNode：显式的依赖图

每个服务导出 `node = LayerNode.make({ service, layer, deps })`（`packages/core/src/effect/layer-node.ts`），启动时把节点列表编译成一个大 Layer：

- **类型查漏**：`deps` 缺了 layer 需要的服务，参数类型里会冒出名为 `"Missing dependencies"` 的字段，直接编译报错（`layer-node.ts:10`）。
- **去重与查环**：`compile` 带缓存遍历，同一节点只构建一次；有环则抛 `Cycle detected in layer tree: ...`。
- **可替换**：`compile(root, replacements)` 按节点名换实现，V1 的 `AppNodeBuilderV1.build` 就用它把 `InstanceStore.bootstrapNode` 换成 `InstanceBootstrap.node`。

节点还能打标签，`packages/core/src/effect/app-node.ts` 定义了两种：

```ts
export const tags = LayerNode.tags({
  location: ["global"],
  global: [],
})
```

`location` 节点可以依赖 `global` 节点，反之不行；V2 按位置组装服务时用 `LayerNode.hoist` 把 `global` 部分剥出来只建一次（`packages/core/src/location-services.ts:96`），详见 [Effect 运行时与 Location](https://daiw.net/manual/opencode/effect-runtime)。现役的 `packages/opencode/src/effect/app-runtime.ts` 把四十多个节点放进 `LayerNode.group([...])`，编译后交给 `ManagedRuntime.make`，与各处 `makeRuntime` 共享 core 里的进程级 `memoMap`。

## 两套开关：Flag 与 RuntimeFlags

`Flag`（`packages/core/src/flag/flag.ts`）直接读 `process.env`，多数在模块加载时求值；`RuntimeFlags`（`packages/opencode/src/effect/runtime-flags.ts`）是用 Effect `Config` 声明的服务，测试可用 `RuntimeFlags.layer(...)` 覆盖。实验开关大多在后者，关键是这个辅助函数（`runtime-flags.ts:10`）：

```ts
const experimental = bool("OPENCODE_EXPERIMENTAL")
const enabledByExperimental = (name: string) =>
  Config.all({ experimental, enabled: Config.boolean(name).pipe(Config.option) }).pipe(
    Config.map((flags) => Option.getOrElse(flags.enabled, () => flags.experimental)),
  )
```

单独的开关没设置时跟随总开关，设置了以自己为准。据此分三类（省略 `OPENCODE_` 前缀）：

| 类别 | 开关 |
| --- | --- |
| 跟随总开关 | `EXPERIMENTAL_REFERENCES`、`EXPERIMENTAL_BACKGROUND_SUBAGENTS`、`EXPERIMENTAL_LSP_TOOL`、`EXPERIMENTAL_OXFMT`、`EXPERIMENTAL_PLAN_MODE`、`EXPERIMENTAL_CODE_MODE`、`EXPERIMENTAL_EVENT_SYSTEM`、`EXPERIMENTAL_WORKSPACES`、`EXPERIMENTAL_ICON_DISCOVERY` |
| 只认自己 | `EXPERIMENTAL_NATIVE_LLM`、`EXPERIMENTAL_WEBSOCKETS`、`EXPERIMENTAL_LSP_TY`、`ENABLE_QUESTION_TOOL`、`ENABLE_EXPERIMENTAL_MODELS`、`ENABLE_PARALLEL` |
| 数值与禁用 | `EXPERIMENTAL_OUTPUT_TOKEN_MAX`、`EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS`、`PURE`、`DISABLE_*` 系列等 |

`ENABLE_EXA` 例外：总开关、它自己或旧名 `EXPERIMENTAL_EXA` 任一为真即启用。

## 代码风格

根 `AGENTS.md` 的风格很有个性：能放在一个函数里就不拆；少用 `try`/`catch`、`any` 与 `else`；不做不必要的解构；不给 import 起别名；启动敏感的路径对重模块用动态 `import()`——所以 `cli/cmd/tui.ts`、`cli/cmd/serve.ts` 等处理函数里满是 `await import(...)`。提交信息用 `type(scope): summary`。

## 测试约定

- `bun test` 只能在包目录里跑，类型检查用 `bun typecheck`（背后是 `tsgo`）；`test/preload.ts` 先把 XDG 目录指向临时目录。
- Effect 测试用 `testEffect(layer)`（`test/lib/effect.ts`），分 `it.effect`（`TestClock`）、`it.live`（真实时间）、`it.instance`（附带临时实例目录）；模型调用打到 `test/lib/llm-server.ts` 的假服务。
- 尽量不 mock，局部替换用 `Layer.mock`；等并发任务要等就绪信号，不用 `Effect.sleep` 硬等。

## 推荐阅读路线

除注明外，路径相对 `packages/opencode/src`：

| 顺序 | 入口文件 | 对应篇目 |
| --- | --- | --- |
| 1 | `index.ts`、`cli/cmd/tui.ts`、`cli/tui/worker.ts` | [启动链路](https://daiw.net/manual/opencode/startup) |
| 2 | `server/routes/instance/httpapi/server.ts`、`handlers/session.ts` | [HTTP 服务端](https://daiw.net/manual/opencode/server) |
| 3 | `session/prompt.ts`、`session/llm.ts`、`session/processor.ts`、`session/tools.ts` | [主循环 SessionPrompt](https://daiw.net/manual/opencode/prompt-loop) |
| 4 | `tool/registry.ts`、`permission/index.ts` | [工具契约与注册表](https://daiw.net/manual/opencode/tool-contract) |
| 5 | `packages/tui/src/app.tsx`、`context/sdk.tsx`、`context/sync.tsx` | [终端界面](https://daiw.net/manual/opencode/tui) |
| 6 | 根 `CONTEXT.md`、`specs/v2/session.md`、`packages/core/src/session/` | [V2 会话内核](https://daiw.net/manual/opencode/session-v2) |

下一篇：[启动链路](https://daiw.net/manual/opencode/startup)——从敲下 `opencode` 到 TUI 画出第一帧，Worker、RPC 隧道与服务端各自在做什么。
