feat(core): manage configurable plugin generations

This commit is contained in:
Dax Raad 2026-07-05 15:51:15 -04:00
commit a9b7bd9e2f
83 changed files with 1116 additions and 819 deletions

View file

@ -1,7 +1,7 @@
export * as AgentPlugin from "./agent"
import path from "path"
import { define } from "./internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect } from "effect"
import { AgentV2 } from "../agent"
import { Global } from "../global"
@ -100,7 +100,7 @@ Rules:
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
export const Plugin = define({
id: "agent",
id: "opencode.agent",
effect: Effect.fn(function* (ctx) {
const location = yield* Location.Service
const worktree = location.directory

View file

@ -1,13 +1,13 @@
export * as CommandPlugin from "./command"
import { define } from "./internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
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",
id: "opencode.command",
effect: Effect.fn(function* (ctx) {
const location = yield* Location.Service
yield* ctx.command.transform((draft) => {

View file

@ -273,8 +273,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
},
plugin: {
list: () => response(plugin.list()),
add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect),
remove: (id) => plugin.remove(PluginV2.ID.make(id)),
},
reference: {
list: () => response(reference.list()),

View file

@ -1,16 +1,14 @@
export * as PluginInternal from "./internal"
import { makeLocationNode } from "../effect/app-node"
import { httpClient } from "../effect/app-node-platform"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import { Context, Effect, Layer, Scope } from "effect"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { Context, Effect, Scope } from "effect"
import { HttpClient } from "effect/unstable/http"
import { AgentV2 } from "../agent"
import { Catalog } from "../catalog"
import { CommandV2 } from "../command"
import { Config } from "../config"
import { ConfigAgentPlugin } from "../config/plugin/agent"
import { ConfigCommandPlugin } from "../config/plugin/command"
import { ConfigExternalPlugin } from "../config/plugin/external"
import { ConfigProviderPlugin } from "../config/plugin/provider"
import { ConfigReferencePlugin } from "../config/plugin/reference"
import { ConfigSkillPlugin } from "../config/plugin/skill"
@ -25,8 +23,6 @@ import { Location } from "../location"
import { LocationMutation } from "../location-mutation"
import { ModelsDev } from "../models-dev"
import { Npm } from "../npm"
import { PluginV2 } from "../plugin"
import { PluginRuntime } from "../plugin/runtime"
import { PermissionV2 } from "../permission"
import { QuestionV2 } from "../question"
import { Reference } from "../reference"
@ -35,177 +31,137 @@ import { SessionInstructions } from "../session/instructions"
import { SessionTodo } from "../session/todo"
import { Shell } from "../shell"
import { SkillV2 } from "../skill"
import { State } from "../state"
import { ToolRegistry } from "../tool/registry"
import { Tools } from "../tool/tools"
import { HttpClient } from "effect/unstable/http"
import { AgentPlugin } from "./agent"
import { CommandPlugin } from "./command"
import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider"
import { SdkPlugins } from "./sdk"
import { SkillPlugin } from "./skill"
import { VariantPlugin } from "./variant"
import { ApplyPatchTool } from "../tool/apply-patch"
import { EditTool } from "../tool/edit"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { QuestionTool } from "../tool/question"
import { ReadTool } from "../tool/read"
import { ReadToolFileSystem } from "../tool/read-filesystem"
import { ReadTool } from "../tool/read"
import { ShellTool } from "../tool/shell"
import { SkillTool } from "../tool/skill"
import { SubagentTool } from "../tool/subagent"
import { TodoWriteTool } from "../tool/todowrite"
import { Tools } from "../tool/tools"
import { WebFetchTool } from "../tool/webfetch"
import { WebSearchTool } from "../tool/websearch"
import { WriteTool } from "../tool/write"
import { AgentPlugin } from "./agent"
import { CommandPlugin } from "./command"
import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider"
import { PluginRuntime } from "./runtime"
import { SkillPlugin } from "./skill"
import { VariantPlugin } from "./variant"
export type Requirements =
| AgentV2.Service
| Catalog.Service
| CommandV2.Service
| Config.Service
| EventV2.Service
| FileMutation.Service
| FileSystem.Service
| FSUtil.Service
| Global.Service
| HttpClient.HttpClient
| Image.Service
| Integration.Service
| Location.Service
| LocationMutation.Service
| ModelsDev.Service
| Npm.Service
| PermissionV2.Service
| PluginRuntime.Service
| QuestionV2.Service
| ReadToolFileSystem.Service
| Reference.Service
| Ripgrep.Service
| SessionInstructions.Service
| SessionTodo.Service
| Shell.Service
| SkillV2.Service
| Tools.Service
| WebSearchTool.ConfigService
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
}
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const sdkPlugins = yield* SdkPlugins.Service
const services = Context.mergeAll(
Context.make(Catalog.Service, yield* Catalog.Service),
Context.make(CommandV2.Service, yield* CommandV2.Service),
Context.make(Integration.Service, yield* Integration.Service),
Context.make(AgentV2.Service, yield* AgentV2.Service),
Context.make(Config.Service, yield* Config.Service),
Context.make(Location.Service, yield* Location.Service),
Context.make(ModelsDev.Service, yield* ModelsDev.Service),
Context.make(Npm.Service, yield* Npm.Service),
Context.make(EventV2.Service, yield* EventV2.Service),
Context.make(FSUtil.Service, yield* FSUtil.Service),
Context.make(FileSystem.Service, yield* FileSystem.Service),
Context.make(Global.Service, yield* Global.Service),
Context.make(HttpClient.HttpClient, yield* HttpClient.HttpClient),
Context.make(LocationMutation.Service, yield* LocationMutation.Service),
Context.make(FileMutation.Service, yield* FileMutation.Service),
Context.make(Image.Service, yield* Image.Service),
Context.make(PermissionV2.Service, yield* PermissionV2.Service),
Context.make(QuestionV2.Service, yield* QuestionV2.Service),
Context.make(ReadToolFileSystem.Service, yield* ReadToolFileSystem.Service),
Context.make(SessionInstructions.Service, yield* SessionInstructions.Service),
Context.make(SessionTodo.Service, yield* SessionTodo.Service),
Context.make(SkillV2.Service, yield* SkillV2.Service),
Context.make(Reference.Service, yield* Reference.Service),
Context.make(Ripgrep.Service, yield* Ripgrep.Service),
Context.make(Shell.Service, yield* Shell.Service),
Context.make(Tools.Service, yield* Tools.Service),
Context.make(PluginRuntime.Service, yield* PluginRuntime.Service),
Context.make(WebSearchTool.ConfigService, yield* WebSearchTool.ConfigService),
)
const add = (input: Plugin<Requirements | Scope.Scope>) =>
plugin.add(PluginV2.ID.make(input.id), (context: PluginContext) =>
input.effect(context).pipe(Effect.provide(services)),
)
yield* State.batch(
Effect.gen(function* () {
yield* add(ConfigReferencePlugin.Plugin)
yield* add(AgentPlugin.Plugin)
yield* add(CommandPlugin.Plugin)
yield* add(SkillPlugin.Plugin)
yield* add(ModelsDevPlugin)
yield* add(ConfigExternalPlugin.Plugin)
yield* add(ApplyPatchTool.Plugin)
yield* add(EditTool.Plugin)
yield* add(GlobTool.Plugin)
yield* add(GrepTool.Plugin)
yield* add(QuestionTool.Plugin)
yield* add(ReadTool.Plugin)
yield* add(ShellTool.Plugin)
yield* add(SkillTool.Plugin)
yield* add(SubagentTool.Plugin)
yield* add(TodoWriteTool.Plugin)
yield* add(WebFetchTool.Plugin)
yield* add(WebSearchTool.Plugin)
yield* add(WriteTool.Plugin)
yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin)
for (const item of ProviderPlugins) yield* add(item)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(VariantPlugin.Plugin)
// Embedder-contributed plugins are added last so they layer over config.
for (const plugin of sdkPlugins.all()) yield* add(plugin)
}),
).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
}),
)
export const node = makeLocationNode({
name: "plugin-internal",
layer,
deps: [
Catalog.node,
CommandV2.node,
PluginV2.node,
Integration.node,
AgentV2.node,
Config.node,
Location.node,
LocationMutation.node,
FileMutation.node,
Image.node,
ModelsDev.node,
Npm.node,
EventV2.node,
FSUtil.node,
FileSystem.node,
Global.node,
httpClient,
PermissionV2.node,
QuestionV2.node,
ReadToolFileSystem.node,
SessionInstructions.node,
SessionTodo.node,
SkillV2.node,
Reference.node,
Ripgrep.node,
Shell.node,
ToolRegistry.toolsNode,
PluginRuntime.node,
SdkPlugins.node,
WebSearchTool.configNode,
],
const services = Effect.fn("PluginInternal.services")(function* () {
const agent = yield* AgentV2.Service
const catalog = yield* Catalog.Service
const command = yield* CommandV2.Service
const config = yield* Config.Service
const events = yield* EventV2.Service
const mutation = yield* FileMutation.Service
const filesystem = yield* FileSystem.Service
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const http = yield* HttpClient.HttpClient
const image = yield* Image.Service
const integration = yield* Integration.Service
const location = yield* Location.Service
const locationMutation = yield* LocationMutation.Service
const models = yield* ModelsDev.Service
const npm = yield* Npm.Service
const permission = yield* PermissionV2.Service
const runtime = yield* PluginRuntime.Service
const question = yield* QuestionV2.Service
const read = yield* ReadToolFileSystem.Service
const reference = yield* Reference.Service
const ripgrep = yield* Ripgrep.Service
const instructions = yield* SessionInstructions.Service
const todo = yield* SessionTodo.Service
const shell = yield* Shell.Service
const skill = yield* SkillV2.Service
const tools = yield* Tools.Service
const websearch = yield* WebSearchTool.ConfigService
return Context.mergeAll(
Context.make(AgentV2.Service, agent),
Context.make(Catalog.Service, catalog),
Context.make(CommandV2.Service, command),
Context.make(Config.Service, config),
Context.make(EventV2.Service, events),
Context.make(FileMutation.Service, mutation),
Context.make(FileSystem.Service, filesystem),
Context.make(FSUtil.Service, fs),
Context.make(Global.Service, global),
Context.make(HttpClient.HttpClient, http),
Context.make(Image.Service, image),
Context.make(Integration.Service, integration),
Context.make(Location.Service, location),
Context.make(LocationMutation.Service, locationMutation),
Context.make(ModelsDev.Service, models),
Context.make(Npm.Service, npm),
Context.make(PermissionV2.Service, permission),
Context.make(PluginRuntime.Service, runtime),
Context.make(QuestionV2.Service, question),
Context.make(ReadToolFileSystem.Service, read),
Context.make(Reference.Service, reference),
Context.make(Ripgrep.Service, ripgrep),
Context.make(SessionInstructions.Service, instructions),
Context.make(SessionTodo.Service, todo),
Context.make(Shell.Service, shell),
Context.make(SkillV2.Service, skill),
Context.make(Tools.Service, tools),
Context.make(WebSearchTool.ConfigService, websearch),
)
})
type ContextServices<A> = A extends Context.Context<infer R> ? R : never
export type Requirements = ContextServices<Effect.Success<ReturnType<typeof services>>>
export type InternalPlugin = Plugin<Requirements | Scope.Scope>
const pre = [
AgentPlugin.Plugin,
CommandPlugin.Plugin,
SkillPlugin.Plugin,
ModelsDevPlugin,
...ProviderPlugins,
ApplyPatchTool.Plugin,
EditTool.Plugin,
GlobTool.Plugin,
GrepTool.Plugin,
QuestionTool.Plugin,
ReadTool.Plugin,
ShellTool.Plugin,
SkillTool.Plugin,
SubagentTool.Plugin,
TodoWriteTool.Plugin,
WebFetchTool.Plugin,
WebSearchTool.Plugin,
WriteTool.Plugin,
] as const satisfies readonly InternalPlugin[]
const post = [
ConfigReferencePlugin.Plugin,
ConfigAgentPlugin.Plugin,
ConfigCommandPlugin.Plugin,
ConfigSkillPlugin.Plugin,
ConfigProviderPlugin.Plugin,
VariantPlugin.Plugin,
] as const satisfies readonly InternalPlugin[]
export const list = Effect.fn("PluginInternal.list")(function* () {
const context = yield* services()
const resolve = (plugins: readonly InternalPlugin[]) =>
plugins.map(
(plugin): Plugin => ({
id: plugin.id,
effect: (host) => plugin.effect(host).pipe(Effect.provide(context)),
}),
)
return {
pre: resolve(pre),
post: resolve(post),
}
})

View file

@ -1,4 +1,4 @@
import { define } from "./internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
@ -197,7 +197,7 @@ function applyModel(
}
export const ModelsDevPlugin = define({
id: "models-dev",
id: "opencode.models-dev",
effect: Effect.fn(function* (ctx) {
const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service

View file

@ -9,7 +9,7 @@ type Registration = { readonly dispose: () => Promise<void> }
/**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
* loader (`PluginV2` / `PluginInternal`) can run it unchanged.
* loader (`PluginV2` / `PluginSupervisor`) 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
@ -93,11 +93,6 @@ export function fromPromise(plugin: Plugin) {
},
plugin: {
list: (input) => run(host.plugin.list(input)),
add: (input) => {
const child = fromPromise(input)
return run(host.plugin.add(child))
},
remove: (id) => run(host.plugin.remove(id)),
},
reference: {
list: (input) => run(host.reference.list(input)),

View file

@ -31,9 +31,8 @@ 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: PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>[] = [
export const ProviderPlugins: PluginInternal.InternalPlugin[] = [
AlibabaPlugin,
AmazonBedrockPlugin,
AnthropicPlugin,

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const AlibabaPlugin = define({
id: "alibaba",
id: "opencode.provider.alibaba",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
type MantleSDK = {
@ -60,7 +60,7 @@ function selectMantleModel(sdk: MantleSDK, modelID: string) {
}
export const AmazonBedrockPlugin = define({
id: "amazon-bedrock",
id: "opencode.provider.amazon-bedrock",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const AnthropicPlugin = define({
id: "anthropic",
id: "opencode.provider.anthropic",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
@ -11,7 +11,7 @@ function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
}
export const AzurePlugin = define({
id: "azure",
id: "opencode.provider.azure",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
@ -54,7 +54,7 @@ export const AzurePlugin = define({
})
export const AzureCognitiveServicesPlugin = define({
id: "azure-cognitive-services",
id: "opencode.provider.azure-cognitive-services",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const CerebrasPlugin = define({
id: "cerebras",
id: "opencode.provider.cerebras",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -1,10 +1,10 @@
import os from "os"
import { InstallationVersion } from "../../installation/version"
import { Effect, Option, Schema } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const CloudflareAIGatewayPlugin = define({
id: "cloudflare-ai-gateway",
id: "opencode.provider.cloudflare-ai-gateway",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,13 +1,13 @@
import os from "os"
import { InstallationVersion } from "../../installation/version"
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
export const CloudflareWorkersAIPlugin = define({
id: "cloudflare-workers-ai",
id: "opencode.provider.cloudflare-workers-ai",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
const item = evt.provider.get(providerID)

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const CoherePlugin = define({
id: "cohere",
id: "opencode.provider.cohere",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const DeepInfraPlugin = define({
id: "deepinfra",
id: "opencode.provider.deepinfra",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,10 +1,10 @@
import { Effect } from "effect"
import { pathToFileURL } from "url"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Npm } from "../../npm"
export const DynamicProviderPlugin = define({
id: "dynamic-provider",
id: "opencode.provider.dynamic",
effect: Effect.fn(function* (ctx) {
const npm = yield* Npm.Service
yield* ctx.aisdk.sdk(

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GatewayPlugin = define({
id: "gateway",
id: "opencode.provider.gateway",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
function shouldUseResponses(modelID: string) {
@ -12,7 +12,7 @@ function shouldUseResponses(modelID: string) {
}
export const GithubCopilotPlugin = define({
id: "github-copilot",
id: "opencode.provider.github-copilot",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
const item = evt.provider.get(ProviderV2.ID.githubCopilot)

View file

@ -1,11 +1,11 @@
import os from "os"
import { InstallationVersion } from "../../installation/version"
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
export const GitLabPlugin = define({
id: "gitlab",
id: "opencode.provider.gitlab",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
function resolveProject(options: Record<string, any>) {
@ -55,7 +55,7 @@ function authFetch(fetchWithRuntimeOptions?: unknown) {
}
export const GoogleVertexPlugin = define({
id: "google-vertex",
id: "opencode.provider.google-vertex",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
@ -111,7 +111,7 @@ export const GoogleVertexPlugin = define({
})
export const GoogleVertexAnthropicPlugin = define({
id: "google-vertex-anthropic",
id: "opencode.provider.google-vertex-anthropic",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GooglePlugin = define({
id: "google",
id: "opencode.provider.google",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GroqPlugin = define({
id: "groq",
id: "opencode.provider.groq",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const KiloPlugin = define({
id: "kilo",
id: "opencode.provider.kilo",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -1,9 +1,9 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Integration } from "../../integration"
export const LLMGatewayPlugin = define({
id: "llmgateway",
id: "opencode.provider.llmgateway",
effect: Effect.fn(function* (ctx) {
const integrations = yield* Integration.Service
const configured = new Set((yield* integrations.list()).map((integration) => integration.id))

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const MistralPlugin = define({
id: "mistral",
id: "opencode.provider.mistral",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const NvidiaPlugin = define({
id: "nvidia",
id: "opencode.provider.nvidia",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const OpenAICompatiblePlugin = define({
id: "openai-compatible",
id: "opencode.provider.openai-compatible",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -2,7 +2,6 @@ import { createServer } from "node:http"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
import type { Scope } from "effect"
import { Credential } from "../../credential"
import { EventV2 } from "../../event"
import { InstallationVersion } from "../../installation/version"
@ -159,7 +158,7 @@ const headless = {
} satisfies IntegrationOAuthMethodRegistration
export const OpenAIPlugin = define({
id: "openai",
id: "opencode.provider.openai",
effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service
const loading = Semaphore.makeUnsafe(1)
@ -225,7 +224,7 @@ export const OpenAIPlugin = define({
}),
)
}),
} satisfies PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>)
} satisfies PluginInternal.InternalPlugin)
function headers(contentType: string) {
return { "Content-Type": contentType, "User-Agent": `opencode/${InstallationVersion}` }

View file

@ -75,7 +75,7 @@ function oauth(http: HttpClient.HttpClient) {
}
export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | Scope.Scope>({
id: "opencode",
id: "opencode.provider.opencode",
effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service
const http = yield* HttpClient.HttpClient

View file

@ -1,9 +1,9 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const OpenRouterPlugin = define({
id: "openrouter",
id: "opencode.provider.openrouter",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const PerplexityPlugin = define({
id: "perplexity",
id: "opencode.provider.perplexity",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,11 +1,11 @@
import { Effect } from "effect"
import { pathToFileURL } from "url"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Npm } from "../../npm"
import { ProviderV2 } from "../../provider"
export const SapAICorePlugin = define({
id: "sap-ai-core",
id: "opencode.provider.sap-ai-core",
effect: Effect.fn(function* (ctx) {
const npm = yield* Npm.Service
yield* ctx.aisdk.sdk(

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
@ -65,7 +65,7 @@ export function cortexFetch(upstream: FetchLike = fetch) {
}
export const SnowflakeCortexPlugin = define({
id: "snowflake-cortex",
id: "opencode.provider.snowflake-cortex",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const TogetherAIPlugin = define({
id: "togetherai",
id: "opencode.provider.togetherai",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const VenicePlugin = define({
id: "venice",
id: "opencode.provider.venice",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const VercelPlugin = define({
id: "vercel",
id: "opencode.provider.vercel",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -1,9 +1,9 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
export const XAIPlugin = define({
id: "xai",
id: "opencode.provider.xai",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { define } from "../internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const ZenmuxPlugin = define({
id: "zenmux",
id: "opencode.provider.zenmux",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {

View file

@ -14,9 +14,9 @@ const defaultStore = makeStore()
/**
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
* so `PluginInternal` can add them on every Location boot through the ordinary
* `ctx.plugin.add` seam the same path `ConfigExternalPlugin` uses for plugins
* discovered from config. A plugin registered after a Location has booted only
* so `PluginSupervisor` can add them on every Location boot through the ordinary
* generation path that `PluginSupervisor` uses for plugins discovered from
* config. A plugin registered after a Location has booted only
* applies to Locations booted afterward, matching config-plugin timing;
* embedders register at startup before creating Sessions.
*

View file

@ -2,7 +2,7 @@
export * as SkillPlugin from "./skill"
import { define } from "./internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect } from "effect"
import { AbsolutePath } from "../schema"
import { SkillV2 } from "../skill"
@ -25,7 +25,7 @@ const REPORT_DESCRIPTION =
"Use when the user wants to report an opencode issue or bug. Collect standard diagnostics, add user-specific reproduction context, and publish the issue with GitHub CLI."
export const Plugin = define({
id: "skill",
id: "opencode.skill",
effect: Effect.fn(function* (ctx) {
const reportContent = yield* reportContentWithDiagnostics()
yield* ctx.skill.transform((draft) => {

View file

@ -0,0 +1,270 @@
export * as PluginSupervisor from "./supervisor"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { Event } from "@opencode-ai/schema/config"
import { Context, Effect, Fiber, Layer, Schema, Semaphore, Stream } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { Config } from "../config"
import { ConfigPlugin } from "../config/plugin"
import { EventV2 } from "../event"
import { FSUtil } from "../fs-util"
import { Location } from "../location"
import { Npm } from "../npm"
import { PluginV2 } from "../plugin"
import { PluginPromise } from "../plugin/promise"
import { PluginInternal } from "./internal"
import { SdkPlugins } from "./sdk"
const PluginModule = Schema.Struct({
default: Schema.Union([
Schema.Struct({
id: Schema.String,
effect: Schema.declare<Plugin["effect"]>((input): input is Plugin["effect"] => typeof input === "function"),
}),
Schema.Struct({
id: Schema.String,
setup: Schema.declare<Parameters<typeof PluginPromise.fromPromise>[0]["setup"]>(
(input): input is Parameters<typeof PluginPromise.fromPromise>[0]["setup"] => typeof input === "function",
),
}),
]),
})
const PluginPackage = Schema.Struct({
exports: Schema.optional(Schema.Unknown),
main: Schema.optional(Schema.String),
module: Schema.optional(Schema.String),
})
type Operation =
| {
readonly type: "add"
readonly target: string
readonly options: Record<string, unknown>
}
| {
readonly type: "remove"
readonly target: string
}
type Candidate =
| {
readonly type: "definition"
readonly definition: Plugin
}
| {
readonly type: "package"
readonly specifier: string
readonly options: Record<string, unknown>
}
type ConfiguredPackage = {
readonly operation: Extract<Operation, { type: "add" }>
enabled: boolean
}
function parse(input: ConfigPlugin.Plugin): Operation {
if (typeof input !== "string") {
return { type: "add", target: input.package, options: input.options ?? {} }
}
if (!input.startsWith("-")) return { type: "add", target: input, options: {} }
if (input.length === 1) throw new Error("Plugin remove operation requires a target")
return { type: "remove", target: input.slice(1) }
}
const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Config.Entry[]) {
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const discovered = yield* Effect.forEach(
entries.filter((entry): entry is Config.Directory => entry.type === "directory"),
(entry) => discoverDirectory(fs, entry.path),
).pipe(Effect.map((items) => items.flat()))
const configured = entries
.filter((entry): entry is Config.Document => entry.type === "document")
.flatMap((entry) =>
(entry.info.plugins ?? []).map(parse).map((operation) => {
const directory = entry.path ? path.dirname(entry.path) : location.directory
const target = operation.target.startsWith("file://")
? fileURLToPath(operation.target)
: operation.target.startsWith("./") || operation.target.startsWith("../")
? path.resolve(directory, operation.target)
: operation.target
return operation.type === "add" ? { ...operation, target } : { type: "remove" as const, target }
}),
)
// Explicit config is applied last so it can remove auto-discovered packages.
return [...discovered, ...configured]
})
const resolve = Effect.fn("PluginSupervisor.resolve")(function* (
pre: readonly Plugin[],
post: readonly Plugin[],
operations: readonly Operation[],
) {
const plan = apply(pre, post, operations)
return yield* load(plan)
})
function apply(pre: readonly Plugin[], post: readonly Plugin[], operations: readonly Operation[]) {
const matches = (selector: string, target: string) =>
selector === "*" || (selector.endsWith(".*") ? target.startsWith(selector.slice(0, -1)) : selector === target)
const plugins = [...pre, ...post]
const enabled = new Set(plugins.map((plugin) => plugin.id))
const packages = new Map<string, ConfiguredPackage>()
for (const operation of operations) {
if (operation.type === "remove") {
plugins.filter((plugin) => matches(operation.target, plugin.id)).forEach((plugin) => enabled.delete(plugin.id))
packages.forEach((item, target) => {
if (matches(operation.target, target)) item.enabled = false
})
continue
}
const matched = plugins.filter((plugin) => matches(operation.target, plugin.id))
const selectsDefinitions =
matched.length > 0 ||
operation.target === "*" ||
operation.target.endsWith(".*") ||
operation.target.startsWith("opencode.")
if (selectsDefinitions) {
matched.forEach((plugin) => enabled.add(plugin.id))
packages.forEach((item, target) => {
if (matches(operation.target, target)) item.enabled = true
})
continue
}
packages.set(operation.target, { operation, enabled: true })
}
const definitions: Candidate[] = pre.flatMap((definition) =>
enabled.has(definition.id) ? [{ type: "definition", definition }] : [],
)
const configured: Candidate[] = Array.from(packages.values()).flatMap((item) =>
item.enabled ? [{ type: "package", specifier: item.operation.target, options: item.operation.options }] : [],
)
const posts: Candidate[] = post.flatMap((definition) =>
enabled.has(definition.id) ? [{ type: "definition", definition }] : [],
)
return [...definitions, ...configured, ...posts]
}
const load = Effect.fn("PluginSupervisor.load")(function* (plan: readonly Candidate[]) {
return yield* Effect.forEach(plan, (candidate) => {
if (candidate.type === "definition") return Effect.succeed(candidate.definition)
return Effect.gen(function* () {
const npm = yield* Npm.Service
const entrypoint = path.isAbsolute(candidate.specifier)
? pathToFileURL(candidate.specifier).href
: (yield* npm.add(candidate.specifier)).entrypoint
if (!entrypoint) return
yield* Effect.log({ msg: "loading plugin", id: candidate.specifier, entrypoint })
const mod = yield* Effect.promise(() => import(entrypoint))
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
return {
id: plugin.id,
effect: (host) => plugin.effect({ ...host, options: candidate.options }),
} satisfies Plugin
}).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
}).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined)))
})
function discoverDirectory(fs: FSUtil.Interface, directory: string) {
return Effect.gen(function* () {
const files = yield* fs
.glob("{plugin,plugins}/*.{ts,js}", {
cwd: directory,
absolute: true,
include: "file",
dot: true,
symlink: true,
})
.pipe(Effect.orElseSucceed(() => []))
const directories = yield* fs
.glob("{plugin,plugins}/*", {
cwd: directory,
absolute: true,
include: "all",
dot: true,
symlink: true,
})
.pipe(
Effect.flatMap((items) => Effect.filter(items, (item) => fs.isDir(item), { concurrency: "unbounded" })),
Effect.orElseSucceed(() => []),
)
const packages = yield* Effect.forEach(directories.sort(), (directory) => resolvePackageEntrypoint(fs, directory), {
concurrency: "unbounded",
}).pipe(Effect.map((items) => items.filter((item): item is string => item !== undefined)))
return [...files.sort(), ...packages].map((target): Operation => ({ type: "add", target, options: {} }))
})
}
const resolvePackageEntrypoint = Effect.fnUntraced(function* (fs: FSUtil.Interface, directory: string) {
const pkg = yield* fs.readJson(path.join(directory, "package.json")).pipe(
Effect.flatMap(Schema.decodeUnknownEffect(PluginPackage)),
Effect.catch(() => Effect.succeed(undefined)),
)
const exported = typeof pkg?.exports === "string" ? pkg.exports : undefined
const entries = [exported, pkg?.module, pkg?.main, "index.ts", "index.js"]
return yield* Effect.forEach(entries, (entry) => {
if (!entry) return Effect.succeed(undefined)
const file = path.resolve(directory, entry)
return fs.isFile(file).pipe(Effect.map((exists) => (exists ? file : undefined)))
}).pipe(Effect.map((items) => items.find((item): item is string => item !== undefined)))
})
export interface Interface {
readonly ready: Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/PluginSupervisor") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const registry = yield* PluginV2.Service
const sdk = yield* SdkPlugins.Service
const config = yield* Config.Service
const events = yield* EventV2.Service
const lock = Semaphore.makeUnsafe(1)
let applied: string | undefined
const reload = Effect.fn("PluginSupervisor.reload")(() =>
lock.withPermit(
Effect.gen(function* () {
// Resolve OpenCode's internal plugins with their privileged Location services.
const internal = yield* PluginInternal.list()
// Combine internal plugins with host-contributed SDK plugins in boot order.
const pre = [...internal.pre, ...sdk.all()]
// Read the current layered config before resolving plugin directives and packages.
const entries = yield* config.entries()
// Skip duplicate watcher notifications and config edits unrelated to plugins.
const operations = yield* scan(entries)
const version = JSON.stringify(operations)
if (version === applied) return
// Apply config operations and load enabled package plugins into one ordered generation.
const plugins = yield* resolve(pre, internal.post, operations)
// Replace the active generation in one scoped, batched activation.
yield* registry.activate(plugins)
applied = version
}),
),
)
yield* events.subscribe(Event.Updated).pipe(
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload plugins", { cause }))),
),
Effect.forkScoped({ startImmediately: true }),
)
const fiber = yield* reload().pipe(
Effect.withSpan("PluginSupervisor.boot"),
Effect.forkScoped({ startImmediately: true }),
)
return Service.of({ ready: Fiber.join(fiber) })
}),
)
export { layer }

View file

@ -2,10 +2,10 @@ export * as VariantPlugin from "./variant"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect } from "effect"
import { define } from "./internal"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const Plugin = define({
id: "variant",
id: "opencode.variant",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((catalog) => {
for (const record of catalog.provider.list()) {