feat(plugin): add namespaced hook API (#33416)
This commit is contained in:
parent
dc468bdcfd
commit
909a1a6d78
150 changed files with 3276 additions and 3906 deletions
|
|
@ -108,7 +108,7 @@ export const layer = Layer.effect(
|
|||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
get: Effect.fn("AgentV2.get")(function* (id) {
|
||||
return state.get().agents.get(id)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,27 @@
|
|||
export * as AISDK from "./aisdk"
|
||||
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Cause, Context, Effect, Layer, Schema } from "effect"
|
||||
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
|
||||
import { ModelV2 } from "./model"
|
||||
import { EventV2 } from "./event"
|
||||
import { PluginV2 } from "./plugin"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { State } from "./state"
|
||||
|
||||
type SDK = any
|
||||
|
||||
export interface SDKEvent {
|
||||
readonly model: ModelV2.Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: SDK
|
||||
}
|
||||
|
||||
export interface LanguageEvent {
|
||||
readonly model: ModelV2.Info
|
||||
readonly sdk: SDK
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}
|
||||
|
||||
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
||||
if (typeof ms !== "number" || ms <= 0) return res
|
||||
if (!res.body) return res
|
||||
|
|
@ -117,19 +130,70 @@ function initError(providerID: ProviderV2.ID) {
|
|||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly hook: {
|
||||
readonly sdk: (
|
||||
callback: (event: SDKEvent) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
readonly language: (
|
||||
callback: (event: LanguageEvent) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
}
|
||||
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
|
||||
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
|
||||
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
export const locationLayer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
|
||||
let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
|
||||
const languages = new Map<string, LanguageModelV3>()
|
||||
const sdks = new Map<string, SDK>()
|
||||
|
||||
return Service.of({
|
||||
const register = <Event>(
|
||||
hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
|
||||
update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void,
|
||||
) =>
|
||||
Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) {
|
||||
const scope = yield* Scope.Scope
|
||||
let active = true
|
||||
update([...hooks(), callback])
|
||||
const dispose = Effect.sync(() => {
|
||||
if (!active) return
|
||||
active = false
|
||||
update(hooks().filter((item) => item !== callback))
|
||||
})
|
||||
yield* Scope.addFinalizer(scope, dispose)
|
||||
return { dispose }
|
||||
})
|
||||
|
||||
const run = Effect.fnUntraced(function* <Event>(
|
||||
hooks: readonly ((event: Event) => Effect.Effect<void> | void)[],
|
||||
event: Event,
|
||||
) {
|
||||
for (const hook of hooks) {
|
||||
const result = hook(event)
|
||||
if (Effect.isEffect(result)) yield* result
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
const service = Service.of({
|
||||
hook: {
|
||||
sdk: register(
|
||||
() => sdkHooks,
|
||||
(next) => (sdkHooks = next),
|
||||
),
|
||||
language: register(
|
||||
() => languageHooks,
|
||||
(next) => (languageHooks = next),
|
||||
),
|
||||
},
|
||||
runSDK: (event) => run(sdkHooks, event),
|
||||
runLanguage: (event) => run(languageHooks, event),
|
||||
language: Effect.fn("AISDK.language")(function* (model) {
|
||||
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
|
||||
const existing = languages.get(key)
|
||||
|
|
@ -148,26 +212,14 @@ export const layer = Layer.effect(
|
|||
})
|
||||
const sdk =
|
||||
sdks.get(sdkKey) ??
|
||||
(yield* plugin
|
||||
.trigger("aisdk.sdk", { model, package: model.api.package, options }, {})
|
||||
.pipe(initError(model.providerID))).sdk
|
||||
(yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
|
||||
if (!sdk)
|
||||
return yield* new InitError({
|
||||
providerID: model.providerID,
|
||||
cause: new Error("No AISDK provider plugin returned an SDK"),
|
||||
})
|
||||
sdks.set(sdkKey, sdk)
|
||||
const result = yield* plugin
|
||||
.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model,
|
||||
sdk,
|
||||
options,
|
||||
},
|
||||
{},
|
||||
)
|
||||
.pipe(initError(model.providerID))
|
||||
const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
|
||||
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
|
||||
initError(model.providerID),
|
||||
)
|
||||
|
|
@ -175,7 +227,8 @@ export const layer = Layer.effect(
|
|||
return language
|
||||
}),
|
||||
})
|
||||
return service
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))
|
||||
export const defaultLayer = locationLayer
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export const layer = Layer.effect(
|
|||
})
|
||||
const result: Interface = {
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
|
||||
provider: {
|
||||
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export const layer = Layer.effect(
|
|||
})
|
||||
|
||||
return Service.of({
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
transform: state.transform,
|
||||
get: Effect.fn("CommandV2.get")(function* (name) {
|
||||
return state.get().commands.get(name)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export * as ConfigAgentPlugin from "./agent"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import path from "path"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { AgentV2 } from "../../agent"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export * as ConfigCommandPlugin from "./command"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import path from "path"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { CommandV2 } from "../../command"
|
||||
|
|
|
|||
99
packages/core/src/config/plugin/external.ts
Normal file
99
packages/core/src/config/plugin/external.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
export * as ConfigExternalPlugin from "./external"
|
||||
|
||||
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Effect, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { fileURLToPath, pathToFileURL } from "url"
|
||||
import { Config } from "../../config"
|
||||
import { FSUtil } from "../../fs-util"
|
||||
import { Location } from "../../location"
|
||||
import { Npm } from "../../npm"
|
||||
import { define } from "../../plugin/internal"
|
||||
import { PluginPromise } from "../../plugin/promise"
|
||||
|
||||
const PluginModule = Schema.Struct({
|
||||
default: Schema.Union([
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
effect: Schema.declare<EffectPlugin["effect"]>(
|
||||
(input): input is EffectPlugin["effect"] => typeof input === "function",
|
||||
),
|
||||
}),
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
setup: Schema.declare<PromisePlugin["setup"]>(
|
||||
(input): input is PromisePlugin["setup"] => typeof input === "function",
|
||||
),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
export const Plugin = define({
|
||||
id: "config-plugin",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
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> }[] = []
|
||||
|
||||
for (const entry of yield* config.entries()) {
|
||||
if (entry.type === "document") {
|
||||
const directory = entry.path ? path.dirname(entry.path) : location.directory
|
||||
for (const item of entry.info.plugins ?? []) {
|
||||
const ref = typeof item === "string" ? { package: item } : item
|
||||
const packageName = (() => {
|
||||
if (ref.package.startsWith("file://")) return fileURLToPath(ref.package)
|
||||
if (ref.package.startsWith("./") || ref.package.startsWith("../")) {
|
||||
return path.resolve(directory, ref.package)
|
||||
}
|
||||
return ref.package
|
||||
})()
|
||||
configured.push({ package: packageName, options: ref.options })
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.type === "directory") {
|
||||
const files = yield* fs
|
||||
.glob("{plugin,plugins}/*.{ts,js}", {
|
||||
cwd: entry.path,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})
|
||||
.pipe(Effect.orElseSucceed(() => []))
|
||||
files.sort()
|
||||
for (const file of files) configured.push({ package: file })
|
||||
}
|
||||
}
|
||||
|
||||
for (const ref of configured) {
|
||||
yield* Effect.gen(function* () {
|
||||
const entrypoint = path.isAbsolute(ref.package)
|
||||
? pathToFileURL(ref.package).href
|
||||
: (yield* npm.add(ref.package)).entrypoint
|
||||
if (!entrypoint) return
|
||||
|
||||
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({
|
||||
id: plugin.id,
|
||||
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
|
||||
})
|
||||
}).pipe(Effect.ignoreCause)
|
||||
}
|
||||
|
||||
yield* ctx.plugin.reload()
|
||||
}).pipe(Effect.forkScoped({ startImmediately: true }))
|
||||
}),
|
||||
})
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
export * as ConfigProviderPlugin from "./provider"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ModelV2 } from "../../model"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
export * as ConfigReferencePlugin from "./reference"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ConfigReference } from "../reference"
|
||||
import { Reference } from "../../reference"
|
||||
import { AbsolutePath } from "../../schema"
|
||||
import { Global } from "../../global"
|
||||
import { Location } from "../../location"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "core/config-reference",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const global = yield* Global.Service
|
||||
yield* ctx.reference.transform(
|
||||
Effect.fn(function* (draft) {
|
||||
const entries = new Map<string, Reference.Source>()
|
||||
for (const doc of (yield* config.entries()).filter(
|
||||
(entry): entry is Config.Document => entry.type === "document",
|
||||
)) {
|
||||
const directory = doc.path ? path.dirname(doc.path) : ctx.location.directory
|
||||
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
||||
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
||||
if (!validAlias(name)) continue
|
||||
entries.set(
|
||||
|
|
@ -27,7 +31,7 @@ export const Plugin = define({
|
|||
? new Reference.LocalSource({
|
||||
type: "local",
|
||||
path: AbsolutePath.make(
|
||||
localPath(directory, ctx.path.home, typeof entry === "string" ? entry : entry.path),
|
||||
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
|
||||
),
|
||||
description: typeof entry === "string" ? undefined : entry.description,
|
||||
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
export * as ConfigSkillPlugin from "./skill"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { AbsolutePath } from "../../schema"
|
||||
import { SkillV2 } from "../../skill"
|
||||
import { Global } from "../../global"
|
||||
import { Location } from "../../location"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "config-skill",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const global = yield* Global.Service
|
||||
const location = yield* Location.Service
|
||||
yield* ctx.skill.transform(
|
||||
Effect.fn(function* (draft) {
|
||||
const entries = yield* config.entries()
|
||||
|
|
@ -29,13 +33,11 @@ export const Plugin = define({
|
|||
draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
|
||||
continue
|
||||
}
|
||||
const expanded = item.startsWith("~/") ? path.join(ctx.path.home, item.slice(2)) : item
|
||||
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
|
||||
draft.source(
|
||||
new SkillV2.DirectorySource({
|
||||
type: "directory",
|
||||
path: AbsolutePath.make(
|
||||
path.isAbsolute(expanded) ? expanded : path.join(ctx.location.directory, expanded),
|
||||
),
|
||||
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ export const locationLayer = Layer.effect(
|
|||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
get: Effect.fn("Integration.get")(function* (id) {
|
||||
const entry = state.get().integrations.get(id)
|
||||
if (!entry) return undefined
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
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 type { Plugin, PluginDraft } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "./agent"
|
||||
import { AISDK } from "./aisdk"
|
||||
import { Catalog } from "./catalog"
|
||||
import { CommandV2 } from "./command"
|
||||
import { EventV2 } from "./event"
|
||||
import { Integration } from "./integration"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||
import { PluginHost } from "./plugin/host"
|
||||
import { Reference } from "./reference"
|
||||
import { SkillV2 } from "./skill"
|
||||
import { State } from "./state"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
||||
|
|
@ -21,69 +26,9 @@ export const Event = {
|
|||
}),
|
||||
}
|
||||
|
||||
type HookSpec = {
|
||||
"catalog.transform": {
|
||||
input: Catalog.Draft
|
||||
output: {}
|
||||
}
|
||||
"aisdk.language": {
|
||||
input: {
|
||||
model: ModelV2.Info
|
||||
sdk: any
|
||||
options: Record<string, any>
|
||||
}
|
||||
output: {
|
||||
language?: LanguageModelV3
|
||||
}
|
||||
}
|
||||
"aisdk.sdk": {
|
||||
input: {
|
||||
model: ModelV2.Info
|
||||
package: string
|
||||
options: Record<string, any>
|
||||
}
|
||||
output: {
|
||||
sdk?: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Hooks = {
|
||||
[Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
|
||||
-readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
|
||||
? Draft<HookSpec[Name]["output"][Field]>
|
||||
: HookSpec[Name]["output"][Field]
|
||||
}
|
||||
}
|
||||
|
||||
export type HookFunctions = {
|
||||
[key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
|
||||
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
|
||||
|
||||
export interface Interface {
|
||||
readonly add: (input: {
|
||||
id: string
|
||||
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
|
||||
}) => Effect.Effect<void, never, never>
|
||||
readonly remove: (id: ID) => Effect.Effect<void>
|
||||
readonly hook: <Name extends keyof Hooks>(
|
||||
name: Name,
|
||||
callback: (input: Hooks[Name]) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
readonly triggerFor: <Name extends keyof Hooks>(
|
||||
id: ID,
|
||||
name: Name,
|
||||
input: HookInput<Name>,
|
||||
output: HookOutput<Name>,
|
||||
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
|
||||
readonly trigger: <Name extends keyof Hooks>(
|
||||
name: Name,
|
||||
input: HookInput<Name>,
|
||||
output: HookOutput<Name>,
|
||||
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
|
||||
readonly transform: State.Transform<PluginDraft>
|
||||
readonly reload: State.Reload
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
|
||||
|
|
@ -91,127 +36,85 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||
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> | void
|
||||
}
|
||||
}[keyof Hooks][] = []
|
||||
const events = yield* EventV2.Service
|
||||
const locks = KeyedMutex.makeUnsafe<ID>()
|
||||
const scope = yield* Scope.make()
|
||||
const active = new Map<ID, Scope.Closeable>()
|
||||
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 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 })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
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 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) =>
|
||||
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")),
|
||||
),
|
||||
})
|
||||
|
||||
// One registry-owned scope lets shutdown remove every plugin transform in one batch.
|
||||
yield* Effect.addFinalizer((exit) =>
|
||||
Effect.gen(function* () {
|
||||
hooks = []
|
||||
active.clear()
|
||||
yield* State.batch(Scope.close(scope, exit))
|
||||
}),
|
||||
)
|
||||
|
||||
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* State.batch(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<string, ReturnType<typeof createDraft>>()
|
||||
const event = {
|
||||
...input,
|
||||
...output,
|
||||
} as Record<string, unknown>
|
||||
|
||||
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* State.batch(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 }
|
||||
}),
|
||||
const service = Service.of({
|
||||
transform: state.transform,
|
||||
reload: state.reload,
|
||||
})
|
||||
return svc
|
||||
host = yield* PluginHost.make(service)
|
||||
return service
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
// opencode
|
||||
// sdcok
|
||||
export const locationLayer = layer.pipe(
|
||||
Layer.provideMerge(AgentV2.locationLayer),
|
||||
Layer.provideMerge(AISDK.locationLayer),
|
||||
Layer.provideMerge(Catalog.locationLayer),
|
||||
Layer.provideMerge(CommandV2.locationLayer),
|
||||
Layer.provideMerge(Integration.locationLayer),
|
||||
Layer.provideMerge(Reference.locationLayer),
|
||||
Layer.provideMerge(SkillV2.locationLayer),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
export * as AgentPlugin from "./agent"
|
||||
|
||||
import path from "path"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "./internal"
|
||||
import { Effect } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Global } from "../global"
|
||||
import { Location } from "../location"
|
||||
import { PermissionV2 } from "../permission"
|
||||
|
||||
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
|
||||
|
|
@ -99,7 +100,8 @@ Rules:
|
|||
export const Plugin = define({
|
||||
id: "agent",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const worktree = ctx.location.directory
|
||||
const location = yield* Location.Service
|
||||
const worktree = location.directory
|
||||
const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
|
||||
const readonlyExternalDirectory: PermissionV2.Ruleset = [
|
||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
export * as PluginBoot from "./boot"
|
||||
|
||||
import type { Plugin as PublicPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Context, Deferred, Effect, Layer } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Integration } from "../integration"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { AISDK } from "../aisdk"
|
||||
import { Catalog } from "../catalog"
|
||||
import { CommandV2 } from "../command"
|
||||
import { Config } from "../config"
|
||||
|
|
@ -11,6 +11,7 @@ import { ConfigAgentPlugin } from "../config/plugin/agent"
|
|||
import { ConfigCommandPlugin } from "../config/plugin/command"
|
||||
import { ConfigSkillPlugin } from "../config/plugin/skill"
|
||||
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
||||
import { ConfigExternalPlugin } from "../config/plugin/external"
|
||||
import { EventV2 } from "../event"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { FileSystem } from "../filesystem"
|
||||
|
|
@ -29,18 +30,9 @@ import { SkillV2 } from "../skill"
|
|||
import { Reference } from "../reference"
|
||||
import { State } from "../state"
|
||||
import { PluginHost } from "./host"
|
||||
import { PluginInternal } from "./internal"
|
||||
|
||||
type InternalPlugin = PublicPlugin<any>
|
||||
|
||||
export interface Interface {
|
||||
readonly add: (plugin: PublicPlugin<any>) => Effect.Effect<void>
|
||||
readonly wait: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginBoot") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
export const locationLayer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const commands = yield* CommandV2.Service
|
||||
|
|
@ -57,75 +49,47 @@ export const layer = Layer.effect(
|
|||
const global = yield* Global.Service
|
||||
const skill = yield* SkillV2.Service
|
||||
const reference = yield* Reference.Service
|
||||
const host = yield* PluginHost.make()
|
||||
const done = yield* Deferred.make<void>()
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
|
||||
const add = Effect.fn("PluginBoot.add")(function* (input: InternalPlugin) {
|
||||
yield* plugin.add({
|
||||
id: input.id,
|
||||
effect: input
|
||||
.effect(host)
|
||||
.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),
|
||||
),
|
||||
})
|
||||
})
|
||||
const add = <R>(input: PluginInternal.Plugin<R>) =>
|
||||
input
|
||||
.effect({ ...host, options: {} })
|
||||
.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),
|
||||
)
|
||||
|
||||
const boot = Effect.gen(function* () {
|
||||
yield* State.batch(
|
||||
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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}).pipe(Effect.withSpan("PluginBoot.boot"))
|
||||
|
||||
yield* boot.pipe(
|
||||
Effect.exit,
|
||||
Effect.flatMap((exit) => Deferred.done(done, exit)),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
return Service.of({
|
||||
add: (input) =>
|
||||
Deferred.await(done).pipe(
|
||||
Effect.andThen(
|
||||
plugin.add({
|
||||
id: input.id,
|
||||
effect: input.effect(host),
|
||||
}),
|
||||
),
|
||||
),
|
||||
wait: () => Deferred.await(done),
|
||||
})
|
||||
yield* State.batch(
|
||||
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("PluginBoot.boot"))
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer.pipe(
|
||||
).pipe(
|
||||
Layer.provideMerge(PluginV2.locationLayer),
|
||||
Layer.provideMerge(AISDK.locationLayer),
|
||||
Layer.provideMerge(Integration.locationLayer),
|
||||
Layer.provideMerge(Catalog.locationLayer),
|
||||
Layer.provideMerge(CommandV2.locationLayer),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
export * as CommandPlugin from "./command"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "./internal"
|
||||
import { Effect } from "effect"
|
||||
import { Location } from "../location"
|
||||
import PROMPT_INITIALIZE from "./command/initialize.txt"
|
||||
import PROMPT_REVIEW from "./command/review.txt"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "command",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const location = yield* Location.Service
|
||||
yield* ctx.command.transform((draft) => {
|
||||
draft.update("init", (command) => {
|
||||
command.template = PROMPT_INITIALIZE.replace("${path}", ctx.location.project.directory)
|
||||
command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory)
|
||||
command.description = "guided AGENTS.md setup"
|
||||
})
|
||||
draft.update("review", (command) => {
|
||||
command.template = PROMPT_REVIEW.replace("${path}", ctx.location.project.directory)
|
||||
command.template = PROMPT_REVIEW.replace("${path}", location.project.directory)
|
||||
command.description = "review changes [commit|branch|pr], defaults to uncommitted"
|
||||
command.subtask = true
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,58 +1,31 @@
|
|||
export * as PluginHost from "./host"
|
||||
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import type { PluginHost as Interface } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Event as SDKEvent, ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { AISDK } from "../aisdk"
|
||||
import { Catalog } from "../catalog"
|
||||
import { CommandV2 } from "../command"
|
||||
import { EventV2 } from "../event"
|
||||
import { FileSystem } from "../filesystem"
|
||||
import { Global } from "../global"
|
||||
import { Integration } from "../integration"
|
||||
import { Location } from "../location"
|
||||
import { ModelV2 } from "../model"
|
||||
import { Npm } from "../npm"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import type { PluginV2 } from "../plugin"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { Reference } from "../reference"
|
||||
import { SkillV2 } from "../skill"
|
||||
|
||||
type EventMap = { [Item in SDKEvent as Item["type"]]: Item }
|
||||
type SDKHook = (event: {
|
||||
readonly model: ModelV2Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: any
|
||||
}) => Effect.Effect<void> | void
|
||||
type LanguageHook = (event: {
|
||||
readonly model: ModelV2Info
|
||||
readonly sdk: any
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}) => Effect.Effect<void> | void
|
||||
|
||||
export const make = Effect.fn("PluginHost.make")(function* () {
|
||||
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
||||
const agents = yield* AgentV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
const commands = yield* CommandV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const filesystem = yield* FileSystem.Service
|
||||
const global = yield* Global.Service
|
||||
const integration = yield* Integration.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const reference = yield* Reference.Service
|
||||
const skill = yield* SkillV2.Service
|
||||
|
||||
return {
|
||||
options: {},
|
||||
agent: {
|
||||
get: (id) => agents.get(AgentV2.ID.make(id)),
|
||||
default: agents.default,
|
||||
list: agents.all,
|
||||
rebuild: agents.rebuild,
|
||||
reload: agents.reload,
|
||||
transform: (callback) =>
|
||||
agents.transform((draft) =>
|
||||
callback({
|
||||
|
|
@ -65,51 +38,35 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
|||
),
|
||||
},
|
||||
aisdk: {
|
||||
hook: (name, callback) => {
|
||||
if (name === "sdk") {
|
||||
const run = callback as SDKHook
|
||||
return plugin.hook("aisdk.sdk", (event) => {
|
||||
const output = {
|
||||
model: event.model,
|
||||
package: event.package,
|
||||
options: event.options,
|
||||
sdk: event.sdk,
|
||||
}
|
||||
const result = run(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||
)
|
||||
})
|
||||
}
|
||||
const run = callback as LanguageHook
|
||||
return plugin.hook("aisdk.language", (event) => {
|
||||
sdk: (callback) =>
|
||||
aisdk.hook.sdk((event) => {
|
||||
const output = {
|
||||
model: event.model,
|
||||
package: event.package,
|
||||
options: event.options,
|
||||
sdk: event.sdk,
|
||||
}
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||
)
|
||||
}),
|
||||
language: (callback) =>
|
||||
aisdk.hook.language((event) => {
|
||||
const output = {
|
||||
model: event.model,
|
||||
sdk: event.sdk,
|
||||
options: event.options,
|
||||
language: event.language,
|
||||
}
|
||||
const result = run(output)
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
||||
)
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
catalog: {
|
||||
provider: {
|
||||
get: (id) => catalog.provider.get(ProviderV2.ID.make(id)),
|
||||
list: catalog.provider.all,
|
||||
available: catalog.provider.available,
|
||||
},
|
||||
model: {
|
||||
get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||
list: catalog.model.all,
|
||||
available: catalog.model.available,
|
||||
default: catalog.model.default,
|
||||
small: (providerID) => catalog.model.small(ProviderV2.ID.make(providerID)),
|
||||
},
|
||||
rebuild: catalog.rebuild,
|
||||
reload: catalog.reload,
|
||||
transform: (callback) =>
|
||||
catalog.transform((draft) =>
|
||||
callback({
|
||||
|
|
@ -135,41 +92,11 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
|||
),
|
||||
},
|
||||
command: {
|
||||
get: commands.get,
|
||||
list: commands.list,
|
||||
rebuild: commands.rebuild,
|
||||
reload: commands.reload,
|
||||
transform: commands.transform,
|
||||
},
|
||||
event: {
|
||||
subscribe: <Type extends keyof EventMap>(type: Type): Stream.Stream<EventMap[Type]> =>
|
||||
Stream.unwrap(
|
||||
Effect.sync(() => {
|
||||
const definition = EventV2.registry.get(type)
|
||||
if (!definition) throw new Error(`Unknown event type: ${type}`)
|
||||
const encode = Schema.encodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)
|
||||
return events.subscribe(definition).pipe(
|
||||
Stream.map(
|
||||
(event) =>
|
||||
({
|
||||
id: event.id,
|
||||
type: event.type,
|
||||
properties: encode(event.data),
|
||||
}) as unknown as EventMap[Type],
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
},
|
||||
filesystem: {
|
||||
read: (input) => filesystem.read(Schema.decodeUnknownSync(FileSystem.ReadInput)(input)),
|
||||
list: (input) => filesystem.list(Schema.decodeUnknownSync(FileSystem.ListInput)(input ?? {})),
|
||||
find: (input) => filesystem.find(Schema.decodeUnknownSync(FileSystem.FindInput)(input)),
|
||||
glob: (input) => filesystem.glob(Schema.decodeUnknownSync(FileSystem.GlobInput)(input)),
|
||||
},
|
||||
integration: {
|
||||
get: (id) => integration.get(Integration.ID.make(id)),
|
||||
list: integration.list,
|
||||
rebuild: integration.rebuild,
|
||||
reload: integration.reload,
|
||||
transform: (callback) =>
|
||||
integration.transform((draft) =>
|
||||
callback({
|
||||
|
|
@ -198,19 +125,12 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
|||
}),
|
||||
),
|
||||
},
|
||||
location,
|
||||
npm,
|
||||
path: {
|
||||
home: global.home,
|
||||
data: global.data,
|
||||
cache: global.cache,
|
||||
config: global.config,
|
||||
state: global.state,
|
||||
temp: global.tmp,
|
||||
plugin: {
|
||||
reload: plugin.reload,
|
||||
transform: plugin.transform,
|
||||
},
|
||||
reference: {
|
||||
list: reference.list,
|
||||
rebuild: reference.rebuild,
|
||||
reload: reference.reload,
|
||||
transform: (callback) =>
|
||||
reference.transform((draft) =>
|
||||
callback({
|
||||
|
|
@ -221,9 +141,7 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
|||
),
|
||||
},
|
||||
skill: {
|
||||
sources: skill.sources,
|
||||
list: skill.list,
|
||||
rebuild: skill.rebuild,
|
||||
reload: skill.reload,
|
||||
transform: (callback) =>
|
||||
skill.transform((draft) =>
|
||||
callback({
|
||||
|
|
|
|||
43
packages/core/src/plugin/internal.ts
Normal file
43
packages/core/src/plugin/internal.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
export * as PluginInternal from "./internal"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Effect, Scope } from "effect"
|
||||
import type { AgentV2 } from "../agent"
|
||||
import type { Catalog } from "../catalog"
|
||||
import type { CommandV2 } from "../command"
|
||||
import type { Config } from "../config"
|
||||
import type { EventV2 } from "../event"
|
||||
import type { FileSystem } from "../filesystem"
|
||||
import type { FSUtil } from "../fs-util"
|
||||
import type { Global } from "../global"
|
||||
import type { Integration } from "../integration"
|
||||
import type { Location } from "../location"
|
||||
import type { ModelsDev } from "../models-dev"
|
||||
import type { Npm } from "../npm"
|
||||
import type { Reference } from "../reference"
|
||||
import type { SkillV2 } from "../skill"
|
||||
|
||||
export type Requirements =
|
||||
| AgentV2.Service
|
||||
| Catalog.Service
|
||||
| CommandV2.Service
|
||||
| Config.Service
|
||||
| EventV2.Service
|
||||
| FileSystem.Service
|
||||
| FSUtil.Service
|
||||
| Global.Service
|
||||
| Integration.Service
|
||||
| Location.Service
|
||||
| ModelsDev.Service
|
||||
| Npm.Service
|
||||
| Reference.Service
|
||||
| SkillV2.Service
|
||||
|
||||
export interface Plugin<R = never> {
|
||||
readonly id: string
|
||||
readonly effect: (context: PluginContext) => Effect.Effect<void, never, R | Scope.Scope>
|
||||
}
|
||||
|
||||
export function define<R>(plugin: Plugin<R>) {
|
||||
return plugin
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "./internal"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { EventV2 } from "../event"
|
||||
import { ModelV2 } from "../model"
|
||||
import { ModelRequest } from "../model-request"
|
||||
import { ModelsDev } from "../models-dev"
|
||||
|
|
@ -52,6 +53,7 @@ export const ModelsDevPlugin = define({
|
|||
id: "models-dev",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* ctx.integration.transform(
|
||||
Effect.fn(function* (integrations) {
|
||||
const data = yield* modelsDev.get()
|
||||
|
|
@ -128,8 +130,8 @@ export const ModelsDevPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.event.subscribe("models-dev.refreshed").pipe(
|
||||
Stream.runForEach(() => ctx.integration.rebuild().pipe(Effect.andThen(ctx.catalog.rebuild()))),
|
||||
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||
Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
86
packages/core/src/plugin/promise.ts
Normal file
86
packages/core/src/plugin/promise.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
export * as PluginPromise from "./promise"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin, PluginContext, Registration } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Effect, Scope } from "effect"
|
||||
|
||||
// The Effect host hands back this registration shape; mirror it structurally so
|
||||
// we do not have to alias the Effect package's `Registration` against the Promise one.
|
||||
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
||||
|
||||
/**
|
||||
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
||||
* loader (`PluginV2` / `PluginBoot`) can run it unchanged.
|
||||
*
|
||||
* Hook registrations created during the async `setup` attach to the plugin's
|
||||
* scope, so unloading the plugin disposes them. The captured fiber context
|
||||
* preserves boot-time batching, so Promise-plugin transforms still coalesce
|
||||
* into one reload per domain.
|
||||
*/
|
||||
export function fromPromise(plugin: Plugin) {
|
||||
return define({
|
||||
id: plugin.id,
|
||||
effect: (host) =>
|
||||
Effect.gen(function* () {
|
||||
const scope = yield* Scope.Scope
|
||||
const context = yield* Effect.context<Scope.Scope>()
|
||||
|
||||
// Run a hook registration on the plugin scope and resolve once it is registered.
|
||||
const register = (effect: Effect.Effect<HostRegistration, never, Scope.Scope>): Promise<Registration> =>
|
||||
Effect.runPromiseWith(context)(Scope.provide(scope)(effect)).then((registration) => ({
|
||||
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
|
||||
}))
|
||||
|
||||
const run = (effect: Effect.Effect<void>) => Effect.runPromiseWith(context)(effect)
|
||||
|
||||
const transform =
|
||||
<Draft>(domain: {
|
||||
transform: (
|
||||
callback: (draft: Draft) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<HostRegistration, never, Scope.Scope>
|
||||
}) =>
|
||||
(callback: (draft: Draft) => Promise<void> | void) =>
|
||||
register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft)))))
|
||||
|
||||
const context2: PluginContext = {
|
||||
options: host.options,
|
||||
agent: {
|
||||
transform: transform(host.agent),
|
||||
reload: () => run(host.agent.reload()),
|
||||
},
|
||||
aisdk: {
|
||||
sdk: (callback) =>
|
||||
register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
language: (callback) =>
|
||||
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
},
|
||||
catalog: {
|
||||
transform: transform(host.catalog),
|
||||
reload: () => run(host.catalog.reload()),
|
||||
},
|
||||
command: {
|
||||
transform: transform(host.command),
|
||||
reload: () => run(host.command.reload()),
|
||||
},
|
||||
integration: {
|
||||
transform: transform(host.integration),
|
||||
reload: () => run(host.integration.reload()),
|
||||
},
|
||||
plugin: {
|
||||
transform: transform(host.plugin),
|
||||
reload: () => run(host.plugin.reload()),
|
||||
},
|
||||
reference: {
|
||||
transform: transform(host.reference),
|
||||
reload: () => run(host.reference.reload()),
|
||||
},
|
||||
skill: {
|
||||
transform: transform(host.skill),
|
||||
reload: () => run(host.skill.reload()),
|
||||
},
|
||||
}
|
||||
|
||||
yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
@ -30,8 +30,10 @@ import { VercelPlugin } from "./provider/vercel"
|
|||
import { VenicePlugin } from "./provider/venice"
|
||||
import { XAIPlugin } from "./provider/xai"
|
||||
import { ZenmuxPlugin } from "./provider/zenmux"
|
||||
import type { PluginInternal } from "./internal"
|
||||
import type { Scope } from "effect"
|
||||
|
||||
export const ProviderPlugins = [
|
||||
export const ProviderPlugins: PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>[] = [
|
||||
AlibabaPlugin,
|
||||
AmazonBedrockPlugin,
|
||||
AnthropicPlugin,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const AlibabaPlugin = define({
|
||||
id: "alibaba",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/alibaba") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
type MantleSDK = {
|
||||
|
|
@ -78,8 +78,7 @@ export const AmazonBedrockPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
||||
const options = { ...evt.options }
|
||||
|
|
@ -112,8 +111,7 @@ export const AmazonBedrockPlugin = define({
|
|||
evt.sdk = mod.createAmazonBedrock(options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const AnthropicPlugin = define({
|
||||
id: "anthropic",
|
||||
|
|
@ -16,8 +16,7 @@ export const AnthropicPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
||||
|
|
@ -28,8 +28,7 @@ export const AzurePlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/azure") return
|
||||
if (evt.model.providerID === ProviderV2.ID.azure) {
|
||||
|
|
@ -47,8 +46,7 @@ export const AzurePlugin = define({
|
|||
evt.sdk = mod.createAzure(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
||||
|
|
@ -74,8 +72,7 @@ export const AzureCognitiveServicesPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const CerebrasPlugin = define({
|
||||
id: "cerebras",
|
||||
|
|
@ -15,8 +15,7 @@ export const CerebrasPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cerebras") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const CloudflareAIGatewayPlugin = define({
|
||||
id: "cloudflare-ai-gateway",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "ai-gateway-provider") return
|
||||
if (evt.options.baseURL) return
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
||||
|
|
@ -21,8 +21,7 @@ export const CloudflareWorkersAIPlugin = define({
|
|||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
|
|
@ -38,8 +37,7 @@ export const CloudflareWorkersAIPlugin = define({
|
|||
)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const CoherePlugin = define({
|
||||
id: "cohere",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cohere") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const DeepInfraPlugin = define({
|
||||
id: "deepinfra",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/deepinfra") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { Effect } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { Npm } from "../../npm"
|
||||
|
||||
export const DynamicProviderPlugin = define({
|
||||
id: "dynamic-provider",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
const npm = yield* Npm.Service
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
|
||||
const installedPath = evt.package.startsWith("file://")
|
||||
? evt.package
|
||||
: (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||
|
||||
const mod = yield* Effect.promise(async () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const GatewayPlugin = define({
|
||||
id: "gateway",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/gateway") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function shouldUseResponses(modelID: string) {
|
||||
|
|
@ -25,16 +25,14 @@ export const GithubCopilotPlugin = define({
|
|||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/github-copilot") return
|
||||
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
|
||||
evt.sdk = mod.createOpenaiCompatible(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const GitLabPlugin = define({
|
||||
id: "gitlab",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "gitlab-ai-provider") return
|
||||
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
|
||||
|
|
@ -32,8 +31,7 @@ export const GitLabPlugin = define({
|
|||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||
const featureFlags =
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function resolveProject(options: Record<string, any>) {
|
||||
|
|
@ -84,8 +84,7 @@ export const GoogleVertexPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
|
||||
evt.options.fetch = authFetch(evt.options.fetch)
|
||||
|
|
@ -104,8 +103,7 @@ export const GoogleVertexPlugin = define({
|
|||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
||||
|
|
@ -139,8 +137,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
||||
|
|
@ -166,8 +163,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const GooglePlugin = define({
|
||||
id: "google",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const GroqPlugin = define({
|
||||
id: "groq",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/groq") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const KiloPlugin = define({
|
||||
id: "kilo",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { Integration } from "../../integration"
|
||||
|
||||
export const LLMGatewayPlugin = define({
|
||||
id: "llmgateway",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const integrations = yield* Integration.Service
|
||||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
|
|
@ -11,7 +13,7 @@ export const LLMGatewayPlugin = define({
|
|||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
|
||||
if (!(yield* ctx.integration.get(item.provider.id))) continue
|
||||
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
provider.request.headers["X-Title"] = "opencode"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const MistralPlugin = define({
|
||||
id: "mistral",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/mistral") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const NvidiaPlugin = define({
|
||||
id: "nvidia",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const OpenAICompatiblePlugin = define({
|
||||
id: "openai-compatible",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
if (!evt.package.includes("@ai-sdk/openai-compatible")) return
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { Integration } from "../../integration"
|
||||
import { browser, headless } from "./openai-auth"
|
||||
|
|
@ -27,16 +27,14 @@ export const OpenAIPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/openai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
|
||||
evt.sdk = mod.createOpenAI(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { Integration } from "../../integration"
|
||||
|
||||
export const OpencodePlugin = define({
|
||||
id: "opencode",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const integrations = yield* Integration.Service
|
||||
let hasKey = false
|
||||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
const item = evt.provider.get(ProviderV2.ID.opencode)
|
||||
if (!item) return
|
||||
const integration = yield* ctx.integration.get(item.provider.id)
|
||||
const integration = yield* integrations.get(Integration.ID.make(item.provider.id))
|
||||
hasKey = Boolean(
|
||||
process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const OpenRouterPlugin = define({
|
||||
id: "openrouter",
|
||||
|
|
@ -25,8 +25,7 @@ export const OpenRouterPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@openrouter/ai-sdk-provider") return
|
||||
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const PerplexityPlugin = define({
|
||||
id: "perplexity",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/perplexity") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Effect } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { Npm } from "../../npm"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const SapAICorePlugin = define({
|
||||
id: "sap-ai-core",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
const npm = yield* Npm.Service
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
const serviceKey =
|
||||
|
|
@ -17,7 +18,7 @@ export const SapAICorePlugin = define({
|
|||
|
||||
const installedPath = evt.package.startsWith("file://")
|
||||
? evt.package
|
||||
: (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||
|
||||
const mod = yield* Effect.promise(async () => {
|
||||
|
|
@ -35,8 +36,7 @@ export const SapAICorePlugin = define({
|
|||
)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
evt.language = evt.sdk(evt.model.api.id)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
|
||||
|
|
@ -67,8 +67,7 @@ export function cortexFetch(upstream: FetchLike = fetch) {
|
|||
export const SnowflakeCortexPlugin = define({
|
||||
id: "snowflake-cortex",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
|
||||
const token =
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const TogetherAIPlugin = define({
|
||||
id: "togetherai",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/togetherai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const VenicePlugin = define({
|
||||
id: "venice",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "venice-ai-sdk-provider") return
|
||||
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const VercelPlugin = define({
|
||||
id: "vercel",
|
||||
|
|
@ -16,8 +16,7 @@ export const VercelPlugin = define({
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/vercel") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const XAIPlugin = define({
|
||||
id: "xai",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/xai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
||||
evt.sdk = mod.createXai(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const ZenmuxPlugin = define({
|
||||
id: "zenmux",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
export * as SkillPlugin from "./skill"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "./internal"
|
||||
import { Effect } from "effect"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { SkillV2 } from "../skill"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { Slug } from "../util/slug"
|
|||
import { EventV2 } from "../event"
|
||||
import { Database } from "../database/database"
|
||||
import { Location } from "../location"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
|
||||
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
|
||||
export type StrategyID = typeof StrategyID.Type
|
||||
|
|
@ -125,10 +124,8 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
|
|||
|
||||
export const refreshAfterBoot = Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
const boot = yield* PluginBoot.Service
|
||||
const copies = yield* Service
|
||||
yield* Effect.gen(function* () {
|
||||
yield* boot.wait()
|
||||
yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id })
|
||||
const result = yield* copies.refresh({ projectID: location.project.id })
|
||||
yield* Effect.logInfo("project copy refresh done", {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export const layer = Layer.effect(
|
|||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
list: Effect.fn("Reference.list")(function* () {
|
||||
return Array.from(materialized.values())
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
export * as ReferenceGuidance from "./guidance"
|
||||
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
import { Reference } from "../reference"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
|
||||
|
|
@ -34,12 +33,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const boot = yield* PluginBoot.Service
|
||||
const references = yield* Reference.Service
|
||||
|
||||
return Service.of({
|
||||
load: Effect.fn("ReferenceGuidance.load")(function* () {
|
||||
yield* boot.wait()
|
||||
const available = (yield* references.list())
|
||||
.filter((reference) => reference.description !== undefined)
|
||||
.map((reference) => ({
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { Integration } from "../../integration"
|
|||
import { IntegrationConnection } from "../../integration/connection"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ModelRequest } from "../../model-request"
|
||||
import { PluginBoot } from "../../plugin/boot"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { SessionSchema } from "../schema"
|
||||
|
||||
|
|
@ -178,11 +177,9 @@ export const locationLayer = Layer.effect(
|
|||
const catalog = yield* Catalog.Service
|
||||
const credentials = yield* Credential.Service
|
||||
const integrations = yield* Integration.Service
|
||||
const boot = yield* PluginBoot.Service
|
||||
return Service.of({
|
||||
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
||||
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
||||
yield* boot.wait()
|
||||
const defaultModel = session.model ? undefined : yield* catalog.model.default()
|
||||
const selected = session.model
|
||||
? (yield* catalog.model.available()).find(
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export const layer = Layer.effect(
|
|||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
sources: Effect.fn("SkillV2.sources")(function* () {
|
||||
return state.get().sources
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export * as SkillGuidance from "./guidance"
|
|||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
|
||||
|
|
@ -40,12 +39,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const boot = yield* PluginBoot.Service
|
||||
const skills = yield* SkillV2.Service
|
||||
|
||||
return Service.of({
|
||||
load: Effect.fn("SkillGuidance.load")(function* (selection) {
|
||||
yield* boot.wait()
|
||||
const agent = selection.info
|
||||
if (!agent) return SystemContext.empty
|
||||
const permitted = SkillV2.available(yield* skills.list(), agent)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export * as State from "./state"
|
|||
import { Context, Effect, Scope, Semaphore } from "effect"
|
||||
|
||||
/**
|
||||
* A replayable transform applied to a draft during rebuild.
|
||||
* A replayable transform applied to a draft during reload.
|
||||
*
|
||||
* Domain drafts expose readable and writable state while preserving concise
|
||||
* plugin/config code. Transforms may perform Effects before returning.
|
||||
|
|
@ -19,14 +19,14 @@ export type Transform<DraftApi> = (
|
|||
transform: TransformCallback<DraftApi>,
|
||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
|
||||
export type Rebuild = () => Effect.Effect<void>
|
||||
export type Reload = () => Effect.Effect<void>
|
||||
|
||||
export interface Transformable<DraftApi> {
|
||||
readonly transform: Transform<DraftApi>
|
||||
readonly rebuild: Rebuild
|
||||
readonly reload: Reload
|
||||
}
|
||||
|
||||
const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", {
|
||||
const CurrentBatch = Context.Reference<Set<Reload> | undefined>("@opencode/State/CurrentBatch", {
|
||||
defaultValue: () => undefined,
|
||||
})
|
||||
|
||||
|
|
@ -34,15 +34,15 @@ export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|||
return Effect.gen(function* () {
|
||||
const current = yield* CurrentBatch
|
||||
if (current) return yield* effect
|
||||
const rebuilds = new Set<Rebuild>()
|
||||
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds))
|
||||
yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true })
|
||||
const reloads = new Set<Reload>()
|
||||
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, reloads))
|
||||
yield* Effect.forEach(reloads, (reload) => reload(), { discard: true })
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export interface Options<State, DraftApi> {
|
||||
/** Creates the base value for initial state and every scoped-transform rebuild. */
|
||||
/** Creates the base value for initial state and every scoped-transform reload. */
|
||||
readonly initial: () => State
|
||||
/** Wraps mutable state in a domain-specific draft API. */
|
||||
readonly draft: MakeDraft<State, DraftApi>
|
||||
|
|
@ -54,7 +54,7 @@ export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
|
|||
readonly get: () => State
|
||||
/**
|
||||
* Registers and applies a scoped transform. Closing the owning Scope removes
|
||||
* the transform and rebuilds the materialized state.
|
||||
* the transform and reloads the materialized state.
|
||||
*/
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +78,11 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
|||
const materialize = Effect.fnUntraced(function* () {
|
||||
const next = options.initial()
|
||||
const api = options.draft(next)
|
||||
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.rebuild.update"))
|
||||
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.reload.update"))
|
||||
yield* commit(next)
|
||||
})
|
||||
|
||||
const rebuild = () => semaphore.withPermit(materialize())
|
||||
const reload = () => semaphore.withPermit(materialize())
|
||||
|
||||
const result: Interface<State, DraftApi> = {
|
||||
get: () => state,
|
||||
|
|
@ -101,7 +101,7 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
|||
return Effect.gen(function* () {
|
||||
const batch = yield* CurrentBatch
|
||||
if (batch) {
|
||||
batch.add(rebuild)
|
||||
batch.add(reload)
|
||||
return
|
||||
}
|
||||
yield* materialize()
|
||||
|
|
@ -116,13 +116,13 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
|||
)
|
||||
yield* Scope.addFinalizer(scope, dispose)
|
||||
const batch = yield* CurrentBatch
|
||||
if (batch) batch.add(rebuild)
|
||||
else yield* rebuild()
|
||||
if (batch) batch.add(reload)
|
||||
else yield* reload()
|
||||
return { dispose }
|
||||
}),
|
||||
)
|
||||
}),
|
||||
rebuild,
|
||||
reload,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { pathToFileURL } from "url"
|
|||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { Tool } from "./tool"
|
||||
|
|
@ -58,10 +57,8 @@ export const layer = Layer.effectDiscard(
|
|||
Effect.gen(function* () {
|
||||
const tools = yield* Tools.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const boot = yield* PluginBoot.Service
|
||||
const skills = yield* SkillV2.Service
|
||||
const permission = yield* PermissionV2.Service
|
||||
yield* boot.wait()
|
||||
yield* tools
|
||||
.register({
|
||||
[name]: Tool.make({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue