refactor(core): consolidate tool architecture

This commit is contained in:
Dax Raad 2026-07-26 20:08:55 -04:00
commit 8db7487c89
466 changed files with 9405 additions and 11071 deletions

View file

@ -1,4 +1,4 @@
export * as AccountV2 from "./account"
export * as Account from "./account"
import { Schema } from "effect"
import type { HttpClientError } from "effect/unstable/http"

View file

@ -1,14 +1,14 @@
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
import { AccountV2 } from "../account"
import { Account } from "../account"
import { Timestamps } from "../database/schema.sql"
export const AccountTable = sqliteTable("account", {
id: text().$type<AccountV2.ID>().primaryKey(),
id: text().$type<Account.ID>().primaryKey(),
email: text().notNull(),
url: text().notNull(),
access_token: text().$type<AccountV2.AccessToken>().notNull(),
refresh_token: text().$type<AccountV2.RefreshToken>().notNull(),
access_token: text().$type<Account.AccessToken>().notNull(),
refresh_token: text().$type<Account.RefreshToken>().notNull(),
token_expiry: integer(),
...Timestamps,
})
@ -16,9 +16,9 @@ export const AccountTable = sqliteTable("account", {
export const AccountStateTable = sqliteTable("account_state", {
id: integer().primaryKey(),
active_account_id: text()
.$type<AccountV2.ID>()
.$type<Account.ID>()
.references(() => AccountTable.id, { onDelete: "set null" }),
active_org_id: text().$type<AccountV2.OrgID>(),
active_org_id: text().$type<Account.OrgID>(),
})
// LEGACY
@ -27,8 +27,8 @@ export const ControlAccountTable = sqliteTable(
{
email: text().notNull(),
url: text().notNull(),
access_token: text().$type<AccountV2.AccessToken>().notNull(),
refresh_token: text().$type<AccountV2.RefreshToken>().notNull(),
access_token: text().$type<Account.AccessToken>().notNull(),
refresh_token: text().$type<Account.RefreshToken>().notNull(),
token_expiry: integer(),
active: integer({ mode: "boolean" })
.notNull()

View file

@ -1,9 +1,9 @@
export * as AgentV2 from "./agent"
export * as Agent from "./agent"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Array, Context, Effect, Layer, Types } from "effect"
import { Agent } from "@opencode-ai/schema/agent"
import { EventV2 } from "./event"
import { Bus } from "./bus"
import { State } from "./state"
export const ID = Agent.ID
@ -17,7 +17,7 @@ export const Color = Agent.Color
export const Info = Agent.Info
export type Info = Agent.Info
export const Event = Agent.Event
export { Event } from "@opencode-ai/schema/agent"
export interface Selection {
readonly id: ID
@ -45,12 +45,12 @@ export interface Interface extends State.Transformable<Draft> {
readonly list: () => Effect.Effect<Info[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const state = State.create<Data, Draft>({
name: "agent",
initial: () => ({ agents: new Map() }),
@ -70,7 +70,7 @@ const layer = Layer.effect(
draft.agents.delete(id)
},
}),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => bus.publish(Agent.Event.Updated, {}).pipe(Effect.asVoid),
})
const selectable = (agent: Info | undefined) =>
agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined
@ -89,17 +89,17 @@ const layer = Layer.effect(
return Service.of({
transform: state.transform,
reload: state.reload,
get: Effect.fn("AgentV2.get")(function* (id) {
get: Effect.fn("Agent.get")(function* (id) {
return state.get().agents.get(id)
}),
default: Effect.fn("AgentV2.default")(function* () {
default: Effect.fn("Agent.default")(function* () {
return selectedDefault()
}),
resolve: Effect.fn("AgentV2.resolve")(function* (id) {
resolve: Effect.fn("Agent.resolve")(function* (id) {
if (id !== undefined) return state.get().agents.get(ID.make(id))
return selectedDefault()
}),
select: Effect.fn("AgentV2.select")(function* (id) {
select: Effect.fn("Agent.select")(function* (id) {
if (id !== undefined) {
const selected = ID.make(id)
return { id: selected, info: state.get().agents.get(selected) }
@ -107,11 +107,11 @@ const layer = Layer.effect(
const info = selectedDefault()
return { id: info?.id ?? defaultID, info }
}),
list: Effect.fn("AgentV2.list")(function* () {
list: Effect.fn("Agent.list")(function* () {
return Array.fromIterable(state.get().agents.values())
}),
})
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })

View file

@ -32,8 +32,8 @@ import {
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route"
import { ProviderShared } from "@opencode-ai/ai/protocols/shared"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import type { ID, Info } from "./model"
import { Provider } from "./provider"
import { State } from "./state"
type SDK = any
@ -42,14 +42,14 @@ type AssistantContent = Extract<LanguageModelV3Message, { role: "assistant" }>["
type ToolResultContent = Extract<AssistantContent[number], { type: "tool-result" }>
export interface SDKEvent {
readonly model: ModelV2.Info
readonly model: Info
readonly package: string
readonly options: Record<string, any>
sdk?: SDK
}
export interface LanguageEvent {
readonly model: ModelV2.Info
readonly model: Info
readonly sdk: SDK
readonly options: Record<string, any>
language?: LanguageModelV3
@ -103,7 +103,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
})
}
function prepareOptions(model: ModelV2.Info, pkg: string) {
function prepareOptions(model: Info, pkg: string) {
const projected = mapBodyToProviderOptions(model, pkg)
const options: Record<string, any> = {
name: model.providerID,
@ -146,7 +146,7 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
if (typeof opts.body === "string" && model.body !== undefined) {
const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body))
opts.body = JSON.stringify(Provider.mergeOverlay(decoded, model.body))
}
}
@ -162,11 +162,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
}
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
providerID: ProviderV2.ID,
providerID: Provider.ID,
cause: Schema.Defect(),
}) {}
function initError(providerID: ProviderV2.ID) {
function initError(providerID: Provider.ID) {
return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
}
@ -181,11 +181,11 @@ export interface Interface {
}
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
readonly model: (model: ModelV2.Info) => Effect.Effect<Model, InitError>
readonly language: (model: Info) => Effect.Effect<LanguageModelV3, InitError>
readonly model: (model: Info) => Effect.Effect<Model, InitError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/AISDK") {}
export const locationLayer = Layer.effect(
Service,
@ -260,13 +260,13 @@ export const locationLayer = Layer.effect(
})
const existing = languages.get(key)
if (existing) return existing
if (!ProviderV2.isAISDK(model.package))
if (!Provider.isAISDK(model.package))
return yield* new InitError({
providerID: model.providerID,
cause: new Error(`Unsupported package ${model.package}`),
})
const packageName = ProviderV2.packageName(model.package)
const packageName = Provider.packageName(model.package)
const options = prepareOptions(model, packageName)
const sdkKey = cacheKey({
providerID: model.providerID,
@ -301,8 +301,8 @@ export const locationLayer = Layer.effect(
export const defaultLayer = locationLayer
function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
const packageName = ProviderV2.packageName(info.package!)
function modelFromLanguage(info: Info, language: LanguageModelV3) {
const packageName = Provider.packageName(info.package!)
const projected = mapBodyToProviderOptions(info, packageName)
const optionKey = providerOptionKey(packageName, info.providerID)
const providerOptions = (() => {
@ -352,7 +352,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
})
}
function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<string, unknown>>) {
function gatewayProviderOptions(modelID: ID, settings: Readonly<Record<string, unknown>>) {
const gateway =
typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway)
? Object.fromEntries(Object.entries(settings.gateway))
@ -369,7 +369,7 @@ function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<s
return { gateway: model }
}
function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
function providerOptionKey(packageName: string | undefined, providerID: Provider.ID) {
if (packageName === "@ai-sdk/google") return "google"
if (packageName === "@ai-sdk/google-vertex") return "vertex"
if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic"
@ -395,18 +395,18 @@ function requestSettings(settings: Readonly<Record<string, unknown>> | undefined
return Object.keys(result).length === 0 ? undefined : result
}
function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) {
function mapBodyToProviderOptions(model: Info, packageName: string) {
const settings = requestSettings(model.settings)
const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning)
const forceReasoning =
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) &&
(pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined)
const normalized = forceReasoning ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings
const normalized = forceReasoning ? Provider.mergeOverlay(settings, { forceReasoning: true }) : settings
if (!pro) return { settings: normalized, body: model.body }
const body = { ...model.body }
delete body.reasoning
return {
settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }),
settings: Provider.mergeOverlay(normalized, { reasoningMode: "pro" }),
body: Object.keys(body).length === 0 ? undefined : body,
}
}

View file

@ -1,8 +1,7 @@
export * as EventV2 from "./event"
export * as Bus from "./bus"
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event"
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
import type { EventLog } from "@opencode-ai/schema/event-log"
import { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm"
import { Database } from "./database/database"
@ -12,18 +11,10 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
export const ID = Event.ID
export type ID = import("@opencode-ai/schema/event").ID
export const Seq = Event.Seq
export type Seq = import("@opencode-ai/schema/event").Seq
export const Version = Event.Version
export type Version = import("@opencode-ai/schema/event").Version
export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
export type Subscriber<D extends Event.Definition = Event.Definition> = (event: Event.Payload<D>) => Effect.Effect<void>
export type Unsubscribe = Effect.Effect<void>
export const latestSequence = Effect.fn("EventV2.latestSequence")(function* (
export const latestSequence = Effect.fn("Bus.latestSequence")(function* (
db: Database.Interface["db"],
aggregateID: string,
) {
@ -36,7 +27,7 @@ export const latestSequence = Effect.fn("EventV2.latestSequence")(function* (
return row?.seq ?? -1
})
export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* (
export const reserveSequence = Effect.fn("Bus.reserveSequence")(function* (
db: Database.Interface["db"],
aggregateID: string,
seq: number,
@ -53,7 +44,7 @@ export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* (
})
export type SerializedEvent = {
readonly id: ID
readonly id: Event.ID
readonly type: string
readonly created?: DateTime.Utc
readonly seq: number
@ -62,7 +53,7 @@ export type SerializedEvent = {
}
export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDurableEventError>()(
"EventV2.InvalidDurableEvent",
"Bus.InvalidDurableEvent",
{
type: Schema.String,
message: Schema.String,
@ -71,11 +62,11 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
const envelope = (aggregateID: string, seq: number, version: number) => ({
aggregateID,
seq: Seq.make(seq),
version: Version.make(version),
seq: Event.Seq.make(seq),
version: Event.Version.make(version),
})
const decodeSerializedEvent = (event: SerializedEvent): Payload => {
const decodeSerializedEvent = (event: SerializedEvent): Event.Payload => {
const definition = Durable.get(event.type)
if (!definition?.durable) {
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
@ -94,7 +85,7 @@ export const durable = Event.durable
export const ephemeral = Event.ephemeral
export interface PublishOptions {
readonly id?: ID
readonly id?: Event.ID
readonly metadata?: Record<string, unknown>
readonly location?: Location.Ref
/** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */
@ -102,13 +93,13 @@ export interface PublishOptions {
}
/** Marker/event union emitted by `log`. */
export type LogItem = Payload | EventLog.Synced
export type LogItem = Event.Payload | EventLog.Synced
export const isSynced = (item: LogItem): item is EventLog.Synced => item.type === "log.synced"
export type SubscribePayload<D extends readonly Definition[]> = D[number] extends infer Item
? Item extends Definition
? Payload<Item>
export type SubscribePayload<D extends readonly Event.Definition[]> = D[number] extends infer Item
? Item extends Event.Definition
? Event.Payload<Item>
: never
: never
@ -117,19 +108,19 @@ export interface Subscribe {
* Volatile live channel: every event published from now on, nothing before or
* across a disconnect. Consumers that need reliability combine it with `log`.
*/
(): Stream.Stream<Payload>
<D extends Definition>(definition: D): Stream.Stream<Payload<D>>
<const D extends readonly [Definition, ...Definition[]]>(definitions: D): Stream.Stream<SubscribePayload<D>>
(): Stream.Stream<Event.Payload>
<D extends Event.Definition>(definition: D): Stream.Stream<Event.Payload<D>>
<const D extends readonly [Event.Definition, ...Event.Definition[]]>(definitions: D): Stream.Stream<SubscribePayload<D>>
}
const isDefinition = (input: Definition | readonly Definition[]): input is Definition => !Array.isArray(input)
const isDefinition = (input: Event.Definition | readonly Event.Definition[]): input is Event.Definition => !Array.isArray(input)
export interface Interface {
readonly publish: <D extends Definition>(
readonly publish: <D extends Event.Definition>(
definition: D,
data: Data<D>,
data: Event.Data<D>,
options?: PublishOptions,
) => Effect.Effect<Payload<D>>
) => Effect.Effect<Event.Payload<D>>
readonly subscribe: Subscribe
/**
* Durable, ordered per-aggregate log read. Forked aggregates may reserve an
@ -144,10 +135,10 @@ export interface Interface {
readonly follow?: boolean
}) => Stream.Stream<LogItem>
/** Latest committed seq per aggregate. Aggregates without events are absent. */
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Seq>>
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Event.Seq>>
/** @deprecated Use `subscribe()` and consume the returned stream. */
readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
readonly project: <D extends Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
readonly project: <D extends Event.Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
readonly replay: (
event: SerializedEvent,
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
@ -160,7 +151,8 @@ export interface Interface {
readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Event") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Bus") {}
export interface LayerOptions {
readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void>
@ -173,20 +165,20 @@ export const layerWith = (options?: LayerOptions) =>
Service,
Effect.gen(function* () {
const pubsub = {
live: yield* PubSub.unbounded<Payload>(),
live: yield* PubSub.unbounded<Event.Payload>(),
durable: new Map<string, Set<PubSub.PubSub<void>>>(),
typed: new Map<string, PubSub.PubSub<Payload>>(),
typed: new Map<string, PubSub.PubSub<Event.Payload>>(),
}
const projectors = new Map<string, Subscriber[]>()
const listeners = new Array<Subscriber>()
const { db } = yield* Database.Service
const logReadPageSize = options?.logReadPageSize ?? 512
const getOrCreate = (definition: Definition) =>
const getOrCreate = (definition: Event.Definition) =>
Effect.gen(function* () {
const existing = pubsub.typed.get(definition.type)
if (existing) return existing
const created = yield* PubSub.unbounded<Payload>()
const created = yield* PubSub.unbounded<Event.Payload>()
pubsub.typed.set(definition.type, created)
return created
})
@ -204,8 +196,8 @@ export const layerWith = (options?: LayerOptions) =>
)
function commitDurableEvent(
definition: Definition,
event: Payload,
definition: Event.Definition,
event: Event.Payload,
input?: {
readonly seq: number
readonly aggregateID: string
@ -318,7 +310,7 @@ export const layerWith = (options?: LayerOptions) =>
const committed = {
...event,
durable: { aggregateID, seq, version: durable.version },
} as Payload
} as Event.Payload
for (const projector of list) {
yield* projector(committed)
}
@ -369,7 +361,7 @@ export const layerWith = (options?: LayerOptions) =>
})
}
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
function publishEvent<D extends Event.Definition>(definition: D, event: Event.Payload<D>, commit?: PublishOptions["commit"]) {
return Effect.gen(function* () {
if (!definition?.durable && commit)
return yield* Effect.die(
@ -379,22 +371,22 @@ export const layerWith = (options?: LayerOptions) =>
}),
)
if (definition?.durable) {
const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit)
const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
if (committed) {
event = {
...event,
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
}
yield* notify(event as Payload, true)
yield* notify(event as Event.Payload, true)
return event
}
}
yield* notify(event as Payload, false)
yield* notify(event as Event.Payload, false)
return event
})
}
const observe = (event: Payload, observer: (event: Payload) => Effect.Effect<void>) =>
const observe = (event: Event.Payload, observer: (event: Event.Payload) => Effect.Effect<void>) =>
Effect.suspend(() => observer(event)).pipe(
Effect.catchCauseIf(
(cause) => !Cause.hasInterrupts(cause),
@ -402,7 +394,7 @@ export const layerWith = (options?: LayerOptions) =>
),
)
function notify(event: Payload, isolateListeners: boolean) {
function notify(event: Event.Payload, isolateListeners: boolean) {
return Effect.gen(function* () {
yield* Effect.forEach(
listeners,
@ -415,7 +407,7 @@ export const layerWith = (options?: LayerOptions) =>
})
}
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
function publish<D extends Event.Definition>(definition: D, data: Event.Data<D>, options?: PublishOptions) {
return Effect.gen(function* () {
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
const location =
@ -426,13 +418,13 @@ export const layerWith = (options?: LayerOptions) =>
return yield* publishEvent(
definition,
{
id: options?.id ?? ID.create(),
id: options?.id ?? Event.ID.create(),
created: yield* DateTime.now,
...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type,
...(location ? { location } : {}),
data,
} as Payload<D>,
} as Event.Payload<D>,
options?.commit,
)
})
@ -454,7 +446,7 @@ export const layerWith = (options?: LayerOptions) =>
created: event.created ?? DateTime.makeUnsafe(0),
type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data),
} as Payload
} as Event.Payload
const committed = yield* commitDurableEvent(definition, payload, {
seq: event.seq,
aggregateID: event.aggregateID,
@ -516,7 +508,7 @@ export const layerWith = (options?: LayerOptions) =>
.pipe(Effect.orDie)
}
const local = <A extends Payload>(stream: Stream.Stream<A>) =>
const local = <A extends Event.Payload>(stream: Stream.Stream<A>) =>
Stream.unwrap(
Effect.serviceOption(Location.Service).pipe(
Effect.map((location) =>
@ -536,12 +528,12 @@ export const layerWith = (options?: LayerOptions) =>
),
)
function subscribe(): Stream.Stream<Payload>
function subscribe<D extends Definition>(definition: D): Stream.Stream<Payload<D>>
function subscribe<const D extends readonly [Definition, ...Definition[]]>(
function subscribe(): Stream.Stream<Event.Payload>
function subscribe<D extends Event.Definition>(definition: D): Stream.Stream<Event.Payload<D>>
function subscribe<const D extends readonly [Event.Definition, ...Event.Definition[]]>(
definitions: D,
): Stream.Stream<SubscribePayload<D>>
function subscribe(input?: Definition | readonly Definition[]): Stream.Stream<Payload> {
function subscribe(input?: Event.Definition | readonly Event.Definition[]): Stream.Stream<Event.Payload> {
if (input === undefined) return streamLive()
if (isDefinition(input)) {
return local(Stream.unwrap(getOrCreate(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))))
@ -550,7 +542,7 @@ export const layerWith = (options?: LayerOptions) =>
return streamLive().pipe(Stream.filter((event) => types.has(event.type)))
}
const streamLive = (): Stream.Stream<Payload> => local(Stream.fromPubSub(pubsub.live))
const streamLive = (): Stream.Stream<Event.Payload> => local(Stream.fromPubSub(pubsub.live))
const readAfter = (
aggregateID: string,
@ -624,7 +616,7 @@ export const layerWith = (options?: LayerOptions) =>
Stream.unwrap(
Effect.gen(function* () {
let sequence = input.after ?? -1
const readThrough = (through: number): Stream.Stream<Payload> =>
const readThrough = (through: number): Stream.Stream<Event.Payload> =>
Stream.paginate(sequence, (cursor) =>
readAfter(input.aggregateID, cursor, { through, limit: logReadPageSize }).pipe(
Effect.tap((page) =>
@ -648,7 +640,7 @@ export const layerWith = (options?: LayerOptions) =>
const marker: EventLog.Synced = {
type: "log.synced",
aggregateID: input.aggregateID,
...(target >= 0 ? { seq: Seq.make(target) } : {}),
...(target >= 0 ? { seq: Event.Seq.make(target) } : {}),
}
const replay: Stream.Stream<LogItem> = readThrough(target).pipe(
Stream.map((event): LogItem => event),
@ -665,7 +657,7 @@ export const layerWith = (options?: LayerOptions) =>
}),
)
const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Seq>> => {
const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Event.Seq>> => {
if (aggregateIDs.length === 0) return Effect.succeed(new Map())
return db
.select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq })
@ -674,7 +666,7 @@ export const layerWith = (options?: LayerOptions) =>
.all()
.pipe(
Effect.orDie,
Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Seq.make(row.seq)]))),
Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Event.Seq.make(row.seq)]))),
)
}
@ -687,11 +679,11 @@ export const layerWith = (options?: LayerOptions) =>
})
})
const project = <D extends Definition>(definition: D, projector: Subscriber<D>): Effect.Effect<void> =>
const project = <D extends Event.Definition>(definition: D, projector: Subscriber<D>): Effect.Effect<void> =>
Effect.sync(() => {
const key = definition.durable ? versionedType(definition.type, definition.durable.version) : definition.type
const list = projectors.get(key) ?? []
list.push((event) => projector(event as Payload<D>))
list.push((event) => projector(event as Event.Payload<D>))
projectors.set(key, list)
})

View file

@ -3,82 +3,82 @@ export * as Catalog from "./catalog"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Array, Context, Effect, Layer, Order, pipe } from "effect"
import { Catalog } from "@opencode-ai/schema/catalog"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { EventV2 } from "./event"
import { Model } from "./model"
import { Provider } from "./provider"
import { Bus } from "./bus"
import { State } from "./state"
import { Integration } from "./integration"
export type ProviderRecord = {
provider: ProviderV2.MutableInfo
models: Map<ModelV2.ID, ModelV2.MutableInfo>
provider: Provider.MutableInfo
models: Map<Model.ID, Model.MutableInfo>
}
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
export type DefaultModel = { providerID: Provider.ID; modelID: Model.ID }
export const Event = Catalog.Event
export { Event } from "@opencode-ai/schema/catalog"
type Data = {
providers: Map<ProviderV2.ID, ProviderRecord>
providers: Map<Provider.ID, ProviderRecord>
defaultModel?: DefaultModel
}
export type Draft = {
provider: {
list: () => readonly ProviderRecord[]
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID) => void
get: (providerID: Provider.ID) => ProviderRecord | undefined
update: (providerID: Provider.ID, fn: (provider: Provider.MutableInfo) => void) => void
remove: (providerID: Provider.ID) => void
}
model: {
get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
get: (providerID: Provider.ID, modelID: Model.ID) => Model.Info | undefined
update: (providerID: Provider.ID, modelID: Model.ID, fn: (model: Model.MutableInfo) => void) => void
remove: (providerID: Provider.ID, modelID: Model.ID) => void
default: {
get: () => DefaultModel | undefined
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
set: (providerID: Provider.ID, modelID: Model.ID) => void
}
}
}
export interface Interface extends State.Transformable<Draft> {
readonly provider: {
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info | undefined>
readonly all: () => Effect.Effect<ProviderV2.Info[]>
readonly available: () => Effect.Effect<ProviderV2.Info[]>
readonly get: (providerID: Provider.ID) => Effect.Effect<Provider.Info | undefined>
readonly all: () => Effect.Effect<Provider.Info[]>
readonly available: () => Effect.Effect<Provider.Info[]>
}
readonly model: {
readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect<ModelV2.Info | undefined>
readonly all: () => Effect.Effect<ModelV2.Info[]>
readonly available: () => Effect.Effect<ModelV2.Info[]>
readonly default: () => Effect.Effect<ModelV2.Info | undefined>
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<ModelV2.Info | undefined>
readonly get: (providerID: Provider.ID, modelID: Model.ID) => Effect.Effect<Model.Info | undefined>
readonly all: () => Effect.Effect<Model.Info[]>
readonly available: () => Effect.Effect<Model.Info[]>
readonly default: () => Effect.Effect<Model.Info | undefined>
readonly small: (providerID: Provider.ID) => Effect.Effect<Model.Info | undefined>
}
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Catalog") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const integrations = yield* Integration.Service
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
const available = (provider: Provider.Info, integration: Integration.Info | undefined) => {
if (provider.disabled) return false
if (typeof provider.settings?.apiKey === "string") return true
if (integration?.connections.length) return true
return provider.integrationID === undefined && !integration
}
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
const projectModel = (model: Model.Info, provider: Provider.Info) => {
return {
...model,
package: model.package ?? provider.package,
settings: ProviderV2.mergeOverlay(provider.settings, model.settings),
headers: ProviderV2.mergeHeaders(provider.headers, model.headers),
body: ProviderV2.mergeOverlay(provider.body, model.body),
} satisfies ModelV2.Info
settings: Provider.mergeOverlay(provider.settings, model.settings),
headers: Provider.mergeHeaders(provider.headers, model.headers),
body: Provider.mergeOverlay(provider.body, model.body),
} satisfies Model.Info
}
const state = State.create<Data, Draft>({
@ -93,8 +93,8 @@ const layer = Layer.effect(
let current = draft.providers.get(providerID)
if (!current) {
current = {
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<Model.ID, Model.MutableInfo>(),
}
draft.providers.set(providerID, current)
}
@ -110,13 +110,13 @@ const layer = Layer.effect(
let record = draft.providers.get(providerID)
if (!record) {
record = {
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<Model.ID, Model.MutableInfo>(),
}
draft.providers.set(providerID, record)
}
const model =
record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo)
record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
if (!record.models.has(modelID)) record.models.set(modelID, model)
fn(model)
model.id = modelID
@ -135,8 +135,8 @@ const layer = Layer.effect(
}
return result
},
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
yield* events.publish(Event.Updated, {})
finalize: Effect.fn("Catalog.finalize")(function* (catalog) {
yield* bus.publish(Catalog.Event.Updated, {})
}),
})
const result: Interface = {
@ -144,15 +144,15 @@ const layer = Layer.effect(
reload: state.reload,
provider: {
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
get: Effect.fn("Catalog.provider.get")(function* (providerID) {
return state.get().providers.get(providerID)?.provider
}),
all: Effect.fn("CatalogV2.provider.all")(function* () {
all: Effect.fn("Catalog.provider.all")(function* () {
return Array.fromIterable(state.get().providers.values()).map((record) => record.provider)
}),
available: Effect.fn("CatalogV2.provider.available")(function* () {
available: Effect.fn("Catalog.provider.available")(function* () {
const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration]))
return (yield* result.provider.all()).filter((provider) =>
available(provider, active.get(provider.integrationID ?? Integration.ID.make(provider.id))),
@ -161,14 +161,14 @@ const layer = Layer.effect(
},
model: {
get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
get: Effect.fn("Catalog.model.get")(function* (providerID, modelID) {
const record = state.get().providers.get(providerID)
if (!record) return
const model = record.models.get(modelID)
return model && projectModel(model, record.provider)
}),
all: Effect.fn("CatalogV2.model.all")(function* () {
all: Effect.fn("Catalog.model.all")(function* () {
return pipe(
Array.fromIterable(state.get().providers.values()),
Array.flatMap((record) => {
@ -178,9 +178,9 @@ const layer = Layer.effect(
)
}),
available: Effect.fn("CatalogV2.model.available")(function* () {
available: Effect.fn("Catalog.model.available")(function* () {
const providers = new Set((yield* result.provider.available()).map((provider) => provider.id))
const models: ModelV2.Info[] = []
const models: Model.Info[] = []
for (const record of state.get().providers.values()) {
if (!providers.has(record.provider.id)) continue
for (const model of record.models.values()) {
@ -194,7 +194,7 @@ const layer = Layer.effect(
)
}),
default: Effect.fn("CatalogV2.model.default")(function* () {
default: Effect.fn("Catalog.model.default")(function* () {
const defaultModel = state.get().defaultModel
if (defaultModel) {
const provider = yield* result.provider.get(defaultModel.providerID)
@ -207,18 +207,18 @@ const layer = Layer.effect(
return (yield* result.model.available())[0]
}),
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
small: Effect.fn("Catalog.model.small")(function* (providerID) {
const record = state.get().providers.get(providerID)
if (!record) return
const provider = record.provider
// TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments.
if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) {
if (providerID === Provider.ID.azure || providerID === Provider.ID.make("azure-cognitive-services")) {
return
}
if (providerID === ProviderV2.ID.opencode) {
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
if (providerID === Provider.ID.opencode) {
const gpt5Nano = record.models.get(Model.ID.make("gpt-5-nano"))
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider)
}
@ -268,4 +268,4 @@ const layer = Layer.effect(
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Integration.node] })

View file

@ -1,77 +0,0 @@
export * as CodeMode from "./codemode"
import { Context, Effect, Layer, Scope } from "effect"
import { CodeModeCatalog } from "./codemode/catalog"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { PermissionV2 } from "./permission"
import { ExecuteTool } from "./tool/execute"
import type { Any, Registration } from "./tool/tool"
import { Wildcard } from "./util/wildcard"
export interface Materialization {
readonly tool?: Any
readonly catalog?: ReadonlyArray<CodeModeCatalog.Entry>
}
export interface Interface {
readonly register: (
registrations: ReadonlyArray<Registration & { readonly key: string }>,
) => Effect.Effect<void, never, Scope.Scope>
readonly materialize: (permissions?: PermissionV2.Ruleset) => Effect.Effect<Materialization>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/CodeMode") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const local = new Map<string, Array<{ readonly token: object; readonly registration: ExecuteTool.Registration }>>()
return Service.of({
register: Effect.fn("CodeMode.register")(function* (registrations) {
if (registrations.length === 0) return
yield* Effect.uninterruptible(
Effect.gen(function* () {
const token = {}
for (const registration of registrations)
local.set(registration.key, [
...(local.get(registration.key) ?? []),
{
token,
registration,
},
])
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
for (const registration of registrations) {
const remaining = local.get(registration.key)?.filter((item) => item.token !== token) ?? []
if (remaining.length > 0) local.set(registration.key, remaining)
else local.delete(registration.key)
}
}),
)
}),
)
}),
materialize: Effect.fn("CodeMode.materialize")(function* (permissions) {
const registrations = new Map<string, ExecuteTool.Registration>()
const rules = permissions ?? []
for (const [name, entries] of local) {
const registration = entries.at(-1)?.registration
if (!registration) continue
const rule = rules.findLast((rule) => Wildcard.match(registration.permission, rule.action))
if (rule?.resource === "*" && rule.effect === "deny") continue
registrations.set(name, registration)
}
const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action))
if (executeRule?.resource === "*" && executeRule.effect === "deny") return {}
return {
tool: ExecuteTool.create(registrations),
catalog: ExecuteTool.catalog(registrations),
}
}),
})
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [] })

View file

@ -1,10 +1,9 @@
export * as ExecuteTool from "./execute"
export type { Registration } from "./tool"
export * as CodeModeTool from "./tool"
import { CodeMode, Tool, toolError } from "@opencode-ai/codemode"
import type { ToolContent } from "@opencode-ai/ai"
import type { Content, Context, Error, Info, Metadata, Result } from "@opencode-ai/schema/tool"
import { Effect, Ref, Schema, Semaphore } from "effect"
import { execute, make, toLLMDefinition, type Content, type Metadata, type Registration } from "./tool"
import { definition } from "../tool/runtime"
const ExecuteFile = Schema.Struct({
data: Schema.String,
@ -42,8 +41,17 @@ const description = [
"Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.",
].join("\n")
export const create = (registrations: ReadonlyMap<string, Registration>) => {
return make({
export const create = (
registrations: ReadonlyMap<string, Info>,
executeTool: (
name: string,
tool: Info,
input: unknown,
context: Context,
) => Effect.Effect<Result, Error>,
) => {
return ({
name: "execute",
description,
input: CodeMode.Input,
output: ExecuteOutput,
@ -59,17 +67,17 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
)
const result = yield* runtime(
registrations,
(name, registration, input) =>
(name, tool, input) =>
Effect.gen(function* () {
const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1)
const executed = yield* execute(registration.tool, input, {
sessionID: context.sessionID,
agent: context.agent,
messageID: context.messageID,
callID: context.callID,
progress: () => Effect.void,
}).pipe(Effect.mapError((failure) => toolError(failure.message, failure)))
const outputFileParts = outputFiles(executed.content)
const executed = yield* executeTool(name, tool, input, context).pipe(
Effect.mapError((failure) => toolError(failure.message, failure)),
)
const content =
typeof executed.content === "string"
? [{ type: "text" as const, text: executed.content }]
: executed.content ?? []
const outputFileParts = outputFiles(content)
if (outputFileParts.length > 0)
yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }])
return executed.output
@ -107,11 +115,11 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
files: collected,
...(result.ok ? {} : { error: true }),
}
const content: [Content, ...Content[]] = [{ type: "text", text: value.output }]
const content: Array<Content> = [{ type: "text", text: value.output }]
content.push(
...value.files.map((file) => ({
type: "file" as const,
data: file.data,
uri: `data:${file.mime};base64,${file.data}`,
mime: file.mime,
...(file.name === undefined ? {} : { name: file.name }),
})),
@ -126,23 +134,26 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
metadata,
}
}),
})
}) satisfies Info
}
export const catalog = (registrations: ReadonlyMap<string, Registration>) => {
export const catalog = (registrations: ReadonlyMap<string, Info>) => {
return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).catalog()
}
function runtime(
registrations: ReadonlyMap<string, Registration>,
executeTool: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
registrations: ReadonlyMap<string, Info>,
executeTool: (name: string, tool: Info, input: unknown) => Effect.Effect<unknown, unknown>,
hooks?: CodeMode.ToolCallHooks,
) {
const tools: Record<string, Tool.Tool<never>> = {}
for (const [name, registration] of registrations) {
const child = toLLMDefinition(name, registration.tool)
const child = definition(registration)
const normalized = registration.name.replace(/[^a-zA-Z0-9_-]/g, "_")
const path =
registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}`
registration.options?.namespace === undefined
? normalized
: `${registration.options.namespace}.${normalized}`
tools[path] = Tool.make({
description: child.description,
input: child.inputSchema,
@ -180,7 +191,7 @@ function formatValue(value: CodeMode.DataValue) {
return JSON.stringify(value, null, 2) ?? String(value)
}
function outputFiles(content: ReadonlyArray<ToolContent>): Array<typeof ExecuteFile.Type> {
function outputFiles(content: ReadonlyArray<Content>): Array<typeof ExecuteFile.Type> {
return content.flatMap((part) => {
if (part.type !== "file") return []
const prefix = `data:${part.mime};base64,`

View file

@ -1,11 +1,11 @@
export * as CommandV2 from "./command"
export * as Command from "./command"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Schema, Types } from "effect"
import { Command } from "@opencode-ai/schema/command"
import { State } from "./state"
import { MCP } from "./mcp/index"
import { EventV2 } from "./event"
import { Bus } from "./bus"
import { AppProcess } from "@opencode-ai/util/process"
import { ChildProcess } from "effect/unstable/process"
import { Config } from "./config"
@ -14,7 +14,7 @@ import { ShellSelect } from "./shell/select"
export const Info = Command.Info
export type Info = Command.Info
export const Event = Command.Event
export { Event } from "@opencode-ai/schema/command"
export type Evaluation = {
readonly text: string
@ -50,13 +50,13 @@ export interface Interface extends State.Transformable<Draft> {
}) => Effect.Effect<Evaluation, NotFoundError | EvaluationError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Command") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
export const layer = (options?: ShellSelect.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const mcp = yield* MCP.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const processes = yield* AppProcess.Service
const config = yield* Config.Service
const location = yield* Location.Service
@ -76,7 +76,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
draft.commands.delete(name)
},
}),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
})
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
const mcpCommands = Effect.fnUntraced(function* () {
@ -92,12 +92,12 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
return Service.of({
reload: state.reload,
transform: state.transform,
get: Effect.fn("CommandV2.get")(function* (name) {
get: Effect.fn("Command.get")(function* (name) {
const command = staticCommand(name)
if (command) return command
return (yield* mcpCommands()).find((command) => command.name === name)
}),
list: Effect.fn("CommandV2.list")(function* () {
list: Effect.fn("Command.list")(function* () {
const commands = Array.from(state.get().commands.values()) as Info[]
const names = new Set(commands.map((command) => command.name))
return [
@ -105,7 +105,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
...(yield* mcpCommands()).filter((command) => !names.has(command.name)),
]
}),
evaluate: Effect.fn("CommandV2.evaluate")(function* (input) {
evaluate: Effect.fn("Command.evaluate")(function* (input) {
const command = staticCommand(input.name)
if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", {
config,
@ -247,7 +247,7 @@ export function configured(options?: ShellSelect.Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [MCP.node, EventV2.node, AppProcess.node, Config.node, Location.node],
deps: [MCP.node, Bus.node, AppProcess.node, Config.node, Location.node],
})
}

View file

@ -9,7 +9,7 @@ import { Permission } from "@opencode-ai/schema/permission"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential"
import { EventV2 } from "./event"
import { Bus } from "./bus"
import { Watcher } from "./filesystem/watcher"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Global } from "@opencode-ai/util/global"
@ -168,7 +168,7 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Config") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
export const layer = (options?: Options) => Layer.effect(
Service,
@ -177,7 +177,7 @@ export const layer = (options?: Options) => Layer.effect(
const global = yield* Global.Service
const location = yield* Location.Service
const watcher = yield* Watcher.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const credentials = yield* Credential.Service
const wellknown = yield* WellKnown.Service
const names = ["opencode.json", "opencode.jsonc"]
@ -375,7 +375,7 @@ export const layer = (options?: Options) => Layer.effect(
if (isDeepStrictEqual(configs, next)) return
configs = next
yield* reconcile(next)
yield* events.publish(ConfigSchema.Event.Updated, {})
yield* bus.publish(ConfigSchema.Event.Updated, {})
}),
),
)
@ -389,7 +389,7 @@ export const layer = (options?: Options) => Layer.effect(
),
Effect.forkScoped({ startImmediately: true }),
)
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filterEffect((event) =>
wellknown.entries().pipe(
Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
@ -401,7 +401,7 @@ export const layer = (options?: Options) => Layer.effect(
),
Effect.forkScoped({ startImmediately: true }),
)
yield* events.subscribe(WellKnown.Event.Updated).pipe(
yield* bus.subscribe(WellKnown.Event.Updated).pipe(
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
),
@ -438,7 +438,7 @@ export function configured(options?: Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
deps: [Watcher.node, Bus.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
})
}

View file

@ -8,7 +8,7 @@ import { PositiveInt } from "../schema"
export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
export class Info extends Schema.Class<Info>("Config.Agent")({
model: ConfigModel.Selection.pipe(Schema.optional),
request: ConfigProvider.Request.pipe(Schema.optional),
system: Schema.String.pipe(Schema.optional),

View file

@ -3,13 +3,13 @@ export * as ConfigAttachments from "./attachments"
import { Schema } from "effect"
import { PositiveInt } from "../schema"
export class Image extends Schema.Class<Image>("ConfigV2.Attachments.Image")({
export class Image extends Schema.Class<Image>("Config.Attachments.Image")({
auto_resize: Schema.Boolean.pipe(Schema.optional),
max_width: PositiveInt.pipe(Schema.optional),
max_height: PositiveInt.pipe(Schema.optional),
max_base64_bytes: PositiveInt.pipe(Schema.optional),
}) {}
export class Info extends Schema.Class<Info>("ConfigV2.Attachments")({
export class Info extends Schema.Class<Info>("Config.Attachments")({
image: Image.pipe(Schema.optional),
}) {}

View file

@ -3,7 +3,7 @@ export * as ConfigCommand from "./command"
import { Schema } from "effect"
import { ConfigModel } from "./model"
export class Info extends Schema.Class<Info>("ConfigV2.Command")({
export class Info extends Schema.Class<Info>("Config.Command")({
template: Schema.String,
description: Schema.String.pipe(Schema.optional),
agent: Schema.String.pipe(Schema.optional),

View file

@ -3,11 +3,11 @@ export * as ConfigCompaction from "./compaction"
import { Schema } from "effect"
import { NonNegativeInt } from "../schema"
export class Keep extends Schema.Class<Keep>("ConfigV2.Compaction.Keep")({
export class Keep extends Schema.Class<Keep>("Config.Compaction.Keep")({
tokens: NonNegativeInt.pipe(Schema.optional),
}) {}
export class Info extends Schema.Class<Info>("ConfigV2.Compaction")({
export class Info extends Schema.Class<Info>("Config.Compaction")({
auto: Schema.Boolean.pipe(Schema.optional),
keep: Keep.pipe(Schema.optional),
buffer: NonNegativeInt.pipe(Schema.optional),

View file

@ -2,7 +2,7 @@ export * as ConfigFormatter from "./formatter"
import { Schema } from "effect"
export class Entry extends Schema.Class<Entry>("ConfigV2.Formatter.Entry")({
export class Entry extends Schema.Class<Entry>("Config.Formatter.Entry")({
disabled: Schema.Boolean.pipe(Schema.optional),
command: Schema.String.pipe(Schema.Array, Schema.optional),
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),

View file

@ -6,7 +6,7 @@ export const Disabled = Schema.Struct({
disabled: Schema.Literal(true),
})
export class Server extends Schema.Class<Server>("ConfigV2.LSP.Server")({
export class Server extends Schema.Class<Server>("Config.LSP.Server")({
command: Schema.String.pipe(Schema.Array),
extensions: Schema.String.pipe(Schema.Array, Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional),

View file

@ -15,7 +15,7 @@ export const Remote = Mcp.RemoteConfig
export type Remote = Mcp.RemoteConfig
export const Server = Mcp.ServerConfig
export class Info extends Schema.Class<Info>("ConfigV2.MCP")({
export class Info extends Schema.Class<Info>("Config.MCP")({
timeout: Timeout.pipe(Schema.optional),
servers: Schema.Record(Schema.String, Server).pipe(Schema.optional),
}) {}

View file

@ -2,7 +2,7 @@ export * as ConfigPlugin from "./plugin"
import { Schema } from "effect"
export class Entry extends Schema.Class<Entry>("ConfigV2.Plugin.Entry")({
export class Entry extends Schema.Class<Entry>("Config.Plugin.Entry")({
package: Schema.String,
options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}) {}

View file

@ -1,9 +1,9 @@
export * as ConfigAgentPlugin from "./agent"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path"
import { Effect, Option, Schema, Stream } from "effect"
import { AgentV2 } from "../../agent"
import { Agent } from "../../agent"
import { Config } from "../../config"
import { ConfigAgent } from "../agent"
import { ConfigMarkdown } from "../markdown"
@ -11,10 +11,10 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
import { ConfigAgentV1 } from "../../v1/config/agent"
import { ConfigMigrateV1 } from "../../v1/config/migrate"
import { Global } from "@opencode-ai/util/global"
import { PermissionV2 } from "../../permission"
import { Permission } from "../../permission"
import type { LocationMutation } from "../../location-mutation"
import type { ReadTool } from "../../tool/read"
import type { EditTool } from "../../tool/edit"
import type { ReadTool } from "../../tool/plugin/read"
import type { EditTool } from "../../tool/plugin/edit"
const legacySources = [
{ pattern: "{agent,agents}/**/*.md", primary: false },
@ -74,14 +74,14 @@ export const Plugin = define({
global.home,
)
const configuredDefault = Config.latest(loaded.documents, "default_agent")
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
if (configuredDefault !== undefined) draft.default(Agent.ID.make(configuredDefault))
for (const current of draft.list()) {
draft.update(current.id, (agent) => agent.permissions.push(...permissions))
}
for (const document of loaded.documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = AgentV2.ID.make(id)
const agentID = Agent.ID.make(id)
if (item.disabled) {
draft.remove(agentID)
continue
@ -126,7 +126,7 @@ export const Plugin = define({
}),
})
function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset {
function expandPermissions(rules: Permission.Ruleset, home: string): Permission.Ruleset {
// Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text:
// rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing.
return rules.map((rule) =>

View file

@ -1,9 +1,9 @@
export * as ConfigCommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path"
import { Effect, Option, Schema, Stream } from "effect"
import { CommandV2 } from "../../command"
import { Command } from "../../command"
import { Config } from "../../config"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { ConfigCommand } from "../command"

View file

@ -1,6 +1,6 @@
export * as ConfigPolicyPlugin from "./policy"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { Wildcard } from "../../util/wildcard"

View file

@ -1,10 +1,10 @@
export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
export const Plugin = define({
id: "opencode.config.provider",
@ -49,9 +49,9 @@ export const Plugin = define({
if (item.name !== undefined) provider.name = item.name
if (item.package !== undefined) provider.package = item.package
if (item.settings !== undefined)
provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings)
if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers)
if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body)
provider.settings = Provider.mergeOverlay(provider.settings, item.settings)
if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers)
if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body)
})
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
@ -62,9 +62,9 @@ export const Plugin = define({
model.compatibility = { ...model.compatibility, ...config.compatibility }
if (config.package !== undefined) model.package = config.package
if (config.settings !== undefined)
model.settings = ProviderV2.mergeOverlay(model.settings, config.settings)
if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers)
if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body)
model.settings = Provider.mergeOverlay(model.settings, config.settings)
if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers)
if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body)
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
@ -81,10 +81,10 @@ export const Plugin = define({
model.variants.push(existing)
}
if (variant.settings !== undefined)
existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings)
existing.settings = Provider.mergeOverlay(existing.settings, variant.settings)
if (variant.headers !== undefined)
existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body)
existing.headers = Provider.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body)
}
}
if (config.cost !== undefined) {

View file

@ -1,6 +1,6 @@
export * as ConfigReferencePlugin from "./reference"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path"
import { Effect, Stream } from "effect"
import { Config } from "../../config"

View file

@ -1,11 +1,11 @@
export * as ConfigSkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { AbsolutePath } from "../../schema"
import { SkillV2 } from "../../skill"
import { Skill } from "../../skill"
import { Global } from "@opencode-ai/util/global"
import { Location } from "../../location"
@ -23,7 +23,7 @@ export const Plugin = define({
const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
for (const directory of [...claude, ...agents]) {
draft.source(
SkillV2.DirectorySource.make({
Skill.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")),
}),
@ -31,10 +31,10 @@ export const Plugin = define({
}
for (const directory of directories) {
draft.source(
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
)
draft.source(
SkillV2.DirectorySource.make({
Skill.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")),
}),
@ -42,12 +42,12 @@ export const Plugin = define({
}
for (const item of items) {
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
draft.source(SkillV2.UrlSource.make({ type: "url", url: item }))
draft.source(Skill.UrlSource.make({ type: "url", url: item }))
continue
}
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
draft.source(
SkillV2.DirectorySource.make({
Skill.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
}),

View file

@ -1,6 +1,6 @@
export * as ConfigWebSearchPlugin from "./websearch"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect, Stream } from "effect"
import { Config } from "../../config"

View file

@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider"
import { Schema } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { ModelV2 } from "../model"
import { Capabilities, Compatibility, Family, ID, VariantID } from "../model"
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
@ -12,17 +12,17 @@ export const Overlays = {
body: JsonRecord.pipe(Schema.optional),
}
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
export class Request extends Schema.Class<Request>("Config.Provider.Request")({
headers: Overlays.headers,
body: Overlays.body,
}) {}
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
class Cache extends Schema.Class<Cache>("Config.Model.Cost.Cache")({
read: Money.USDPerMillionTokens.pipe(Schema.optional),
write: Money.USDPerMillionTokens.pipe(Schema.optional),
}) {}
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
class Cost extends Schema.Class<Cost>("Config.Model.Cost")({
tier: Schema.Struct({
type: Schema.Literal("context"),
size: Schema.Int,
@ -32,22 +32,22 @@ class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
cache: Cache.pipe(Schema.optional),
}) {}
class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
class Limit extends Schema.Class<Limit>("Config.Model.Limit")({
context: Schema.Int.pipe(Schema.optional),
input: Schema.Int.pipe(Schema.optional),
output: Schema.Int.pipe(Schema.optional),
}) {}
class Model extends Schema.Class<Model>("ConfigV2.Model")({
modelID: ModelV2.ID.pipe(Schema.optional),
family: ModelV2.Family.pipe(Schema.optional),
class Model extends Schema.Class<Model>("Config.Model")({
modelID: ID.pipe(Schema.optional),
family: Family.pipe(Schema.optional),
name: Schema.String.pipe(Schema.optional),
compatibility: ModelV2.Compatibility.pipe(Schema.optional),
compatibility: Compatibility.pipe(Schema.optional),
package: Schema.String.pipe(Schema.optional),
...Overlays,
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
capabilities: Capabilities.pipe(Schema.optional),
variants: Schema.Struct({
id: ModelV2.VariantID,
id: VariantID,
...Overlays,
}).pipe(Schema.Array, Schema.optional),
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
@ -55,7 +55,7 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
limit: Limit.pipe(Schema.optional),
}) {}
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
export class Info extends Schema.Class<Info>("Config.Provider")({
name: Schema.String.pipe(Schema.optional),
env: Schema.String.pipe(Schema.Array, Schema.optional),
package: Schema.String.pipe(Schema.optional),

View file

@ -2,14 +2,14 @@ export * as ConfigReference from "./reference"
import { Schema } from "effect"
export class Git extends Schema.Class<Git>("ConfigV2.Reference.Git")({
export class Git extends Schema.Class<Git>("Config.Reference.Git")({
repository: Schema.String,
branch: Schema.String.pipe(Schema.optional),
description: Schema.String.pipe(Schema.optional),
hidden: Schema.Boolean.pipe(Schema.optional),
}) {}
export class Local extends Schema.Class<Local>("ConfigV2.Reference.Local")({
export class Local extends Schema.Class<Local>("Config.Reference.Local")({
path: Schema.String,
description: Schema.String.pipe(Schema.optional),
hidden: Schema.Boolean.pipe(Schema.optional),

View file

@ -3,7 +3,7 @@ export * as ConfigToolOutput from "./tool-output"
import { Schema } from "effect"
import { PositiveInt } from "../schema"
export class Info extends Schema.Class<Info>("ConfigV2.ToolOutput")({
export class Info extends Schema.Class<Info>("Config.ToolOutput")({
max_lines: PositiveInt.pipe(Schema.optional),
max_bytes: PositiveInt.pipe(Schema.optional),
}) {}

View file

@ -2,7 +2,7 @@ export * as ConfigWarming from "./warming"
import { Schema } from "effect"
export class Info extends Schema.Class<Info>("ConfigV2.Warming")({
export class Info extends Schema.Class<Info>("Config.Warming")({
prompt: Schema.String.pipe(Schema.optional).annotate({
description: "Prompt sent for keep-alive requests",
}),

View file

@ -2,6 +2,6 @@ export * as ConfigWatcher from "./watcher"
import { Schema } from "effect"
export class Info extends Schema.Class<Info>("ConfigV2.Watcher")({
export class Info extends Schema.Class<Info>("Config.Watcher")({
ignore: Schema.String.pipe(Schema.Array, Schema.optional),
}) {}

View file

@ -5,8 +5,8 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "../git"
import { Global } from "@opencode-ai/util/global"
import { ProjectV2 } from "../project"
import { SessionV2 } from "../session"
import { Project } from "../project"
import { Session } from "../session"
import { SessionExecution } from "../session/execution"
import { SessionSchema } from "../session/schema"
import { SessionStore } from "../session/store"
@ -28,8 +28,8 @@ export type Input = typeof Input.Type
export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<DestinationProjectMismatchError>()(
"MoveSession.DestinationProjectMismatchError",
{
expected: ProjectV2.ID,
actual: ProjectV2.ID,
expected: Project.ID,
actual: Project.ID,
},
) {}
@ -64,12 +64,12 @@ export class ResetSourceChangesError extends Schema.TaggedErrorClass<ResetSource
) {}
export type Error =
| SessionV2.NotFoundError
| Session.NotFoundError
| DestinationProjectMismatchError
| DestinationNotFoundError
| DestinationNotDirectoryError
| SessionV2.DestinationNotFoundError
| SessionV2.DestinationNotDirectoryError
| Session.DestinationNotFoundError
| Session.DestinationNotDirectoryError
| CaptureChangesError
| ApplyChangesError
| ResetSourceChangesError
@ -86,14 +86,14 @@ const layer = Layer.effect(
const git = yield* Git.Service
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const project = yield* ProjectV2.Service
const project = yield* Project.Service
const sessions = yield* SessionStore.Service
const session = yield* SessionV2.Service
const session = yield* Session.Service
const execution = yield* SessionExecution.Service
const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) {
const current = yield* sessions.get(input.sessionID)
if (!current) return yield* new SessionV2.NotFoundError({ sessionID: input.sessionID })
if (!current) return yield* new Session.NotFoundError({ sessionID: input.sessionID })
const value = input.destination.directory.trim()
const expanded = value === "~" ? global.home : value.startsWith("~/") ? path.join(global.home, value.slice(2)) : value
const directory = AbsolutePath.make(path.resolve(current.location.directory, expanded))
@ -173,8 +173,8 @@ export const node = makeGlobalNode({
FSUtil.node,
Git.node,
Global.node,
ProjectV2.node,
SessionV2.node,
Project.node,
Session.node,
SessionStore.node,
SessionExecution.node,
],

View file

@ -1,17 +1,17 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/sql"
import { ProjectV2 } from "../project"
import { WorkspaceV2 } from "../workspace"
import { Project } from "../project"
import { Workspace } from "../workspace"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().$type<WorkspaceV2.ID>().primaryKey(),
id: text().$type<Workspace.ID>().primaryKey(),
type: text().notNull(),
name: text().notNull().default(""),
branch: text(),
directory: text(),
extra: text({ mode: "json" }),
project_id: text()
.$type<ProjectV2.ID>()
.$type<Project.ID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
time_used: integer()

View file

@ -46,7 +46,7 @@ export interface Interface {
readonly remove: (id: ID) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Credential") {}
const layer = Layer.effect(
Service,

View file

@ -20,7 +20,7 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/storage/Database") {}
const databaseLayer = Layer.effect(
Service,

View file

@ -2,7 +2,7 @@ export * as EventLogger from "./event-logger"
import { Effect, Layer } from "effect"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "./event"
import { Bus } from "./bus"
const Types = new Set([
"agent.updated",
@ -13,12 +13,12 @@ const Types = new Set([
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const events = yield* EventV2.Service
const unsubscribe = yield* events.listen((event) =>
const bus = yield* Bus.Service
const unsubscribe = yield* bus.listen((event) =>
Types.has(event.type) ? Effect.logInfo("event", { event }) : Effect.void,
)
yield* Effect.addFinalizer(() => unsubscribe)
}),
)
export const node = makeGlobalNode({ name: "event-logger", layer, deps: [EventV2.node] })
export const node = makeGlobalNode({ name: "event-logger", layer, deps: [Bus.node] })

View file

@ -1,5 +1,5 @@
import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core"
import type { EventV2 } from "../event"
import { Event } from "@opencode-ai/schema/event"
export const EventSequenceTable = sqliteTable("event_sequence", {
aggregate_id: text().notNull().primaryKey(),
@ -10,7 +10,7 @@ export const EventSequenceTable = sqliteTable("event_sequence", {
export const EventTable = sqliteTable(
"event",
{
id: text().$type<EventV2.ID>().primaryKey(),
id: text().$type<Event.ID>().primaryKey(),
aggregate_id: text()
.notNull()
.references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }),

View file

@ -64,7 +64,7 @@ export interface Interface {
readonly remove: (input: RemoveInput) => Effect.Effect<RemoveResult, FSUtil.Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileMutation") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileMutation") {}
/**
* Serialize file changes by canonical target. Conditional writes compare and
@ -194,12 +194,12 @@ function sameBytes(left: Uint8Array, right: Uint8Array) {
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })
/**
* Deferred until the corresponding V2 integrations exist.
* Deferred until the corresponding integrations exist.
*/
// TODO: Add formatter integration after V2 formatter runtime exists.
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
// TODO: Add snapshots / undo after V2 snapshot design exists.
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
// TODO: Add formatter integration after formatter runtime exists.
// TODO: Publish watcher/file-edit events after watcher integration exists.
// TODO: Add snapshots / undo after snapshot design exists.
// TODO: Notify LSP and collect diagnostics after LSP runtime exists.
// TODO: Design multi-file transactions / rollback if patch needs atomic edits.
// Until then, edits are sequential and report partial application.
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.

View file

@ -56,7 +56,7 @@ export interface Interface {
readonly grep: (input: GrepInput) => Effect.Effect<readonly Match[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
const baseLayer = Layer.effect(
Service,

View file

@ -6,7 +6,7 @@ import { FileSystem } from "@opencode-ai/schema/filesystem"
import os from "os"
import path from "path"
import { Config } from "../config"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "../git"
import { Location } from "../location"
@ -30,12 +30,12 @@ const layer = Layer.effect(
Effect.gen(function* () {
const location = yield* Location.Service
const watcher = yield* Watcher.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const fs = yield* FSUtil.Service
const git = yield* Git.Service
const configService = yield* Config.Service
const publish = (update: { type: "create" | "update" | "delete"; path: string }) =>
events.publish(FileSystem.Event.Changed, {
bus.publish(FileSystem.Event.Changed, {
file: update.path,
event: update.type === "create" ? "add" : update.type === "update" ? "change" : "unlink",
})
@ -86,5 +86,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, Bus.node],
})

View file

@ -22,7 +22,7 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem/Search") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem/Search") {}
export const ripgrepLayer = Layer.effect(
Service,

View file

@ -3,7 +3,7 @@ export * as Form from "./form"
import { Form } from "@opencode-ai/schema/form"
import { Cache, Context, Deferred, Duration, Effect, Exit, Layer, Option, Schema } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "./event"
import { Bus } from "./bus"
const RETENTION = Duration.minutes(10)
@ -32,7 +32,7 @@ export type Answer = typeof Answer.Type
export const Reply = Form.Reply
export type Reply = typeof Reply.Type
export const Event = Form.Event
export { Event } from "@opencode-ai/schema/form"
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Form.NotFoundError", {
id: ID,
@ -99,7 +99,7 @@ interface Entry {
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const forms = yield* Cache.makeWith<ID, Entry>(
() => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")),
{
@ -141,7 +141,7 @@ export const layer = Layer.effect(
deferred: yield* Deferred.make<TerminalState>(),
}
yield* Cache.set(forms, id, entry)
yield* events.publish(Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
yield* bus.publish(Form.Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
return form
}),
),
@ -183,7 +183,7 @@ export const layer = Layer.effect(
const invalid = validateAnswer(entry.form, input.answer)
if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid })
const next: TerminalState = { status: "answered", answer: input.answer }
yield* events.publish(Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
yield* bus.publish(Form.Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
yield* Cache.set(forms, input.id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next)
}),
@ -196,7 +196,7 @@ export const layer = Layer.effect(
const entry = yield* find(id)
if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id })
const next: TerminalState = { status: "cancelled" }
yield* events.publish(Event.Cancelled, { id, sessionID: entry.form.sessionID })
yield* bus.publish(Form.Event.Cancelled, { id, sessionID: entry.form.sessionID })
yield* Cache.set(forms, id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next)
}),
@ -221,7 +221,7 @@ export const layer = Layer.effect(
export const locationLayer = layer
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })
function validateAnswer(form: Info, answer: Answer) {
const fields = new Map(form.fields.map((field) => [field.key, field] as const))

View file

@ -5,11 +5,11 @@ import { Context, Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { llmClient } from "./effect/app-node-platform"
import { ModelResolver } from "./model-resolver"
import { ModelV2 } from "./model"
import { Model } from "./model"
export interface TextInput {
readonly prompt: string
readonly model?: ModelV2.Ref
readonly model?: Model.Ref
}
export class ModelSelectionError extends Schema.TaggedErrorClass<ModelSelectionError>()(
@ -28,7 +28,7 @@ export interface Interface {
readonly text: (input: TextInput) => Effect.Effect<string, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Generate") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Generate") {}
export const layer = Layer.effect(
Service,

View file

@ -189,7 +189,7 @@ export interface Interface {
}
}
export class Service extends Context.Service<Service, Interface>()("@opencode/GitV2") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {}
const layer = Layer.effect(
Service,

View file

@ -2,8 +2,8 @@ export * as CopilotModels from "./models"
import { Money } from "@opencode-ai/schema/money"
import { Option, Schema } from "effect"
import { ModelV2 } from "../model"
import { ProviderV2 } from "../provider"
import { Model } from "../model"
import { Provider } from "../provider"
const RemoteModel = Schema.Struct({
model_picker_enabled: Schema.Boolean,
@ -70,7 +70,7 @@ type UsableModel = RemoteModel & {
}
}
export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly ModelV2.Info[]) {
export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly Model.Info[]) {
const response = await fetch(`${baseURL}/models`, {
headers,
signal: AbortSignal.timeout(5_000),
@ -97,7 +97,7 @@ export async function get(baseURL: string, headers: RequestInit["headers"], exis
}
for (const [id, model] of remote) {
const key = ModelV2.ID.make(id)
const key = Model.ID.make(id)
if (result.has(key)) continue
result.set(key, build(key, model, baseURL))
}
@ -114,7 +114,7 @@ function usable(model: RemoteModel): model is UsableModel {
)
}
function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: ModelV2.Info) {
function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Model.Info) {
const messages = remote.supported_endpoints?.includes("/v1/messages") ?? false
const endpoint = messages
? "messages"
@ -134,15 +134,15 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?:
: remote.version
const released = previous?.time.released || Date.parse(version)
return ModelV2.Info.make({
...ModelV2.Info.default(ProviderV2.ID.githubCopilot, id),
return Model.Info.make({
...Model.Info.default(Provider.ID.githubCopilot, id),
id,
modelID: ModelV2.ID.make(remote.id),
providerID: ProviderV2.ID.githubCopilot,
family: previous?.family ?? ModelV2.Family.make(remote.capabilities.family),
modelID: Model.ID.make(remote.id),
providerID: Provider.ID.githubCopilot,
family: previous?.family ?? Model.Family.make(remote.capabilities.family),
name: previous?.name ?? remote.name,
package: ProviderV2.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"),
settings: ProviderV2.mergeOverlay(previous?.settings, {
package: Provider.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"),
settings: Provider.mergeOverlay(previous?.settings, {
baseURL: messages ? `${baseURL}/v1` : baseURL,
...(endpoint ? { endpoint } : {}),
}),
@ -175,11 +175,11 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?:
})
}
function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variants"] {
function variants(remote: UsableModel, messages: boolean): Model.Info["variants"] {
const efforts = remote.capabilities.supports.reasoning_effort ?? []
if (!messages && efforts.length) {
return efforts.map((effort) => ({
id: ModelV2.VariantID.make(effort),
id: Model.VariantID.make(effort),
settings: {
reasoningEffort: effort,
reasoningSummary: "auto",
@ -189,7 +189,7 @@ function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variant
}
if (efforts.length && remote.capabilities.supports.adaptive_thinking) {
return efforts.map((effort) => ({
id: ModelV2.VariantID.make(effort),
id: Model.VariantID.make(effort),
settings: {
thinking: {
type: "adaptive",
@ -203,11 +203,11 @@ function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variant
if (max === undefined) return []
return [
{
id: ModelV2.VariantID.make("max"),
id: Model.VariantID.make("max"),
settings: { thinking: { type: "enabled", budgetTokens: max - 1 } },
},
{
id: ModelV2.VariantID.make("high"),
id: Model.VariantID.make("high"),
settings: { thinking: { type: "enabled", budgetTokens: Math.floor(max / 2) } },
},
]

View file

@ -26,7 +26,7 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionDiscovery") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionDiscovery") {}
export const layer = (options?: Options) => Layer.effect(
Service,

View file

@ -10,7 +10,7 @@ export interface Interface {
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionBuiltIns") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionBuiltIns") {}
const layer = Layer.effect(
Service,

View file

@ -20,7 +20,7 @@ import {
import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential"
import { State } from "./state"
import { EventV2 } from "./event"
import { Bus } from "./bus"
import { IntegrationConnection } from "./integration/connection"
import { AppProcess } from "@opencode-ai/util/process"
import { ChildProcess } from "effect/unstable/process"
@ -129,7 +129,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationErr
export type Error = CodeRequiredError | AuthorizationError
export const Event = Integration.Event
export { Event } from "@opencode-ai/schema/integration"
export const Ref = Integration.Ref
export type Ref = Integration.Ref
@ -222,7 +222,7 @@ export interface Interface extends State.Transformable<Draft> {
}
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Integration") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Integration") {}
const attemptLifetime = Duration.toMillis(Duration.minutes(10))
const terminalRetention = Duration.toMillis(Duration.minutes(1))
@ -271,7 +271,7 @@ const layer = Layer.effect(
Service,
Effect.gen(function* () {
const credentials = yield* Credential.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const processes = yield* AppProcess.Service
const scope = yield* Scope.Scope
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
@ -338,7 +338,7 @@ const layer = Layer.effect(
},
},
}),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => bus.publish(Integration.Event.Updated, {}).pipe(Effect.asVoid),
})
const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => {
@ -433,8 +433,8 @@ const layer = Layer.effect(
// Persisting attempts cannot be cancelled, expired, or claimed again.
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* bus.publish(Integration.Event.Updated, {})
}).pipe(Effect.ensuring(close(attempt.scope)))
}),
)
@ -489,8 +489,8 @@ const layer = Layer.effect(
yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal))
yield* close(attempt.scope)
if (Exit.isFailure(persistence)) return
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* bus.publish(Integration.Event.Updated, {})
}),
)
})
@ -706,24 +706,24 @@ const layer = Layer.effect(
label: input.label,
value: Credential.Key.make({ type: "key", key: input.key }),
})
yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* bus.publish(Integration.Event.Updated, {})
}),
update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) {
const credential = yield* credentials.get(credentialID)
yield* credentials.update(credentialID, updates)
if (credential) {
yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID })
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID })
}
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.Updated, {})
}),
remove: Effect.fn("Integration.connection.remove")(function* (credentialID) {
const credential = yield* credentials.get(credentialID)
yield* credentials.remove(credentialID)
if (credential) {
yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID })
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID })
}
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.Updated, {})
}),
},
oauth: {
@ -809,5 +809,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [Credential.node, EventV2.node, AppProcess.node],
deps: [Credential.node, Bus.node, AppProcess.node],
})

View file

@ -14,7 +14,7 @@ export interface Interface {
readonly remove: (key: string) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/KV") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/KV") {}
const layer = Layer.effect(
Service,

View file

@ -60,7 +60,7 @@ export interface Interface {
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, PathError | FSUtil.Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/LocationMutation") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationMutation") {}
interface ResolvedPath {
readonly canonical: string

View file

@ -1,13 +1,12 @@
import { Effect, Layer, LayerMap } from "effect"
import { AgentV2 } from "./agent"
import { Agent } from "./agent"
import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { CodeMode } from "./codemode"
import { CommandV2 } from "./command"
import { Command } from "./command"
import { Config } from "./config"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Node } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "./event"
import { Bus } from "./bus"
import { FileMutation } from "./file-mutation"
import { FileSystem } from "./filesystem"
import { FileSystemSearch } from "./filesystem/search"
@ -21,12 +20,12 @@ import { LocationMutation } from "./location-mutation"
import { LocationServiceMap } from "./location-service-map"
import { ModelResolver } from "./model-resolver"
import { MCP } from "./mcp/index"
import { PermissionV2 } from "./permission"
import { PluginV2 } from "./plugin"
import { Permission } from "./permission"
import { Plugin } from "./plugin"
import { PluginSupervisor } from "./plugin/supervisor"
import { ProjectCopy } from "./project/copy"
import { Pty } from "./pty"
import { QuestionV2 } from "./question"
import { Question } from "./question"
import { Shell } from "./shell"
import { Reference } from "./reference"
import { WebSearch } from "./websearch"
@ -35,7 +34,7 @@ import { SessionRunnerLLM } from "./session/runner/llm"
import { SessionRunnerModel } from "./session/runner/model"
import { SessionCompaction } from "./session/compaction"
import { SessionTitle } from "./session/title"
import { SkillV2 } from "./skill"
import { Skill } from "./skill"
import { SkillInstructions } from "./skill/instructions"
import { Snapshot } from "./snapshot"
import { InstructionDiscovery } from "./instruction-discovery"
@ -45,8 +44,7 @@ import { SessionInstructions } from "./session/instructions"
import { SessionGenerateNode } from "./session/generate-node"
import { McpTool } from "./tool/mcp"
import { ReadToolFileSystem } from "./tool/read-filesystem"
import { ToolRegistry } from "./tool/registry"
import { ToolOutputStore } from "./tool-output-store"
import { Tool } from "./tool"
import { Vcs } from "./vcs"
export { LocationServiceMap } from "./location-service-map"
@ -54,15 +52,15 @@ export { LocationServiceMap } from "./location-service-map"
const locationServiceNodes = [
Location.node,
Config.node,
AgentV2.node,
CommandV2.node,
Agent.node,
Command.node,
Reference.node,
WebSearch.node,
Integration.node,
Catalog.node,
ModelResolver.node,
AISDK.node,
PluginV2.node,
Plugin.node,
PluginSupervisor.node,
ProjectCopy.node,
ProjectCopy.refreshNode,
@ -70,23 +68,20 @@ const locationServiceNodes = [
FileSystem.node,
Pty.node,
Shell.node,
SkillV2.node,
CodeMode.node,
Skill.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
LocationMutation.node,
FileMutation.node,
MCP.node,
PermissionV2.node,
ToolOutputStore.node,
ToolRegistry.node,
ToolRegistry.toolsNode,
Permission.node,
Tool.node,
Image.node,
SkillInstructions.node,
ReferenceInstructions.node,
InstructionEntry.node,
Form.node,
QuestionV2.node,
Question.node,
Generate.node,
SessionGenerateNode.node,
ReadToolFileSystem.node,

View file

@ -9,7 +9,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Config } from "../config"
import { ConfigMCP } from "../config/mcp"
import { Credential } from "../credential"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { Form } from "../form"
import { Integration } from "../integration"
import { IntegrationConnection } from "../integration/connection"
@ -155,7 +155,7 @@ export interface Interface {
}) => Effect.Effect<ResourceContent | undefined, NotFoundError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/MCP") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/MCP") {}
export const Options = Schema.Struct({
clientInfo: Schema.optional(
@ -172,7 +172,7 @@ export const layer = (options?: Options) => Layer.effect(
Effect.gen(function* () {
const config = yield* Config.Service
const location = yield* Location.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const forms = yield* Form.Service
const integration = yield* Integration.Service
const credentials = yield* Credential.Service
@ -433,9 +433,9 @@ export const layer = (options?: Options) => Layer.effect(
Effect.map((defs) => {
entry.prompts = defs.map((def) => toPrompt(name, def))
}),
Effect.andThen(events.publish(Command.Event.Updated, {})),
Effect.andThen(bus.publish(Command.Event.Updated, {})),
Effect.catch(() =>
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(events.publish(Command.Event.Updated, {}))),
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(bus.publish(Command.Event.Updated, {}))),
),
)
@ -460,10 +460,10 @@ export const layer = (options?: Options) => Layer.effect(
entry.tools = undefined
entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" }
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}),
),
)
@ -471,12 +471,12 @@ export const layer = (options?: Options) => Layer.effect(
connection.onToolsChanged(() =>
live(
refreshTools(name, entry, connection).pipe(
Effect.andThen(events.publish(McpEvent.ToolsChanged, { server: name })),
Effect.andThen(bus.publish(McpEvent.ToolsChanged, { server: name })),
),
),
)
connection.onPromptsChanged(() => live(refreshPrompts(name, entry, connection)))
connection.onResourcesChanged(() => live(events.publish(McpEvent.ResourcesChanged, { server: name })))
connection.onResourcesChanged(() => live(bus.publish(McpEvent.ResourcesChanged, { server: name })))
}
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
@ -502,7 +502,7 @@ export const layer = (options?: Options) => Layer.effect(
// Announce the handshake so connect() and credential reconnects don't show a stale
// disabled/failed status for the duration of the connection attempt.
entry.status = { status: "pending" }
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
const scope = yield* Scope.fork(root)
entry.scope = scope
const authProvider = yield* connectProvider(entry)
@ -530,9 +530,9 @@ export const layer = (options?: Options) => Layer.effect(
// Announce the new tool set so the tool registry registers it. A server that finishes connecting
// after the initial registration sweep and emits no list-changed notification would otherwise
// stay invisible to the model.
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
whenLive(name, entry, result.value.connection)(refreshPrompts(name, entry, result.value.connection))
return
}
@ -544,7 +544,7 @@ export const layer = (options?: Options) => Layer.effect(
? { status: "needs_auth" }
: { status: "failed", error: error instanceof Error ? error.message : String(error) }
yield* Effect.logWarning("mcp connect failed", { server: name, status: entry.status })
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(Effect.ensuring(Deferred.succeed(entry.startup, undefined)))
const stopServer = Effect.fnUntraced(function* (name: ServerName, entry: ServerEntry) {
@ -555,9 +555,9 @@ export const layer = (options?: Options) => Layer.effect(
entry.tools = undefined
entry.prompts = undefined
yield* Scope.close(scope, Exit.void)
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
})
// Disabled servers settle their startup immediately so queries never block on them.
@ -587,7 +587,7 @@ export const layer = (options?: Options) => Layer.effect(
}).pipe(locks.withLock(name))
})
fork(
events.subscribe(Integration.Event.ConnectionUpdated).pipe(
bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => owned.has(event.data.integrationID)),
Stream.runForEach((event) => Effect.sync(() => fork(reconnect(event.data.integrationID)))),
Effect.ignore,
@ -632,7 +632,7 @@ export const layer = (options?: Options) => Layer.effect(
yield* register(name, entry)
if (config.disabled) {
entry.status = { status: "disabled" }
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
return
}
yield* startServer(name, entry)
@ -657,7 +657,7 @@ export const layer = (options?: Options) => Layer.effect(
const target = yield* requireServer(name)
yield* stopServer(name, target.entry)
target.entry.status = { status: "disabled" }
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(locks.withLock(name))
}),
remove: Effect.fn("MCP.remove")(function* (server) {
@ -670,7 +670,7 @@ export const layer = (options?: Options) => Layer.effect(
// Credentials are kept: they are keyed by name + url, so re-adding the same server
// reuses them without forcing re-auth, matching add()'s replacement semantics.
runtime.delete(name)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(locks.withLock(name))
}),
tools: Effect.fn("MCP.tools")(function* () {
@ -793,7 +793,7 @@ export function configured(options?: Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node],
deps: [Config.node, Location.node, Bus.node, Form.node, Integration.node, Credential.node],
})
}

View file

@ -2,8 +2,8 @@ export * as McpInstructions from "./instructions"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Schema } from "effect"
import { AgentV2 } from "../agent"
import { PermissionV2 } from "../permission"
import { Agent } from "../agent"
import { Permission } from "../permission"
import { McpTool } from "../tool/mcp"
import { MCP } from "./index"
import { Instructions } from "../instructions/index"
@ -55,10 +55,10 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
}
export interface Interface {
readonly load: (agent: AgentV2.Selection) => Effect.Effect<Instructions.Instructions>
readonly load: (agent: Agent.Selection) => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/McpInstructions") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/McpInstructions") {}
export const layer = Layer.effect(
Service,
@ -83,7 +83,7 @@ export const layer = Layer.effect(
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
concurrency: "unbounded",
})
const canExecute = PermissionV2.evaluate("execute", "*", agent.permissions).effect !== "deny"
const canExecute = Permission.evaluate("execute", "*", agent.permissions).effect !== "deny"
// Instructions are useful only when this agent can reach at least one server tool.
const visible = instructions
.flatMap((item) => {
@ -93,7 +93,7 @@ export const layer = Layer.effect(
if (
!owned.some(
(tool) =>
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
Permission.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
)
)
return []

View file

@ -15,17 +15,17 @@ import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { Credential } from "./credential"
import { Integration } from "./integration"
import { ModelV2 } from "./model"
import { Capabilities, ID, Info, Ref, VariantID } from "./model"
import { Npm } from "@opencode-ai/util/npm"
import { OpenAICodex } from "./plugin/provider/openai-codex"
import { ProviderV2 } from "./provider"
import { Provider } from "./provider"
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
"SessionRunnerModel.VariantUnavailableError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
variant: ModelV2.VariantID,
providerID: Provider.ID,
modelID: ID,
variant: VariantID,
},
) {
override get message() {
@ -36,8 +36,8 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedPackageError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
providerID: Provider.ID,
modelID: ID,
package: Schema.String,
},
) {
@ -52,21 +52,21 @@ export interface Resolved {
/** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
readonly model: Model
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
readonly ref: ModelV2.Ref
readonly ref: Ref
/** Catalog capabilities used to shape requests before provider lowering. */
readonly capabilities: ModelV2.Capabilities
readonly capabilities: Capabilities
/** Catalog pricing in dollars per million tokens. */
readonly cost: ModelV2.Info["cost"]
readonly cost: Info["cost"]
}
export interface Interface {
readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect<Resolved | undefined, Error>
readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect<Resolved, Error>
readonly resolve: (requested?: Ref) => Effect.Effect<Resolved | undefined, Error>
readonly resolveModel: (model: Info, variant?: VariantID) => Effect.Effect<Resolved, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ModelResolver") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelResolver") {}
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
const apiKey = (model: Info, credential?: Credential.Value) => {
if (credential?.type === "key") return Auth.value(credential.key)
if (credential?.type === "oauth") return Auth.value(credential.access)
const value = model.settings?.apiKey
@ -74,7 +74,7 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
return undefined
}
const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
const withDefaults = (model: Info, route: AnyRoute) =>
route.with({
provider: model.providerID,
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
@ -84,8 +84,8 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
limits: { context: model.limit.context, output: model.limit.output },
})
const providerHeaders = (model: ModelV2.Info) => {
const packageName = ProviderV2.packageName(model.package)
const providerHeaders = (model: Info) => {
const packageName = Provider.packageName(model.package)
const generated = new Map<string, string>()
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
generated.set("OpenAI-Organization", model.settings.organization)
@ -93,16 +93,16 @@ const providerHeaders = (model: ModelV2.Info) => {
generated.set("OpenAI-Project", model.settings.project)
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
generated.set("Authorization", `Bearer ${model.settings.authToken}`)
return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
return Provider.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
}
const providerOptions = (
model: ModelV2.Info,
model: Info,
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined
if (!Provider.isAISDK(model.package) || model.settings === undefined) return undefined
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
if (Object.keys(settings).length === 0) return undefined
const packageName = ProviderV2.packageName(model.package)
const packageName = Provider.packageName(model.package)
if (packageName === "@ai-sdk/openai") return { openai: settings }
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
@ -110,9 +110,9 @@ const providerOptions = (
}
export const withVariant = (
model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
model: Info,
variantID: VariantID | undefined,
): Effect.Effect<Info, VariantUnavailableError> => {
const id = variantID === "default" ? undefined : variantID
const variant = model.variants?.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default")
@ -126,37 +126,37 @@ export const withVariant = (
return Effect.succeed(
variant
? produce(model, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
draft.settings = Provider.mergeOverlay(draft.settings, variant.settings)
draft.headers = Provider.mergeHeaders(draft.headers, variant.headers)
draft.body = Provider.mergeOverlay(draft.body, variant.body)
})
: model,
)
}
export interface Dependencies {
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
readonly loadPackage?: (specifier: string) => Effect.Effect<Provider.ProviderPackage, Provider.LoadError>
readonly loadAISDK?: (model: Info) => Effect.Effect<Model, AISDK.InitError>
}
export const fromCatalogModel = (
model: ModelV2.Info,
model: Info,
credential?: Credential.Value,
dependencies?: Dependencies,
): Effect.Effect<Model, UnsupportedPackageError> => {
const resolved = produce(model, (draft) => {
if (draft.settings?.apiKey === "") delete draft.settings.apiKey
if (credential?.type === "key" && credential.metadata !== undefined)
draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata)
draft.body = Provider.mergeOverlay(draft.body, credential.metadata)
})
const packageName = ProviderV2.packageName(resolved.package)
const packageName = Provider.packageName(resolved.package)
const key = apiKey(resolved, credential)
if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
if (OpenAICodex.isChatGPT(credential) && !Provider.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
return Effect.succeed(codexModel(resolved, credential, key))
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route)
@ -164,7 +164,7 @@ export const fromCatalogModel = (
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
return Effect.succeed(
withDefaults(resolved, AnthropicMessages.route)
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
@ -172,7 +172,7 @@ export const fromCatalogModel = (
)
}
if (
ProviderV2.isAISDK(resolved.package) &&
Provider.isAISDK(resolved.package) &&
packageName === "@ai-sdk/openai-compatible" &&
typeof resolved.settings?.baseURL === "string"
) {
@ -182,10 +182,10 @@ export const fromCatalogModel = (
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (ProviderV2.isAISDK(resolved.package)) {
if (Provider.isAISDK(resolved.package)) {
if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved))
const runtime = produce(resolved, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, {
draft.settings = Provider.mergeOverlay(draft.settings, {
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
...credential?.metadata,
@ -197,7 +197,7 @@ export const fromCatalogModel = (
const specifier = resolved.package
return Effect.gen(function* () {
const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe(
const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe(
Effect.mapError(() => unsupported(resolved)),
)
const configured = { ...resolved.settings, ...credential?.metadata }
@ -249,7 +249,7 @@ const withoutNativeAuthSettings = (settings: Record<string, unknown>) => {
}
const codexModel = (
model: ModelV2.Info,
model: Info,
credential: Credential.Value | undefined,
key: ReturnType<typeof Auth.value> | undefined,
) => {
@ -264,7 +264,7 @@ const codexModel = (
.model({ id: model.modelID ?? model.id, compatibility: model.compatibility })
}
const unsupported = (model: ModelV2.Info) =>
const unsupported = (model: Info) =>
new UnsupportedPackageError({
providerID: model.providerID,
modelID: model.id,
@ -272,13 +272,13 @@ const unsupported = (model: ModelV2.Info) =>
})
export const resolveModel = (
model: ModelV2.Info,
variant: ModelV2.VariantID | undefined,
model: Info,
variant: VariantID | undefined,
credential?: Credential.Value,
dependencies?: Dependencies,
) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)))
export const supported = (model: ModelV2.Info) => Boolean(model.package)
export const supported = (model: Info) => Boolean(model.package)
/** Resolves catalog selections into runtime models for the current Location. */
export const layer = Layer.effect(
@ -289,8 +289,8 @@ export const layer = Layer.effect(
const npm = yield* Npm.Service
const aisdk = yield* AISDK.Service
const load = Effect.fn("ModelResolver.resolveModel")(function* (
selected: ModelV2.Info,
variant?: ModelV2.VariantID,
selected: Info,
variant?: VariantID,
) {
const provider = yield* catalog.provider.get(selected.providerID)
const connection = yield* integrations.connection.active(
@ -301,13 +301,13 @@ export const layer = Layer.effect(
variant,
connection ? yield* integrations.connection.resolve(connection) : undefined,
{
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
},
)
return {
model,
ref: ModelV2.Ref.make({
ref: Ref.make({
id: selected.id,
providerID: selected.providerID,
...(variant === undefined ? {} : { variant }),

View file

@ -1,5 +1,5 @@
import { Model } from "@opencode-ai/schema/model"
import { ProviderV2 } from "./provider"
import { Provider } from "./provider"
import type { DeepMutable } from "./schema"
export const ID = Model.ID
@ -37,12 +37,12 @@ export function compatibility(input: unknown): Compatibility | undefined {
return typeof input.field === "string" ? { reasoningField: input.field } : undefined
}
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
export function parse(input: string): { providerID: Provider.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/")
return {
providerID: ProviderV2.ID.make(providerID),
providerID: Provider.ID.make(providerID),
modelID: ID.make(modelID.join("/")),
}
}
export * as ModelV2 from "./model"
export * as Model from "./model"

View file

@ -8,11 +8,11 @@ import { Global } from "@opencode-ai/util/global"
import { Flock } from "@opencode-ai/util/flock"
import { Hash } from "@opencode-ai/util/hash"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { EventV2 } from "./event"
import { Bus } from "./bus"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { Model } from "./model"
import { Provider } from "./provider"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type
@ -54,7 +54,7 @@ type SourceModel = {
{
readonly cost?: Cost
readonly provider?: {
readonly body?: ProviderV2.Settings
readonly body?: Provider.Settings
readonly headers?: Readonly<Record<string, string>>
}
}
@ -75,29 +75,29 @@ type SourceProvider = {
}
export type Snapshot = {
readonly info: ProviderV2.Info
readonly models: readonly ModelV2.Info[]
readonly info: Provider.Info
readonly models: readonly Model.Info[]
readonly environment: readonly string[]
}
function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
const providers: Snapshot[] = []
for (const item of Object.values(input)) {
const providerID = ProviderV2.ID.make(item.id)
const providerID = Provider.ID.make(item.id)
const info = {
id: providerID,
name: item.name,
package: ProviderV2.aisdk(item.npm),
package: Provider.aisdk(item.npm),
...(item.api ? { settings: { baseURL: item.api } } : {}),
} satisfies ProviderV2.Info
const models: ModelV2.Info[] = []
} satisfies Provider.Info
const models: Model.Info[] = []
for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost)
const variants = reasoningVariants(item, model)
const id = ModelV2.ID.make(model.id)
const id = Model.ID.make(model.id)
models.push(modelInfo(providerID, id, model, { cost: baseCost, variants }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
const modeID = ModelV2.ID.make(`${model.id}-${mode}`)
const modeID = Model.ID.make(`${model.id}-${mode}`)
models.push(
modelInfo(providerID, modeID, model, {
name: modeName(model, mode),
@ -118,7 +118,7 @@ function released(date: string) {
return Number.isFinite(time) ? time : 0
}
function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] {
function cost(input: SourceModel["cost"]): Model.Info["cost"] {
const base = {
input: input?.input ?? Money.USDPerMillionTokens.zero,
output: input?.output ?? Money.USDPerMillionTokens.zero,
@ -154,13 +154,13 @@ function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] {
]
}
function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | undefined) {
function mergeCost(base: Model.Info["cost"], override: SourceModel["cost"] | undefined) {
if (!override) return base
const next = cost(override)
const [baseDefault, ...baseTiers] = base
const [nextDefault, ...nextTiers] = next
const tierKey = (item: ModelV2.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelV2.Info["cost"][number], right: ModelV2.Info["cost"][number]) => ({
const tierKey = (item: Model.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: Model.Info["cost"][number], right: Model.Info["cost"][number]) => ({
...left,
...right,
tier: right.tier ?? left.tier,
@ -187,7 +187,7 @@ function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | u
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
const OUTPUT_TOKEN_MAX = 32_000
function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<ModelV2.Info["variants"]> {
function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<Model.Info["variants"]> {
const npm = model.provider?.npm ?? provider.npm
const options = model.reasoning_options
if (!options?.length) return []
@ -203,7 +203,7 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul
if (id === undefined) return []
if (id === "none" && off.length > 0) return []
const settings = settingsForEffort(npm, model.id, id)
return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
return settings ? [{ id: Model.VariantID.make(id), settings }] : []
}),
]
return [...new Map(variants.map((variant) => [variant.id, variant])).values()]
@ -218,7 +218,7 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul
return []
}
function settingsForEffort(npm: string, modelID: string, effort: string): ProviderV2.Settings | undefined {
function settingsForEffort(npm: string, modelID: string, effort: string): Provider.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
if (anthropicManualThinking(modelID)) return { effort }
@ -287,7 +287,7 @@ function budgetVariants(
npm: string,
model: SourceModel,
option: Extract<NonNullable<SourceModel["reasoning_options"]>[number], { type: "budget_tokens" }>,
): NonNullable<ModelV2.Info["variants"]> {
): NonNullable<Model.Info["variants"]> {
const maximum = Math.min(option.max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1)
if (maximum <= 0) return []
const high = Math.min(Math.max(option.min ?? 0, Math.floor((maximum + 1) / 2)), maximum)
@ -296,35 +296,35 @@ function budgetVariants(
{ id: "max", budget: maximum },
].flatMap((item) => {
const settings = settingsForBudget(npm, model.id, item.budget)
return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : []
return settings ? [{ id: Model.VariantID.make(item.id), settings }] : []
})
}
function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info["variants"]> {
function toggleVariants(npm: string, modelID: string): NonNullable<Model.Info["variants"]> {
if (npm === "@ai-sdk/gateway") {
const upstream = gatewayPackage(modelID)
if (upstream) return toggleVariants(upstream, modelID)
return [
{
id: ModelV2.VariantID.make("none"),
id: Model.VariantID.make("none"),
settings: { reasoning: { enabled: false } },
},
{
id: ModelV2.VariantID.make("thinking"),
id: Model.VariantID.make("thinking"),
settings: { reasoning: { enabled: true } },
},
]
}
if (npm === "@openrouter/ai-sdk-provider")
return [
{ id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
{ id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
{ id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
{ id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
]
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
return [
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
{ id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
{
id: ModelV2.VariantID.make("thinking"),
id: Model.VariantID.make("thinking"),
settings: {
thinking: { type: "adaptive", display: "summarized" },
},
@ -333,11 +333,11 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
return [
{
id: ModelV2.VariantID.make("none"),
id: Model.VariantID.make("none"),
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
},
{
id: ModelV2.VariantID.make("thinking"),
id: Model.VariantID.make("thinking"),
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } },
},
]
@ -345,7 +345,7 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
const anthropic = modelID.includes("anthropic")
return [
{
id: ModelV2.VariantID.make("none"),
id: Model.VariantID.make("none"),
settings: {
additionalModelRequestFields: anthropic
? { thinking: { type: "disabled" } }
@ -353,7 +353,7 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
},
},
{
id: ModelV2.VariantID.make("thinking"),
id: Model.VariantID.make("thinking"),
settings: {
additionalModelRequestFields: anthropic
? { thinking: { type: "adaptive", display: "summarized" } }
@ -364,58 +364,58 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
}
if (npm === "@ai-sdk/alibaba")
return [
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
{ id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } },
{ id: Model.VariantID.make("none"), settings: { enableThinking: false } },
{ id: Model.VariantID.make("thinking"), settings: { enableThinking: true } },
]
if (npm === "@ai-sdk/cohere")
return [
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
{ id: ModelV2.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } },
{ id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
{ id: Model.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } },
]
if (npm === "@jerome-benoit/sap-ai-provider-v2") {
if (modelID.includes("gemini"))
return [
{
id: ModelV2.VariantID.make("none"),
id: Model.VariantID.make("none"),
settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
},
{
id: ModelV2.VariantID.make("thinking"),
id: Model.VariantID.make("thinking"),
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } },
},
]
if (modelID.includes("cohere"))
return [
{
id: ModelV2.VariantID.make("none"),
id: Model.VariantID.make("none"),
settings: { modelParams: { thinking: { type: "disabled" } } },
},
{
id: ModelV2.VariantID.make("thinking"),
id: Model.VariantID.make("thinking"),
settings: { modelParams: { thinking: { type: "enabled" } } },
},
]
if (modelID.includes("amazon--nova"))
return [
{
id: ModelV2.VariantID.make("none"),
id: Model.VariantID.make("none"),
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } },
},
{
id: ModelV2.VariantID.make("thinking"),
id: Model.VariantID.make("thinking"),
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled" } } } },
},
]
if (modelID.includes("anthropic"))
return [
{
id: ModelV2.VariantID.make("none"),
id: Model.VariantID.make("none"),
settings: {
modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
},
},
{
id: ModelV2.VariantID.make("thinking"),
id: Model.VariantID.make("thinking"),
settings: {
modelParams: {
additionalModelRequestFields: {
@ -429,7 +429,7 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
return []
}
function settingsForBudget(npm: string, modelID: string, budget: number): ProviderV2.Settings | undefined {
function settingsForBudget(npm: string, modelID: string, budget: number): Provider.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
return { thinking: { type: "enabled", budgetTokens: budget } }
@ -480,24 +480,24 @@ function modeName(model: SourceModel, mode: string) {
}
function modelInfo(
providerID: ProviderV2.ID,
id: ModelV2.ID,
providerID: Provider.ID,
id: Model.ID,
model: SourceModel,
input: {
readonly name?: string
readonly cost?: ModelV2.Info["cost"]
readonly cost?: Model.Info["cost"]
readonly request?: NonNullable<NonNullable<SourceModel["experimental"]>["modes"]>[string]["provider"]
readonly variants?: NonNullable<ModelV2.Info["variants"]>
readonly variants?: NonNullable<Model.Info["variants"]>
} = {},
): ModelV2.Info {
): Model.Info {
return {
id,
modelID: ModelV2.ID.make(model.id),
modelID: Model.ID.make(model.id),
providerID,
name: input.name ?? model.name,
compatibility: ModelV2.compatibility(model.interleaved),
family: model.family ? ModelV2.Family.make(model.family) : undefined,
package: model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined,
compatibility: Model.compatibility(model.interleaved),
family: model.family ? Model.Family.make(model.family) : undefined,
package: model.provider?.npm ? Provider.aisdk(model.provider.npm) : undefined,
settings: model.provider?.api ? { baseURL: model.provider.api } : undefined,
capabilities: {
tools: model.tool_call,
@ -519,7 +519,7 @@ function modelInfo(
}
}
export const Event = ModelsDev.Event
export { Event } from "@opencode-ai/schema/models-dev"
declare const OPENCODE_MODELS_DEV: Record<string, SourceProvider> | undefined
@ -542,7 +542,7 @@ export const layer = (options?: Options) =>
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const app = yield* App.Metadata
const http = HttpClient.filterStatusOk(
(yield* HttpClient.HttpClient).pipe(
@ -639,7 +639,7 @@ export const layer = (options?: Options) =>
if (!force && (yield* fresh())) return
yield* fetchAndWrite()
yield* invalidate
yield* events.publish(Event.Refreshed, {})
yield* bus.publish(ModelsDev.Event.Refreshed, {})
}),
).pipe(
Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })),
@ -660,7 +660,7 @@ export function configured(options?: Options) {
return makeGlobalNode({
service: Service,
layer: layer(options),
deps: [FSUtil.node, EventV2.node, App.node, httpClient],
deps: [FSUtil.node, Bus.node, App.node, httpClient],
})
}

View file

@ -1,11 +1,11 @@
export * as PermissionV2 from "./permission"
export * as Permission from "./permission"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Deferred, Effect, Layer, Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission"
import { EventV2 } from "./event"
import { Bus } from "./bus"
import { Location } from "./location"
import { AgentV2 } from "./agent"
import { Agent } from "./agent"
import { SessionErrors } from "./session/error"
import { SessionSchema } from "./session/schema"
import { SessionStore } from "./session/store"
@ -41,32 +41,32 @@ export type Reply = typeof Reply.Type
export const AssertInput = Schema.Struct({
id: ID.pipe(Schema.optional),
...RequestFields,
agent: AgentV2.ID.pipe(Schema.optional),
}).annotate({ identifier: "PermissionV2.AssertInput" })
agent: Agent.ID.pipe(Schema.optional),
}).annotate({ identifier: "Permission.AssertInput" })
export type AssertInput = typeof AssertInput.Type
export const ReplyInput = Schema.Struct({
requestID: ID,
reply: Reply,
message: Schema.String.pipe(Schema.optional),
}).annotate({ identifier: "PermissionV2.ReplyInput" })
}).annotate({ identifier: "Permission.ReplyInput" })
export type ReplyInput = typeof ReplyInput.Type
export const AskResult = Schema.Struct({
id: ID,
effect: Permission.Effect,
}).annotate({ identifier: "PermissionV2.AskResult" })
}).annotate({ identifier: "Permission.AskResult" })
export type AskResult = typeof AskResult.Type
export const Event = Permission.Event
export { Event } from "@opencode-ai/schema/permission"
export class DeclinedError extends Schema.TaggedErrorClass<DeclinedError>()("PermissionV2.DeclinedError", {}) {}
export class DeclinedError extends Schema.TaggedErrorClass<DeclinedError>()("Permission.DeclinedError", {}) {}
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionV2.CorrectedError", {
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("Permission.CorrectedError", {
feedback: Schema.String,
}) {}
export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("PermissionV2.BlockedError", {
export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("Permission.BlockedError", {
rules: Permission.Ruleset,
permission: Schema.String,
resources: Schema.Array(Schema.String),
@ -76,7 +76,7 @@ export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("Permi
}
}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Permission.NotFoundError", {
requestID: ID,
}) {}
@ -107,20 +107,20 @@ export interface Interface {
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Permission") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Permission") {}
interface Pending {
readonly request: Request
readonly agent?: AgentV2.ID
readonly agent?: Agent.ID
readonly deferred: Deferred.Deferred<void, DeclinedError | CorrectedError>
}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const location = yield* Location.Service
const agents = yield* AgentV2.Service
const agents = yield* Agent.Service
const sessions = yield* SessionStore.Service
const saved = yield* PermissionSaved.Service
const pending = new Map<ID, Pending>()
@ -143,9 +143,9 @@ const layer = Layer.effect(
)
})
const configured = Effect.fn("PermissionV2.configured")(function* (
const configured = Effect.fn("Permission.configured")(function* (
sessionID: SessionSchema.ID,
agentID?: AgentV2.ID,
agentID?: Agent.ID,
) {
const session = yield* sessions.get(sessionID)
if (!session) return yield* new SessionErrors.NotFoundError({ sessionID })
@ -182,7 +182,7 @@ const layer = Layer.effect(
}
}
const create = (request: Request, agent?: AgentV2.ID) =>
const create = (request: Request, agent?: Agent.ID) =>
Effect.uninterruptible(
Effect.gen(function* () {
const deferred = yield* Deferred.make<void, DeclinedError | CorrectedError>()
@ -190,21 +190,21 @@ const layer = Layer.effect(
if (pending.has(request.id))
return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`))
pending.set(request.id, item)
yield* events
.publish(Event.Asked, request)
yield* bus
.publish(Permission.Event.Asked, request)
.pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id))))
return item
}),
)
const ask = Effect.fn("PermissionV2.ask")(function* (input: AssertInput) {
const ask = Effect.fn("Permission.ask")(function* (input: AssertInput) {
const result = yield* evaluateInput(input)
const value = request(input)
if (result.effect === "ask") yield* create(value, input.agent)
return { id: value.id, effect: result.effect }
})
const assert = Effect.fn("PermissionV2.assert")((input: AssertInput) =>
const assert = Effect.fn("Permission.assert")((input: AssertInput) =>
Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const result = yield* evaluateInput(input)
@ -223,7 +223,7 @@ const layer = Layer.effect(
// resurfaces as a typed failure at SessionModelRequest.executeTool. A decline
// WITH feedback (CorrectedError) intentionally stays typed so the leaf can turn
// it into ToolFailure and the model continues.
Effect.catchTag("PermissionV2.DeclinedError", (error) => Effect.die(error)),
Effect.catchTag("Permission.DeclinedError", (error) => Effect.die(error)),
Effect.ensuring(
Effect.sync(() => {
pending.delete(item.request.id)
@ -234,12 +234,12 @@ const layer = Layer.effect(
),
)
const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) =>
const reply = Effect.fn("Permission.reply")((input: ReplyInput) =>
Effect.uninterruptible(
Effect.gen(function* () {
const existing = pending.get(input.requestID)
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
yield* events.publish(Event.Replied, {
yield* bus.publish(Permission.Event.Replied, {
sessionID: existing.request.sessionID,
requestID: existing.request.id,
reply: input.reply,
@ -253,7 +253,7 @@ const layer = Layer.effect(
pending.delete(input.requestID)
for (const [id, item] of pending) {
if (item.request.sessionID !== existing.request.sessionID) continue
yield* events.publish(Event.Replied, {
yield* bus.publish(Permission.Event.Replied, {
sessionID: item.request.sessionID,
requestID: item.request.id,
reply: "reject",
@ -290,7 +290,7 @@ const layer = Layer.effect(
)
)
continue
yield* events.publish(Event.Replied, {
yield* bus.publish(Permission.Event.Replied, {
sessionID: item.request.sessionID,
requestID: item.request.id,
reply: "always",
@ -302,15 +302,15 @@ const layer = Layer.effect(
),
)
const list = Effect.fn("PermissionV2.list")(function* () {
const list = Effect.fn("Permission.list")(function* () {
return Array.from(pending.values(), (item) => item.request)
})
const get = Effect.fn("PermissionV2.get")(function* (id: ID) {
const get = Effect.fn("Permission.get")(function* (id: ID) {
return pending.get(id)?.request
})
const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionSchema.ID) {
const forSession = Effect.fn("Permission.forSession")(function* (sessionID: SessionSchema.ID) {
return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID)
})
@ -321,5 +321,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [EventV2.node, Location.node, AgentV2.node, SessionStore.node, PermissionSaved.node],
deps: [Bus.node, Location.node, Agent.node, SessionStore.node, PermissionSaved.node],
})

View file

@ -4,7 +4,7 @@ import { eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { ProjectV2 } from "../project"
import { Project } from "../project"
import { PermissionTable } from "./sql"
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
@ -15,12 +15,12 @@ export const Info = PermissionSaved.Info
export type Info = typeof Info.Type
export const ListInput = Schema.Struct({
projectID: ProjectV2.ID.pipe(Schema.optional),
projectID: Project.ID.pipe(Schema.optional),
}).annotate({ identifier: "PermissionSaved.ListInput" })
export type ListInput = typeof ListInput.Type
export const AddInput = Schema.Struct({
projectID: ProjectV2.ID,
projectID: Project.ID,
action: Schema.String,
resources: Schema.Array(Schema.String),
}).annotate({ identifier: "PermissionSaved.AddInput" })
@ -32,7 +32,7 @@ export interface Interface {
readonly remove: (id: ID) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PermissionSaved") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/PermissionSaved") {}
const layer = Layer.effect(
Service,

View file

@ -1,6 +1,6 @@
import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"
import { Timestamps } from "../database/schema.sql"
import { ProjectV2 } from "../project"
import { Project } from "../project"
import { ProjectTable } from "../project/sql"
import type { PermissionSaved } from "./saved"
@ -9,7 +9,7 @@ export const PermissionTable = sqliteTable(
{
id: text().$type<PermissionSaved.ID>().primaryKey(),
project_id: text()
.$type<ProjectV2.ID>()
.$type<Project.ID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
action: text().notNull(),

View file

@ -1,46 +1,43 @@
export * as PluginV2 from "./plugin"
export * as Plugin from "./plugin"
export { Event, ID, Info } from "@opencode-ai/schema/plugin"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
import { Plugin } from "@opencode-ai/schema/plugin"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { App } from "./app"
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
import { AgentV2 } from "./agent"
import { Agent } from "./agent"
import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { CommandV2 } from "./command"
import { EventV2 } from "./event"
import { Command } from "./command"
import { Bus } from "./bus"
import { Integration } from "./integration"
import { Location } from "./location"
import { PluginHost } from "./plugin/host"
import { PluginRuntime } from "./plugin/runtime"
import { WebSearch } from "./websearch"
import { Reference } from "./reference"
import { SkillV2 } from "./skill"
import { Skill } from "./skill"
import { State } from "./state"
import { ToolRegistry } from "./tool/registry"
import { ToolHooks } from "./tool/hooks"
import { Tool } from "./tool"
import { PluginHooks } from "./plugin/hooks"
export interface Interface {
readonly activate: (plugins: readonly Versioned[]) => Effect.Effect<void>
readonly list: () => Effect.Effect<Info[]>
readonly list: () => Effect.Effect<Plugin.Info[]>
}
export interface Versioned extends Plugin {
readonly version: string
}
export type Versioned = import("@opencode-ai/plugin/effect/plugin").Plugin & { readonly version: string }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Plugin") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const scope = yield* Scope.make()
const active = new Map<typeof ID.Type, { readonly plugin: Versioned; readonly scope: Scope.Closeable }>()
const active = new Map<Plugin.ID, { readonly plugin: Versioned; readonly scope: Scope.Closeable }>()
const lock = Semaphore.makeUnsafe(1)
let host: Parameters<Plugin["effect"]>[0]
let host: Parameters<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>[0]
const load = Effect.fnUntraced(function* (plugin: Versioned) {
const child = yield* Scope.fork(scope)
@ -49,7 +46,7 @@ const layer = Layer.effect(
inherit,
Effect.updateContext((_context: Context.Context<never>) => Context.make(Scope.Scope, child)),
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": plugin.id } }),
Effect.andThen(events.publish(Event.Added, { id: ID.make(plugin.id) })),
Effect.andThen(bus.publish(Plugin.Event.Added, { id: Plugin.ID.make(plugin.id) })),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
Effect.exit,
)
@ -62,8 +59,8 @@ const layer = Layer.effect(
})
const activate = Effect.fn("Plugin.activate")(function* (plugins: readonly Versioned[]) {
const definitions = plugins.map((plugin) => ({ ...plugin, id: ID.make(plugin.id) }))
const ids = new Set<typeof ID.Type>()
const definitions = plugins.map((plugin) => ({ ...plugin, id: Plugin.ID.make(plugin.id) }))
const ids = new Set<Plugin.ID>()
for (const definition of definitions) {
if (ids.has(definition.id)) yield* Effect.die(new Error(`Duplicate plugin ID: ${definition.id}`))
ids.add(definition.id)
@ -118,7 +115,7 @@ const layer = Layer.effect(
})
}),
)
yield* events.publish(Event.Updated, {})
yield* bus.publish(Plugin.Event.Updated, {})
}),
)
})
@ -145,18 +142,17 @@ export const node = makeLocationNode({
service: Service,
layer,
deps: [
EventV2.node,
Bus.node,
App.node,
AgentV2.node,
Agent.node,
AISDK.node,
Catalog.node,
CommandV2.node,
Command.node,
Integration.node,
Location.node,
Reference.node,
SkillV2.node,
ToolRegistry.toolsNode,
ToolHooks.node,
Skill.node,
Tool.node,
PluginHooks.node,
PluginRuntime.node,
WebSearch.node,

View file

@ -1,12 +1,12 @@
export * as AgentPlugin from "./agent"
import path from "path"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect } from "effect"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { Global } from "@opencode-ai/util/global"
import { Location } from "../location"
import { PermissionV2 } from "../permission"
import { Permission } from "../permission"
// Combined output files written by the Shell service, e.g. `<data>/shell/<projectID>/<shellID>.out`.
// Whitelisted so agents can read a command's full captured output without an external-directory prompt.
@ -105,13 +105,13 @@ export const Plugin = define({
const location = yield* Location.Service
const worktree = location.directory
const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")]
const readonlyExternalDirectory: PermissionV2.Ruleset = [
const readonlyExternalDirectory: Permission.Ruleset = [
{ action: "external_directory", resource: "*", effect: "ask" },
...whitelistedDirs.map(
(resource): PermissionV2.Rule => ({ action: "external_directory", resource, effect: "allow" }),
(resource): Permission.Rule => ({ action: "external_directory", resource, effect: "allow" }),
),
]
const defaults: PermissionV2.Ruleset = [
const defaults: Permission.Ruleset = [
{ action: "*", resource: "*", effect: "allow" },
...readonlyExternalDirectory,
{ action: "question", resource: "*", effect: "deny" },
@ -124,24 +124,24 @@ export const Plugin = define({
]
yield* ctx.agent.transform((draft) => {
draft.update(AgentV2.defaultID, (item) => {
item.name = AgentV2.Name.make("Build")
draft.update(Agent.defaultID, (item) => {
item.name = Agent.Name.make("Build")
item.description = "The default agent. Executes tools based on configured permissions."
item.mode = "primary"
item.permissions.push(
...PermissionV2.merge(defaults, [
...Permission.merge(defaults, [
{ action: "question", resource: "*", effect: "allow" },
{ action: "plan_enter", resource: "*", effect: "allow" },
]),
)
})
draft.update(AgentV2.ID.make("plan"), (item) => {
item.name = AgentV2.Name.make("Plan")
draft.update(Agent.ID.make("plan"), (item) => {
item.name = Agent.Name.make("Plan")
item.description = "Plan mode. Disallows all edit tools."
item.mode = "primary"
item.permissions.push(
...PermissionV2.merge(defaults, [
...Permission.merge(defaults, [
{ action: "question", resource: "*", effect: "allow" },
{ action: "plan_exit", resource: "*", effect: "allow" },
{ action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" },
@ -156,22 +156,22 @@ export const Plugin = define({
)
})
draft.update(AgentV2.ID.make("general"), (item) => {
item.name = AgentV2.Name.make("General")
draft.update(Agent.ID.make("general"), (item) => {
item.name = Agent.Name.make("General")
item.description =
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
item.mode = "subagent"
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
item.permissions.push(...Permission.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
})
draft.update(AgentV2.ID.make("explore"), (item) => {
item.name = AgentV2.Name.make("Explore")
draft.update(Agent.ID.make("explore"), (item) => {
item.name = Agent.Name.make("Explore")
item.description =
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
item.system = PROMPT_EXPLORE
item.mode = "subagent"
item.permissions.push(
...PermissionV2.merge(
...Permission.merge(
defaults,
[
{ action: "*", resource: "*", effect: "deny" },
@ -187,28 +187,28 @@ export const Plugin = define({
)
})
draft.update(AgentV2.ID.make("compaction"), (item) => {
item.name = AgentV2.Name.make("Compaction")
draft.update(Agent.ID.make("compaction"), (item) => {
item.name = Agent.Name.make("Compaction")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_COMPACTION
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
})
draft.update(AgentV2.ID.make("title"), (item) => {
item.name = AgentV2.Name.make("Title")
draft.update(Agent.ID.make("title"), (item) => {
item.name = Agent.Name.make("Title")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_TITLE
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
})
draft.update(AgentV2.ID.make("summary"), (item) => {
item.name = AgentV2.Name.make("Summary")
draft.update(Agent.ID.make("summary"), (item) => {
item.name = Agent.Name.make("Summary")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_SUMMARY
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
})
})
}),

View file

@ -1,6 +1,6 @@
export * as CommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect } from "effect"
import { Location } from "../location"
import PROMPT_INITIALIZE from "./command/initialize.txt"

View file

@ -1,8 +1,8 @@
export * as PluginHooks from "./hooks"
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
import type { AISDKHooks } from "@opencode-ai/plugin/effect/aisdk"
import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
import type { ToolHooks } from "@opencode-ai/plugin/effect/tool"
import { Context, Effect, Layer, Scope } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { State } from "../state"
@ -28,7 +28,7 @@ export interface Interface {
) => Effect.Effect<Domains[Domain][Name]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginHooks") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/PluginHooks") {}
const layer = Layer.effect(
Service,

View file

@ -1,48 +1,44 @@
export * as PluginHost from "./host"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { Plugin } from "@opencode-ai/plugin/effect"
import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types"
import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { App } from "../app"
import { Effect, Schema, Stream } from "effect"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { AISDK } from "../aisdk"
import { Catalog } from "../catalog"
import { CommandV2 } from "../command"
import { Command } from "../command"
import { Credential } from "../credential"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { Integration } from "../integration"
import { Location } from "../location"
import { ModelV2 } from "../model"
import type { PluginV2 } from "../plugin"
import { Model } from "../model"
import { PluginRuntime } from "./runtime"
import { ProviderV2 } from "../provider"
import { Provider } from "../provider"
import { Reference } from "../reference"
import { AbsolutePath, type DeepMutable } from "../schema"
import { SkillV2 } from "../skill"
import { Tool } from "../tool/tool"
import { Tools } from "../tool/tools"
import { ToolHooks } from "../tool/hooks"
import { WorkspaceV2 } from "../workspace"
import { Skill } from "../skill"
import { Tool } from "../tool"
import { Workspace } from "../workspace"
import { WebSearch } from "../websearch"
import { PluginHooks } from "./hooks"
const mutable = <T>(value: T) => value as DeepMutable<T>
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../plugin").Interface) {
const app = yield* App.Metadata
const agents = yield* AgentV2.Service
const agents = yield* Agent.Service
const aisdk = yield* AISDK.Service
const catalog = yield* Catalog.Service
const commands = yield* CommandV2.Service
const events = yield* EventV2.Service
const commands = yield* Command.Service
const bus = yield* Bus.Service
const integration = yield* Integration.Service
const location = yield* Location.Service
const reference = yield* Reference.Service
const skill = yield* SkillV2.Service
const tools = yield* Tools.Service
const skill = yield* Skill.Service
const tools = yield* Tool.Service
const websearch = yield* WebSearch.Service
const toolHooks = yield* ToolHooks.Service
const hooks = yield* PluginHooks.Service
const runtime = yield* PluginRuntime.Service
const locationInfo = () =>
@ -51,7 +47,9 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID: location.workspaceID,
project: location.project,
})
const locationRef = (input?: Parameters<Plugin.Context["agent"]["list"]>[0]) =>
const locationRef = (input?: {
readonly location?: { readonly directory?: string; readonly workspace?: string }
}) =>
input?.location === undefined
? undefined
: Location.Ref.make({
@ -59,7 +57,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID:
input.location.workspace === undefined
? location.workspaceID
: WorkspaceV2.ID.make(input.location.workspace),
: Workspace.ID.make(input.location.workspace),
})
const isCurrentLocation = (ref: Location.Ref) =>
ref.directory === location.directory && ref.workspaceID === location.workspaceID
@ -70,7 +68,22 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
app,
options: {},
agent: {
get: (id) => agents.get(AgentV2.ID.make(id)),
get: (input) => {
const ref = locationRef(input)
const output =
ref && !isCurrentLocation(ref)
? runtime.location.agent
.list(ref)
.pipe(Effect.map((result) => ({ ...result, data: result.data.find((agent) => agent.id === input.agentID) })))
: response(agents.get(input.agentID))
return output.pipe(
Effect.flatMap((result) =>
result.data
? Effect.succeed({ ...result, data: result.data })
: Effect.fail(new Error(`Agent not found: ${input.agentID}`)),
),
)
},
list: (input) => {
const ref = locationRef(input)
if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref)
@ -81,10 +94,10 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
agents.transform((draft) => {
callback({
list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(AgentV2.ID.make(id))),
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) => draft.update(AgentV2.ID.make(id), update),
remove: (id) => draft.remove(AgentV2.ID.make(id)),
get: (id) => mutable(draft.get(Agent.ID.make(id))),
default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)),
update: (id, update) => draft.update(Agent.ID.make(id), update),
remove: (id) => draft.remove(Agent.ID.make(id)),
})
}),
},
@ -121,7 +134,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
list: () => response(catalog.provider.available()),
get: (input) =>
catalog.provider
.get(ProviderV2.ID.make(input.providerID))
.get(Provider.ID.make(input.providerID))
.pipe(
Effect.flatMap((provider) =>
provider === undefined
@ -131,7 +144,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
),
},
model: {
get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
list: () => response(catalog.model.available()),
default: () => response(catalog.model.default()),
},
@ -141,21 +153,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
callback({
provider: {
list: () => mutable(draft.provider.list()),
get: (id) => mutable(draft.provider.get(ProviderV2.ID.make(id))),
update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update),
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
get: (id) => mutable(draft.provider.get(Provider.ID.make(id))),
update: (id, update) => draft.provider.update(Provider.ID.make(id), update),
remove: (id) => draft.provider.remove(Provider.ID.make(id)),
},
model: {
get: (providerID, modelID) =>
mutable(draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))),
mutable(draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))),
update: (providerID, modelID, update) =>
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update),
draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), update),
remove: (providerID, modelID) =>
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)),
default: {
get: draft.model.default.get,
set: (providerID, modelID) =>
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)),
},
},
})
@ -170,7 +182,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}),
},
event: {
subscribe: () => events.subscribe().pipe(Stream.filter(EventManifest.isServer)),
subscribe: () => bus.subscribe().pipe(Stream.filter(EventManifest.isServer)),
},
integration: {
list: () => response(integration.list()),
@ -279,88 +291,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
transform: (callback) =>
skill.transform((draft) => {
callback({
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
source: (source) => draft.source(Schema.decodeUnknownSync(Skill.Source)(source)),
list: draft.list,
})
}),
},
tool: {
transform: (callback) =>
Effect.gen(function* () {
const registrations: Array<{
readonly name: string
readonly tool: Tool.Any
readonly options?: Tool.RegisterOptions
}> = []
yield* Effect.sync(() =>
tools
.transform((draft) =>
callback({
add: (name, tool, options) => {
registrations.push({ name, tool, ...(options ? { options } : {}) })
},
add: (tool) => draft.add(tool),
}),
)
yield* tools
.registerBatch(
registrations.map((registration) => ({
tools: { [registration.name]: registration.tool },
...(registration.options === undefined ? {} : { options: registration.options }),
})),
)
.pipe(Effect.orDie)
return { dispose: Effect.void }
}),
hook: (name, callback) => {
if (name === "execute.before") {
return toolHooks.hook.before((event) => {
const output = {
tool: event.tool,
sessionID: event.sessionID,
agent: event.agent,
messageID: event.messageID,
callID: event.callID,
input: event.input,
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() => Effect.sync(() => (event.input = output.input))),
)
})
}
return toolHooks.hook.after((event) => {
// Decode first so plugin mutations cannot alias the canonical outcome.
const output = {
tool: event.tool,
sessionID: event.sessionID,
agent: event.agent,
messageID: event.messageID,
callID: event.callID,
input: event.input,
...Schema.decodeUnknownSync(Tool.ExecuteAfterOutcome)(event),
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() => {
const decoded = Schema.decodeUnknownOption(Tool.ExecuteAfterOutcome)(output)
if (decoded._tag === "None")
return Effect.logWarning("ignoring invalid execute.after tool outcome", { tool: event.tool })
if (decoded.value.status !== event.status)
return Effect.logWarning("ignoring execute.after tool status change", { tool: event.tool })
return Effect.sync(() => {
if (event.status === "completed" && decoded.value.status === "completed") {
event.content = decoded.value.content
event.metadata = decoded.value.metadata
event.outputPaths = decoded.value.outputPaths
return
}
if (event.status === "error" && decoded.value.status === "error") {
event.error = decoded.value.error
event.content = decoded.value.content
event.metadata = decoded.value.metadata
event.outputPaths = decoded.value.outputPaths
}
})
}),
)
})
},
.pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
hook: (name, callback) => hooks.register("tool", name, callback),
},
websearch: {
providers: () => response(websearch.providers()),

View file

@ -1,11 +1,11 @@
export * as PluginInternal from "./internal"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import type { Plugin } from "@opencode-ai/plugin/effect/plugin"
import { Context, Effect, Scope } from "effect"
import { HttpClient } from "effect/unstable/http"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { Catalog } from "../catalog"
import { CommandV2 } from "../command"
import { Command } from "../command"
import { Config } from "../config"
import { ConfigAgentPlugin } from "../config/plugin/agent"
import { ConfigCommandPlugin } from "../config/plugin/command"
@ -14,7 +14,7 @@ import { ConfigPolicyPlugin } from "../config/plugin/policy"
import { ConfigReferencePlugin } from "../config/plugin/reference"
import { ConfigSkillPlugin } from "../config/plugin/skill"
import { ConfigWebSearchPlugin } from "../config/plugin/websearch"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { FileMutation } from "../file-mutation"
import { Form } from "../form"
import { FileSystem } from "../filesystem"
@ -27,28 +27,28 @@ import { Location } from "../location"
import { LocationMutation } from "../location-mutation"
import { ModelsDev } from "../models-dev"
import { Npm } from "@opencode-ai/util/npm"
import { PermissionV2 } from "../permission"
import { Permission } from "../permission"
import { Reference } from "../reference"
import { WebSearch } from "../websearch"
import { Ripgrep } from "../ripgrep"
import { SessionInstructions } from "../session/instructions"
import { Shell } from "../shell"
import { SkillV2 } from "../skill"
import { PatchTool } from "../tool/patch"
import { EditTool } from "../tool/edit"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { QuestionTool } from "../tool/question"
import { Skill } from "../skill"
import { PatchTool } from "../tool/plugin/patch"
import { EditTool } from "../tool/plugin/edit"
import { GlobTool } from "../tool/plugin/glob"
import { GrepTool } from "../tool/plugin/grep"
import { QuestionTool } from "../tool/plugin/question"
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 { Tools } from "../tool/tools"
import { WebFetchTool } from "../tool/webfetch"
import { WebSearchTool } from "../tool/websearch"
import { ReadTool } from "../tool/plugin/read"
import { ShellTool } from "../tool/plugin/shell"
import { SkillTool } from "../tool/plugin/skill"
import { SubagentTool } from "../tool/plugin/subagent"
import { Tool } from "../tool"
import { WebFetchTool } from "../tool/plugin/webfetch"
import { WebSearchTool } from "../tool/plugin/websearch"
import { WellKnown } from "../wellknown"
import { WriteTool } from "../tool/write"
import { WriteTool } from "../tool/plugin/write"
import { AgentPlugin } from "./agent"
import { CommandPlugin } from "./command"
import { ModelsDevPlugin } from "./models-dev"
@ -62,11 +62,11 @@ import { WarmingPlugin } from "./warming"
import { WellKnownPlugin } from "../wellknown/plugin"
const services = Effect.fn("PluginInternal.services")(function* () {
const agent = yield* AgentV2.Service
const agent = yield* Agent.Service
const catalog = yield* Catalog.Service
const command = yield* CommandV2.Service
const command = yield* Command.Service
const config = yield* Config.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const mutation = yield* FileMutation.Service
const filesystem = yield* FileSystem.Service
const fs = yield* FSUtil.Service
@ -79,7 +79,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const locationMutation = yield* LocationMutation.Service
const models = yield* ModelsDev.Service
const npm = yield* Npm.Service
const permission = yield* PermissionV2.Service
const permission = yield* Permission.Service
const runtime = yield* PluginRuntime.Service
const form = yield* Form.Service
const read = yield* ReadToolFileSystem.Service
@ -88,15 +88,15 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const ripgrep = yield* Ripgrep.Service
const instructions = yield* SessionInstructions.Service
const shell = yield* Shell.Service
const skill = yield* SkillV2.Service
const tools = yield* Tools.Service
const skill = yield* Skill.Service
const tools = yield* Tool.Service
const wellknown = yield* WellKnown.Service
return Context.mergeAll(
Context.make(AgentV2.Service, agent),
Context.make(Agent.Service, agent),
Context.make(Catalog.Service, catalog),
Context.make(CommandV2.Service, command),
Context.make(Command.Service, command),
Context.make(Config.Service, config),
Context.make(EventV2.Service, events),
Context.make(Bus.Service, bus),
Context.make(FileMutation.Service, mutation),
Context.make(FileSystem.Service, filesystem),
Context.make(FSUtil.Service, fs),
@ -109,7 +109,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(LocationMutation.Service, locationMutation),
Context.make(ModelsDev.Service, models),
Context.make(Npm.Service, npm),
Context.make(PermissionV2.Service, permission),
Context.make(Permission.Service, permission),
Context.make(PluginRuntime.Service, runtime),
Context.make(Form.Service, form),
Context.make(ReadToolFileSystem.Service, read),
@ -118,8 +118,8 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(Ripgrep.Service, ripgrep),
Context.make(SessionInstructions.Service, instructions),
Context.make(Shell.Service, shell),
Context.make(SkillV2.Service, skill),
Context.make(Tools.Service, tools),
Context.make(Skill.Service, skill),
Context.make(Tool.Service, tools),
Context.make(WellKnown.Service, wellknown),
)
})

View file

@ -1,13 +1,14 @@
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Integration } from "@opencode-ai/schema/integration"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { ModelsDev } from "../models-dev"
export const ModelsDevPlugin = define({
id: "opencode.models-dev",
effect: Effect.fn(function* (ctx) {
const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const loaded = { data: structuredClone(yield* modelsDev.get()) }
yield* ctx.integration.transform((integrations) => {
for (const provider of loaded.data) {
@ -28,14 +29,14 @@ export const ModelsDevPlugin = define({
for (const provider of loaded.data) {
catalog.provider.update(provider.info.id, (draft) => {
Object.assign(draft, provider.info)
draft.integrationID = provider.info.id
draft.integrationID = Integration.ID.make(provider.info.id)
})
for (const model of provider.models) {
catalog.model.update(provider.info.id, model.id, (draft) => Object.assign(draft, model))
}
}
})
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
yield* bus.subscribe(ModelsDev.Event.Refreshed).pipe(
Stream.runForEach(() =>
modelsDev.get().pipe(
Effect.tap((data) => Effect.sync(() => (loaded.data = structuredClone(data)))),

View file

@ -1,8 +1,8 @@
export * as PluginPromise from "./promise"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import type { Context, Plugin } from "@opencode-ai/plugin/v2/plugin"
import type { Any, RegisterOptions } from "@opencode-ai/plugin/v2/tool"
import { define } from "@opencode-ai/plugin/effect/plugin"
import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin"
import type { Info } from "@opencode-ai/plugin/promise/tool"
import { Agent } from "@opencode-ai/schema/agent"
import { Integration } from "@opencode-ai/schema/integration"
import { Location } from "@opencode-ai/schema/location"
@ -14,7 +14,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace"
import { WebSearch } from "@opencode-ai/schema/websearch"
import { DateTime, Effect, Scope, Stream } from "effect"
import { Tool } from "../tool/tool"
import { Tool } from "../tool"
type HostRegistration = { readonly dispose: Effect.Effect<void> }
type Registration = { readonly dispose: () => Promise<void> }
@ -23,7 +23,7 @@ type JsonValue = null | boolean | number | string | Array<JsonValue> | { [key: s
/**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
* loader (`PluginV2` / `PluginSupervisor`) can run it unchanged.
* loader (`Plugin` / `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
@ -61,7 +61,7 @@ export function fromPromise(plugin: Plugin) {
app: host.app,
options: host.options,
agent: {
get: (id) => run(host.agent.get(id)),
get: (input) => run(host.agent.get({ ...input, agentID: Agent.ID.make(input.agentID) })),
list: (input) => run(host.agent.list(input)),
transform: transform(host.agent),
reload: () => run(host.agent.reload()),
@ -77,7 +77,6 @@ export function fromPromise(plugin: Plugin) {
run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
},
model: {
get: (providerID, modelID) => run(host.catalog.model.get(providerID, modelID)),
list: (input) => run(host.catalog.model.list(input)),
default: (input) =>
run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })),
@ -165,7 +164,44 @@ export function fromPromise(plugin: Plugin) {
}),
),
},
transform: transform(host.integration),
transform: (callback) =>
register(
host.integration.transform((draft) =>
callback({
list: draft.list,
get: draft.get,
update: draft.update,
remove: draft.remove,
method: {
list: draft.method.list,
update: (input) => {
if (!("authorize" in input)) return draft.method.update(input)
const refresh = input.refresh
draft.method.update({
...input,
authorize: (inputs) =>
Effect.promise(() => input.authorize(inputs)).pipe(
Effect.map((authorization) =>
authorization.mode === "auto"
? {
...authorization,
callback: Effect.promise(() => authorization.callback),
}
: {
...authorization,
callback: (code) => Effect.promise(() => authorization.callback(code)),
},
),
),
refresh:
refresh === undefined ? undefined : (credential) => Effect.promise(() => refresh(credential)),
})
},
remove: draft.method.remove,
},
}),
),
),
reload: () => run(host.integration.reload()),
connection: {
active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)),
@ -190,8 +226,11 @@ export function fromPromise(plugin: Plugin) {
register(
host.tool.transform((draft) =>
callback({
add: (name: string, tool: Any, options?: RegisterOptions) =>
draft.add(name, fromPromiseTool(tool), options),
add: (tool: Info) =>
draft.add({
...tool,
execute: (input, context) => executePromiseTool(tool, input, context),
}),
}),
),
),
@ -333,15 +372,10 @@ function wireEvent(value: unknown): unknown {
return wire(value)
}
function fromPromiseTool(tool: Any): Tool.Any {
return {
...tool,
execute: (input, context) =>
Effect.promise(() =>
tool.execute(input, {
...context,
progress: (update) => Effect.runPromise(context.progress(update)),
}),
),
}
}
const executePromiseTool = (tool: Info, input: any, context: Tool.Context) =>
Effect.promise(() =>
tool.execute(input, {
...context,
progress: (update) => Effect.runPromise(context.progress(update)),
}),
)

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const AlibabaPlugin = define({
id: "opencode.provider.alibaba",

View file

@ -1,7 +1,7 @@
import { Effect } from "effect"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
type MantleSDK = {
languageModel: (modelID: string) => LanguageModelV3
@ -64,8 +64,8 @@ export const AmazonBedrockPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
evt.provider.update(item.provider.id, (provider) => {
if (typeof provider.settings?.endpoint !== "string") return
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
@ -112,10 +112,10 @@ export const AmazonBedrockPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (evt.model.providerID !== Provider.ID.amazonBedrock) return
if (
ProviderV2.isAISDK(evt.model.package) &&
ProviderV2.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
Provider.isAISDK(evt.model.package) &&
Provider.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
) {
evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
return

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const AnthropicPlugin = define({
id: "opencode.provider.anthropic",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {
...provider.headers,

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
if (useChat && sdk.chat) return sdk.chat(modelID)
@ -15,8 +15,8 @@ export const AzurePlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/azure") continue
const configured = item.provider.settings?.resourceName
const resourceName =
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
@ -30,11 +30,11 @@ export const AzurePlugin = define({
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return
if (evt.model.providerID === ProviderV2.ID.azure) {
if (evt.model.providerID === Provider.ID.azure) {
if (
!evt.options.resourceName &&
!evt.options.baseURL &&
(!ProviderV2.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string")
(!Provider.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string")
) {
throw new Error(
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
@ -48,7 +48,7 @@ export const AzurePlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return
if (evt.model.providerID !== Provider.ID.azure) return
evt.language = selectLanguage(
evt.sdk,
evt.model.modelID ?? evt.model.id,
@ -66,8 +66,8 @@ export const AzureCognitiveServicesPlugin = define({
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!item.provider.id.includes("azure-cognitive-services")) continue
evt.provider.update(item.provider.id, (provider) => {
provider.settings = {
@ -80,7 +80,7 @@ export const AzureCognitiveServicesPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
if (evt.model.providerID !== Provider.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage(
evt.sdk,
evt.model.modelID ?? evt.model.id,

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const CerebrasPlugin = define({
id: "opencode.provider.cerebras",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
})

View file

@ -1,7 +1,7 @@
import os from "os"
import { App } from "../../app"
import { Effect, Option, Schema } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const CloudflareAIGatewayPlugin = define({
id: "opencode.provider.cloudflare-ai-gateway",

View file

@ -1,10 +1,10 @@
import os from "os"
import { App } from "../../app"
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
const providerID = Provider.ID.make("cloudflare-workers-ai")
export const CloudflareWorkersAIPlugin = define({
id: "opencode.provider.cloudflare-workers-ai",
@ -13,7 +13,7 @@ export const CloudflareWorkersAIPlugin = define({
const item = evt.provider.get(providerID)
if (!item) return
evt.provider.update(item.provider.id, (provider) => {
if (!ProviderV2.isAISDK(provider.package)) return
if (!Provider.isAISDK(provider.package)) return
if (typeof provider.settings?.baseURL === "string") return
const accountId = resolveAccountId(provider.settings ?? {})
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
@ -61,7 +61,7 @@ function hasWorkersEndpoint(model: {
readonly package?: string
readonly settings?: Readonly<Record<string, unknown>>
}) {
return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
return Provider.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
}
function sdkOptions(options: Record<string, any>, app: App.Info) {

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const CoherePlugin = define({
id: "opencode.provider.cohere",

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const DeepInfraPlugin = define({
id: "opencode.provider.deepinfra",

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { pathToFileURL } from "url"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Npm } from "@opencode-ai/util/npm"
import { importModule } from "@opencode-ai/util/runtime-import"

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const GatewayPlugin = define({
id: "opencode.provider.gateway",

View file

@ -1,14 +1,14 @@
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { Effect, Option, Schema, Semaphore, Stream } from "effect"
import { Catalog } from "../../catalog"
import { Credential } from "../../credential"
import { EventV2 } from "../../event"
import { Bus } from "../../bus"
import { CopilotModels } from "../../github-copilot/models"
import { App } from "../../app"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { Model } from "../../model"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
import type { PluginInternal } from "../internal"
const clientID = "Ov23li8tweQw6odWQebz"
@ -152,11 +152,11 @@ export const GithubCopilotPlugin = define({
id: "opencode.provider.github-copilot",
effect: Effect.fn(function* (ctx) {
const catalog = yield* Catalog.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const loading = Semaphore.makeUnsafe(1)
const loaded: {
baseURL?: string
models?: Map<ModelV2.ID, ModelV2.Info>
models?: Map<Model.ID, Model.Info>
} = {}
const load = Effect.fn("GithubCopilotPlugin.load")(function* () {
@ -171,8 +171,8 @@ export const GithubCopilotPlugin = define({
}
loaded.baseURL = copilotBaseURL(credential.metadata)
const provider = yield* catalog.provider.get(ProviderV2.ID.githubCopilot)
const existing = (yield* catalog.model.all()).filter((model) => model.providerID === ProviderV2.ID.githubCopilot)
const provider = yield* catalog.provider.get(Provider.ID.githubCopilot)
const existing = (yield* catalog.model.all()).filter((model) => model.providerID === Provider.ID.githubCopilot)
loaded.models = yield* Effect.tryPromise({
try: () =>
CopilotModels.get(
@ -197,11 +197,11 @@ export const GithubCopilotPlugin = define({
draft.method.update(oauth(ctx.app))
})
yield* ctx.catalog.transform((evt) => {
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
const item = evt.provider.get(Provider.ID.githubCopilot)
if (!item) return
if (loaded.models) {
for (const id of item.models.keys()) {
if (!loaded.models.has(ModelV2.ID.make(id))) evt.model.remove(item.provider.id, id)
if (!loaded.models.has(Model.ID.make(id))) evt.model.remove(item.provider.id, id)
}
for (const [id, model] of loaded.models) {
evt.model.update(item.provider.id, id, (draft) => Object.assign(draft, structuredClone(model)))
@ -209,12 +209,12 @@ export const GithubCopilotPlugin = define({
} else if (loaded.baseURL) {
for (const id of item.models.keys()) {
evt.model.update(item.provider.id, id, (model) => {
model.settings = ProviderV2.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
})
}
}
if (item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) {
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
if (item.models.has(Model.ID.make("gpt-5-chat-latest"))) {
evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => {
// This chat-only alias conflicts with the Copilot GPT-5 Responses route,
// so hide it only for Copilot rather than for every provider catalog.
model.enabled = false
@ -222,7 +222,7 @@ export const GithubCopilotPlugin = define({
}
})
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("github-copilot")),
Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }),
@ -231,7 +231,7 @@ export const GithubCopilotPlugin = define({
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.model.providerID !== Provider.ID.githubCopilot) return
if (evt.package !== "@ai-sdk/github-copilot" && evt.package !== "@ai-sdk/anthropic") return
evt.options.fetch = copilotFetch(
typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined,
@ -255,7 +255,7 @@ export const GithubCopilotPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.model.providerID !== Provider.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
return

View file

@ -1,8 +1,8 @@
import os from "os"
import { App } from "../../app"
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const GitLabPlugin = define({
id: "opencode.provider.gitlab",
@ -35,7 +35,7 @@ export const GitLabPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
if (evt.model.providerID !== Provider.ID.gitlab) return
const featureFlags =
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
const id = evt.model.modelID ?? evt.model.id

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
function resolveProject(options: Record<string, any>) {
// models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex, while Google SDKs
@ -59,12 +59,12 @@ export const GoogleVertexPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (!Provider.isAISDK(item.provider.package)) continue
if (
ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
!(
item.provider.id === ProviderV2.ID.googleVertex &&
ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
item.provider.id === Provider.ID.googleVertex &&
Provider.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
)
)
continue
@ -78,7 +78,7 @@ export const GoogleVertexPlugin = define({
...(typeof provider.settings?.baseURL === "string"
? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
: {}),
...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
...(Provider.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
? { fetch: authFetch(provider.settings?.fetch) }
: {}),
}
@ -88,7 +88,7 @@ export const GoogleVertexPlugin = define({
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
if (evt.model.providerID === Provider.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
evt.options.fetch = authFetch(evt.options.fetch)
return
}
@ -108,7 +108,7 @@ export const GoogleVertexPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
if (evt.model.providerID !== Provider.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
}),
)
@ -120,8 +120,8 @@ export const GoogleVertexAnthropicPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
const project =
item.provider.settings?.project ??
process.env.GOOGLE_CLOUD_PROJECT ??
@ -167,7 +167,7 @@ export const GoogleVertexAnthropicPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
if (evt.model.providerID !== Provider.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
}),
)

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const GooglePlugin = define({
id: "opencode.provider.google",

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const GroqPlugin = define({
id: "opencode.provider.groq",

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const KiloPlugin = define({
id: "opencode.provider.kilo",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }

View file

@ -1,7 +1,7 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Integration } from "../../integration"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
export const LLMGatewayPlugin = define({
id: "opencode.provider.llmgateway",
@ -11,8 +11,8 @@ export const LLMGatewayPlugin = define({
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (item.provider.disabled) continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
if (!configured.has(Integration.ID.make(item.provider.id))) continue
evt.provider.update(item.provider.id, (provider) => {

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const MistralPlugin = define({
id: "opencode.provider.mistral",

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const NvidiaPlugin = define({
id: "opencode.provider.nvidia",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const OpenAICompatiblePlugin = define({
id: "opencode.provider.openai-compatible",

View file

@ -1,14 +1,14 @@
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 type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
import { App } from "../../app"
import { Credential } from "../../credential"
import { EventV2 } from "../../event"
import { Bus } from "../../bus"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { Model } from "../../model"
import { OauthCallbackPage } from "../../oauth/page"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
import type { PluginInternal } from "../internal"
import { OpenAICodex } from "./openai-codex"
@ -162,7 +162,7 @@ const headless = (app: App.Info) => ({
export const OpenAIPlugin = define({
id: "opencode.provider.openai",
effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const loading = Semaphore.makeUnsafe(1)
let chatgpt = false
@ -181,17 +181,17 @@ export const OpenAIPlugin = define({
yield* load()
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") continue
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai") continue
if (!item.models.has(Model.ID.make("gpt-5-chat-latest"))) continue
evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => {
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
// chat-completions-only model, so hide it only from OpenAI's catalog.
model.enabled = false
})
}
if (!chatgpt) return
const item = evt.provider.get(ProviderV2.ID.openai)
const item = evt.provider.get(Provider.ID.openai)
if (!item) return
for (const model of item.models.values()) {
// ChatGPT-plan tokens only authorize codex-eligible models, and the
@ -211,7 +211,7 @@ export const OpenAIPlugin = define({
})
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")),
Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }),
@ -227,7 +227,7 @@ export const OpenAIPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return
if (evt.model.providerID !== Provider.ID.openai) return
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
}),
)

View file

@ -1,14 +1,14 @@
import { Duration, Effect, Schema, Semaphore, Stream } from "effect"
import type { Scope } from "effect"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import type { CredentialValue } from "@opencode-ai/sdk/v2/types"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { EventV2 } from "../../event"
import { Bus } from "../../bus"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { Model } from "../../model"
import { Provider } from "../../provider"
import { ConfigProviderV1 } from "../../v1/config/provider"
import { Money } from "@opencode-ai/schema/money"
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options"
@ -82,10 +82,10 @@ function oauth(http: HttpClient.HttpClient) {
} satisfies IntegrationOAuthMethodRegistration
}
export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | Scope.Scope>({
export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope.Scope>({
id: "opencode.provider.opencode",
effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const http = yield* HttpClient.HttpClient
const loading = Semaphore.makeUnsafe(1)
let connected = false
@ -120,7 +120,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
catalog.provider.update(providerID, (provider) => {
provider.integrationID = Integration.ID.make("opencode")
if (item.name !== undefined) provider.name = item.name
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
provider.package = item.npm ? Provider.aisdk(item.npm) : ""
provider.settings = {
...provider.settings,
...withoutCredentials(item.options),
@ -131,12 +131,12 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
for (const [modelID, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, modelID, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.family !== undefined) model.family = Model.Family.make(config.family)
if (config.name !== undefined) model.name = config.name
if (config.id !== undefined) model.modelID = config.id
model.compatibility = ModelV2.compatibility(config.interleaved) ?? model.compatibility
if (config.id !== undefined) model.modelID = Model.ID.make(config.id)
model.compatibility = Model.compatibility(config.interleaved) ?? model.compatibility
if (config.provider !== undefined) {
model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined
model.package = config.provider.npm ? Provider.aisdk(config.provider.npm) : undefined
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
}
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
@ -147,7 +147,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
if (config.variants !== undefined) {
model.variants ??= []
for (const [id, options] of Object.entries(config.variants)) {
const variantID = ModelV2.VariantID.make(id)
const variantID = Model.VariantID.make(id)
let existing = model.variants.find((item) => item.id === variantID)
if (!existing) {
existing = { id: variantID }
@ -174,7 +174,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
}
}
const item = catalog.provider.get(ProviderV2.ID.opencode)
const item = catalog.provider.get(Provider.ID.opencode)
if (!item) return
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
catalog.provider.update(item.provider.id, (provider) => {
@ -190,7 +190,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
})
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")),
Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }),

View file

@ -1,19 +1,19 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Model } from "../../model"
import { Provider } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const OpenRouterPlugin = define({
id: "opencode.provider.openrouter",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
})
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
for (const modelID of [Model.ID.make("gpt-5-chat-latest"), Model.ID.make("openai/gpt-5-chat")]) {
if (!item.models.has(modelID)) continue
evt.model.update(item.provider.id, modelID, (model) => {
// These are OpenRouter-specific OpenAI chat aliases that do not work

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const PerplexityPlugin = define({
id: "opencode.provider.perplexity",

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { pathToFileURL } from "url"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Npm } from "@opencode-ai/util/npm"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
import { importModule } from "@opencode-ai/util/runtime-import"
export const SapAICorePlugin = define({
@ -12,7 +12,7 @@ export const SapAICorePlugin = define({
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
if (evt.model.providerID !== Provider.ID.make("sap-ai-core")) return
const serviceKey =
process.env.AICORE_SERVICE_KEY ??
(typeof evt.options.serviceKey === "string" ? evt.options.serviceKey : undefined)
@ -42,7 +42,7 @@ export const SapAICorePlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
if (evt.model.providerID !== Provider.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
}),
)

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
@ -70,7 +70,7 @@ export const SnowflakeCortexPlugin = define({
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
if (evt.model.providerID !== Provider.ID.make("snowflake-cortex")) return
const token =
process.env.SNOWFLAKE_CORTEX_TOKEN ??
process.env.SNOWFLAKE_CORTEX_PAT ??

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const TogetherAIPlugin = define({
id: "opencode.provider.togetherai",

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const VenicePlugin = define({
id: "opencode.provider.venice",

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const VercelPlugin = define({
id: "opencode.provider.vercel",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
})

View file

@ -1,12 +1,12 @@
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 type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Clock, Deferred, Effect, Option, Schema } from "effect"
import { App } from "../../app"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { OauthCallbackPage } from "../../oauth/page"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
const clientID = "b1a00492-073a-47ea-816f-4c329264a828"
const issuer = "https://auth.x.ai/oauth2"
@ -173,7 +173,7 @@ export const XAIPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
if (evt.model.providerID !== Provider.ID.make("xai")) return
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
}),
)

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const ZenmuxPlugin = define({
id: "opencode.provider.zenmux",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {

View file

@ -1,16 +1,16 @@
export * as PluginRuntime from "./runtime"
import { Context, Effect, Layer } from "effect"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { Job } from "../job"
import { Location } from "../location"
import { LocationServiceMap } from "../location-service-map"
import { SessionV2 } from "../session"
import { Session } from "../session"
export interface Interface {
readonly session: Pick<
SessionV2.Interface,
Session.Interface,
"get" | "create" | "messages" | "prompt" | "generate" | "command" | "resume" | "interrupt" | "synthetic"
>
readonly job: Pick<Job.Interface, "start" | "wait" | "block" | "background" | "cancel">
@ -18,7 +18,7 @@ export interface Interface {
readonly agent: {
readonly list: (
ref: Location.Ref,
) => Effect.Effect<{ readonly location: Location.Info; readonly data: AgentV2.Info[] }>
) => Effect.Effect<{ readonly location: Location.Info; readonly data: Agent.Info[] }>
}
}
}
@ -74,7 +74,7 @@ export const layerWithCell = (cell: Cell) =>
export const providerLayerWithCell = (cell: Cell) =>
Layer.effectDiscard(
Effect.gen(function* () {
const sessions = yield* SessionV2.Service
const sessions = yield* Session.Service
const jobs = yield* Job.Service
const locations = yield* LocationServiceMap.Service
const runtime: Interface = {
@ -85,7 +85,7 @@ export const providerLayerWithCell = (cell: Cell) =>
list: (ref) =>
Effect.gen(function* () {
const location = yield* Location.Service
const agents = yield* AgentV2.Service
const agents = yield* Agent.Service
return {
location: new Location.Info({
directory: location.directory,
@ -118,7 +118,7 @@ export const providerNodeWithCell = (cell: Cell) =>
makeGlobalNode({
name: "plugin-runtime-provider",
layer: providerLayerWithCell(cell),
deps: [node, SessionV2.node, Job.node, LocationServiceMap.node],
deps: [node, Session.node, Job.node, LocationServiceMap.node],
})
export const providerNode = providerNodeWithCell(defaultCell)

View file

@ -1,12 +1,12 @@
export * as SdkPlugins from "./sdk"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import type { Plugin } from "@opencode-ai/plugin/effect/plugin"
import { Context, Effect, Layer } from "effect"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "../event"
import type { PluginV2 } from "../plugin"
import { Bus } from "../bus"
import type { Versioned } from "../plugin"
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
export const Updated = Bus.ephemeral({ type: "sdk.plugin.updated", schema: {} })
/**
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
@ -21,7 +21,7 @@ export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {
*/
export interface Interface {
readonly register: (plugin: Plugin) => Effect.Effect<void>
readonly all: () => readonly PluginV2.Versioned[]
readonly all: () => readonly Versioned[]
}
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
@ -29,17 +29,17 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Sd
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const plugins = new Map<string, PluginV2.Versioned>()
const bus = yield* Bus.Service
const plugins = new Map<string, Versioned>()
let revision = 0
return Service.of({
register: (plugin) =>
Effect.sync(() => {
plugins.set(plugin.id, { ...plugin, version: String(++revision) })
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
}).pipe(Effect.andThen(bus.publish(Updated, {})), Effect.asVoid),
all: () => [...plugins.values()],
})
}),
)
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })
export const node = makeGlobalNode({ service: Service, layer, deps: [Bus.node] })

Some files were not shown because too many files have changed in this diff Show more