Преглед изворни кода

feat(sdk-next): let embedders contribute plugins via opencode.plugin (#34356)

Kit Langton пре 1 месец
родитељ
комит
53b93b6991

+ 5 - 0
packages/core/src/plugin/internal.ts

@@ -31,6 +31,7 @@ import { AgentPlugin } from "./agent"
 import { CommandPlugin } from "./command"
 import { ModelsDevPlugin } from "./models-dev"
 import { ProviderPlugins } from "./provider"
+import { SdkPlugins } from "./sdk"
 import { SkillPlugin } from "./skill"
 import { VariantPlugin } from "./variant"
 
@@ -65,6 +66,7 @@ const layer = Layer.effectDiscard(
     const catalog = yield* Catalog.Service
     const commands = yield* CommandV2.Service
     const plugin = yield* PluginV2.Service
+    const sdkPlugins = yield* SdkPlugins.Service
     const integration = yield* Integration.Service
     const agents = yield* AgentV2.Service
     const config = yield* Config.Service
@@ -119,6 +121,8 @@ const layer = Layer.effectDiscard(
         yield* add(ConfigExternalPlugin.Plugin)
         yield* add(ConfigProviderPlugin.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 }))
   }),
@@ -151,5 +155,6 @@ export const node = makeLocationNode({
     httpClient,
     SkillV2.node,
     Reference.node,
+    SdkPlugins.node,
   ],
 })

+ 37 - 0
packages/core/src/plugin/sdk.ts

@@ -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: [] })

+ 9 - 1
packages/sdk-next/src/opencode.ts

@@ -1,5 +1,6 @@
 import { OpenCode } from "@opencode-ai/client/effect"
 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 { createEmbeddedRoutes } from "@opencode-ai/server/routes"
 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 memoMap = yield* Layer.makeMemoMap
   const context = yield* Layer.buildWithMemoMap(
-    Layer.merge(ApplicationTools.layer, PermissionSaved.defaultLayer),
+    Layer.mergeAll(ApplicationTools.layer, PermissionSaved.defaultLayer, SdkPlugins.layer),
     memoMap,
     scope,
   )
   const tools = Context.get(context, ApplicationTools.Service)
+  const plugins = Context.get(context, SdkPlugins.Service)
   const permissions = Context.get(context, PermissionSaved.Service)
   const web = yield* Effect.acquireRelease(
     Effect.sync(() =>
@@ -37,6 +39,12 @@ export const create = Effect.fn("OpenCode.create")(function* () {
   return {
     ...client,
     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,
   }
 })