export * as PluginV2 from "./plugin" import { createDraft, finishDraft, type Draft } from "immer" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Context, Effect, Exit, Layer, Schema, Scope } from "effect" import type { ModelV2 } from "./model" import type { Catalog } from "./catalog" import { EventV2 } from "./event" import { KeyedMutex } from "./effect/keyed-mutex" import { State } from "./state" export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export type ID = typeof ID.Type export const Event = { Added: EventV2.define({ type: "plugin.added", schema: { id: ID, }, }), } type HookSpec = { "catalog.transform": { input: Catalog.Draft output: {} } "aisdk.language": { input: { model: ModelV2.Info sdk: any options: Record } output: { language?: LanguageModelV3 } } "aisdk.sdk": { input: { model: ModelV2.Info package: string options: Record } output: { sdk?: any } } } export type Hooks = { [Name in keyof HookSpec]: Readonly & { -readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object ? Draft : HookSpec[Name]["output"][Field] } } export type HookFunctions = { [key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect } export type HookInput = HookSpec[Name]["input"] export type HookOutput = HookSpec[Name]["output"] export interface Interface { readonly add: (input: { id: string effect: Effect.Effect }) => Effect.Effect readonly remove: (id: ID) => Effect.Effect readonly hook: ( name: Name, callback: (input: Hooks[Name]) => Effect.Effect | void, ) => Effect.Effect readonly triggerFor: ( id: ID, name: Name, input: HookInput, output: HookOutput, ) => Effect.Effect & HookOutput> readonly trigger: ( name: Name, input: HookInput, output: HookOutput, ) => Effect.Effect & HookOutput> } export class Service extends Context.Service()("@opencode/v2/Plugin") {} export const layer = Layer.effect( Service, Effect.gen(function* () { let hooks: { id: ID hooks: HookFunctions scope: Scope.Closeable }[] = [] let registrations: { [Name in keyof Hooks]: { name: Name callback: (input: Hooks[Name]) => Effect.Effect | void } }[keyof Hooks][] = [] const events = yield* EventV2.Service const scope = yield* Scope.Scope const locks = KeyedMutex.makeUnsafe() const svc = Service.of({ add: Effect.fn("Plugin.add")(function* (input) { const id = ID.make(input.id) yield* locks.withLock(id)( Effect.gen(function* () { const existing = hooks.find((item) => item.id === id) if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) const childScope = yield* Scope.fork(scope) const result = yield* input.effect.pipe( Scope.provide(childScope), Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id, }, }), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)), ) const next = { id, hooks: result ?? {}, scope: childScope, } hooks = existing ? hooks.with(hooks.indexOf(existing), next) : [...hooks, next] yield* events.publish(Event.Added, { id }) }), ) }), trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) { return yield* svc.triggerFor(ID.make("*"), name, input, output) }), triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) { const draftEntries = new Map>() const event = { ...input, ...output, } as Record for (const [field, value] of Object.entries(output)) { if (value && typeof value === "object") { draftEntries.set(field, createDraft(value)) event[field] = draftEntries.get(field) } } for (const item of hooks) { if (id !== ID.make("*") && item.id !== id) continue const match = item.hooks[name] if (!match) continue yield* match(event as any).pipe( Effect.withSpan(`Plugin.hook.${name}`, { attributes: { plugin: item.id, hook: name, }, }), ) } for (const item of registrations) { if (item.name !== name) continue const result = item.callback(event as never) if (Effect.isEffect(result)) yield* result } for (const [field, draft] of draftEntries) { event[field] = finishDraft(draft) } return event as any }), remove: Effect.fn("Plugin.remove")(function* (id) { yield* locks.withLock(id)( Effect.gen(function* () { const existing = hooks.find((item) => item.id === id) hooks = hooks.filter((item) => item.id !== id) if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) }), ) }), hook: Effect.fn("Plugin.hook")(function* (name, callback) { const scope = yield* Scope.Scope const registration = { name, callback } as (typeof registrations)[number] let active = true registrations = [...registrations, registration] const dispose = Effect.sync(() => { if (!active) return active = false registrations = registrations.filter((item) => item !== registration) }) yield* Scope.addFinalizer(scope, dispose) return { dispose } }), }) return svc }), ) export const locationLayer = layer // opencode // sdcok