feat(sdk-next): let embedders contribute plugins via opencode.plugin (#34356)
This commit is contained in:
@@ -31,6 +31,7 @@ import { AgentPlugin } from "./agent"
|
|||||||
import { CommandPlugin } from "./command"
|
import { CommandPlugin } from "./command"
|
||||||
import { ModelsDevPlugin } from "./models-dev"
|
import { ModelsDevPlugin } from "./models-dev"
|
||||||
import { ProviderPlugins } from "./provider"
|
import { ProviderPlugins } from "./provider"
|
||||||
|
import { SdkPlugins } from "./sdk"
|
||||||
import { SkillPlugin } from "./skill"
|
import { SkillPlugin } from "./skill"
|
||||||
import { VariantPlugin } from "./variant"
|
import { VariantPlugin } from "./variant"
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ const layer = Layer.effectDiscard(
|
|||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const commands = yield* CommandV2.Service
|
const commands = yield* CommandV2.Service
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const sdkPlugins = yield* SdkPlugins.Service
|
||||||
const integration = yield* Integration.Service
|
const integration = yield* Integration.Service
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
@@ -119,6 +121,8 @@ const layer = Layer.effectDiscard(
|
|||||||
yield* add(ConfigExternalPlugin.Plugin)
|
yield* add(ConfigExternalPlugin.Plugin)
|
||||||
yield* add(ConfigProviderPlugin.Plugin)
|
yield* add(ConfigProviderPlugin.Plugin)
|
||||||
yield* add(VariantPlugin.Plugin)
|
yield* add(VariantPlugin.Plugin)
|
||||||
|
// Embedder-contributed plugins are added last so they layer over config.
|
||||||
|
for (const plugin of sdkPlugins.all()) yield* add(plugin)
|
||||||
}),
|
}),
|
||||||
).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
||||||
}),
|
}),
|
||||||
@@ -151,5 +155,6 @@ export const node = makeLocationNode({
|
|||||||
httpClient,
|
httpClient,
|
||||||
SkillV2.node,
|
SkillV2.node,
|
||||||
Reference.node,
|
Reference.node,
|
||||||
|
SdkPlugins.node,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export * as SdkPlugins from "./sdk"
|
||||||
|
|
||||||
|
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
import { makeGlobalNode } from "../effect/app-node"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
||||||
|
* so `PluginInternal` can add them on every Location boot through the ordinary
|
||||||
|
* `ctx.plugin.add` seam — the same path `ConfigExternalPlugin` uses for plugins
|
||||||
|
* discovered from config. A plugin registered after a Location has booted only
|
||||||
|
* applies to Locations booted afterward, matching config-plugin timing;
|
||||||
|
* embedders register at startup before creating Sessions.
|
||||||
|
*
|
||||||
|
* State lives in this global-node service (like `ApplicationTools`) rather than
|
||||||
|
* module scope, so the list belongs to one embedded instance and is disposed
|
||||||
|
* with it instead of leaking across `OpenCode.create` calls.
|
||||||
|
*/
|
||||||
|
export interface Interface {
|
||||||
|
readonly register: (plugin: Plugin) => Effect.Effect<void>
|
||||||
|
readonly all: () => readonly Plugin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
|
||||||
|
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.sync(() => {
|
||||||
|
const plugins: Plugin[] = []
|
||||||
|
return Service.of({
|
||||||
|
register: (plugin) => Effect.sync(() => void plugins.push(plugin)),
|
||||||
|
all: () => plugins,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { OpenCode } from "@opencode-ai/client/effect"
|
import { OpenCode } from "@opencode-ai/client/effect"
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
|
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
||||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
@@ -9,11 +10,12 @@ export const create = Effect.fn("OpenCode.create")(function* () {
|
|||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const memoMap = yield* Layer.makeMemoMap
|
const memoMap = yield* Layer.makeMemoMap
|
||||||
const context = yield* Layer.buildWithMemoMap(
|
const context = yield* Layer.buildWithMemoMap(
|
||||||
Layer.merge(ApplicationTools.layer, PermissionSaved.defaultLayer),
|
Layer.mergeAll(ApplicationTools.layer, PermissionSaved.defaultLayer, SdkPlugins.layer),
|
||||||
memoMap,
|
memoMap,
|
||||||
scope,
|
scope,
|
||||||
)
|
)
|
||||||
const tools = Context.get(context, ApplicationTools.Service)
|
const tools = Context.get(context, ApplicationTools.Service)
|
||||||
|
const plugins = Context.get(context, SdkPlugins.Service)
|
||||||
const permissions = Context.get(context, PermissionSaved.Service)
|
const permissions = Context.get(context, PermissionSaved.Service)
|
||||||
const web = yield* Effect.acquireRelease(
|
const web = yield* Effect.acquireRelease(
|
||||||
Effect.sync(() =>
|
Effect.sync(() =>
|
||||||
@@ -37,6 +39,12 @@ export const create = Effect.fn("OpenCode.create")(function* () {
|
|||||||
return {
|
return {
|
||||||
...client,
|
...client,
|
||||||
tools: { register: tools.register },
|
tools: { register: tools.register },
|
||||||
|
// The embedded host contributes plugins through the ordinary discovery flow:
|
||||||
|
// each plugin's `effect` runs inside every Location with the real
|
||||||
|
// `PluginContext`, so `ctx.agent.transform` and every other hook behave exactly
|
||||||
|
// as they do for a config-discovered plugin. Define agent profiles here at
|
||||||
|
// startup, then select one per Session with `sessions.create({ agent })`.
|
||||||
|
plugin: plugins.register,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user