Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}),
Expand Down
95 changes: 74 additions & 21 deletions packages/core/src/aisdk.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -148,34 +212,23 @@ 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),
)
languages.set(key, language)
return language
}),
})
return service
}),
)

export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))
export const defaultLayer = locationLayer
2 changes: 1 addition & 1 deletion packages/core/src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/config/plugin/agent.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/config/plugin/command.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
99 changes: 99 additions & 0 deletions packages/core/src/config/plugin/external.ts
Original file line number Diff line number Diff line change
@@ -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 }))
}),
})
2 changes: 1 addition & 1 deletion packages/core/src/config/plugin/provider.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/config/plugin/reference.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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,
Expand Down
12 changes: 7 additions & 5 deletions packages/core/src/config/plugin/skill.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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)),
}),
)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading