fix(plugin): reload late SDK plugins (#35576)

This commit is contained in:
Kit Langton 2026-07-06 13:36:19 -04:00 committed by GitHub
commit 2830176972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1187 additions and 922 deletions

View file

@ -18,6 +18,7 @@ import { ProviderV2 } from "../provider"
import { Reference } from "../reference"
import { AbsolutePath, type DeepMutable } from "../schema"
import { SkillV2 } from "../skill"
import { Tool } from "../tool/tool"
import { Tools } from "../tool/tools"
import { ToolHooks } from "../tool/hooks"
import { WorkspaceV2 } from "../workspace"
@ -298,7 +299,26 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}),
},
tool: {
register: (input, options) => tools.register(input, options),
transform: (callback) =>
Effect.gen(function* () {
const registrations: Array<{
readonly name: string
readonly tool: Tool.AnyTool
readonly options?: Tool.RegisterOptions
}> = []
yield* Effect.sync(() =>
callback({
add: (name, tool, options) => {
registrations.push({ name, tool, ...(options ? { options } : {}) })
},
}),
)
yield* Effect.forEach(
registrations,
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
{ discard: true },
)
}),
execute: {
before: (callback) =>
toolHooks.hook.before((event) => {

View file

@ -3,6 +3,9 @@ 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"
import { EventV2 } from "../event"
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
export interface Store {
readonly plugins: Map<string, Plugin>
@ -16,9 +19,8 @@ const defaultStore = makeStore()
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
* so `PluginSupervisor` can add them on every Location boot through the ordinary
* generation path that `PluginSupervisor` 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.
* config. Registration publishes an unlocated update so every booted Location
* reloads its plugin generation from the shared store.
*
* The store is shared explicitly between the SDK construction graph and the
* embedded route graph because `LocationServiceMap` builds Location layers lazily
@ -36,6 +38,7 @@ export const layerWithStore = (store: Store) =>
Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
store.plugins.clear()
@ -45,7 +48,7 @@ export const layerWithStore = (store: Store) =>
register: (plugin) =>
Effect.sync(() => {
store.plugins.set(plugin.id, plugin)
}),
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
all: () => [...store.plugins.values()],
})
}),
@ -53,4 +56,4 @@ export const layerWithStore = (store: Store) =>
export const layer = layerWithStore(defaultStore)
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })

View file

@ -276,7 +276,7 @@ const layer = Layer.effect(
}),
),
)
yield* events.subscribe(Event.Updated).pipe(
yield* events.subscribe([Event.Updated, SdkPlugins.Updated]).pipe(
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload plugins", { cause }))),
),