refactor(opencode): build runtimes from layer nodes (#34515)

This commit is contained in:
James Long
2026-06-29 21:21:28 -04:00
committed by GitHub
parent 60b6229c4e
commit 5b62211167
10 changed files with 97 additions and 82 deletions
+15 -10
View File
@@ -30,6 +30,7 @@ import {
type SetSessionModeResponse, type SetSessionModeResponse,
} from "@agentclientprotocol/sdk" } from "@agentclientprotocol/sdk"
import { InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import type { AssistantMessage, Message, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import type { AssistantMessage, Message, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { Context, Effect, Layer, ManagedRuntime } from "effect" import { Context, Effect, Layer, ManagedRuntime } from "effect"
import * as ACPError from "./error" import * as ACPError from "./error"
@@ -569,22 +570,26 @@ export function make(input: {
} }
function makeSessionService() { function makeSessionService() {
return ManagedRuntime.make(ACPSession.defaultLayer).runSync( return ManagedRuntime.make(AppNodeBuilder.build(ACPSession.node)).runSync(
ACPSession.Service.use((service) => Effect.succeed(service)), ACPSession.Service.use((service) => Effect.succeed(service)),
) )
} }
function makeDirectoryService(sdk: OpencodeClient) { function makeDirectoryService(sdk: OpencodeClient) {
return ManagedRuntime.make( return ManagedRuntime.make(
Directory.layer.pipe( AppNodeBuilder.build(
Layer.provide( Directory.node,
Layer.succeed( [
Directory.Loader, [
Directory.Loader.of({ Directory.loaderNode,
load: (directory) => request(() => loadDirectorySnapshot(sdk, directory), "directory"), Layer.succeed(
}), Directory.Loader,
), Directory.Loader.of({
), load: (directory) => request(() => loadDirectorySnapshot(sdk, directory), "directory"),
}),
),
],
],
), ),
).runSync(Directory.Service.use((service) => Effect.succeed(service))) ).runSync(Directory.Service.use((service) => Effect.succeed(service)))
} }
+2 -1
View File
@@ -7,8 +7,9 @@ export const ScrapCommand = cmd({
builder: (yargs) => yargs, builder: (yargs) => yargs,
async handler() { async handler() {
const { Project } = await import("@/project/project") const { Project } = await import("@/project/project")
const { AppNodeBuilder } = await import("@opencode-ai/core/effect/app-node-builder")
const { makeRuntime } = await import("@opencode-ai/core/effect/runtime") const { makeRuntime } = await import("@opencode-ai/core/effect/runtime")
const runtime = makeRuntime(Project.Service, Project.defaultLayer) const runtime = makeRuntime(Project.Service, AppNodeBuilder.build(Project.node))
const list = await runtime.runPromise((project) => project.list()) const list = await runtime.runPromise((project) => project.list())
process.stdout.write(JSON.stringify(list, null, 2) + EOL) process.stdout.write(JSON.stringify(list, null, 2) + EOL)
}, },
+5 -3
View File
@@ -2,6 +2,8 @@ export * as TuiConfig from "./tui"
import path from "path" import path from "path"
import { mergeDeep, unique } from "remeda" import { mergeDeep, unique } from "remeda"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Context, Effect, Fiber, Layer } from "effect" import { Cause, Context, Effect, Fiber, Layer } from "effect"
import { ConfigParse } from "@/config/parse" import { ConfigParse } from "@/config/parse"
import * as ConfigPaths from "@/config/paths" import * as ConfigPaths from "@/config/paths"
@@ -223,7 +225,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
} }
}) })
export const layer = Layer.effect( const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const directory = yield* CurrentWorkingDirectory const directory = yield* CurrentWorkingDirectory
@@ -257,9 +259,9 @@ export const layer = Layer.effect(
}).pipe(Effect.withSpan("TuiConfig.layer")), }).pipe(Effect.withSpan("TuiConfig.layer")),
) )
export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(FSUtil.defaultLayer)) export const node = LayerNode.make({ service: Service, layer, deps: [Npm.node, FSUtil.node] })
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, AppNodeBuilder.build(node))
export async function waitForDependencies() { export async function waitForDependencies() {
await runPromise((svc) => svc.waitForDependencies()) await runPromise((svc) => svc.waitForDependencies())
+55 -49
View File
@@ -53,57 +53,63 @@ import { BackgroundJob } from "@/background/job"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { SessionProjector } from "@opencode-ai/core/session/projector"
export const AppLayer = Layer.mergeAll( export const AppLayer = AppNodeBuilder.build(
Npm.defaultLayer, LayerNode.group([
FSUtil.defaultLayer, Npm.node,
Database.defaultLayer, FSUtil.node,
Auth.defaultLayer, Database.node,
Account.defaultLayer, Auth.node,
Config.defaultLayer, Account.node,
Git.defaultLayer, Config.node,
Storage.defaultLayer, Git.node,
Snapshot.defaultLayer, Storage.node,
Plugin.defaultLayer, Snapshot.node,
ModelsDev.defaultLayer, Plugin.node,
Provider.defaultLayer, ModelsDev.node,
ProviderAuth.defaultLayer, Provider.node,
Agent.defaultLayer, ProviderAuth.node,
Skill.defaultLayer, Agent.node,
Discovery.defaultLayer, Skill.node,
Question.defaultLayer, Discovery.node,
Permission.defaultLayer, Question.node,
Todo.defaultLayer, Permission.node,
Session.defaultLayer, Todo.node,
SessionStatus.defaultLayer, Session.node,
BackgroundJob.defaultLayer, SessionProjector.node,
RuntimeFlags.defaultLayer, SessionStatus.node,
EventV2Bridge.defaultLayer, BackgroundJob.node,
SessionRunState.defaultLayer, RuntimeFlags.node,
SessionProcessor.defaultLayer, EventV2Bridge.node,
SessionCompaction.defaultLayer, SessionRunState.node,
SessionRevert.defaultLayer, SessionProcessor.node,
SessionSummary.defaultLayer, SessionCompaction.node,
SessionPrompt.defaultLayer, SessionRevert.node,
Instruction.defaultLayer, SessionSummary.node,
LLM.defaultLayer, SessionPrompt.node,
LSP.defaultLayer, Instruction.node,
MCP.defaultLayer, LLM.node,
McpAuth.defaultLayer, LSP.node,
Command.defaultLayer, MCP.node,
Truncate.defaultLayer, McpAuth.node,
ToolRegistry.defaultLayer, Command.node,
Format.defaultLayer, Truncate.node,
Project.defaultLayer, ToolRegistry.node,
Vcs.defaultLayer, Format.node,
Workspace.defaultLayer, InstanceStore.node,
Worktree.appLayer, Project.node,
Installation.defaultLayer, Vcs.node,
ShareNext.defaultLayer, Workspace.node,
SessionShare.defaultLayer, Worktree.node,
Installation.node,
ShareNext.node,
SessionShare.node,
]),
[[InstanceStore.bootstrapNode, InstanceBootstrap.node]],
).pipe( ).pipe(
Layer.provideMerge(Ripgrep.defaultLayer), Layer.provideMerge(AppNodeBuilder.build(Ripgrep.node)),
Layer.provideMerge(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
Layer.provideMerge(Observability.layer), Layer.provideMerge(Observability.layer),
) )
@@ -1,4 +1,6 @@
import { Layer, ManagedRuntime } from "effect" import { Layer, ManagedRuntime } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { LSP } from "@/lsp/lsp" import { LSP } from "@/lsp/lsp"
@@ -10,14 +12,8 @@ import { Config } from "@/config/config"
import * as Observability from "@opencode-ai/core/observability" import * as Observability from "@opencode-ai/core/observability"
import { memoMap } from "@opencode-ai/core/effect/memo-map" import { memoMap } from "@opencode-ai/core/effect/memo-map"
export const BootstrapLayer = Layer.mergeAll( export const BootstrapLayer = AppNodeBuilder.build(
Config.defaultLayer, LayerNode.group([Config.node, Plugin.node, ShareNext.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
Plugin.defaultLayer,
ShareNext.defaultLayer,
Format.defaultLayer,
LSP.defaultLayer,
Vcs.defaultLayer,
Snapshot.defaultLayer,
).pipe(Layer.provide(Observability.layer)) ).pipe(Layer.provide(Observability.layer))
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap }) export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })
+5 -6
View File
@@ -1,8 +1,9 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform" import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Effect, Layer, Schema, Context, Stream } from "effect" import { Effect, Layer, Schema, Context, Stream } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { errorMessage } from "@/util/error" import { errorMessage } from "@/util/error"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
@@ -82,7 +83,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/In
export const use = serviceUse(Service) export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect( const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const http = yield* HttpClient.HttpClient const http = yield* HttpClient.HttpClient
@@ -324,14 +325,12 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProce
}), }),
) )
export const defaultLayer = layer.pipe(Layer.provide(FetchHttpClient.layer), Layer.provide(AppProcess.defaultLayer)) export const node = LayerNode.make({ service: Service, layer: layer, deps: [httpClient, AppProcess.node] })
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, AppNodeBuilder.build(node))
export const latest = (...args: Parameters<Interface["latest"]>) => runPromise((s) => s.latest(...args)) export const latest = (...args: Parameters<Interface["latest"]>) => runPromise((s) => s.latest(...args))
export const method = () => runPromise((s) => s.method()) export const method = () => runPromise((s) => s.method())
export const upgrade = (...args: Parameters<Interface["upgrade"]>) => runPromise((s) => s.upgrade(...args)) export const upgrade = (...args: Parameters<Interface["upgrade"]>) => runPromise((s) => s.upgrade(...args))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [httpClient, AppProcess.node] })
export * as Installation from "." export * as Installation from "."
+2 -1
View File
@@ -14,6 +14,7 @@ import {
import path from "path" import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config/tui"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { errorData, errorMessage } from "@opencode-ai/tui/util/error" import { errorData, errorMessage } from "@opencode-ai/tui/util/error"
import { isRecord } from "@opencode-ai/tui/util/record" import { isRecord } from "@opencode-ai/tui/util/record"
import { resolveHostAttentionSoundPaths } from "@/config/tui-host-attention" import { resolveHostAttentionSoundPaths } from "@/config/tui-host-attention"
@@ -1082,7 +1083,7 @@ async function load(input: {
const flags = await Effect.runPromise( const flags = await Effect.runPromise(
Effect.gen(function* () { Effect.gen(function* () {
return yield* RuntimeFlags.Service return yield* RuntimeFlags.Service
}).pipe(Effect.provide(RuntimeFlags.defaultLayer)), }).pipe(Effect.provide(AppNodeBuilder.build(RuntimeFlags.node))),
) )
const pluginOrigins = config.plugin_origins ?? (await TuiConfig.pluginOrigins()) const pluginOrigins = config.plugin_origins ?? (await TuiConfig.pluginOrigins())
const records = Flag.OPENCODE_PURE ? [] : pluginOrigins const records = Flag.OPENCODE_PURE ? [] : pluginOrigins
@@ -1,4 +1,5 @@
import { Context, Effect, Layer, Option } from "effect" import { Context, Effect, Layer, Option } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import * as Socket from "effect/unstable/socket/Socket" import * as Socket from "effect/unstable/socket/Socket"
export const SERVER_CLOSING_EVENT = () => new Socket.CloseEvent(1001, "server closing") export const SERVER_CLOSING_EVENT = () => new Socket.CloseEvent(1001, "server closing")
@@ -13,7 +14,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/HttpApiWebSocketTracker") {} export class Service extends Context.Service<Service, Interface>()("@opencode/HttpApiWebSocketTracker") {}
export const layer = Layer.sync(Service)(() => { const layer = Layer.sync(Service)(() => {
const sockets = new Set<Close>() const sockets = new Set<Close>()
let closing = false let closing = false
return Service.of({ return Service.of({
@@ -44,6 +45,8 @@ export const layer = Layer.sync(Service)(() => {
}) })
}) })
export const node = LayerNode.make({ service: Service, layer, deps: [] })
export const register = (close: Close) => export const register = (close: Close) =>
Effect.gen(function* () { Effect.gen(function* () {
const tracker = yield* Effect.serviceOption(Service) const tracker = yield* Effect.serviceOption(Service)
+2 -1
View File
@@ -1,6 +1,7 @@
import "./init-projectors" import "./init-projectors"
import { NodeHttpServer } from "@effect/platform-node" import { NodeHttpServer } from "@effect/platform-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect" import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect"
import { HttpRouter, HttpServer } from "effect/unstable/http" import { HttpRouter, HttpServer } from "effect/unstable/http"
import { OpenApi } from "effect/unstable/httpapi" import { OpenApi } from "effect/unstable/httpapi"
@@ -102,7 +103,7 @@ function listenerLayer(opts: ListenOptions, port: number) {
disableLogger: true, disableLogger: true,
disableListenLog: true, disableListenLog: true,
}).pipe( }).pipe(
Layer.provideMerge(WebSocketTracker.layer), Layer.provideMerge(AppNodeBuilder.build(WebSocketTracker.node)),
Layer.provideMerge(serverLayer({ port, hostname: opts.hostname })), Layer.provideMerge(serverLayer({ port, hostname: opts.hostname })),
// Install a fresh `ConfigProvider` per listener so `Config.string(...)` // Install a fresh `ConfigProvider` per listener so `Config.string(...)`
// reads reflect the current `process.env`. Effect's default // reads reflect the current `process.env`. Effect's default
+3 -2
View File
@@ -1,6 +1,7 @@
import { expect } from "bun:test" import { expect } from "bun:test"
import path from "path" import path from "path"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -70,12 +71,12 @@ const withPlatform = <A, E, R>(platform: typeof process.platform, self: Effect.E
const getTuiConfig = (directory: string) => const getTuiConfig = (directory: string) =>
TuiConfig.Service.use((svc) => svc.get()).pipe( TuiConfig.Service.use((svc) => svc.get()).pipe(
Effect.provide(TuiConfig.defaultLayer.pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))), Effect.provide(AppNodeBuilder.build(TuiConfig.node).pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))),
) )
const getTuiPluginOrigins = (directory: string) => const getTuiPluginOrigins = (directory: string) =>
TuiConfig.Service.use((svc) => svc.pluginOrigins()).pipe( TuiConfig.Service.use((svc) => svc.pluginOrigins()).pipe(
Effect.provide(TuiConfig.defaultLayer.pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))), Effect.provide(AppNodeBuilder.build(TuiConfig.node).pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))),
) )
it.instance("keeps server and tui plugin merge semantics aligned", () => it.instance("keeps server and tui plugin merge semantics aligned", () =>