From d7de2de6730717a93f27d147e09e7b93c83e62df Mon Sep 17 00:00:00 2001 From: James Long <17031+jlongster@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:33:57 +0000 Subject: [PATCH] refactor(core): bridge plugin runtime in app builder --- packages/core/src/effect/app-node-builder.ts | 13 ++-- packages/core/src/plugin/runtime.ts | 69 +++++++++++--------- packages/server/src/routes.ts | 5 +- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/packages/core/src/effect/app-node-builder.ts b/packages/core/src/effect/app-node-builder.ts index 5f01469dab0..d41410dbb14 100644 --- a/packages/core/src/effect/app-node-builder.ts +++ b/packages/core/src/effect/app-node-builder.ts @@ -1,16 +1,21 @@ import { buildLocationServiceMap } from "../location-services" import { LocationServiceMap } from "../location-service-map" +import { PluginRuntime } from "../plugin/runtime" import { LayerNode } from "./layer-node" import { makeGlobalNode } from "./app-node" export function build(root: LayerNode.Node, replacements: LayerNode.Replacements = []) { - let allReplacements = replacements + const bridge = PluginRuntime.makeBridge() + let allReplacements = replacements.concat([ + [PluginRuntime.node, PluginRuntime.nodeWithBridge(bridge)], + [PluginRuntime.providerNode, PluginRuntime.providerNodeWithBridge(bridge)], + ]) // Only build the location service map if it's actually needed - if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(replacements, LocationServiceMap.node)) { - const locationMap = buildLocationServiceMap(replacements) + if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(allReplacements, LocationServiceMap.node)) { + const locationMap = buildLocationServiceMap(allReplacements) const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] }) - allReplacements = replacements.concat([[LocationServiceMap.node, locationMapNode]]) + allReplacements = allReplacements.concat([[LocationServiceMap.node, locationMapNode]]) } return LayerNode.compile(root, allReplacements) diff --git a/packages/core/src/plugin/runtime.ts b/packages/core/src/plugin/runtime.ts index b3e1d93742d..d347a9233bc 100644 --- a/packages/core/src/plugin/runtime.ts +++ b/packages/core/src/plugin/runtime.ts @@ -1,6 +1,6 @@ export * as PluginRuntime from "./runtime" -import { Context, Effect, Layer } from "effect" +import { Context, Deferred, Effect, Layer } from "effect" import { AgentV2 } from "../agent" import { makeGlobalNode } from "../effect/app-node" import { Job } from "../job" @@ -25,51 +25,44 @@ export interface Interface { export class Service extends Context.Service()("@opencode/PluginRuntime") {} -export interface Cell { - runtime?: Interface +export interface Bridge { + runtime: Deferred.Deferred } -export const makeCell = (): Cell => ({}) +export const makeBridge = (): Bridge => ({ runtime: Deferred.makeUnsafe() }) -const unavailable = () => Effect.die("Plugin runtime is unavailable") as Effect.Effect -const require = (cell: Cell, f: (runtime: Interface) => Effect.Effect) => - Effect.suspend(() => { - const runtime = cell.runtime - if (runtime === undefined) return unavailable() - return f(runtime) - }) +const require = (bridge: Bridge, f: (runtime: Interface) => Effect.Effect) => + Effect.suspend(() => Deferred.await(bridge.runtime).pipe(Effect.flatMap(f))) -const defaultCell = makeCell() - -export const layerWithCell = (cell: Cell) => +export const layerWithBridge = (bridge: Bridge) => Layer.succeed( Service, Service.of({ session: { - get: (sessionID) => require(cell, (runtime) => runtime.session.get(sessionID)), - create: (input) => require(cell, (runtime) => runtime.session.create(input)), - messages: (input) => require(cell, (runtime) => runtime.session.messages(input)), - prompt: (input) => require(cell, (runtime) => runtime.session.prompt(input)), - resume: (sessionID) => require(cell, (runtime) => runtime.session.resume(sessionID)), - interrupt: (sessionID) => require(cell, (runtime) => runtime.session.interrupt(sessionID)), - synthetic: (input) => require(cell, (runtime) => runtime.session.synthetic(input)), + get: (sessionID) => require(bridge, (runtime) => runtime.session.get(sessionID)), + create: (input) => require(bridge, (runtime) => runtime.session.create(input)), + messages: (input) => require(bridge, (runtime) => runtime.session.messages(input)), + prompt: (input) => require(bridge, (runtime) => runtime.session.prompt(input)), + resume: (sessionID) => require(bridge, (runtime) => runtime.session.resume(sessionID)), + interrupt: (sessionID) => require(bridge, (runtime) => runtime.session.interrupt(sessionID)), + synthetic: (input) => require(bridge, (runtime) => runtime.session.synthetic(input)), }, job: { - start: (input) => require(cell, (runtime) => runtime.job.start(input)), - wait: (input) => require(cell, (runtime) => runtime.job.wait(input)), - block: (input) => require(cell, (runtime) => runtime.job.block(input)), - background: (id) => require(cell, (runtime) => runtime.job.background(id)), - cancel: (id) => require(cell, (runtime) => runtime.job.cancel(id)), + start: (input) => require(bridge, (runtime) => runtime.job.start(input)), + wait: (input) => require(bridge, (runtime) => runtime.job.wait(input)), + block: (input) => require(bridge, (runtime) => runtime.job.block(input)), + background: (id) => require(bridge, (runtime) => runtime.job.background(id)), + cancel: (id) => require(bridge, (runtime) => runtime.job.cancel(id)), }, location: { agent: { - list: (ref) => require(cell, (runtime) => runtime.location.agent.list(ref)), + list: (ref) => require(bridge, (runtime) => runtime.location.agent.list(ref)), }, }, }), ) -export const providerLayerWithCell = (cell: Cell) => +export const providerLayerWithBridge = (bridge: Bridge) => Layer.effectDiscard( Effect.gen(function* () { const sessions = yield* SessionV2.Service @@ -96,22 +89,34 @@ export const providerLayerWithCell = (cell: Cell) => }, }, } - cell.runtime = runtime + yield* Deferred.succeed(bridge.runtime, runtime) yield* Effect.addFinalizer(() => Effect.sync(() => { - if (cell.runtime === runtime) cell.runtime = undefined + bridge.runtime = Deferred.makeUnsafe() }), ) }), ) -export const layer = layerWithCell(defaultCell) -export const providerLayer = providerLayerWithCell(defaultCell) +const unsafeBridge = makeBridge() + +export const layer = layerWithBridge(unsafeBridge) +export const providerLayer = providerLayerWithBridge(unsafeBridge) export const node = makeGlobalNode({ service: Service, layer, deps: [] }) +export const nodeWithBridge = (bridge: Bridge) => + makeGlobalNode({ service: Service, layer: layerWithBridge(bridge), deps: [] }) + export const providerNode = makeGlobalNode({ name: "plugin-runtime-provider", layer: providerLayer, deps: [node, SessionV2.node, Job.node, LocationServiceMap.node], }) + +export const providerNodeWithBridge = (bridge: Bridge) => + makeGlobalNode({ + name: "plugin-runtime-provider", + layer: providerLayerWithBridge(bridge), + deps: [node, SessionV2.node, Job.node, LocationServiceMap.node], + }) diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index 38ecac06bda..6a11541311c 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -50,20 +50,17 @@ export function createRoutes(password?: string) { } export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store) { - return makeRoutes(ServerAuth.Config.layer({ username: "opencode", password: Option.none() }), sdkPlugins) + return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), sdkPlugins) } function makeRoutes( auth: Layer.Layer, sdkPlugins?: SdkPlugins.Store, ) { - const pluginRuntimeCell = PluginRuntime.makeCell() const serviceLayer = AppNodeBuilder.build( applicationServices, [ [SessionExecution.node, SessionExecutionLocal.node], - [PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)], - [PluginRuntime.providerNode, PluginRuntime.providerLayerWithCell(pluginRuntimeCell)], ...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []), ], )