Compare commits

...

1 Commits

Author SHA1 Message Date
James Long d7de2de673 refactor(core): bridge plugin runtime in app builder 2026-07-01 22:33:57 +00:00
3 changed files with 47 additions and 40 deletions
+9 -4
View File
@@ -1,16 +1,21 @@
import { buildLocationServiceMap } from "../location-services" import { buildLocationServiceMap } from "../location-services"
import { LocationServiceMap } from "../location-service-map" import { LocationServiceMap } from "../location-service-map"
import { PluginRuntime } from "../plugin/runtime"
import { LayerNode } from "./layer-node" import { LayerNode } from "./layer-node"
import { makeGlobalNode } from "./app-node" import { makeGlobalNode } from "./app-node"
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements: LayerNode.Replacements = []) { export function build<A, E>(root: LayerNode.Node<A, E, any>, 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 // Only build the location service map if it's actually needed
if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(replacements, LocationServiceMap.node)) { if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(allReplacements, LocationServiceMap.node)) {
const locationMap = buildLocationServiceMap(replacements) const locationMap = buildLocationServiceMap(allReplacements)
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] }) 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) return LayerNode.compile(root, allReplacements)
+37 -32
View File
@@ -1,6 +1,6 @@
export * as PluginRuntime from "./runtime" export * as PluginRuntime from "./runtime"
import { Context, Effect, Layer } from "effect" import { Context, Deferred, Effect, Layer } from "effect"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
import { makeGlobalNode } from "../effect/app-node" import { makeGlobalNode } from "../effect/app-node"
import { Job } from "../job" import { Job } from "../job"
@@ -25,51 +25,44 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/PluginRuntime") {} export class Service extends Context.Service<Service, Interface>()("@opencode/PluginRuntime") {}
export interface Cell { export interface Bridge {
runtime?: Interface runtime: Deferred.Deferred<Interface>
} }
export const makeCell = (): Cell => ({}) export const makeBridge = (): Bridge => ({ runtime: Deferred.makeUnsafe<Interface>() })
const unavailable = <A, E, R>() => Effect.die("Plugin runtime is unavailable") as Effect.Effect<A, E, R> const require = <A, E, R>(bridge: Bridge, f: (runtime: Interface) => Effect.Effect<A, E, R>) =>
const require = <A, E, R>(cell: Cell, f: (runtime: Interface) => Effect.Effect<A, E, R>) => Effect.suspend(() => Deferred.await(bridge.runtime).pipe(Effect.flatMap(f)))
Effect.suspend(() => {
const runtime = cell.runtime
if (runtime === undefined) return unavailable<A, E, R>()
return f(runtime)
})
const defaultCell = makeCell() export const layerWithBridge = (bridge: Bridge) =>
export const layerWithCell = (cell: Cell) =>
Layer.succeed( Layer.succeed(
Service, Service,
Service.of({ Service.of({
session: { session: {
get: (sessionID) => require(cell, (runtime) => runtime.session.get(sessionID)), get: (sessionID) => require(bridge, (runtime) => runtime.session.get(sessionID)),
create: (input) => require(cell, (runtime) => runtime.session.create(input)), create: (input) => require(bridge, (runtime) => runtime.session.create(input)),
messages: (input) => require(cell, (runtime) => runtime.session.messages(input)), messages: (input) => require(bridge, (runtime) => runtime.session.messages(input)),
prompt: (input) => require(cell, (runtime) => runtime.session.prompt(input)), prompt: (input) => require(bridge, (runtime) => runtime.session.prompt(input)),
resume: (sessionID) => require(cell, (runtime) => runtime.session.resume(sessionID)), resume: (sessionID) => require(bridge, (runtime) => runtime.session.resume(sessionID)),
interrupt: (sessionID) => require(cell, (runtime) => runtime.session.interrupt(sessionID)), interrupt: (sessionID) => require(bridge, (runtime) => runtime.session.interrupt(sessionID)),
synthetic: (input) => require(cell, (runtime) => runtime.session.synthetic(input)), synthetic: (input) => require(bridge, (runtime) => runtime.session.synthetic(input)),
}, },
job: { job: {
start: (input) => require(cell, (runtime) => runtime.job.start(input)), start: (input) => require(bridge, (runtime) => runtime.job.start(input)),
wait: (input) => require(cell, (runtime) => runtime.job.wait(input)), wait: (input) => require(bridge, (runtime) => runtime.job.wait(input)),
block: (input) => require(cell, (runtime) => runtime.job.block(input)), block: (input) => require(bridge, (runtime) => runtime.job.block(input)),
background: (id) => require(cell, (runtime) => runtime.job.background(id)), background: (id) => require(bridge, (runtime) => runtime.job.background(id)),
cancel: (id) => require(cell, (runtime) => runtime.job.cancel(id)), cancel: (id) => require(bridge, (runtime) => runtime.job.cancel(id)),
}, },
location: { location: {
agent: { 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( Layer.effectDiscard(
Effect.gen(function* () { Effect.gen(function* () {
const sessions = yield* SessionV2.Service 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(() => yield* Effect.addFinalizer(() =>
Effect.sync(() => { Effect.sync(() => {
if (cell.runtime === runtime) cell.runtime = undefined bridge.runtime = Deferred.makeUnsafe<Interface>()
}), }),
) )
}), }),
) )
export const layer = layerWithCell(defaultCell) const unsafeBridge = makeBridge()
export const providerLayer = providerLayerWithCell(defaultCell)
export const layer = layerWithBridge(unsafeBridge)
export const providerLayer = providerLayerWithBridge(unsafeBridge)
export const node = makeGlobalNode({ service: Service, layer, deps: [] }) export const node = makeGlobalNode({ service: Service, layer, deps: [] })
export const nodeWithBridge = (bridge: Bridge) =>
makeGlobalNode({ service: Service, layer: layerWithBridge(bridge), deps: [] })
export const providerNode = makeGlobalNode({ export const providerNode = makeGlobalNode({
name: "plugin-runtime-provider", name: "plugin-runtime-provider",
layer: providerLayer, layer: providerLayer,
deps: [node, SessionV2.node, Job.node, LocationServiceMap.node], 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],
})
+1 -4
View File
@@ -50,20 +50,17 @@ export function createRoutes(password?: string) {
} }
export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store) { 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<AuthError, AuthServices>( function makeRoutes<AuthError, AuthServices>(
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>, auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
sdkPlugins?: SdkPlugins.Store, sdkPlugins?: SdkPlugins.Store,
) { ) {
const pluginRuntimeCell = PluginRuntime.makeCell()
const serviceLayer = AppNodeBuilder.build( const serviceLayer = AppNodeBuilder.build(
applicationServices, applicationServices,
[ [
[SessionExecution.node, SessionExecutionLocal.node], [SessionExecution.node, SessionExecutionLocal.node],
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
[PluginRuntime.providerNode, PluginRuntime.providerLayerWithCell(pluginRuntimeCell)],
...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []), ...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []),
], ],
) )