浏览代码

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

James Long 1 月之前
父节点
当前提交
5b62211167

+ 15 - 10
packages/opencode/src/acp/service.ts

@@ -30,6 +30,7 @@ import {
   type SetSessionModeResponse,
 } from "@agentclientprotocol/sdk"
 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 { Context, Effect, Layer, ManagedRuntime } from "effect"
 import * as ACPError from "./error"
@@ -569,22 +570,26 @@ export function make(input: {
 }
 
 function makeSessionService() {
-  return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
+  return ManagedRuntime.make(AppNodeBuilder.build(ACPSession.node)).runSync(
     ACPSession.Service.use((service) => Effect.succeed(service)),
   )
 }
 
 function makeDirectoryService(sdk: OpencodeClient) {
   return ManagedRuntime.make(
-    Directory.layer.pipe(
-      Layer.provide(
-        Layer.succeed(
-          Directory.Loader,
-          Directory.Loader.of({
-            load: (directory) => request(() => loadDirectorySnapshot(sdk, directory), "directory"),
-          }),
-        ),
-      ),
+    AppNodeBuilder.build(
+      Directory.node,
+      [
+        [
+          Directory.loaderNode,
+          Layer.succeed(
+            Directory.Loader,
+            Directory.Loader.of({
+              load: (directory) => request(() => loadDirectorySnapshot(sdk, directory), "directory"),
+            }),
+          ),
+        ],
+      ],
     ),
   ).runSync(Directory.Service.use((service) => Effect.succeed(service)))
 }

+ 2 - 1
packages/opencode/src/cli/cmd/debug/scrap.ts

@@ -7,8 +7,9 @@ export const ScrapCommand = cmd({
   builder: (yargs) => yargs,
   async handler() {
     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 runtime = makeRuntime(Project.Service, Project.defaultLayer)
+    const runtime = makeRuntime(Project.Service, AppNodeBuilder.build(Project.node))
     const list = await runtime.runPromise((project) => project.list())
     process.stdout.write(JSON.stringify(list, null, 2) + EOL)
   },

+ 5 - 3
packages/opencode/src/config/tui.ts

@@ -2,6 +2,8 @@ export * as TuiConfig from "./tui"
 
 import path from "path"
 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 { ConfigParse } from "@/config/parse"
 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,
   Effect.gen(function* () {
     const directory = yield* CurrentWorkingDirectory
@@ -257,9 +259,9 @@ export const layer = Layer.effect(
   }).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() {
   await runPromise((svc) => svc.waitForDependencies())

+ 55 - 49
packages/opencode/src/effect/app-runtime.ts

@@ -53,57 +53,63 @@ import { BackgroundJob } from "@/background/job"
 import { RuntimeFlags } from "@/effect/runtime-flags"
 import { EventV2Bridge } from "@/event-v2-bridge"
 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(
-  Npm.defaultLayer,
-  FSUtil.defaultLayer,
-  Database.defaultLayer,
-  Auth.defaultLayer,
-  Account.defaultLayer,
-  Config.defaultLayer,
-  Git.defaultLayer,
-  Storage.defaultLayer,
-  Snapshot.defaultLayer,
-  Plugin.defaultLayer,
-  ModelsDev.defaultLayer,
-  Provider.defaultLayer,
-  ProviderAuth.defaultLayer,
-  Agent.defaultLayer,
-  Skill.defaultLayer,
-  Discovery.defaultLayer,
-  Question.defaultLayer,
-  Permission.defaultLayer,
-  Todo.defaultLayer,
-  Session.defaultLayer,
-  SessionStatus.defaultLayer,
-  BackgroundJob.defaultLayer,
-  RuntimeFlags.defaultLayer,
-  EventV2Bridge.defaultLayer,
-  SessionRunState.defaultLayer,
-  SessionProcessor.defaultLayer,
-  SessionCompaction.defaultLayer,
-  SessionRevert.defaultLayer,
-  SessionSummary.defaultLayer,
-  SessionPrompt.defaultLayer,
-  Instruction.defaultLayer,
-  LLM.defaultLayer,
-  LSP.defaultLayer,
-  MCP.defaultLayer,
-  McpAuth.defaultLayer,
-  Command.defaultLayer,
-  Truncate.defaultLayer,
-  ToolRegistry.defaultLayer,
-  Format.defaultLayer,
-  Project.defaultLayer,
-  Vcs.defaultLayer,
-  Workspace.defaultLayer,
-  Worktree.appLayer,
-  Installation.defaultLayer,
-  ShareNext.defaultLayer,
-  SessionShare.defaultLayer,
+export const AppLayer = AppNodeBuilder.build(
+  LayerNode.group([
+    Npm.node,
+    FSUtil.node,
+    Database.node,
+    Auth.node,
+    Account.node,
+    Config.node,
+    Git.node,
+    Storage.node,
+    Snapshot.node,
+    Plugin.node,
+    ModelsDev.node,
+    Provider.node,
+    ProviderAuth.node,
+    Agent.node,
+    Skill.node,
+    Discovery.node,
+    Question.node,
+    Permission.node,
+    Todo.node,
+    Session.node,
+    SessionProjector.node,
+    SessionStatus.node,
+    BackgroundJob.node,
+    RuntimeFlags.node,
+    EventV2Bridge.node,
+    SessionRunState.node,
+    SessionProcessor.node,
+    SessionCompaction.node,
+    SessionRevert.node,
+    SessionSummary.node,
+    SessionPrompt.node,
+    Instruction.node,
+    LLM.node,
+    LSP.node,
+    MCP.node,
+    McpAuth.node,
+    Command.node,
+    Truncate.node,
+    ToolRegistry.node,
+    Format.node,
+    InstanceStore.node,
+    Project.node,
+    Vcs.node,
+    Workspace.node,
+    Worktree.node,
+    Installation.node,
+    ShareNext.node,
+    SessionShare.node,
+  ]),
+  [[InstanceStore.bootstrapNode, InstanceBootstrap.node]],
 ).pipe(
-  Layer.provideMerge(Ripgrep.defaultLayer),
-  Layer.provideMerge(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
+  Layer.provideMerge(AppNodeBuilder.build(Ripgrep.node)),
   Layer.provideMerge(Observability.layer),
 )
 

+ 4 - 8
packages/opencode/src/effect/bootstrap-runtime.ts

@@ -1,4 +1,6 @@
 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 { LSP } from "@/lsp/lsp"
@@ -10,14 +12,8 @@ import { Config } from "@/config/config"
 import * as Observability from "@opencode-ai/core/observability"
 import { memoMap } from "@opencode-ai/core/effect/memo-map"
 
-export const BootstrapLayer = Layer.mergeAll(
-  Config.defaultLayer,
-  Plugin.defaultLayer,
-  ShareNext.defaultLayer,
-  Format.defaultLayer,
-  LSP.defaultLayer,
-  Vcs.defaultLayer,
-  Snapshot.defaultLayer,
+export const BootstrapLayer = AppNodeBuilder.build(
+  LayerNode.group([Config.node, Plugin.node, ShareNext.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
 ).pipe(Layer.provide(Observability.layer))
 
 export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })

+ 5 - 6
packages/opencode/src/installation/index.ts

@@ -1,8 +1,9 @@
 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 { Effect, Layer, Schema, Context, Stream } from "effect"
 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 { errorMessage } from "@/util/error"
 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 layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect(
+const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect(
   Service,
   Effect.gen(function* () {
     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 method = () => runPromise((s) => s.method())
 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 "."

+ 2 - 1
packages/opencode/src/plugin/tui/runtime.ts

@@ -14,6 +14,7 @@ import {
 import path from "path"
 import { fileURLToPath } from "url"
 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 { isRecord } from "@opencode-ai/tui/util/record"
 import { resolveHostAttentionSoundPaths } from "@/config/tui-host-attention"
@@ -1082,7 +1083,7 @@ async function load(input: {
     const flags = await Effect.runPromise(
       Effect.gen(function* () {
         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 records = Flag.OPENCODE_PURE ? [] : pluginOrigins

+ 4 - 1
packages/opencode/src/server/routes/instance/httpapi/websocket-tracker.ts

@@ -1,4 +1,5 @@
 import { Context, Effect, Layer, Option } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import * as Socket from "effect/unstable/socket/Socket"
 
 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 const layer = Layer.sync(Service)(() => {
+const layer = Layer.sync(Service)(() => {
   const sockets = new Set<Close>()
   let closing = false
   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) =>
   Effect.gen(function* () {
     const tracker = yield* Effect.serviceOption(Service)

+ 2 - 1
packages/opencode/src/server/server.ts

@@ -1,6 +1,7 @@
 import "./init-projectors"
 
 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 { HttpRouter, HttpServer } from "effect/unstable/http"
 import { OpenApi } from "effect/unstable/httpapi"
@@ -102,7 +103,7 @@ function listenerLayer(opts: ListenOptions, port: number) {
     disableLogger: true,
     disableListenLog: true,
   }).pipe(
-    Layer.provideMerge(WebSocketTracker.layer),
+    Layer.provideMerge(AppNodeBuilder.build(WebSocketTracker.node)),
     Layer.provideMerge(serverLayer({ port, hostname: opts.hostname })),
     // Install a fresh `ConfigProvider` per listener so `Config.string(...)`
     // reads reflect the current `process.env`. Effect's default

+ 3 - 2
packages/opencode/test/config/tui.test.ts

@@ -1,6 +1,7 @@
 import { expect } from "bun:test"
 import path from "path"
 import { pathToFileURL } from "url"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 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) =>
   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) =>
   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", () =>