refactor(plugin): use direct runtime registry
This commit is contained in:
parent
cd97de7391
commit
975b1132f1
@ -36,12 +36,6 @@ export const Plugin = define({
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const loaded: EffectPlugin[] = []
|
||||
|
||||
yield* ctx.plugin.transform((plugins) => {
|
||||
for (const plugin of loaded) plugins.add(plugin)
|
||||
})
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const configured: { package: string; options?: Record<string, any> }[] = []
|
||||
|
||||
@ -86,14 +80,12 @@ export const Plugin = define({
|
||||
const mod = yield* Effect.promise(() => import(entrypoint))
|
||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||
loaded.push({
|
||||
yield* ctx.plugin.add({
|
||||
id: plugin.id,
|
||||
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
|
||||
})
|
||||
}).pipe(Effect.ignoreCause)
|
||||
}
|
||||
|
||||
yield* ctx.plugin.reload()
|
||||
}).pipe(Effect.forkScoped({ startImmediately: true }))
|
||||
}),
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export * as PluginV2 from "./plugin"
|
||||
|
||||
import { Context, Effect, Exit, Layer, Schema, Scope } from "effect"
|
||||
import type { Plugin, PluginDraft } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "./agent"
|
||||
import { AISDK } from "./aisdk"
|
||||
import { Catalog } from "./catalog"
|
||||
@ -27,8 +27,8 @@ export const Event = {
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly transform: State.Transform<PluginDraft>
|
||||
readonly reload: State.Reload
|
||||
readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void>
|
||||
readonly remove: (id: ID) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
|
||||
@ -40,57 +40,49 @@ export const layer = Layer.effect(
|
||||
const locks = KeyedMutex.makeUnsafe<ID>()
|
||||
const scope = yield* Scope.make()
|
||||
const active = new Map<ID, Scope.Closeable>()
|
||||
const loading = new Set<ID>()
|
||||
let host: Parameters<Plugin["effect"]>[0]
|
||||
|
||||
const attach = Effect.fn("Plugin.attach")(function* (plugin: Plugin, host: Parameters<Plugin["effect"]>[0]) {
|
||||
const id = ID.make(plugin.id)
|
||||
yield* locks.withLock(id)(
|
||||
Effect.gen(function* () {
|
||||
const existing = active.get(id)
|
||||
if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore)
|
||||
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: Plugin["effect"]) {
|
||||
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
|
||||
|
||||
const child = yield* Scope.fork(scope)
|
||||
yield* plugin.effect(host).pipe(
|
||||
Scope.provide(child),
|
||||
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }),
|
||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
||||
)
|
||||
active.set(id, child)
|
||||
yield* events.publish(Event.Added, { id })
|
||||
}),
|
||||
yield* locks.withLock(id)(
|
||||
Effect.sync(() => loading.add(id)).pipe(
|
||||
Effect.andThen(
|
||||
State.batch(
|
||||
Effect.gen(function* () {
|
||||
const existing = active.get(id)
|
||||
active.delete(id)
|
||||
if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore)
|
||||
|
||||
const child = yield* Scope.fork(scope)
|
||||
yield* effect(host).pipe(
|
||||
Scope.provide(child),
|
||||
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }),
|
||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
||||
)
|
||||
active.set(id, child)
|
||||
yield* events.publish(Event.Added, { id })
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.ensuring(Effect.sync(() => loading.delete(id))),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const detach = Effect.fn("Plugin.detach")(function* (id: ID) {
|
||||
yield* locks.withLock(id)(
|
||||
Effect.gen(function* () {
|
||||
const current = active.get(id)
|
||||
active.delete(id)
|
||||
if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore)
|
||||
}),
|
||||
)
|
||||
})
|
||||
const remove = Effect.fn("Plugin.remove")(function* (id: ID) {
|
||||
if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`)
|
||||
|
||||
const state = State.create<Map<ID, Plugin>, PluginDraft>({
|
||||
initial: () => new Map(),
|
||||
draft: (draft) => ({
|
||||
list: () => Array.from(draft.values()),
|
||||
add: (plugin) => draft.set(ID.make(plugin.id), plugin),
|
||||
remove: (id) => draft.delete(ID.make(id)),
|
||||
}),
|
||||
finalize: (draft) =>
|
||||
yield* locks.withLock(id)(
|
||||
State.batch(
|
||||
Effect.gen(function* () {
|
||||
const desired = new Set<ID>()
|
||||
for (const plugin of draft.list()) desired.add(ID.make(plugin.id))
|
||||
|
||||
for (const id of active.keys()) {
|
||||
if (!desired.has(id)) yield* detach(id)
|
||||
}
|
||||
|
||||
for (const plugin of draft.list()) yield* attach(plugin, host)
|
||||
}).pipe(Effect.withSpan("Plugin.reconcile")),
|
||||
const current = active.get(id)
|
||||
active.delete(id)
|
||||
if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
yield* Effect.addFinalizer((exit) =>
|
||||
@ -101,8 +93,8 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
const service = Service.of({
|
||||
transform: state.transform,
|
||||
reload: state.reload,
|
||||
add,
|
||||
remove,
|
||||
})
|
||||
host = yield* PluginHost.make(service)
|
||||
return service
|
||||
|
||||
@ -8,7 +8,7 @@ import { Catalog } from "../catalog"
|
||||
import { CommandV2 } from "../command"
|
||||
import { Integration } from "../integration"
|
||||
import { ModelV2 } from "../model"
|
||||
import type { PluginV2 } from "../plugin"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { Reference } from "../reference"
|
||||
import { SkillV2 } from "../skill"
|
||||
@ -126,8 +126,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
),
|
||||
},
|
||||
plugin: {
|
||||
reload: plugin.reload,
|
||||
transform: plugin.transform,
|
||||
add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect),
|
||||
remove: (id) => plugin.remove(PluginV2.ID.make(id)),
|
||||
},
|
||||
reference: {
|
||||
reload: reference.reload,
|
||||
|
||||
@ -23,10 +23,8 @@ import { Npm } from "../npm"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import { Reference } from "../reference"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { State } from "../state"
|
||||
import { AgentPlugin } from "./agent"
|
||||
import { CommandPlugin } from "./command"
|
||||
import { PluginHost } from "./host"
|
||||
import { ModelsDevPlugin } from "./models-dev"
|
||||
import { ProviderPlugins } from "./provider"
|
||||
import { SkillPlugin } from "./skill"
|
||||
@ -73,49 +71,45 @@ export const locationLayer = Layer.effectDiscard(
|
||||
const global = yield* Global.Service
|
||||
const skill = yield* SkillV2.Service
|
||||
const reference = yield* Reference.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
const add = <R>(input: Plugin<R>) => {
|
||||
const loaded = {
|
||||
id: input.id,
|
||||
effect: (context: PluginContext) =>
|
||||
input
|
||||
.effect(context)
|
||||
.pipe(
|
||||
Effect.provideService(Catalog.Service, catalog),
|
||||
Effect.provideService(CommandV2.Service, commands),
|
||||
Effect.provideService(Integration.Service, integration),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(ModelsDev.Service, modelsDev),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(FileSystem.Service, filesystem),
|
||||
Effect.provideService(Global.Service, global),
|
||||
Effect.provideService(SkillV2.Service, skill),
|
||||
Effect.provideService(Reference.Service, reference),
|
||||
),
|
||||
}
|
||||
return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect)
|
||||
}
|
||||
|
||||
const wrap = <R>(input: Plugin<R>) => ({
|
||||
id: input.id,
|
||||
effect: (context: PluginContext) =>
|
||||
input
|
||||
.effect(context)
|
||||
.pipe(
|
||||
Effect.provideService(Catalog.Service, catalog),
|
||||
Effect.provideService(CommandV2.Service, commands),
|
||||
Effect.provideService(Integration.Service, integration),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(ModelsDev.Service, modelsDev),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(FileSystem.Service, filesystem),
|
||||
Effect.provideService(Global.Service, global),
|
||||
Effect.provideService(SkillV2.Service, skill),
|
||||
Effect.provideService(Reference.Service, reference),
|
||||
),
|
||||
})
|
||||
|
||||
yield* State.batch(
|
||||
Effect.gen(function* () {
|
||||
yield* plugin.transform((plugins) => {
|
||||
plugins.add(wrap(AgentPlugin.Plugin))
|
||||
plugins.add(wrap(CommandPlugin.Plugin))
|
||||
plugins.add(wrap(SkillPlugin.Plugin))
|
||||
plugins.add(wrap(ModelsDevPlugin))
|
||||
plugins.add(wrap(ConfigProviderPlugin.Plugin))
|
||||
plugins.add(wrap(ConfigAgentPlugin.Plugin))
|
||||
plugins.add(wrap(ConfigCommandPlugin.Plugin))
|
||||
plugins.add(wrap(ConfigSkillPlugin.Plugin))
|
||||
plugins.add(wrap(ConfigReferencePlugin.Plugin))
|
||||
for (const item of ProviderPlugins) plugins.add(wrap(item))
|
||||
})
|
||||
|
||||
yield* wrap(ConfigExternalPlugin.Plugin).effect(host)
|
||||
}),
|
||||
).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
||||
yield* Effect.gen(function* () {
|
||||
yield* add(AgentPlugin.Plugin)
|
||||
yield* add(CommandPlugin.Plugin)
|
||||
yield* add(SkillPlugin.Plugin)
|
||||
yield* add(ModelsDevPlugin)
|
||||
yield* add(ConfigProviderPlugin.Plugin)
|
||||
yield* add(ConfigAgentPlugin.Plugin)
|
||||
yield* add(ConfigCommandPlugin.Plugin)
|
||||
yield* add(ConfigSkillPlugin.Plugin)
|
||||
yield* add(ConfigReferencePlugin.Plugin)
|
||||
for (const item of ProviderPlugins) yield* add(item)
|
||||
yield* add(ConfigExternalPlugin.Plugin)
|
||||
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
||||
}),
|
||||
).pipe(
|
||||
Layer.provideMerge(PluginV2.locationLayer),
|
||||
|
||||
@ -67,8 +67,11 @@ export function fromPromise(plugin: Plugin) {
|
||||
reload: () => run(host.integration.reload()),
|
||||
},
|
||||
plugin: {
|
||||
transform: transform(host.plugin),
|
||||
reload: () => run(host.plugin.reload()),
|
||||
add: (input) => {
|
||||
const child = fromPromise(input)
|
||||
return run(host.plugin.add(child))
|
||||
},
|
||||
remove: (id) => run(host.plugin.remove(id)),
|
||||
},
|
||||
reference: {
|
||||
transform: transform(host.reference),
|
||||
|
||||
@ -197,22 +197,19 @@ describe("LocationServiceMap", () => {
|
||||
Effect.flatMap((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
yield* plugins.transform((draft) =>
|
||||
draft.add(
|
||||
define({
|
||||
id: "reviewer",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
.transform((agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
})
|
||||
.pipe(Effect.asVoid),
|
||||
}),
|
||||
),
|
||||
)
|
||||
const reviewer = define({
|
||||
id: "reviewer",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
.transform((agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
})
|
||||
.pipe(Effect.asVoid),
|
||||
})
|
||||
yield* plugins.add(PluginV2.ID.make(reviewer.id), reviewer.effect)
|
||||
|
||||
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
||||
description: "Reviews code",
|
||||
|
||||
@ -9,35 +9,34 @@ import { PluginTestLayer } from "./plugin/fixture"
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
describe("PluginV2", () => {
|
||||
it.effect("reconciles transformed plugins", () =>
|
||||
it.effect("adds, replaces, and removes plugins", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
let description = "first"
|
||||
|
||||
const registration = yield* plugins.transform((draft) => {
|
||||
draft.add(
|
||||
define({
|
||||
id: "managed",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
.transform((agents) =>
|
||||
agents.update("configured", (agent) => {
|
||||
agent.description = description
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.asVoid),
|
||||
}),
|
||||
)
|
||||
})
|
||||
const managed = () =>
|
||||
define({
|
||||
id: "managed",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
.transform((agents) =>
|
||||
agents.update("configured", (agent) => {
|
||||
agent.description = description
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.asVoid),
|
||||
})
|
||||
|
||||
yield* plugins.add(PluginV2.ID.make("managed"), managed().effect)
|
||||
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
|
||||
|
||||
description = "second"
|
||||
yield* plugins.reload()
|
||||
yield* plugins.add(PluginV2.ID.make("managed"), managed().effect)
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
|
||||
|
||||
yield* registration.dispose
|
||||
yield* plugins.remove(PluginV2.ID.make("managed"))
|
||||
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
@ -33,8 +33,8 @@ export function host(overrides: Overrides = {}): PluginContext {
|
||||
reload: () => Effect.die("unused integration.reload"),
|
||||
},
|
||||
plugin: overrides.plugin ?? {
|
||||
transform: () => Effect.die("unused plugin.transform"),
|
||||
reload: () => Effect.die("unused plugin.reload"),
|
||||
add: () => Effect.die("unused plugin.add"),
|
||||
remove: () => Effect.die("unused plugin.remove"),
|
||||
},
|
||||
reference: overrides.reference ?? {
|
||||
transform: () => Effect.die("unused reference.transform"),
|
||||
|
||||
@ -4,7 +4,7 @@ import type { AISDKHooks } from "./aisdk.js"
|
||||
import type { CatalogHooks } from "./catalog.js"
|
||||
import type { CommandHooks } from "./command.js"
|
||||
import type { IntegrationHooks } from "./integration.js"
|
||||
import type { PluginHooks } from "./plugin.js"
|
||||
import type { PluginDomain } from "./plugin.js"
|
||||
import type { ReferenceHooks } from "./reference.js"
|
||||
import type { SkillHooks } from "./skill.js"
|
||||
import type { Reload } from "./registration.js"
|
||||
@ -16,7 +16,7 @@ export interface PluginContext {
|
||||
readonly catalog: CatalogHooks & Reload
|
||||
readonly command: CommandHooks & Reload
|
||||
readonly integration: IntegrationHooks & Reload
|
||||
readonly plugin: PluginHooks & Reload
|
||||
readonly plugin: PluginDomain
|
||||
readonly reference: ReferenceHooks & Reload
|
||||
readonly skill: SkillHooks & Reload
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export type { PluginContext } from "./context.js"
|
||||
export { define } from "./plugin.js"
|
||||
export type { Plugin, PluginDraft } from "./plugin.js"
|
||||
export type { Plugin } from "./plugin.js"
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import type { Effect, Scope } from "effect"
|
||||
import type { PluginContext } from "./context.js"
|
||||
import type { PluginOptions } from "../options.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface Plugin {
|
||||
readonly id: string
|
||||
@ -12,17 +10,7 @@ export function define(plugin: Plugin) {
|
||||
return plugin
|
||||
}
|
||||
|
||||
export interface PluginRef {
|
||||
readonly package: string
|
||||
readonly options?: PluginOptions
|
||||
export interface PluginDomain {
|
||||
readonly add: (plugin: Plugin) => Effect.Effect<void>
|
||||
readonly remove: (id: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface PluginDraft {
|
||||
list(): readonly Plugin[]
|
||||
add(plugin: Plugin): void
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
export type PluginHooks = Hooks<{
|
||||
transform: PluginDraft
|
||||
}>
|
||||
|
||||
@ -4,7 +4,7 @@ import type { AISDKHooks } from "./aisdk.js"
|
||||
import type { CatalogHooks } from "./catalog.js"
|
||||
import type { CommandHooks } from "./command.js"
|
||||
import type { IntegrationHooks } from "./integration.js"
|
||||
import type { PluginHooks } from "./plugin.js"
|
||||
import type { PluginDomain } from "./plugin.js"
|
||||
import type { ReferenceHooks } from "./reference.js"
|
||||
import type { SkillHooks } from "./skill.js"
|
||||
import type { Reload } from "./registration.js"
|
||||
@ -16,7 +16,7 @@ export interface PluginContext {
|
||||
readonly catalog: CatalogHooks & Reload
|
||||
readonly command: CommandHooks & Reload
|
||||
readonly integration: IntegrationHooks & Reload
|
||||
readonly plugin: PluginHooks & Reload
|
||||
readonly plugin: PluginDomain
|
||||
readonly reference: ReferenceHooks & Reload
|
||||
readonly skill: SkillHooks & Reload
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export type { PluginContext } from "./context.js"
|
||||
export type { PluginOptions } from "../options.js"
|
||||
export { define } from "./plugin.js"
|
||||
export type { Plugin, PluginDraft, PluginHooks, PluginRef } from "./plugin.js"
|
||||
export type { Plugin, PluginDomain } from "./plugin.js"
|
||||
export type { Registration, Reload } from "./registration.js"
|
||||
export type { AgentDraft, AgentHooks } from "./agent.js"
|
||||
export type { AISDKHooks } from "./aisdk.js"
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import type { PluginContext } from "./context.js"
|
||||
import type { PluginDraft, PluginRef } from "../effect/plugin.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface Plugin {
|
||||
readonly id: string
|
||||
@ -11,8 +9,7 @@ export function define(plugin: Plugin) {
|
||||
return plugin
|
||||
}
|
||||
|
||||
export type { PluginDraft, PluginRef }
|
||||
|
||||
export type PluginHooks = Hooks<{
|
||||
transform: PluginDraft
|
||||
}>
|
||||
export interface PluginDomain {
|
||||
readonly add: (plugin: Plugin) => Promise<void>
|
||||
readonly remove: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user