mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 03:19:59 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3eb74673c2 |
@@ -99,8 +99,7 @@ jobs:
|
|||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
# Playwright 1.59 hangs while extracting Chromium with Node 24.16.
|
node-version: "24"
|
||||||
node-version: "24.15"
|
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|||||||
@@ -276,7 +276,6 @@
|
|||||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
|
||||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
"@openrouter/ai-sdk-provider": "2.9.0",
|
||||||
"@opentelemetry/api": "1.9.0",
|
"@opentelemetry/api": "1.9.0",
|
||||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||||
@@ -626,7 +625,6 @@
|
|||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.9",
|
"version": "1.17.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-oWSGu+SP66Aquy/0Vaq7Bgp8404ZdOWbQX+O7h3jxHU=",
|
"x86_64-linux": "sha256-g0tDvRf7MErZ1PEeUazEYi492ZHiRT8kYv3bPdkss/I=",
|
||||||
"aarch64-linux": "sha256-UsS0+c+GwtIukmWwQeFbY/3Oaz3t4Q7C6cFMGkmlyAY=",
|
"aarch64-linux": "sha256-6sKgf3ftbIqlPxlFkoPzoWPsJp3IwXD+H3Y6g874xmk=",
|
||||||
"aarch64-darwin": "sha256-CArz92ewPmXO+ORFCBkCH8LzMpU/DjyaO4ic7QL0UpI=",
|
"aarch64-darwin": "sha256-Se/Nls/KlkuK2ysDQ9DeAzSaX3NsL2iDdf/dsv2GIXc=",
|
||||||
"x86_64-darwin": "sha256-rhnz9gmG6L06wIzfMhTaXDDEf6IbMD32CavqwXoqcUs="
|
"x86_64-darwin": "sha256-V9MCkqnvQ1nkD2PaaTfNFKkBZGymj6KxrSAK6+DTF8Y="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,6 @@
|
|||||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
|
||||||
"@opentelemetry/api": "1.9.0",
|
"@opentelemetry/api": "1.9.0",
|
||||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * as AgentV2 from "./agent"
|
export * as AgentV2 from "./agent"
|
||||||
|
|
||||||
import { Array, Context, Effect, Layer, Schema, Scope, Types } from "effect"
|
import { Array, Context, Effect, Layer, Schema, Scope } from "effect"
|
||||||
|
import { castDraft, enableMapSet, type Draft } from "immer"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { PermissionSchema } from "./permission/schema"
|
import { PermissionSchema } from "./permission/schema"
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
@@ -48,19 +49,21 @@ export interface Selection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
agents: Map<ID, Types.DeepMutable<Info>>
|
agents: Map<ID, Info>
|
||||||
default?: ID
|
default?: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Draft = {
|
export type Editor = {
|
||||||
list: () => readonly Info[]
|
list: () => readonly Info[]
|
||||||
get: (id: ID) => Info | undefined
|
get: (id: ID) => Info | undefined
|
||||||
default: (id: ID | undefined) => void
|
default: (id: ID | undefined) => void
|
||||||
update: (id: ID, fn: (agent: Types.DeepMutable<Info>) => void) => void
|
update: (id: ID, fn: (agent: Draft<Info>) => void) => void
|
||||||
remove: (id: ID) => void
|
remove: (id: ID) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface extends State.Transformable<Draft> {
|
export interface Interface {
|
||||||
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
|
readonly update: State.Interface<Data, Editor>["update"]
|
||||||
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
||||||
readonly default: () => Effect.Effect<Info | undefined>
|
readonly default: () => Effect.Effect<Info | undefined>
|
||||||
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
|
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
|
||||||
@@ -70,19 +73,21 @@ export interface Interface extends State.Transformable<Draft> {
|
|||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
|
||||||
|
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Editor>({
|
||||||
initial: () => ({ agents: new Map() }),
|
initial: () => ({ agents: new Map() }),
|
||||||
draft: (draft) => ({
|
editor: (draft) => ({
|
||||||
list: () => Array.fromIterable(draft.agents.values()) as Info[],
|
list: () => Array.fromIterable(draft.agents.values()) as Info[],
|
||||||
get: (id) => draft.agents.get(id),
|
get: (id) => draft.agents.get(id),
|
||||||
default: (id) => {
|
default: (id) => {
|
||||||
draft.default = id
|
draft.default = id
|
||||||
},
|
},
|
||||||
update: (id, fn) => {
|
update: (id, fn) => {
|
||||||
const current = draft.agents.get(id) ?? (Info.empty(id) as Types.DeepMutable<Info>)
|
const current = draft.agents.get(id) ?? castDraft(Info.empty(id))
|
||||||
if (!draft.agents.has(id)) draft.agents.set(id, current)
|
if (!draft.agents.has(id)) draft.agents.set(id, current)
|
||||||
fn(current)
|
fn(current)
|
||||||
current.id = id
|
current.id = id
|
||||||
@@ -108,7 +113,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
update: state.update,
|
||||||
get: Effect.fn("AgentV2.get")(function* (id) {
|
get: Effect.fn("AgentV2.get")(function* (id) {
|
||||||
return state.get().agents.get(id)
|
return state.get().agents.get(id)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
export * as Catalog from "./catalog"
|
export * as Catalog from "./catalog"
|
||||||
|
|
||||||
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
|
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema, Scope, Stream } from "effect"
|
||||||
|
import { castDraft, enableMapSet, type Draft } from "immer"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { ModelRequest } from "./model-request"
|
import { ModelRequest } from "./model-request"
|
||||||
|
import { PluginV2 } from "./plugin"
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
|
import { Location } from "./location"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Policy } from "./policy"
|
import { Policy } from "./policy"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
import { Integration } from "./integration"
|
import { Integration } from "./integration"
|
||||||
|
|
||||||
export type ProviderRecord = {
|
export type ProviderRecord = {
|
||||||
provider: ProviderV2.MutableInfo
|
provider: ProviderV2.Info
|
||||||
models: Map<ModelV2.ID, ModelV2.MutableInfo>
|
models: Map<ModelV2.ID, ModelV2.Info>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||||
|
|
||||||
|
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
|
||||||
|
"CatalogV2.ProviderNotFound",
|
||||||
|
{
|
||||||
|
providerID: ProviderV2.ID,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("CatalogV2.ModelNotFound", {
|
||||||
|
providerID: ProviderV2.ID,
|
||||||
|
modelID: ModelV2.ID,
|
||||||
|
}) {}
|
||||||
|
|
||||||
export const PolicyActions = Schema.Literals(["provider.use"])
|
export const PolicyActions = Schema.Literals(["provider.use"])
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
@@ -27,16 +42,16 @@ type Data = {
|
|||||||
defaultModel?: DefaultModel
|
defaultModel?: DefaultModel
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Draft = {
|
export type Editor = {
|
||||||
provider: {
|
provider: {
|
||||||
list: () => readonly ProviderRecord[]
|
list: () => readonly ProviderRecord[]
|
||||||
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
|
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
|
||||||
update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void
|
update: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => void
|
||||||
remove: (providerID: ProviderV2.ID) => void
|
remove: (providerID: ProviderV2.ID) => void
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
|
get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
|
||||||
update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void
|
update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft<ModelV2.Info>) => void) => void
|
||||||
remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
||||||
default: {
|
default: {
|
||||||
get: () => DefaultModel | undefined
|
get: () => DefaultModel | undefined
|
||||||
@@ -45,29 +60,38 @@ export type Draft = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface extends State.Transformable<Draft> {
|
export interface Interface {
|
||||||
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
readonly provider: {
|
readonly provider: {
|
||||||
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info | undefined>
|
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
|
||||||
readonly all: () => Effect.Effect<ProviderV2.Info[]>
|
readonly all: () => Effect.Effect<ProviderV2.Info[]>
|
||||||
readonly available: () => Effect.Effect<ProviderV2.Info[]>
|
readonly available: () => Effect.Effect<ProviderV2.Info[]>
|
||||||
}
|
}
|
||||||
readonly model: {
|
readonly model: {
|
||||||
readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect<ModelV2.Info | undefined>
|
readonly get: (
|
||||||
|
providerID: ProviderV2.ID,
|
||||||
|
modelID: ModelV2.ID,
|
||||||
|
) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
|
||||||
readonly all: () => Effect.Effect<ModelV2.Info[]>
|
readonly all: () => Effect.Effect<ModelV2.Info[]>
|
||||||
readonly available: () => Effect.Effect<ModelV2.Info[]>
|
readonly available: () => Effect.Effect<ModelV2.Info[]>
|
||||||
readonly default: () => Effect.Effect<ModelV2.Info | undefined>
|
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||||
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<ModelV2.Info | undefined>
|
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
|
||||||
|
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const policy = yield* Policy.Service
|
const policy = yield* Policy.Service
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
|
const scope = yield* Scope.Scope
|
||||||
|
|
||||||
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined, connected: boolean) => {
|
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined, connected: boolean) => {
|
||||||
if (provider.disabled) return false
|
if (provider.disabled) return false
|
||||||
@@ -96,26 +120,32 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => {
|
function* getRecord(providerID: ProviderV2.ID) {
|
||||||
|
const match = state.get().providers.get(providerID)
|
||||||
|
if (!match) return yield* new ProviderNotFoundError({ providerID })
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeApi = (item: Draft<ProviderV2.Info> | Draft<ModelV2.Info>) => {
|
||||||
if (typeof item.request.body.baseURL !== "string") return
|
if (typeof item.request.body.baseURL !== "string") return
|
||||||
item.api.url = item.request.body.baseURL
|
item.api.url = item.request.body.baseURL
|
||||||
delete item.request.body.baseURL
|
delete item.request.body.baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Editor>({
|
||||||
initial: () => ({ providers: new Map() }),
|
initial: () => ({ providers: new Map() }),
|
||||||
draft: (draft) => {
|
editor: (draft) => {
|
||||||
const result: Draft = {
|
const result: Editor = {
|
||||||
provider: {
|
provider: {
|
||||||
list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[],
|
list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[],
|
||||||
get: (providerID) => draft.providers.get(providerID),
|
get: (providerID) => draft.providers.get(providerID),
|
||||||
update: (providerID, fn) => {
|
update: (providerID, fn) => {
|
||||||
let current = draft.providers.get(providerID)
|
let current = draft.providers.get(providerID)
|
||||||
if (!current) {
|
if (!current) {
|
||||||
current = {
|
current = castDraft({
|
||||||
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
|
provider: ProviderV2.Info.empty(providerID),
|
||||||
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
|
models: new Map<ModelV2.ID, ModelV2.Info>(),
|
||||||
}
|
})
|
||||||
draft.providers.set(providerID, current)
|
draft.providers.set(providerID, current)
|
||||||
}
|
}
|
||||||
fn(current.provider)
|
fn(current.provider)
|
||||||
@@ -130,14 +160,13 @@ export const layer = Layer.effect(
|
|||||||
update: (providerID, modelID, fn) => {
|
update: (providerID, modelID, fn) => {
|
||||||
let record = draft.providers.get(providerID)
|
let record = draft.providers.get(providerID)
|
||||||
if (!record) {
|
if (!record) {
|
||||||
record = {
|
record = castDraft({
|
||||||
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
|
provider: ProviderV2.Info.empty(providerID),
|
||||||
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
|
models: new Map<ModelV2.ID, ModelV2.Info>(),
|
||||||
}
|
})
|
||||||
draft.providers.set(providerID, record)
|
draft.providers.set(providerID, record)
|
||||||
}
|
}
|
||||||
const model =
|
const model = record.models.get(modelID) ?? castDraft(ModelV2.Info.empty(providerID, modelID))
|
||||||
record.models.get(modelID) ?? (ModelV2.Info.empty(providerID, modelID) as ModelV2.MutableInfo)
|
|
||||||
if (!record.models.has(modelID)) record.models.set(modelID, model)
|
if (!record.models.has(modelID)) record.models.set(modelID, model)
|
||||||
fn(model)
|
fn(model)
|
||||||
model.id = modelID
|
model.id = modelID
|
||||||
@@ -157,7 +186,8 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
|
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) {
|
||||||
|
if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid)
|
||||||
if (policy.hasStatements()) {
|
if (policy.hasStatements()) {
|
||||||
for (const record of [...catalog.provider.list()]) {
|
for (const record of [...catalog.provider.list()]) {
|
||||||
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
|
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
|
||||||
@@ -168,13 +198,25 @@ export const layer = Layer.effect(
|
|||||||
yield* events.publish(Event.Updated, {})
|
yield* events.publish(Event.Updated, {})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
yield* events.subscribe(PluginV2.Event.Added).pipe(
|
||||||
|
// Plugin registries are location scoped even though the event bus is process scoped.
|
||||||
|
Stream.filter(
|
||||||
|
(event) =>
|
||||||
|
event.location?.directory === location.directory && event.location.workspaceID === location.workspaceID,
|
||||||
|
),
|
||||||
|
Stream.runForEach((event) =>
|
||||||
|
state.mutate((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"),
|
||||||
|
),
|
||||||
|
Effect.forkIn(scope, { startImmediately: true }),
|
||||||
|
)
|
||||||
|
|
||||||
const result: Interface = {
|
const result: Interface = {
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
|
||||||
|
|
||||||
provider: {
|
provider: {
|
||||||
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
||||||
return state.get().providers.get(providerID)?.provider
|
const record = yield* getRecord(providerID)
|
||||||
|
return record.provider
|
||||||
}),
|
}),
|
||||||
|
|
||||||
all: Effect.fn("CatalogV2.provider.all")(function* () {
|
all: Effect.fn("CatalogV2.provider.all")(function* () {
|
||||||
@@ -196,10 +238,10 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
model: {
|
model: {
|
||||||
get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
|
get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
|
||||||
const record = state.get().providers.get(providerID)
|
const record = yield* getRecord(providerID)
|
||||||
if (!record) return
|
|
||||||
const model = record.models.get(modelID)
|
const model = record.models.get(modelID)
|
||||||
return model && projectModel(model, record.provider)
|
if (!model) return yield* new ModelNotFoundError({ providerID, modelID })
|
||||||
|
return projectModel(model, record.provider)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
all: Effect.fn("CatalogV2.model.all")(function* () {
|
all: Effect.fn("CatalogV2.model.all")(function* () {
|
||||||
@@ -208,7 +250,7 @@ export const layer = Layer.effect(
|
|||||||
Array.flatMap((record) => {
|
Array.flatMap((record) => {
|
||||||
return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider))
|
return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider))
|
||||||
}),
|
}),
|
||||||
Array.sortWith((item) => item.time.released, Order.flip(Order.Number)),
|
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -220,30 +262,31 @@ export const layer = Layer.effect(
|
|||||||
default: Effect.fn("CatalogV2.model.default")(function* () {
|
default: Effect.fn("CatalogV2.model.default")(function* () {
|
||||||
const defaultModel = state.get().defaultModel
|
const defaultModel = state.get().defaultModel
|
||||||
if (defaultModel) {
|
if (defaultModel) {
|
||||||
const provider = yield* result.provider.get(defaultModel.providerID)
|
const provider = yield* result.provider.get(defaultModel.providerID).pipe(Effect.option)
|
||||||
if (provider && (yield* result.provider.available()).some((item) => item.id === provider.id)) {
|
if (
|
||||||
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID)
|
Option.isSome(provider) &&
|
||||||
if (model?.enabled) return model
|
(yield* result.provider.available()).some((item) => item.id === provider.value.id)
|
||||||
|
) {
|
||||||
|
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
|
||||||
|
if (Option.isSome(model) && model.value.enabled) return model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Option.getOrUndefined(
|
return pipe(
|
||||||
pipe(
|
yield* result.model.available(),
|
||||||
yield* result.model.available(),
|
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
|
||||||
Array.sortWith((item) => item.time.released, Order.flip(Order.Number)),
|
Array.head,
|
||||||
Array.head,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
|
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
|
||||||
const record = state.get().providers.get(providerID)
|
const record = state.get().providers.get(providerID)
|
||||||
if (!record) return
|
if (!record) return Option.none<ModelV2.Info>()
|
||||||
const provider = record.provider
|
const provider = record.provider
|
||||||
|
|
||||||
if (providerID === ProviderV2.ID.opencode) {
|
if (providerID === ProviderV2.ID.opencode) {
|
||||||
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
|
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
|
||||||
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider)
|
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(projectModel(gpt5Nano, provider))
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidates = pipe(
|
const candidates = pipe(
|
||||||
@@ -259,7 +302,7 @@ export const layer = Layer.effect(
|
|||||||
Array.map((model) => ({
|
Array.map((model) => ({
|
||||||
model,
|
model,
|
||||||
cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
|
cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
|
||||||
age: (Date.now() - model.time.released) / (1000 * 60 * 60 * 24 * 30),
|
age: (Date.now() - model.time.released.epochMilliseconds) / (1000 * 60 * 60 * 24 * 30),
|
||||||
small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
|
small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
|
||||||
})),
|
})),
|
||||||
Array.filter((item) => item.cost > 0 && item.age <= 18),
|
Array.filter((item) => item.cost > 0 && item.age <= 18),
|
||||||
@@ -276,12 +319,10 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Option.getOrUndefined(
|
return pipe(
|
||||||
pipe(
|
candidates,
|
||||||
candidates,
|
Array.filter((item) => item.small),
|
||||||
Array.filter((item) => item.small),
|
(items) => (items.length > 0 ? pick(items) : pick(candidates)),
|
||||||
(items) => (items.length > 0 ? pick(items) : pick(candidates)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -295,5 +336,6 @@ const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
|
|||||||
|
|
||||||
export const locationLayer = layer.pipe(
|
export const locationLayer = layer.pipe(
|
||||||
Layer.provideMerge(Integration.locationLayer),
|
Layer.provideMerge(Integration.locationLayer),
|
||||||
|
Layer.provideMerge(PluginV2.locationLayer),
|
||||||
Layer.provideMerge(Policy.locationLayer),
|
Layer.provideMerge(Policy.locationLayer),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * as CommandV2 from "./command"
|
export * as CommandV2 from "./command"
|
||||||
|
|
||||||
import { Context, Effect, Layer, Schema, Types } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
|
import { castDraft, type Draft } from "immer"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
|
||||||
@@ -14,17 +15,19 @@ export class Info extends Schema.Class<Info>("CommandV2.Info")({
|
|||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export type Data = {
|
export type Data = {
|
||||||
commands: Map<string, Types.DeepMutable<Info>>
|
commands: Map<string, Info>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Draft = {
|
export type Editor = {
|
||||||
list: () => readonly Info[]
|
list: () => readonly Info[]
|
||||||
get: (name: string) => Info | undefined
|
get: (name: string) => Info | undefined
|
||||||
update: (name: string, update: (command: Types.DeepMutable<Info>) => void) => void
|
update: (name: string, update: (command: Draft<Info>) => void) => void
|
||||||
remove: (name: string) => void
|
remove: (name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface extends State.Transformable<Draft> {
|
export interface Interface {
|
||||||
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
|
readonly update: State.Interface<Data, Editor>["update"]
|
||||||
readonly get: (name: string) => Effect.Effect<Info | undefined>
|
readonly get: (name: string) => Effect.Effect<Info | undefined>
|
||||||
readonly list: () => Effect.Effect<Info[]>
|
readonly list: () => Effect.Effect<Info[]>
|
||||||
}
|
}
|
||||||
@@ -34,13 +37,13 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Editor>({
|
||||||
initial: () => ({ commands: new Map() }),
|
initial: () => ({ commands: new Map() }),
|
||||||
draft: (draft) => ({
|
editor: (draft) => ({
|
||||||
list: () => Array.from(draft.commands.values()) as Info[],
|
list: () => Array.from(draft.commands.values()) as Info[],
|
||||||
get: (name) => draft.commands.get(name),
|
get: (name) => draft.commands.get(name),
|
||||||
update: (name, update) => {
|
update: (name, update) => {
|
||||||
const current = draft.commands.get(name) ?? (new Info({ name, template: "" }) as Types.DeepMutable<Info>)
|
const current = draft.commands.get(name) ?? castDraft(new Info({ name, template: "" }))
|
||||||
if (!draft.commands.has(name)) draft.commands.set(name, current)
|
if (!draft.commands.has(name)) draft.commands.set(name, current)
|
||||||
update(current)
|
update(current)
|
||||||
current.name = name
|
current.name = name
|
||||||
@@ -52,7 +55,7 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
rebuild: state.rebuild,
|
update: state.update,
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
get: Effect.fn("CommandV2.get")(function* (name) {
|
get: Effect.fn("CommandV2.get")(function* (name) {
|
||||||
return state.get().commands.get(name)
|
return state.get().commands.get(name)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export * as ConfigAgentPlugin from "./agent"
|
export * as ConfigAgentPlugin from "./agent"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Option, Schema } from "effect"
|
import { Effect, Option, Schema } from "effect"
|
||||||
import { AgentV2 } from "../../agent"
|
import { AgentV2 } from "../../agent"
|
||||||
@@ -9,6 +8,7 @@ import { ConfigAgent } from "../agent"
|
|||||||
import { ConfigMarkdown } from "../markdown"
|
import { ConfigMarkdown } from "../markdown"
|
||||||
import { FSUtil } from "../../fs-util"
|
import { FSUtil } from "../../fs-util"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ConfigAgentV1 } from "../../v1/config/agent"
|
import { ConfigAgentV1 } from "../../v1/config/agent"
|
||||||
import { ConfigMigrateV1 } from "../../v1/config/migrate"
|
import { ConfigMigrateV1 } from "../../v1/config/migrate"
|
||||||
|
|
||||||
@@ -33,70 +33,70 @@ const agentKeys = new Set([
|
|||||||
"permissions",
|
"permissions",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = PluginV2.define({
|
||||||
id: "config-agent",
|
id: PluginV2.ID.make("config-agent"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
|
const agent = yield* AgentV2.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
yield* ctx.agent.transform(
|
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
|
||||||
Effect.fn(function* (draft) {
|
if (entry.type === "document") return Effect.succeed([entry])
|
||||||
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
|
return Effect.gen(function* () {
|
||||||
if (entry.type === "document") return Effect.succeed([entry])
|
const files = yield* discover(fs, entry.path)
|
||||||
return Effect.gen(function* () {
|
return yield* Effect.forEach(files, (file) =>
|
||||||
const files = yield* discover(fs, entry.path)
|
fs.readFileStringSafe(file.filepath).pipe(
|
||||||
return yield* Effect.forEach(files, (file) =>
|
Effect.map((content) => content && decode(file, content)),
|
||||||
fs.readFileStringSafe(file.filepath).pipe(
|
Effect.catch(() => Effect.succeed(undefined)),
|
||||||
Effect.map((content) => content && decode(file, content)),
|
),
|
||||||
Effect.catch(() => Effect.succeed(undefined)),
|
).pipe(
|
||||||
),
|
Effect.map((documents) =>
|
||||||
).pipe(
|
documents.filter((document): document is Config.Document => document !== undefined),
|
||||||
Effect.map((documents) =>
|
),
|
||||||
documents.filter((document): document is Config.Document => document !== undefined),
|
)
|
||||||
),
|
})
|
||||||
)
|
}).pipe(Effect.map((documents) => documents.flat()))
|
||||||
})
|
|
||||||
}).pipe(Effect.map((documents) => documents.flat()))
|
|
||||||
const global = documents.flatMap((document) => document.info.permissions ?? [])
|
|
||||||
const configuredDefault = Config.latest(documents, "default_agent")
|
|
||||||
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
|
|
||||||
for (const current of draft.list()) {
|
|
||||||
draft.update(current.id, (agent) => agent.permissions.push(...global))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const document of documents) {
|
yield* agent.update((editor) => {
|
||||||
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
|
const global = documents.flatMap((document) => document.info.permissions ?? [])
|
||||||
const agentID = AgentV2.ID.make(id)
|
const configuredDefault = Config.latest(documents, "default_agent")
|
||||||
if (item.disabled) {
|
if (configuredDefault !== undefined) editor.default(AgentV2.ID.make(configuredDefault))
|
||||||
draft.remove(agentID)
|
for (const current of editor.list()) {
|
||||||
continue
|
editor.update(current.id, (agent) => agent.permissions.push(...global))
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = draft.get(agentID) !== undefined
|
for (const document of documents) {
|
||||||
draft.update(agentID, (agent) => {
|
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
|
||||||
if (!exists) agent.permissions.push(...global)
|
const agentID = AgentV2.ID.make(id)
|
||||||
if (item.model !== undefined) {
|
if (item.disabled) {
|
||||||
const model = ModelV2.parse(item.model)
|
editor.remove(agentID)
|
||||||
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
|
continue
|
||||||
}
|
|
||||||
if (item.variant !== undefined && agent.model !== undefined) {
|
|
||||||
agent.model.variant = ModelV2.VariantID.make(item.variant)
|
|
||||||
}
|
|
||||||
if (item.request !== undefined) {
|
|
||||||
Object.assign(agent.request.headers, item.request.headers ?? {})
|
|
||||||
Object.assign(agent.request.body, item.request.body ?? {})
|
|
||||||
}
|
|
||||||
if (item.system !== undefined) agent.system = item.system
|
|
||||||
if (item.description !== undefined) agent.description = item.description
|
|
||||||
if (item.mode !== undefined) agent.mode = item.mode
|
|
||||||
if (item.hidden !== undefined) agent.hidden = item.hidden
|
|
||||||
if (item.color !== undefined) agent.color = item.color
|
|
||||||
if (item.steps !== undefined) agent.steps = item.steps
|
|
||||||
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exists = editor.get(agentID) !== undefined
|
||||||
|
editor.update(agentID, (agent) => {
|
||||||
|
if (!exists) agent.permissions.push(...global)
|
||||||
|
if (item.model !== undefined) {
|
||||||
|
const model = ModelV2.parse(item.model)
|
||||||
|
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
|
||||||
|
}
|
||||||
|
if (item.variant !== undefined && agent.model !== undefined) {
|
||||||
|
agent.model.variant = ModelV2.VariantID.make(item.variant)
|
||||||
|
}
|
||||||
|
if (item.request !== undefined) {
|
||||||
|
Object.assign(agent.request.headers, item.request.headers ?? {})
|
||||||
|
Object.assign(agent.request.body, item.request.body ?? {})
|
||||||
|
}
|
||||||
|
if (item.system !== undefined) agent.system = item.system
|
||||||
|
if (item.description !== undefined) agent.description = item.description
|
||||||
|
if (item.mode !== undefined) agent.mode = item.mode
|
||||||
|
if (item.hidden !== undefined) agent.hidden = item.hidden
|
||||||
|
if (item.color !== undefined) agent.color = item.color
|
||||||
|
if (item.steps !== undefined) agent.steps = item.steps
|
||||||
|
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
)
|
})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,52 @@
|
|||||||
export * as ConfigCommandPlugin from "./command"
|
export * as ConfigCommandPlugin from "./command"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Option, Schema } from "effect"
|
import { Effect, Option, Schema } from "effect"
|
||||||
import { CommandV2 } from "../../command"
|
import { CommandV2 } from "../../command"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { FSUtil } from "../../fs-util"
|
import { FSUtil } from "../../fs-util"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ConfigCommand } from "../command"
|
import { ConfigCommand } from "../command"
|
||||||
import { ConfigMarkdown } from "../markdown"
|
import { ConfigMarkdown } from "../markdown"
|
||||||
|
|
||||||
const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info)
|
const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info)
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = PluginV2.define({
|
||||||
id: "config-command",
|
id: PluginV2.ID.make("config-command"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
|
const command = yield* CommandV2.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
yield* ctx.command.transform(
|
const transform = yield* command.transform()
|
||||||
Effect.fn(function* (draft) {
|
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
|
||||||
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
|
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
|
||||||
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
|
return loadDirectory(fs, entry.path).pipe(
|
||||||
return loadDirectory(fs, entry.path).pipe(
|
Effect.map((commands) => [
|
||||||
Effect.map((commands) => [
|
{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
|
||||||
{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
|
]),
|
||||||
]),
|
)
|
||||||
)
|
}).pipe(Effect.map((documents) => documents.flat()))
|
||||||
}).pipe(Effect.map((documents) => documents.flat()))
|
|
||||||
for (const document of documents) {
|
yield* transform((editor) => {
|
||||||
for (const [name, command] of Object.entries(document.commands ?? {})) {
|
for (const document of documents) {
|
||||||
draft.update(name, (item) => {
|
for (const [name, command] of Object.entries(document.commands ?? {})) {
|
||||||
item.template = command.template
|
editor.update(name, (item) => {
|
||||||
if (command.description !== undefined) item.description = command.description
|
item.template = command.template
|
||||||
if (command.agent !== undefined) item.agent = command.agent
|
if (command.description !== undefined) item.description = command.description
|
||||||
if (command.model !== undefined) {
|
if (command.agent !== undefined) item.agent = command.agent
|
||||||
const model = ModelV2.parse(command.model)
|
if (command.model !== undefined) {
|
||||||
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
|
const model = ModelV2.parse(command.model)
|
||||||
}
|
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
|
||||||
if (command.variant !== undefined && item.model !== undefined) {
|
}
|
||||||
item.model.variant = ModelV2.VariantID.make(command.variant)
|
if (command.variant !== undefined && item.model !== undefined) {
|
||||||
}
|
item.model.variant = ModelV2.VariantID.make(command.variant)
|
||||||
if (command.subtask !== undefined) item.subtask = command.subtask
|
}
|
||||||
})
|
if (command.subtask !== undefined) item.subtask = command.subtask
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
)
|
})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,124 +1,123 @@
|
|||||||
export * as ConfigProviderPlugin from "./provider"
|
export * as ConfigProviderPlugin from "./provider"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
import { Catalog } from "../../catalog"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
|
import { Integration } from "../../integration"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { ModelRequest } from "../../model-request"
|
import { ModelRequest } from "../../model-request"
|
||||||
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = PluginV2.define({
|
||||||
id: "config-provider",
|
id: PluginV2.ID.make("config-provider"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
yield* ctx.integration.transform(
|
const integrations = yield* Integration.Service
|
||||||
Effect.fn(function* (integrations) {
|
const transform = yield* catalog.transform()
|
||||||
const files = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")
|
const integrationTransform = yield* integrations.transform()
|
||||||
const configuredIntegrations = new Set(
|
const entries = yield* config.entries()
|
||||||
files.flatMap((file) =>
|
const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
|
||||||
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) =>
|
const configuredIntegrations = new Set(
|
||||||
provider.env === undefined ? [] : [id],
|
files.flatMap((file) =>
|
||||||
),
|
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => (provider.env === undefined ? [] : [id])),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for (const file of files) {
|
yield* integrationTransform((integrations) => {
|
||||||
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
for (const file of files) {
|
||||||
const integrationID = id
|
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
||||||
if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
|
const integrationID = Integration.ID.make(id)
|
||||||
integrations.update(integrationID, (integration) => {
|
if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
|
||||||
integration.name = item.name ?? integration.name
|
integrations.update(integrationID, (integration) => {
|
||||||
|
integration.name = item.name ?? integration.name
|
||||||
|
})
|
||||||
|
if (item.env !== undefined) {
|
||||||
|
integrations.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: { type: "env", names: [...item.env] },
|
||||||
})
|
})
|
||||||
if (item.env !== undefined) {
|
|
||||||
integrations.method.update({
|
|
||||||
integrationID,
|
|
||||||
method: { type: "env", names: [...item.env] },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
yield* ctx.catalog.transform(
|
yield* transform((catalog) => {
|
||||||
Effect.fn(function* (catalog) {
|
const configuredDefault = Config.latest(entries, "model")
|
||||||
const entries = yield* config.entries()
|
if (configuredDefault !== undefined) {
|
||||||
const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
|
const model = ModelV2.parse(configuredDefault)
|
||||||
const configuredDefault = Config.latest(entries, "model")
|
catalog.model.default.set(model.providerID, model.modelID)
|
||||||
if (configuredDefault !== undefined) {
|
}
|
||||||
const model = ModelV2.parse(configuredDefault)
|
for (const file of files) {
|
||||||
catalog.model.default.set(model.providerID, model.modelID)
|
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
||||||
}
|
const providerID = ProviderV2.ID.make(id)
|
||||||
for (const file of files) {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
if (item.name !== undefined) provider.name = item.name
|
||||||
const providerID = id
|
if (item.api !== undefined) provider.api = { ...item.api }
|
||||||
catalog.provider.update(providerID, (provider) => {
|
if (item.request !== undefined) {
|
||||||
if (item.name !== undefined) provider.name = item.name
|
Object.assign(provider.request.headers, item.request.headers)
|
||||||
if (item.api !== undefined) provider.api = { ...item.api }
|
Object.assign(provider.request.body, item.request.body)
|
||||||
if (item.request !== undefined) {
|
}
|
||||||
Object.assign(provider.request.headers, item.request.headers)
|
})
|
||||||
Object.assign(provider.request.body, item.request.body)
|
const providerApi = catalog.provider.get(providerID)?.provider.api
|
||||||
|
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
|
||||||
|
|
||||||
|
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||||
|
catalog.model.update(providerID, ModelV2.ID.make(id), (model) => {
|
||||||
|
if (config.family !== undefined) model.family = config.family
|
||||||
|
if (config.name !== undefined) model.name = config.name
|
||||||
|
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
|
||||||
|
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
|
||||||
|
if (config.capabilities !== undefined) {
|
||||||
|
model.capabilities = {
|
||||||
|
tools: config.capabilities.tools,
|
||||||
|
input: [...config.capabilities.input],
|
||||||
|
output: [...config.capabilities.output],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
if (config.request !== undefined) {
|
||||||
const providerApi = catalog.provider.get(providerID)?.provider.api
|
ModelRequest.assign(model.request, {
|
||||||
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
|
headers: config.request.headers,
|
||||||
|
...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
|
||||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
})
|
||||||
catalog.model.update(providerID, id, (model) => {
|
if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
||||||
if (config.family !== undefined) model.family = config.family
|
}
|
||||||
if (config.name !== undefined) model.name = config.name
|
if (config.variants !== undefined) {
|
||||||
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
|
for (const variant of config.variants) {
|
||||||
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
|
let existing = model.variants.find((item) => item.id === variant.id)
|
||||||
if (config.capabilities !== undefined) {
|
if (!existing) {
|
||||||
model.capabilities = {
|
existing = {
|
||||||
tools: config.capabilities.tools,
|
id: variant.id,
|
||||||
input: [...config.capabilities.input],
|
headers: {},
|
||||||
output: [...config.capabilities.output],
|
body: {},
|
||||||
}
|
generation: {},
|
||||||
}
|
options: {},
|
||||||
if (config.request !== undefined) {
|
|
||||||
ModelRequest.assign(model.request, {
|
|
||||||
headers: config.request.headers,
|
|
||||||
...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
|
|
||||||
})
|
|
||||||
if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
|
||||||
}
|
|
||||||
if (config.variants !== undefined) {
|
|
||||||
for (const variant of config.variants) {
|
|
||||||
let existing = model.variants.find((item) => item.id === variant.id)
|
|
||||||
if (!existing) {
|
|
||||||
existing = {
|
|
||||||
id: variant.id,
|
|
||||||
headers: {},
|
|
||||||
body: {},
|
|
||||||
generation: {},
|
|
||||||
options: {},
|
|
||||||
}
|
|
||||||
model.variants.push(existing)
|
|
||||||
}
|
}
|
||||||
ModelRequest.assign(existing, {
|
model.variants.push(existing)
|
||||||
headers: variant.headers,
|
|
||||||
...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
ModelRequest.assign(existing, {
|
||||||
|
headers: variant.headers,
|
||||||
|
...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (config.cost !== undefined) {
|
}
|
||||||
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
|
if (config.cost !== undefined) {
|
||||||
tier: cost.tier && { ...cost.tier },
|
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
|
||||||
input: cost.input,
|
tier: cost.tier && { ...cost.tier },
|
||||||
output: cost.output,
|
input: cost.input,
|
||||||
cache: {
|
output: cost.output,
|
||||||
read: cost.cache?.read ?? 0,
|
cache: {
|
||||||
write: cost.cache?.write ?? 0,
|
read: cost.cache?.read ?? 0,
|
||||||
},
|
write: cost.cache?.write ?? 0,
|
||||||
}))
|
},
|
||||||
}
|
}))
|
||||||
if (config.disabled !== undefined) model.enabled = !config.disabled
|
}
|
||||||
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
|
if (config.disabled !== undefined) model.enabled = !config.disabled
|
||||||
})
|
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
)
|
})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,52 +1,57 @@
|
|||||||
export * as ConfigReferencePlugin from "./reference"
|
export * as ConfigReferencePlugin from "./reference"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ConfigReference } from "../reference"
|
import { ConfigReference } from "../reference"
|
||||||
|
import { Global } from "../../global"
|
||||||
|
import { Location } from "../../location"
|
||||||
|
import { PluginV2 } from "../../plugin"
|
||||||
import { Reference } from "../../reference"
|
import { Reference } from "../../reference"
|
||||||
import { AbsolutePath } from "../../schema"
|
import { AbsolutePath } from "../../schema"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = {
|
||||||
id: "core/config-reference",
|
id: PluginV2.ID.make("core/config-reference"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
yield* ctx.reference.transform(
|
const global = yield* Global.Service
|
||||||
Effect.fn(function* (draft) {
|
const location = yield* Location.Service
|
||||||
const entries = new Map<string, Reference.Source>()
|
const references = yield* Reference.Service
|
||||||
for (const doc of (yield* config.entries()).filter(
|
const update = yield* references.transform()
|
||||||
(entry): entry is Config.Document => entry.type === "document",
|
const entries = new Map<string, Reference.Source>()
|
||||||
)) {
|
for (const doc of (yield* config.entries()).filter(
|
||||||
const directory = doc.path ? path.dirname(doc.path) : ctx.location.directory
|
(entry): entry is Config.Document => entry.type === "document",
|
||||||
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
)) {
|
||||||
if (!validAlias(name)) continue
|
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
||||||
entries.set(
|
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
||||||
name,
|
if (!validAlias(name)) continue
|
||||||
local(entry)
|
entries.set(
|
||||||
? new Reference.LocalSource({
|
name,
|
||||||
type: "local",
|
local(entry)
|
||||||
path: AbsolutePath.make(
|
? new Reference.LocalSource({
|
||||||
localPath(directory, ctx.path.home, typeof entry === "string" ? entry : entry.path),
|
type: "local",
|
||||||
),
|
path: AbsolutePath.make(
|
||||||
description: typeof entry === "string" ? undefined : entry.description,
|
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
|
||||||
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
),
|
||||||
})
|
description: typeof entry === "string" ? undefined : entry.description,
|
||||||
: new Reference.GitSource({
|
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
||||||
type: "git",
|
})
|
||||||
repository: typeof entry === "string" ? entry : entry.repository,
|
: new Reference.GitSource({
|
||||||
branch: typeof entry === "string" ? undefined : entry.branch,
|
type: "git",
|
||||||
description: typeof entry === "string" ? undefined : entry.description,
|
repository: typeof entry === "string" ? entry : entry.repository,
|
||||||
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
branch: typeof entry === "string" ? undefined : entry.branch,
|
||||||
}),
|
description: typeof entry === "string" ? undefined : entry.description,
|
||||||
)
|
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
||||||
}
|
}),
|
||||||
}
|
)
|
||||||
for (const [name, source] of entries) draft.add(name, source)
|
}
|
||||||
}),
|
}
|
||||||
)
|
|
||||||
|
yield* update((editor) => {
|
||||||
|
for (const [name, source] of entries) editor.add(name, source)
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
})
|
}
|
||||||
|
|
||||||
function validAlias(name: string) {
|
function validAlias(name: string) {
|
||||||
return name.length > 0 && !/[\/\s`,]/.test(name)
|
return name.length > 0 && !/[\/\s`,]/.test(name)
|
||||||
|
|||||||
@@ -1,45 +1,48 @@
|
|||||||
export * as ConfigSkillPlugin from "./skill"
|
export * as ConfigSkillPlugin from "./skill"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
|
import { Global } from "../../global"
|
||||||
|
import { Location } from "../../location"
|
||||||
|
import { PluginV2 } from "../../plugin"
|
||||||
import { AbsolutePath } from "../../schema"
|
import { AbsolutePath } from "../../schema"
|
||||||
import { SkillV2 } from "../../skill"
|
import { SkillV2 } from "../../skill"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = PluginV2.define({
|
||||||
id: "config-skill",
|
id: PluginV2.ID.make("config-skill"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
yield* ctx.skill.transform(
|
const global = yield* Global.Service
|
||||||
Effect.fn(function* (draft) {
|
const location = yield* Location.Service
|
||||||
const entries = yield* config.entries()
|
const skill = yield* SkillV2.Service
|
||||||
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
const transform = yield* skill.transform()
|
||||||
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
|
const entries = yield* config.entries()
|
||||||
for (const directory of directories) {
|
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
||||||
draft.source(
|
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
|
||||||
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
|
|
||||||
)
|
yield* transform((editor) => {
|
||||||
draft.source(
|
for (const directory of directories) {
|
||||||
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }),
|
editor.source(
|
||||||
)
|
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
|
||||||
|
)
|
||||||
|
editor.source(
|
||||||
|
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (const item of items) {
|
||||||
|
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
|
||||||
|
editor.source(new SkillV2.UrlSource({ type: "url", url: item }))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
for (const item of items) {
|
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
|
||||||
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
|
editor.source(
|
||||||
draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
|
new SkillV2.DirectorySource({
|
||||||
continue
|
type: "directory",
|
||||||
}
|
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
|
||||||
const expanded = item.startsWith("~/") ? path.join(ctx.path.home, item.slice(2)) : item
|
}),
|
||||||
draft.source(
|
)
|
||||||
new SkillV2.DirectorySource({
|
}
|
||||||
type: "directory",
|
})
|
||||||
path: AbsolutePath.make(
|
|
||||||
path.isAbsolute(expanded) ? expanded : path.join(ctx.location.directory, expanded),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
+177
-130
@@ -5,7 +5,7 @@ import { and, asc, eq, gt } from "drizzle-orm"
|
|||||||
import { Database } from "./database/database"
|
import { Database } from "./database/database"
|
||||||
import { EventSequenceTable, EventTable } from "./event/sql"
|
import { EventSequenceTable, EventTable } from "./event/sql"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { externalID, type ExternalID, withStatics } from "./schema"
|
import { externalID, type ExternalID, NonNegativeInt, withStatics } from "./schema"
|
||||||
import { Identifier } from "./util/identifier"
|
import { Identifier } from "./util/identifier"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { LayerNode } from "./effect/layer-node"
|
||||||
import { isDeepStrictEqual } from "node:util"
|
import { isDeepStrictEqual } from "node:util"
|
||||||
@@ -19,9 +19,16 @@ export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
|
|||||||
)
|
)
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Durable aggregate continuation position for embedded replay streams.
|
||||||
|
* TODO: Decide whether a future HTTP / SDK surface should expose an opaque cursor instead.
|
||||||
|
*/
|
||||||
|
export const Cursor = NonNegativeInt.pipe(Schema.brand("EventV2.Cursor"))
|
||||||
|
export type Cursor = typeof Cursor.Type
|
||||||
|
|
||||||
export type Definition<Type extends string = string, DataSchema extends Schema.Top = Schema.Top> = {
|
export type Definition<Type extends string = string, DataSchema extends Schema.Top = Schema.Top> = {
|
||||||
readonly type: Type
|
readonly type: Type
|
||||||
readonly durable?: {
|
readonly sync?: {
|
||||||
readonly version: number
|
readonly version: number
|
||||||
readonly aggregate: string
|
readonly aggregate: string
|
||||||
}
|
}
|
||||||
@@ -34,16 +41,20 @@ export type Payload<D extends Definition = Definition> = {
|
|||||||
readonly id: ID
|
readonly id: ID
|
||||||
readonly type: D["type"]
|
readonly type: D["type"]
|
||||||
readonly data: Data<D>
|
readonly data: Data<D>
|
||||||
readonly durable?: {
|
/** Durable aggregate order, populated while synchronized events are projected. */
|
||||||
readonly aggregateID: string
|
readonly seq?: number
|
||||||
readonly seq: number
|
readonly version?: number
|
||||||
readonly version: number
|
|
||||||
}
|
|
||||||
readonly location?: Location.Ref
|
readonly location?: Location.Ref
|
||||||
readonly metadata?: Record<string, unknown>
|
readonly metadata?: Record<string, unknown>
|
||||||
|
/** Internal replay marker for projectors that own non-replicated operational state. */
|
||||||
|
readonly replay?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
export type Projector<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
||||||
|
type AnyProjector = (event: Payload) => Effect.Effect<void>
|
||||||
|
export type CommitGuard = (event: Payload) => Effect.Effect<void>
|
||||||
|
export type Listener = (event: Payload) => Effect.Effect<void>
|
||||||
|
export type Sync = (event: Payload) => Effect.Effect<void>
|
||||||
export type Unsubscribe = Effect.Effect<void>
|
export type Unsubscribe = Effect.Effect<void>
|
||||||
|
|
||||||
export type SerializedEvent = {
|
export type SerializedEvent = {
|
||||||
@@ -54,8 +65,13 @@ export type SerializedEvent = {
|
|||||||
readonly data: Record<string, unknown>
|
readonly data: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDurableEventError>()(
|
export type CursorEvent<E extends Payload = Payload> = {
|
||||||
"EventV2.InvalidDurableEvent",
|
readonly cursor: Cursor
|
||||||
|
readonly event: E
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidSyncEventError extends Schema.TaggedErrorClass<InvalidSyncEventError>()(
|
||||||
|
"EventV2.InvalidSyncEvent",
|
||||||
{
|
{
|
||||||
type: Schema.String,
|
type: Schema.String,
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
@@ -67,11 +83,19 @@ export function versionedType(type: string, version: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const registry = new Map<string, Definition>()
|
export const registry = new Map<string, Definition>()
|
||||||
const durableRegistry = new Map<string, Definition>()
|
type SyncDefinition = Definition & {
|
||||||
|
readonly sync: NonNullable<Definition["sync"]>
|
||||||
|
readonly encode: (data: unknown) => unknown
|
||||||
|
readonly decode: (data: unknown) => unknown
|
||||||
|
}
|
||||||
|
const syncRegistry = new Map<string, SyncDefinition>()
|
||||||
|
|
||||||
|
// Synchronized events cross a JSON boundary, so their data schemas must encode and decode without services.
|
||||||
|
const syncCodec = (definition: Definition) => definition.data as Schema.Codec<unknown, unknown, never, never>
|
||||||
|
|
||||||
export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
|
export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
|
||||||
readonly type: Type
|
readonly type: Type
|
||||||
readonly durable?: {
|
readonly sync?: {
|
||||||
readonly version: number
|
readonly version: number
|
||||||
readonly aggregate: string
|
readonly aggregate: string
|
||||||
}
|
}
|
||||||
@@ -82,25 +106,28 @@ export function define<const Type extends string, Fields extends Schema.Struct.F
|
|||||||
id: ID,
|
id: ID,
|
||||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||||
type: Schema.Literal(input.type),
|
type: Schema.Literal(input.type),
|
||||||
durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number })),
|
version: Schema.optional(Schema.Number),
|
||||||
location: Schema.optional(Location.Ref),
|
location: Schema.optional(Location.Ref),
|
||||||
data: Data,
|
data: Data,
|
||||||
}).annotate({ identifier: input.type })
|
}).annotate({ identifier: input.type })
|
||||||
|
|
||||||
const definition = Object.assign(Payload, {
|
const definition = Object.assign(Payload, {
|
||||||
type: input.type,
|
type: input.type,
|
||||||
...(input.durable === undefined ? {} : { durable: input.durable }),
|
...(input.sync === undefined ? {} : { sync: input.sync }),
|
||||||
data: Data,
|
data: Data,
|
||||||
})
|
})
|
||||||
const existing = registry.get(input.type)
|
const existing = registry.get(input.type)
|
||||||
if (
|
if (input.sync === undefined || existing?.sync === undefined || input.sync.version >= existing.sync.version) {
|
||||||
input.durable === undefined ||
|
|
||||||
existing?.durable === undefined ||
|
|
||||||
input.durable.version >= existing.durable.version
|
|
||||||
) {
|
|
||||||
registry.set(input.type, definition)
|
registry.set(input.type, definition)
|
||||||
}
|
}
|
||||||
if (input.durable) durableRegistry.set(versionedType(input.type, input.durable.version), definition)
|
if (input.sync)
|
||||||
|
syncRegistry.set(
|
||||||
|
versionedType(input.type, input.sync.version),
|
||||||
|
Object.assign(definition, {
|
||||||
|
encode: Schema.encodeUnknownSync(syncCodec(definition)),
|
||||||
|
decode: Schema.decodeUnknownSync(syncCodec(definition)),
|
||||||
|
}) as SyncDefinition,
|
||||||
|
)
|
||||||
return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
|
return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
|
||||||
Definition<Type, Schema.Struct<Fields>>
|
Definition<Type, Schema.Struct<Fields>>
|
||||||
}
|
}
|
||||||
@@ -113,7 +140,7 @@ export interface PublishOptions {
|
|||||||
readonly id?: ID
|
readonly id?: ID
|
||||||
readonly metadata?: Record<string, unknown>
|
readonly metadata?: Record<string, unknown>
|
||||||
readonly location?: Location.Ref
|
readonly location?: Location.Ref
|
||||||
/** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */
|
/** Local operational projection committed atomically with a new synchronized event. Not replayed or serialized. */
|
||||||
readonly commit?: (seq: number) => Effect.Effect<void>
|
readonly commit?: (seq: number) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,10 +152,14 @@ export interface Interface {
|
|||||||
) => Effect.Effect<Payload<D>>
|
) => Effect.Effect<Payload<D>>
|
||||||
readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
|
readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
|
||||||
readonly all: () => Stream.Stream<Payload>
|
readonly all: () => Stream.Stream<Payload>
|
||||||
readonly durable: (input: { readonly aggregateID: string; readonly after?: number }) => Stream.Stream<Payload>
|
readonly aggregateEvents: (input: {
|
||||||
/** @deprecated Use `all()` and consume the returned stream. */
|
readonly aggregateID: string
|
||||||
readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
|
readonly after?: Cursor
|
||||||
readonly project: <D extends Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
|
}) => Stream.Stream<CursorEvent>
|
||||||
|
readonly sync: (handler: Sync) => Effect.Effect<Unsubscribe>
|
||||||
|
readonly listen: (listener: Listener) => Effect.Effect<Unsubscribe>
|
||||||
|
readonly beforeCommit: (guard: CommitGuard) => Effect.Effect<void>
|
||||||
|
readonly project: <D extends Definition>(definition: D, projector: Projector<D>) => Effect.Effect<void>
|
||||||
readonly replay: (
|
readonly replay: (
|
||||||
event: SerializedEvent,
|
event: SerializedEvent,
|
||||||
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
||||||
@@ -151,37 +182,37 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
Layer.effect(
|
Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const pubsub = {
|
const all = yield* PubSub.unbounded<Payload>()
|
||||||
all: yield* PubSub.unbounded<Payload>(),
|
const synchronized = new Map<string, Set<PubSub.PubSub<void>>>()
|
||||||
durable: new Map<string, Set<PubSub.PubSub<void>>>(),
|
const typed = new Map<string, PubSub.PubSub<Payload>>()
|
||||||
typed: new Map<string, PubSub.PubSub<Payload>>(),
|
const projectors = new Map<string, AnyProjector[]>()
|
||||||
}
|
const commitGuards = new Array<CommitGuard>()
|
||||||
const projectors = new Map<string, Subscriber[]>()
|
const listeners = new Array<Listener>()
|
||||||
const listeners = new Array<Subscriber>()
|
const syncHandlers = new Array<Sync>()
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
|
|
||||||
const getOrCreate = (definition: Definition) =>
|
const getOrCreate = (definition: Definition) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const existing = pubsub.typed.get(definition.type)
|
const existing = typed.get(definition.type)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
const created = yield* PubSub.unbounded<Payload>()
|
const pubsub = yield* PubSub.unbounded<Payload>()
|
||||||
pubsub.typed.set(definition.type, created)
|
typed.set(definition.type, pubsub)
|
||||||
return created
|
return pubsub
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* PubSub.shutdown(pubsub.all)
|
yield* PubSub.shutdown(all)
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
pubsub.durable.values(),
|
synchronized.values(),
|
||||||
(pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
|
(pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
|
||||||
{ discard: true },
|
{ discard: true },
|
||||||
)
|
)
|
||||||
yield* Effect.forEach(pubsub.typed.values(), PubSub.shutdown, { discard: true })
|
yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
function commitDurableEvent(
|
function commitSyncEvent(
|
||||||
event: Payload,
|
event: Payload,
|
||||||
input?: {
|
input?: {
|
||||||
readonly seq: number
|
readonly seq: number
|
||||||
@@ -193,20 +224,28 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
) {
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const definition = registry.get(event.type)
|
const definition = registry.get(event.type)
|
||||||
const durable = definition?.durable
|
const sync = definition?.sync
|
||||||
if (durable) {
|
if (sync) {
|
||||||
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
|
if (event.version !== sync.version) {
|
||||||
|
yield* Effect.die(
|
||||||
|
new InvalidSyncEventError({
|
||||||
|
type: event.type,
|
||||||
|
message: `Expected event version ${sync.version}, got ${event.version}`,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const aggregateID = (event.data as Record<string, unknown>)[sync.aggregate]
|
||||||
if (typeof aggregateID !== "string") {
|
if (typeof aggregateID !== "string") {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: event.type,
|
type: event.type,
|
||||||
message: `Expected string aggregate field ${durable.aggregate}`,
|
message: `Expected string aggregate field ${sync.aggregate}`,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if (input && input.aggregateID !== aggregateID) {
|
if (input && input.aggregateID !== aggregateID) {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: event.type,
|
type: event.type,
|
||||||
message: `Aggregate mismatch: expected ${input.aggregateID}, got ${aggregateID}`,
|
message: `Aggregate mismatch: expected ${input.aggregateID}, got ${aggregateID}`,
|
||||||
}),
|
}),
|
||||||
@@ -226,12 +265,12 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
const latest = row?.seq ?? -1
|
const latest = row?.seq ?? -1
|
||||||
const encoded = Schema.encodeUnknownSync(
|
const encoded = syncRegistry
|
||||||
definition.data as Schema.Codec<unknown, unknown, never, never>,
|
.get(versionedType(definition.type, sync.version))!
|
||||||
)(event.data) as Record<string, unknown>
|
.encode(event.data) as Record<string, unknown>
|
||||||
if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
|
if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: event.type,
|
type: event.type,
|
||||||
message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`,
|
message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`,
|
||||||
}),
|
}),
|
||||||
@@ -246,7 +285,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (
|
if (
|
||||||
stored?.id === event.id &&
|
stored?.id === event.id &&
|
||||||
stored.type === versionedType(definition.type, durable.version) &&
|
stored.type === versionedType(definition.type, sync.version) &&
|
||||||
isDeepStrictEqual(stored.data, encoded)
|
isDeepStrictEqual(stored.data, encoded)
|
||||||
) {
|
) {
|
||||||
if (input.ownerID && row?.ownerID == null) {
|
if (input.ownerID && row?.ownerID == null) {
|
||||||
@@ -260,7 +299,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: event.type,
|
type: event.type,
|
||||||
message: `Replay diverged at aggregate ${aggregateID} sequence ${input.seq}`,
|
message: `Replay diverged at aggregate ${aggregateID} sequence ${input.seq}`,
|
||||||
}),
|
}),
|
||||||
@@ -272,7 +311,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
const seq = input?.seq ?? latest + 1
|
const seq = input?.seq ?? latest + 1
|
||||||
if (input && seq !== latest + 1) {
|
if (input && seq !== latest + 1) {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: event.type,
|
type: event.type,
|
||||||
message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`,
|
message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`,
|
||||||
}),
|
}),
|
||||||
@@ -286,17 +325,16 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (stored)
|
if (stored)
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: event.type,
|
type: event.type,
|
||||||
message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
|
message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const committed = {
|
for (const guard of commitGuards) {
|
||||||
...event,
|
yield* guard(event)
|
||||||
durable: { aggregateID, seq, version: durable.version },
|
}
|
||||||
} as Payload
|
|
||||||
for (const projector of list) {
|
for (const projector of list) {
|
||||||
yield* projector(committed)
|
yield* projector({ ...event, seq } as Payload)
|
||||||
}
|
}
|
||||||
if (commit) yield* commit(seq)
|
if (commit) yield* commit(seq)
|
||||||
yield* db
|
yield* db
|
||||||
@@ -318,7 +356,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
id: event.id,
|
id: event.id,
|
||||||
aggregate_id: aggregateID,
|
aggregate_id: aggregateID,
|
||||||
seq,
|
seq,
|
||||||
type: versionedType(definition.type, durable.version),
|
type: versionedType(definition.type, sync.version),
|
||||||
data: encoded,
|
data: encoded,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -331,8 +369,8 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (committed) {
|
if (committed) {
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
pubsub.durable.get(committed.aggregateID) ?? [],
|
synchronized.get(committed.aggregateID) ?? [],
|
||||||
(wake) => PubSub.publish(wake, undefined),
|
(pubsub) => PubSub.publish(pubsub, undefined),
|
||||||
{ discard: true },
|
{ discard: true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -346,25 +384,19 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
|
|
||||||
function publishEvent<D extends Definition>(event: Payload<D>, commit?: PublishOptions["commit"]) {
|
function publishEvent<D extends Definition>(event: Payload<D>, commit?: PublishOptions["commit"]) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const definition = registry.get(event.type)
|
const durable = registry.get(event.type)?.sync !== undefined
|
||||||
if (!definition?.durable && commit)
|
if (!durable && commit)
|
||||||
return yield* Effect.die(
|
return yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: event.type,
|
type: event.type,
|
||||||
message: "Local commit hooks require a durable event",
|
message: "Local commit hooks require a synchronized event",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (definition?.durable) {
|
if (durable) {
|
||||||
const committed = yield* commitDurableEvent(event as Payload, undefined, commit)
|
const committed = yield* commitSyncEvent(event as Payload, undefined, commit)
|
||||||
if (committed) {
|
if (committed) {
|
||||||
event = {
|
event = { ...event, seq: committed.seq }
|
||||||
...event,
|
yield* Effect.forEach(syncHandlers, (sync) => observe(event as Payload, "sync", sync), { discard: true })
|
||||||
durable: {
|
|
||||||
aggregateID: committed.aggregateID,
|
|
||||||
seq: committed.seq,
|
|
||||||
version: definition.durable.version,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
yield* notify(event as Payload, true)
|
yield* notify(event as Payload, true)
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
@@ -374,11 +406,12 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const observe = (event: Payload, observer: (event: Payload) => Effect.Effect<void>) =>
|
const observe = (event: Payload, kind: "sync" | "listener", observer: (event: Payload) => Effect.Effect<void>) =>
|
||||||
Effect.suspend(() => observer(event)).pipe(
|
Effect.suspend(() => observer(event)).pipe(
|
||||||
Effect.catchCauseIf(
|
Effect.catchCauseIf(
|
||||||
(cause) => !Cause.hasInterrupts(cause),
|
(cause) => !Cause.hasInterrupts(cause),
|
||||||
(cause) => Effect.logError("Event listener failed", { eventID: event.id, eventType: event.type, cause }),
|
(cause) =>
|
||||||
|
Effect.logError("Event observer failed", { eventID: event.id, eventType: event.type, kind, cause }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -386,12 +419,12 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
listeners,
|
listeners,
|
||||||
(listener) => (isolateListeners ? observe(event, listener) : listener(event)),
|
(listener) => (isolateListeners ? observe(event, "listener", listener) : listener(event)),
|
||||||
{ discard: true },
|
{ discard: true },
|
||||||
)
|
)
|
||||||
const typed = pubsub.typed.get(event.type)
|
const pubsub = typed.get(event.type)
|
||||||
if (typed) yield* PubSub.publish(typed, event)
|
if (pubsub) yield* PubSub.publish(pubsub, event)
|
||||||
yield* PubSub.publish(pubsub.all, event)
|
yield* PubSub.publish(all, event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +441,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
id: options?.id ?? ID.create(),
|
id: options?.id ?? ID.create(),
|
||||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||||
type: definition.type,
|
type: definition.type,
|
||||||
|
...(definition.sync === undefined ? {} : { version: definition.sync.version }),
|
||||||
...(location ? { location } : {}),
|
...(location ? { location } : {}),
|
||||||
data,
|
data,
|
||||||
} as Payload<D>,
|
} as Payload<D>,
|
||||||
@@ -421,37 +455,27 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
||||||
) {
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const definition = durableRegistry.get(event.type)
|
const definition = syncRegistry.get(event.type)
|
||||||
if (!definition?.durable) {
|
if (!definition) {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
|
new InvalidSyncEventError({ type: event.type, message: `Unknown sync event type ${event.type}` }),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const payload = {
|
const payload = {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
type: definition.type,
|
type: definition.type,
|
||||||
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(
|
version: definition.sync.version,
|
||||||
event.data,
|
data: definition.decode(event.data),
|
||||||
),
|
replay: true,
|
||||||
} as Payload
|
} as Payload
|
||||||
const committed = yield* commitDurableEvent(payload, {
|
const committed = yield* commitSyncEvent(payload, {
|
||||||
seq: event.seq,
|
seq: event.seq,
|
||||||
aggregateID: event.aggregateID,
|
aggregateID: event.aggregateID,
|
||||||
ownerID: options?.ownerID,
|
ownerID: options?.ownerID,
|
||||||
strictOwner: options?.strictOwner,
|
strictOwner: options?.strictOwner,
|
||||||
})
|
})
|
||||||
if (committed && options?.publish) {
|
if (committed && options?.publish) {
|
||||||
yield* notify(
|
yield* notify({ ...payload, seq: committed.seq }, true)
|
||||||
{
|
|
||||||
...payload,
|
|
||||||
durable: {
|
|
||||||
aggregateID: committed.aggregateID,
|
|
||||||
seq: committed.seq,
|
|
||||||
version: definition.durable.version,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -466,7 +490,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
if (!source) return undefined
|
if (!source) return undefined
|
||||||
if (events.some((event) => event.aggregateID !== source)) {
|
if (events.some((event) => event.aggregateID !== source)) {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: events[0]?.type ?? "unknown",
|
type: events[0]?.type ?? "unknown",
|
||||||
message: "Replay events must belong to the same aggregate",
|
message: "Replay events must belong to the same aggregate",
|
||||||
}),
|
}),
|
||||||
@@ -477,7 +501,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
const seq = start + index
|
const seq = start + index
|
||||||
if (event.seq !== seq) {
|
if (event.seq !== seq) {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidSyncEventError({
|
||||||
type: event.type,
|
type: event.type,
|
||||||
message: `Replay sequence mismatch at index ${index}: expected ${seq}, got ${event.seq}`,
|
message: `Replay sequence mismatch at index ${index}: expected ${seq}, got ${event.seq}`,
|
||||||
}),
|
}),
|
||||||
@@ -516,18 +540,22 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
Stream.map((event) => event as Payload<D>),
|
Stream.map((event) => event as Payload<D>),
|
||||||
)
|
)
|
||||||
|
|
||||||
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
|
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(all)
|
||||||
|
|
||||||
const decodeSerializedEvent = (event: SerializedEvent): Payload => {
|
const decodeSerializedEvent = (event: SerializedEvent): CursorEvent => {
|
||||||
const definition = durableRegistry.get(event.type)
|
const definition = syncRegistry.get(event.type)
|
||||||
if (!definition?.durable) {
|
if (!definition) {
|
||||||
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
|
throw new InvalidSyncEventError({ type: event.type, message: `Unknown sync event type ${event.type}` })
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
cursor: Cursor.make(event.seq),
|
||||||
type: definition.type,
|
event: {
|
||||||
durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
|
id: event.id,
|
||||||
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(event.data),
|
type: definition.type,
|
||||||
|
version: definition.sync.version,
|
||||||
|
seq: event.seq,
|
||||||
|
data: definition.decode(event.data),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,40 +583,43 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const subscribeDurable = (aggregateID: string) =>
|
const subscribeSynchronized = (aggregateID: string) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const wake = yield* PubSub.sliding<void>(1)
|
const pubsub = yield* PubSub.sliding<void>(1)
|
||||||
const subscription = yield* PubSub.subscribe(wake)
|
const subscription = yield* PubSub.subscribe(pubsub)
|
||||||
yield* Effect.acquireRelease(
|
yield* Effect.acquireRelease(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const wakes = pubsub.durable.get(aggregateID) ?? new Set()
|
const pubsubs = synchronized.get(aggregateID) ?? new Set()
|
||||||
wakes.add(wake)
|
pubsubs.add(pubsub)
|
||||||
pubsub.durable.set(aggregateID, wakes)
|
synchronized.set(aggregateID, pubsubs)
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const wakes = pubsub.durable.get(aggregateID)
|
const pubsubs = synchronized.get(aggregateID)
|
||||||
wakes?.delete(wake)
|
pubsubs?.delete(pubsub)
|
||||||
if (wakes?.size === 0) pubsub.durable.delete(aggregateID)
|
if (pubsubs?.size === 0) synchronized.delete(aggregateID)
|
||||||
}).pipe(Effect.andThen(PubSub.shutdown(wake))),
|
}).pipe(Effect.andThen(PubSub.shutdown(pubsub))),
|
||||||
)
|
)
|
||||||
return subscription
|
return subscription
|
||||||
})
|
})
|
||||||
|
|
||||||
const durable = (input: { readonly aggregateID: string; readonly after?: number }): Stream.Stream<Payload> =>
|
const streamEvents = (input: {
|
||||||
|
readonly aggregateID: string
|
||||||
|
readonly after?: Cursor
|
||||||
|
}): Stream.Stream<CursorEvent> =>
|
||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const wakes = yield* subscribeDurable(input.aggregateID)
|
const synchronized = yield* subscribeSynchronized(input.aggregateID)
|
||||||
let sequence = input.after ?? -1
|
let cursor = input.after ?? -1
|
||||||
const read = Effect.suspend(() => readAfter(input.aggregateID, sequence)).pipe(
|
const read = Effect.suspend(() => readAfter(input.aggregateID, cursor)).pipe(
|
||||||
Effect.tap((events) =>
|
Effect.tap((events) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
sequence = events.at(-1)?.durable?.seq ?? sequence
|
cursor = events.at(-1)?.cursor ?? cursor
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const historical = yield* read
|
const historical = yield* read
|
||||||
const live = Stream.fromSubscription(wakes).pipe(
|
const live = Stream.fromSubscription(synchronized).pipe(
|
||||||
Stream.mapEffect(() => read),
|
Stream.mapEffect(() => read),
|
||||||
Stream.flattenIterable,
|
Stream.flattenIterable,
|
||||||
)
|
)
|
||||||
@@ -596,7 +627,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const listen = (listener: Subscriber): Effect.Effect<Unsubscribe> =>
|
const listen = (listener: Listener): Effect.Effect<Unsubscribe> =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
listeners.push(listener)
|
listeners.push(listener)
|
||||||
return Effect.sync(() => {
|
return Effect.sync(() => {
|
||||||
@@ -605,7 +636,21 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const project = <D extends Definition>(definition: D, projector: Subscriber<D>): Effect.Effect<void> =>
|
const sync = (handler: Sync): Effect.Effect<Unsubscribe> =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
syncHandlers.push(handler)
|
||||||
|
return Effect.sync(() => {
|
||||||
|
const index = syncHandlers.indexOf(handler)
|
||||||
|
if (index >= 0) syncHandlers.splice(index, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const beforeCommit = (guard: CommitGuard): Effect.Effect<void> =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
commitGuards.push(guard)
|
||||||
|
})
|
||||||
|
|
||||||
|
const project = <D extends Definition>(definition: D, projector: Projector<D>): Effect.Effect<void> =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const list = projectors.get(definition.type) ?? []
|
const list = projectors.get(definition.type) ?? []
|
||||||
list.push((event) => projector(event as Payload<D>))
|
list.push((event) => projector(event as Payload<D>))
|
||||||
@@ -616,8 +661,10 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
publish,
|
publish,
|
||||||
subscribe,
|
subscribe,
|
||||||
all: streamAll,
|
all: streamAll,
|
||||||
durable,
|
aggregateEvents: streamEvents,
|
||||||
|
sync,
|
||||||
listen,
|
listen,
|
||||||
|
beforeCommit,
|
||||||
project,
|
project,
|
||||||
replay,
|
replay,
|
||||||
replayAll,
|
replayAll,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { FSUtil } from "./fs-util"
|
|||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { PositiveInt, RelativePath } from "./schema"
|
import { PositiveInt, RelativePath } from "./schema"
|
||||||
import { FileSystemSearch } from "./filesystem/search"
|
import { FileSystemSearch } from "./filesystem/search"
|
||||||
import { Entry, Match, PathError } from "./filesystem/schema"
|
import { Entry, Match } from "./filesystem/schema"
|
||||||
export { Entry, Match, PathError, Submatch } from "./filesystem/schema"
|
export { Entry, Match, Submatch } from "./filesystem/schema"
|
||||||
|
|
||||||
export const ReadInput = Schema.Struct({
|
export const ReadInput = Schema.Struct({
|
||||||
path: RelativePath,
|
path: RelativePath,
|
||||||
@@ -58,10 +58,8 @@ export const Event = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly read: (
|
readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
|
||||||
input: ReadInput,
|
readonly list: (input?: ListInput) => Effect.Effect<Entry[]>
|
||||||
) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }, PathError | FSUtil.Error>
|
|
||||||
readonly list: (input?: ListInput) => Effect.Effect<Entry[], PathError | FSUtil.Error>
|
|
||||||
readonly find: (input: FindInput) => Effect.Effect<Entry[]>
|
readonly find: (input: FindInput) => Effect.Effect<Entry[]>
|
||||||
readonly glob: (input: GlobInput) => Effect.Effect<readonly Entry[]>
|
readonly glob: (input: GlobInput) => Effect.Effect<readonly Entry[]>
|
||||||
readonly grep: (input: GrepInput) => Effect.Effect<readonly Match[]>
|
readonly grep: (input: GrepInput) => Effect.Effect<readonly Match[]>
|
||||||
@@ -79,9 +77,9 @@ const baseLayer = Layer.effect(
|
|||||||
const resolve = Effect.fnUntraced(function* (input?: RelativePath) {
|
const resolve = Effect.fnUntraced(function* (input?: RelativePath) {
|
||||||
const absolute = path.resolve(location.directory, input ?? ".")
|
const absolute = path.resolve(location.directory, input ?? ".")
|
||||||
if (!FSUtil.contains(location.directory, absolute))
|
if (!FSUtil.contains(location.directory, absolute))
|
||||||
return yield* new PathError({ path: input ?? ".", reason: "lexical_escape" })
|
return yield* Effect.die(new Error("Path escapes the location"))
|
||||||
const real = yield* fs.realPath(absolute)
|
const real = yield* fs.realPath(absolute).pipe(Effect.orDie)
|
||||||
if (!FSUtil.contains(root, real)) return yield* new PathError({ path: input ?? ".", reason: "symlink_escape" })
|
if (!FSUtil.contains(root, real)) return yield* Effect.die(new Error("Path escapes the location"))
|
||||||
return { absolute, real, directory: location.directory, root }
|
return { absolute, real, directory: location.directory, root }
|
||||||
})
|
})
|
||||||
return Service.of({
|
return Service.of({
|
||||||
@@ -90,18 +88,19 @@ const baseLayer = Layer.effect(
|
|||||||
grep: search.grep,
|
grep: search.grep,
|
||||||
read: Effect.fn("FileSystem.read")(function* (input) {
|
read: Effect.fn("FileSystem.read")(function* (input) {
|
||||||
const target = yield* resolve(input.path)
|
const target = yield* resolve(input.path)
|
||||||
const info = yield* fs.stat(target.real)
|
const info = yield* fs.stat(target.real).pipe(Effect.orDie)
|
||||||
if (info.type !== "File") return yield* new PathError({ path: input.path, reason: "not_file" })
|
if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file"))
|
||||||
return {
|
return {
|
||||||
content: yield* fs.readFile(target.real),
|
content: yield* fs.readFile(target.real).pipe(Effect.orDie),
|
||||||
mime: FSUtil.mimeType(target.real),
|
mime: FSUtil.mimeType(target.real),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
list: Effect.fn("FileSystem.list")(function* (input = {}) {
|
list: Effect.fn("FileSystem.list")(function* (input = {}) {
|
||||||
const target = yield* resolve(input.path)
|
const target = yield* resolve(input.path)
|
||||||
const info = yield* fs.stat(target.real)
|
const info = yield* fs.stat(target.real).pipe(Effect.orDie)
|
||||||
if (info.type !== "Directory") return yield* new PathError({ path: input.path ?? ".", reason: "not_directory" })
|
if (info.type !== "Directory") return yield* Effect.die(new Error("Path is not a directory"))
|
||||||
return yield* fs.readDirectoryEntries(target.real).pipe(
|
return yield* fs.readDirectoryEntries(target.real).pipe(
|
||||||
|
Effect.orDie,
|
||||||
Effect.map((items) =>
|
Effect.map((items) =>
|
||||||
items
|
items
|
||||||
.flatMap((item) => {
|
.flatMap((item) => {
|
||||||
|
|||||||
@@ -21,8 +21,3 @@ export class Match extends Schema.Class<Match>("FileSystem.Match")({
|
|||||||
text: Schema.String,
|
text: Schema.String,
|
||||||
submatches: Schema.Array(Submatch),
|
submatches: Schema.Array(Submatch),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export class PathError extends Schema.TaggedErrorClass<PathError>()("FileSystem.PathError", {
|
|
||||||
path: Schema.String,
|
|
||||||
reason: Schema.Literals(["lexical_escape", "symlink_escape", "not_file", "not_directory"]),
|
|
||||||
}) {}
|
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
export * as Integration from "./integration"
|
export * as Integration from "./integration"
|
||||||
|
|
||||||
import {
|
import { Cause, Clock, Context, Duration, Effect, Exit, Layer, Schedule, Schema, Scope, SynchronizedRef } from "effect"
|
||||||
Cause,
|
import { castDraft, enableMapSet, type Draft } from "immer"
|
||||||
Clock,
|
|
||||||
Context,
|
|
||||||
Duration,
|
|
||||||
Effect,
|
|
||||||
Exit,
|
|
||||||
Layer,
|
|
||||||
Schedule,
|
|
||||||
Schema,
|
|
||||||
Scope,
|
|
||||||
SynchronizedRef,
|
|
||||||
Types,
|
|
||||||
} from "effect"
|
|
||||||
import { Credential } from "./credential"
|
import { Credential } from "./credential"
|
||||||
import { IntegrationSchema } from "./integration/schema"
|
import { IntegrationSchema } from "./integration/schema"
|
||||||
import { withStatics } from "./schema"
|
import { withStatics } from "./schema"
|
||||||
@@ -54,14 +42,12 @@ export const SelectPrompt = Schema.Struct({
|
|||||||
type: Schema.Literal("select"),
|
type: Schema.Literal("select"),
|
||||||
key: Schema.String,
|
key: Schema.String,
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
options: Schema.mutable(
|
options: Schema.Array(
|
||||||
Schema.Array(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
label: Schema.String,
|
||||||
label: Schema.String,
|
value: Schema.String,
|
||||||
value: Schema.String,
|
hint: Schema.optional(Schema.String),
|
||||||
hint: Schema.optional(Schema.String),
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
when: Schema.optional(When),
|
when: Schema.optional(When),
|
||||||
}).annotate({ identifier: "Integration.SelectPrompt" })
|
}).annotate({ identifier: "Integration.SelectPrompt" })
|
||||||
@@ -74,7 +60,7 @@ export const OAuthMethod = Schema.Struct({
|
|||||||
id: MethodID,
|
id: MethodID,
|
||||||
type: Schema.Literal("oauth"),
|
type: Schema.Literal("oauth"),
|
||||||
label: Schema.String,
|
label: Schema.String,
|
||||||
prompts: Schema.optional(Schema.mutable(Schema.Array(Prompt))),
|
prompts: Schema.optional(Schema.Array(Prompt)),
|
||||||
}).annotate({ identifier: "Integration.OAuthMethod" })
|
}).annotate({ identifier: "Integration.OAuthMethod" })
|
||||||
export type OAuthMethod = typeof OAuthMethod.Type
|
export type OAuthMethod = typeof OAuthMethod.Type
|
||||||
|
|
||||||
@@ -86,7 +72,7 @@ export type KeyMethod = typeof KeyMethod.Type
|
|||||||
|
|
||||||
export const EnvMethod = Schema.Struct({
|
export const EnvMethod = Schema.Struct({
|
||||||
type: Schema.Literal("env"),
|
type: Schema.Literal("env"),
|
||||||
names: Schema.mutable(Schema.Array(Schema.String)),
|
names: Schema.Array(Schema.String),
|
||||||
}).annotate({ identifier: "Integration.EnvMethod" })
|
}).annotate({ identifier: "Integration.EnvMethod" })
|
||||||
export type EnvMethod = typeof EnvMethod.Type
|
export type EnvMethod = typeof EnvMethod.Type
|
||||||
|
|
||||||
@@ -96,8 +82,8 @@ export type Method = typeof Method.Type
|
|||||||
export class Info extends Schema.Class<Info>("Integration.Info")({
|
export class Info extends Schema.Class<Info>("Integration.Info")({
|
||||||
id: ID,
|
id: ID,
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
methods: Schema.mutable(Schema.Array(Method)),
|
methods: Schema.Array(Method),
|
||||||
connections: Schema.mutable(Schema.Array(IntegrationConnection.Info)),
|
connections: Schema.Array(IntegrationConnection.Info),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export type Inputs = Readonly<{ [key: string]: string }>
|
export type Inputs = Readonly<{ [key: string]: string }>
|
||||||
@@ -186,19 +172,19 @@ export type Ref = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Entry = {
|
type Entry = {
|
||||||
ref: Types.DeepMutable<Ref>
|
ref: Ref
|
||||||
methods: Types.DeepMutable<Method>[]
|
methods: Method[]
|
||||||
implementations: Map<MethodID, Types.DeepMutable<OAuthImplementation>>
|
implementations: Map<MethodID, OAuthImplementation>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
integrations: Map<ID, Entry>
|
integrations: Map<ID, Entry>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Draft = {
|
export type Editor = {
|
||||||
list: () => readonly Ref[]
|
list: () => readonly Ref[]
|
||||||
get: (id: ID) => Ref | undefined
|
get: (id: ID) => Ref | undefined
|
||||||
update: (id: ID, update: (integration: Types.DeepMutable<Ref>) => void) => void
|
update: (id: ID, update: (integration: Draft<Ref>) => void) => void
|
||||||
remove: (id: ID) => void
|
remove: (id: ID) => void
|
||||||
method: {
|
method: {
|
||||||
list: (integrationID: ID) => readonly Method[]
|
list: (integrationID: ID) => readonly Method[]
|
||||||
@@ -207,8 +193,11 @@ export type Draft = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface extends State.Transformable<Draft> {
|
export interface Interface {
|
||||||
/** Registers a scoped transform over the integration registry. */
|
/** Registers a scoped transform over the integration registry. */
|
||||||
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
|
/** Registers and immediately applies a scoped integration registry update. */
|
||||||
|
readonly update: State.Interface<Data, Editor>["update"]
|
||||||
/** Returns one integration with its methods and current connections. */
|
/** Returns one integration with its methods and current connections. */
|
||||||
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
||||||
/** Returns all integrations with their methods and current connections. */
|
/** Returns all integrations with their methods and current connections. */
|
||||||
@@ -263,6 +252,8 @@ 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/v2/Integration") {}
|
||||||
|
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
const attemptLifetime = Duration.toMillis(Duration.minutes(10))
|
const attemptLifetime = Duration.toMillis(Duration.minutes(10))
|
||||||
const terminalRetention = Duration.toMillis(Duration.minutes(1))
|
const terminalRetention = Duration.toMillis(Duration.minutes(1))
|
||||||
const scrubInterval = Duration.seconds(30)
|
const scrubInterval = Duration.seconds(30)
|
||||||
@@ -293,17 +284,15 @@ export const locationLayer = Layer.effect(
|
|||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
|
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Editor>({
|
||||||
initial: () => ({ integrations: new Map<ID, Entry>() }),
|
initial: () => ({ integrations: new Map<ID, Entry>() }),
|
||||||
draft: (draft) => ({
|
editor: (draft) => ({
|
||||||
list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],
|
list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],
|
||||||
get: (id) => draft.integrations.get(id)?.ref as Ref | undefined,
|
get: (id) => draft.integrations.get(id)?.ref as Ref | undefined,
|
||||||
update: (id, update) => {
|
update: (id, update) => {
|
||||||
const current = draft.integrations.get(id) ?? {
|
const current =
|
||||||
ref: { id, name: id },
|
draft.integrations.get(id) ??
|
||||||
methods: [],
|
castDraft({ ref: { id, name: id } as Ref, methods: [], implementations: new Map() })
|
||||||
implementations: new Map(),
|
|
||||||
}
|
|
||||||
if (!draft.integrations.has(id)) draft.integrations.set(id, current)
|
if (!draft.integrations.has(id)) draft.integrations.set(id, current)
|
||||||
update(current.ref)
|
update(current.ref)
|
||||||
current.ref.id = id
|
current.ref.id = id
|
||||||
@@ -312,14 +301,16 @@ export const locationLayer = Layer.effect(
|
|||||||
method: {
|
method: {
|
||||||
list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
|
list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
|
||||||
update: (implementation) => {
|
update: (implementation) => {
|
||||||
const current = draft.integrations.get(implementation.integrationID) ?? {
|
const current =
|
||||||
ref: {
|
draft.integrations.get(implementation.integrationID) ??
|
||||||
id: implementation.integrationID,
|
castDraft({
|
||||||
name: implementation.integrationID,
|
ref: {
|
||||||
},
|
id: implementation.integrationID,
|
||||||
methods: [],
|
name: implementation.integrationID,
|
||||||
implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
|
} as Ref,
|
||||||
}
|
methods: [],
|
||||||
|
implementations: new Map<MethodID, OAuthImplementation>(),
|
||||||
|
})
|
||||||
if (!draft.integrations.has(implementation.integrationID)) {
|
if (!draft.integrations.has(implementation.integrationID)) {
|
||||||
draft.integrations.set(implementation.integrationID, current)
|
draft.integrations.set(implementation.integrationID, current)
|
||||||
}
|
}
|
||||||
@@ -328,13 +319,10 @@ export const locationLayer = Layer.effect(
|
|||||||
if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
|
if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
|
||||||
return method.id === implementation.method.id
|
return method.id === implementation.method.id
|
||||||
})
|
})
|
||||||
if (index === -1) current.methods.push(implementation.method as Types.DeepMutable<Method>)
|
if (index === -1) current.methods.push(castDraft(implementation.method))
|
||||||
else current.methods[index] = implementation.method as Types.DeepMutable<Method>
|
else current.methods[index] = castDraft(implementation.method)
|
||||||
if (implementation.method.type === "oauth") {
|
if (isOAuthImplementation(implementation)) {
|
||||||
current.implementations.set(
|
current.implementations.set(implementation.method.id, castDraft(implementation))
|
||||||
implementation.method.id,
|
|
||||||
implementation as Types.DeepMutable<OAuthImplementation>,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
remove: (integrationID, method) => {
|
remove: (integrationID, method) => {
|
||||||
@@ -446,7 +434,7 @@ export const locationLayer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
update: state.update,
|
||||||
get: Effect.fn("Integration.get")(function* (id) {
|
get: Effect.fn("Integration.get")(function* (id) {
|
||||||
const entry = state.get().integrations.get(id)
|
const entry = state.get().integrations.get(id)
|
||||||
if (!entry) return undefined
|
if (!entry) return undefined
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export type Request = typeof Request.Type
|
|||||||
interface MutableRequest {
|
interface MutableRequest {
|
||||||
headers: Record<string, string>
|
headers: Record<string, string>
|
||||||
body: Record<string, unknown>
|
body: Record<string, unknown>
|
||||||
generation?: Record<string, unknown>
|
generation?: Generation
|
||||||
options?: Record<string, unknown>
|
options?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Schema, Types } from "effect"
|
import { DateTime, Schema } from "effect"
|
||||||
|
import { DateTimeUtcFromMillis } from "effect/Schema"
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
import { ModelRequest } from "./model-request"
|
import { ModelRequest } from "./model-request"
|
||||||
|
|
||||||
@@ -15,8 +16,8 @@ export type Family = typeof Family.Type
|
|||||||
export const Capabilities = Schema.Struct({
|
export const Capabilities = Schema.Struct({
|
||||||
tools: Schema.Boolean,
|
tools: Schema.Boolean,
|
||||||
// mime patterns, image, audio, video/*, text/*
|
// mime patterns, image, audio, video/*, text/*
|
||||||
input: Schema.String.pipe(Schema.Array, Schema.mutable),
|
input: Schema.String.pipe(Schema.Array),
|
||||||
output: Schema.String.pipe(Schema.Array, Schema.mutable),
|
output: Schema.String.pipe(Schema.Array),
|
||||||
})
|
})
|
||||||
export type Capabilities = typeof Capabilities.Type
|
export type Capabilities = typeof Capabilities.Type
|
||||||
|
|
||||||
@@ -66,11 +67,11 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
|
|||||||
variants: Schema.Struct({
|
variants: Schema.Struct({
|
||||||
id: VariantID,
|
id: VariantID,
|
||||||
...ModelRequest.Request.fields,
|
...ModelRequest.Request.fields,
|
||||||
}).pipe(Schema.Array, Schema.mutable),
|
}).pipe(Schema.Array),
|
||||||
time: Schema.Struct({
|
time: Schema.Struct({
|
||||||
released: Schema.Finite,
|
released: DateTimeUtcFromMillis,
|
||||||
}),
|
}),
|
||||||
cost: Cost.pipe(Schema.Array, Schema.mutable),
|
cost: Cost.pipe(Schema.Array),
|
||||||
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
||||||
enabled: Schema.Boolean,
|
enabled: Schema.Boolean,
|
||||||
limit: Schema.Struct({
|
limit: Schema.Struct({
|
||||||
@@ -102,7 +103,7 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
|
|||||||
},
|
},
|
||||||
variants: [],
|
variants: [],
|
||||||
time: {
|
time: {
|
||||||
released: 0,
|
released: DateTime.makeUnsafe(0),
|
||||||
},
|
},
|
||||||
cost: [],
|
cost: [],
|
||||||
status: "active",
|
status: "active",
|
||||||
@@ -115,10 +116,6 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
|
|
||||||
api: ProviderV2.MutableApi<Api>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
||||||
const [providerID, ...modelID] = input.split("/")
|
const [providerID, ...modelID] = input.split("/")
|
||||||
return {
|
return {
|
||||||
|
|||||||
+27
-23
@@ -20,7 +20,7 @@ export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedErr
|
|||||||
|
|
||||||
export interface EntryPoint {
|
export interface EntryPoint {
|
||||||
readonly directory: string
|
readonly directory: string
|
||||||
readonly entrypoint?: string
|
readonly entrypoint: Option.Option<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
@@ -34,7 +34,7 @@ export interface Interface {
|
|||||||
}[]
|
}[]
|
||||||
},
|
},
|
||||||
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
|
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
|
||||||
readonly which: (pkg: string, bin?: string) => Effect.Effect<string | undefined>
|
readonly which: (pkg: string, bin?: string) => Effect.Effect<Option.Option<string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
|
||||||
@@ -47,11 +47,12 @@ export function sanitize(pkg: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
|
const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
|
||||||
let entrypoint: string | undefined
|
let entrypoint: Option.Option<string>
|
||||||
try {
|
try {
|
||||||
entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
|
const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
|
||||||
|
entrypoint = Option.some(resolved)
|
||||||
} catch {
|
} catch {
|
||||||
entrypoint = undefined
|
entrypoint = Option.none()
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
directory: dir,
|
directory: dir,
|
||||||
@@ -129,7 +130,7 @@ export const layer = Layer.effect(
|
|||||||
const first = tree.edgesOut.values().next().value?.to
|
const first = tree.edgesOut.values().next().value?.to
|
||||||
if (!first) {
|
if (!first) {
|
||||||
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
||||||
if (result.entrypoint) return result
|
if (Option.isSome(result.entrypoint)) return result
|
||||||
return yield* new InstallFailedError({ add: [pkg], dir })
|
return yield* new InstallFailedError({ add: [pkg], dir })
|
||||||
}
|
}
|
||||||
return resolveEntryPoint(first.name, first.path)
|
return resolveEntryPoint(first.name, first.path)
|
||||||
@@ -218,24 +219,22 @@ export const layer = Layer.effect(
|
|||||||
return Option.some(files[0])
|
return Option.some(files[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
return Option.getOrUndefined(
|
return yield* Effect.gen(function* () {
|
||||||
yield* Effect.gen(function* () {
|
const bin = yield* pick()
|
||||||
const bin = yield* pick()
|
if (Option.isSome(bin)) {
|
||||||
if (Option.isSome(bin)) {
|
return Option.some(path.join(binDir, bin.value))
|
||||||
return Option.some(path.join(binDir, bin.value))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {}))
|
yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {}))
|
||||||
|
|
||||||
yield* add(pkg)
|
yield* add(pkg)
|
||||||
|
|
||||||
const resolved = yield* pick()
|
const resolved = yield* pick()
|
||||||
if (Option.isNone(resolved)) return Option.none<string>()
|
if (Option.isNone(resolved)) return Option.none<string>()
|
||||||
return Option.some(path.join(binDir, resolved.value))
|
return Option.some(path.join(binDir, resolved.value))
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
Effect.orElseSucceed(() => Option.none<string>()),
|
Effect.orElseSucceed(() => Option.none<string>()),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -262,9 +261,14 @@ export async function install(...args: Parameters<Interface["install"]>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function add(...args: Parameters<Interface["add"]>) {
|
export async function add(...args: Parameters<Interface["add"]>) {
|
||||||
return runPromise((svc) => svc.add(...args))
|
const entry = await runPromise((svc) => svc.add(...args))
|
||||||
|
return {
|
||||||
|
directory: entry.directory,
|
||||||
|
entrypoint: Option.getOrUndefined(entry.entrypoint),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function which(...args: Parameters<Interface["which"]>) {
|
export async function which(...args: Parameters<Interface["which"]>) {
|
||||||
return runPromise((svc) => svc.which(...args))
|
const resolved = await runPromise((svc) => svc.which(...args))
|
||||||
|
return Option.getOrUndefined(resolved)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ export const Rule = Schema.Struct({
|
|||||||
}).annotate({ identifier: "PermissionV2.Rule" })
|
}).annotate({ identifier: "PermissionV2.Rule" })
|
||||||
export type Rule = typeof Rule.Type
|
export type Rule = typeof Rule.Type
|
||||||
|
|
||||||
export const Ruleset = Schema.mutable(Schema.Array(Rule)).annotate({ identifier: "PermissionV2.Ruleset" })
|
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" })
|
||||||
export type Ruleset = typeof Ruleset.Type
|
export type Ruleset = typeof Ruleset.Type
|
||||||
|
|||||||
+20
-43
@@ -7,7 +7,6 @@ import type { ModelV2 } from "./model"
|
|||||||
import type { Catalog } from "./catalog"
|
import type { Catalog } from "./catalog"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||||
import { State } from "./state"
|
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
@@ -23,7 +22,7 @@ export const Event = {
|
|||||||
|
|
||||||
type HookSpec = {
|
type HookSpec = {
|
||||||
"catalog.transform": {
|
"catalog.transform": {
|
||||||
input: Catalog.Draft
|
input: Catalog.Editor
|
||||||
output: {}
|
output: {}
|
||||||
}
|
}
|
||||||
"aisdk.language": {
|
"aisdk.language": {
|
||||||
@@ -63,16 +62,18 @@ export type HookFunctions = {
|
|||||||
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
|
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
|
||||||
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
|
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
|
||||||
|
|
||||||
|
export type Effect<R = never> = Effect.Effect<HookFunctions | void, never, R | Scope.Scope>
|
||||||
|
|
||||||
|
export function define<R>(input: { id: ID; effect: Effect.Effect<HookFunctions | void, never, R> }) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly add: (input: {
|
readonly add: (input: {
|
||||||
id: string
|
id: ID
|
||||||
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
|
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
|
||||||
}) => Effect.Effect<void, never, never>
|
}) => Effect.Effect<void, never, never>
|
||||||
readonly remove: (id: ID) => Effect.Effect<void>
|
readonly remove: (id: ID) => Effect.Effect<void>
|
||||||
readonly hook: <Name extends keyof Hooks>(
|
|
||||||
name: Name,
|
|
||||||
callback: (input: Hooks[Name]) => Effect.Effect<void> | void,
|
|
||||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
|
||||||
readonly triggerFor: <Name extends keyof Hooks>(
|
readonly triggerFor: <Name extends keyof Hooks>(
|
||||||
id: ID,
|
id: ID,
|
||||||
name: Name,
|
name: Name,
|
||||||
@@ -96,40 +97,35 @@ export const layer = Layer.effect(
|
|||||||
hooks: HookFunctions
|
hooks: HookFunctions
|
||||||
scope: Scope.Closeable
|
scope: Scope.Closeable
|
||||||
}[] = []
|
}[] = []
|
||||||
let registrations: {
|
|
||||||
[Name in keyof Hooks]: {
|
|
||||||
name: Name
|
|
||||||
callback: (input: Hooks[Name]) => Effect.Effect<void> | void
|
|
||||||
}
|
|
||||||
}[keyof Hooks][] = []
|
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const locks = KeyedMutex.makeUnsafe<ID>()
|
const locks = KeyedMutex.makeUnsafe<ID>()
|
||||||
|
|
||||||
const svc = Service.of({
|
const svc = Service.of({
|
||||||
add: Effect.fn("Plugin.add")(function* (input) {
|
add: Effect.fn("Plugin.add")(function* (input) {
|
||||||
const id = ID.make(input.id)
|
yield* locks.withLock(input.id)(
|
||||||
yield* locks.withLock(id)(
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const existing = hooks.find((item) => item.id === id)
|
const existing = hooks.find((item) => item.id === input.id)
|
||||||
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
|
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
|
||||||
const childScope = yield* Scope.fork(scope)
|
const childScope = yield* Scope.fork(scope)
|
||||||
const result = yield* input.effect.pipe(
|
const result = yield* input.effect.pipe(
|
||||||
Scope.provide(childScope),
|
Scope.provide(childScope),
|
||||||
Effect.withSpan("Plugin.load", {
|
Effect.withSpan("Plugin.load", {
|
||||||
attributes: {
|
attributes: {
|
||||||
"plugin.id": id,
|
"plugin.id": input.id,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
|
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
|
||||||
)
|
)
|
||||||
const next = {
|
hooks = [
|
||||||
id,
|
...hooks.filter((item) => item.id !== input.id),
|
||||||
hooks: result ?? {},
|
{
|
||||||
scope: childScope,
|
id: input.id,
|
||||||
}
|
hooks: result ?? {},
|
||||||
hooks = existing ? hooks.with(hooks.indexOf(existing), next) : [...hooks, next]
|
scope: childScope,
|
||||||
yield* events.publish(Event.Added, { id })
|
},
|
||||||
|
]
|
||||||
|
yield* events.publish(Event.Added, { id: input.id })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@@ -164,12 +160,6 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of registrations) {
|
|
||||||
if (item.name !== name) continue
|
|
||||||
const result = item.callback(event as never)
|
|
||||||
if (Effect.isEffect(result)) yield* result
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [field, draft] of draftEntries) {
|
for (const [field, draft] of draftEntries) {
|
||||||
event[field] = finishDraft(draft)
|
event[field] = finishDraft(draft)
|
||||||
}
|
}
|
||||||
@@ -185,19 +175,6 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
hook: Effect.fn("Plugin.hook")(function* (name, callback) {
|
|
||||||
const scope = yield* Scope.Scope
|
|
||||||
const registration = { name, callback } as (typeof registrations)[number]
|
|
||||||
let active = true
|
|
||||||
registrations = [...registrations, registration]
|
|
||||||
const dispose = Effect.sync(() => {
|
|
||||||
if (!active) return
|
|
||||||
active = false
|
|
||||||
registrations = registrations.filter((item) => item !== registration)
|
|
||||||
})
|
|
||||||
yield* Scope.addFinalizer(scope, dispose)
|
|
||||||
return { dispose }
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
return svc
|
return svc
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
export * as AgentPlugin from "./agent"
|
export * as AgentPlugin from "./agent"
|
||||||
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
|
import { Location } from "../location"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
|
import { PluginV2 } from "../plugin"
|
||||||
|
|
||||||
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
|
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
|
||||||
const BUILD_SYSTEM =
|
const BUILD_SYSTEM =
|
||||||
@@ -96,10 +97,12 @@ Rules:
|
|||||||
- If the conversation ends with an unanswered question to the user, preserve that exact question
|
- If the conversation ends with an unanswered question to the user, preserve that exact question
|
||||||
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
|
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = PluginV2.define({
|
||||||
id: "agent",
|
id: PluginV2.ID.make("agent"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
const worktree = ctx.location.directory
|
const agent = yield* AgentV2.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const worktree = location.directory
|
||||||
const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
|
const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
|
||||||
const readonlyExternalDirectory: PermissionV2.Ruleset = [
|
const readonlyExternalDirectory: PermissionV2.Ruleset = [
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||||
@@ -119,8 +122,8 @@ export const Plugin = define({
|
|||||||
{ action: "read", resource: "*.env.example", effect: "allow" },
|
{ action: "read", resource: "*.env.example", effect: "allow" },
|
||||||
]
|
]
|
||||||
|
|
||||||
yield* ctx.agent.transform((draft) => {
|
yield* agent.update((editor) => {
|
||||||
draft.update(AgentV2.defaultID, (item) => {
|
editor.update(AgentV2.defaultID, (item) => {
|
||||||
item.description = "The default agent. Executes tools based on configured permissions."
|
item.description = "The default agent. Executes tools based on configured permissions."
|
||||||
item.system ??= BUILD_SYSTEM
|
item.system ??= BUILD_SYSTEM
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
@@ -132,7 +135,7 @@ export const Plugin = define({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("plan"), (item) => {
|
editor.update(AgentV2.ID.make("plan"), (item) => {
|
||||||
item.description = "Plan mode. Disallows all edit tools."
|
item.description = "Plan mode. Disallows all edit tools."
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.permissions.push(
|
item.permissions.push(
|
||||||
@@ -151,14 +154,14 @@ export const Plugin = define({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("general"), (item) => {
|
editor.update(AgentV2.ID.make("general"), (item) => {
|
||||||
item.description =
|
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."
|
"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.mode = "subagent"
|
||||||
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "todowrite", resource: "*", effect: "deny" }]))
|
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "todowrite", resource: "*", effect: "deny" }]))
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("explore"), (item) => {
|
editor.update(AgentV2.ID.make("explore"), (item) => {
|
||||||
item.description =
|
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.'
|
'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.system = PROMPT_EXPLORE
|
||||||
@@ -179,21 +182,21 @@ export const Plugin = define({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("compaction"), (item) => {
|
editor.update(AgentV2.ID.make("compaction"), (item) => {
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_COMPACTION
|
item.system = PROMPT_COMPACTION
|
||||||
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("title"), (item) => {
|
editor.update(AgentV2.ID.make("title"), (item) => {
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_TITLE
|
item.system = PROMPT_TITLE
|
||||||
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("summary"), (item) => {
|
editor.update(AgentV2.ID.make("summary"), (item) => {
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_SUMMARY
|
item.system = PROMPT_SUMMARY
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as PluginBoot from "./boot"
|
export * as PluginBoot from "./boot"
|
||||||
|
|
||||||
import type { Plugin as PublicPlugin } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { Context, Deferred, Effect, Layer } from "effect"
|
import { Context, Deferred, Effect, Layer } from "effect"
|
||||||
|
import { Credential } from "../credential"
|
||||||
import { Integration } from "../integration"
|
import { Integration } from "../integration"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { Catalog } from "../catalog"
|
import { Catalog } from "../catalog"
|
||||||
@@ -13,7 +13,6 @@ import { ConfigSkillPlugin } from "../config/plugin/skill"
|
|||||||
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { FileSystem } from "../filesystem"
|
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { ModelsDev } from "../models-dev"
|
import { ModelsDev } from "../models-dev"
|
||||||
@@ -27,13 +26,29 @@ import { ModelsDevPlugin } from "./models-dev"
|
|||||||
import { ProviderPlugins } from "./provider"
|
import { ProviderPlugins } from "./provider"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
import { Reference } from "../reference"
|
import { Reference } from "../reference"
|
||||||
import { State } from "../state"
|
|
||||||
import { PluginHost } from "./host"
|
|
||||||
|
|
||||||
type InternalPlugin = PublicPlugin<any>
|
type Plugin = {
|
||||||
|
id: PluginV2.ID
|
||||||
|
effect: PluginV2.Effect<
|
||||||
|
| Catalog.Service
|
||||||
|
| CommandV2.Service
|
||||||
|
| Credential.Service
|
||||||
|
| Integration.Service
|
||||||
|
| AgentV2.Service
|
||||||
|
| Npm.Service
|
||||||
|
| EventV2.Service
|
||||||
|
| FSUtil.Service
|
||||||
|
| Global.Service
|
||||||
|
| Location.Service
|
||||||
|
| PluginV2.Service
|
||||||
|
| Config.Service
|
||||||
|
| ModelsDev.Service
|
||||||
|
| SkillV2.Service
|
||||||
|
| Reference.Service
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly add: (plugin: PublicPlugin<any>) => Effect.Effect<void>
|
|
||||||
readonly wait: () => Effect.Effect<void>
|
readonly wait: () => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +60,8 @@ export const layer = Layer.effect(
|
|||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const commands = yield* CommandV2.Service
|
const commands = yield* CommandV2.Service
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const integration = yield* Integration.Service
|
const credentials = yield* Credential.Service
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
@@ -53,54 +69,47 @@ export const layer = Layer.effect(
|
|||||||
const npm = yield* Npm.Service
|
const npm = yield* Npm.Service
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const filesystem = yield* FileSystem.Service
|
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
const skill = yield* SkillV2.Service
|
const skill = yield* SkillV2.Service
|
||||||
const reference = yield* Reference.Service
|
const references = yield* Reference.Service
|
||||||
const host = yield* PluginHost.make()
|
|
||||||
const done = yield* Deferred.make<void>()
|
const done = yield* Deferred.make<void>()
|
||||||
|
|
||||||
const add = Effect.fn("PluginBoot.add")(function* (input: InternalPlugin) {
|
const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) {
|
||||||
yield* plugin.add({
|
yield* plugin.add({
|
||||||
id: input.id,
|
id: input.id,
|
||||||
effect: input
|
effect: input.effect.pipe(
|
||||||
.effect(host)
|
Effect.provideService(Catalog.Service, catalog),
|
||||||
.pipe(
|
Effect.provideService(CommandV2.Service, commands),
|
||||||
Effect.provideService(Catalog.Service, catalog),
|
Effect.provideService(Credential.Service, credentials),
|
||||||
Effect.provideService(CommandV2.Service, commands),
|
Effect.provideService(Integration.Service, integrations),
|
||||||
Effect.provideService(Integration.Service, integration),
|
Effect.provideService(AgentV2.Service, agents),
|
||||||
Effect.provideService(AgentV2.Service, agents),
|
Effect.provideService(Config.Service, config),
|
||||||
Effect.provideService(Config.Service, config),
|
Effect.provideService(Location.Service, location),
|
||||||
Effect.provideService(Location.Service, location),
|
Effect.provideService(ModelsDev.Service, modelsDev),
|
||||||
Effect.provideService(ModelsDev.Service, modelsDev),
|
Effect.provideService(Npm.Service, npm),
|
||||||
Effect.provideService(Npm.Service, npm),
|
Effect.provideService(EventV2.Service, events),
|
||||||
Effect.provideService(EventV2.Service, events),
|
Effect.provideService(FSUtil.Service, fs),
|
||||||
Effect.provideService(FSUtil.Service, fs),
|
Effect.provideService(Global.Service, global),
|
||||||
Effect.provideService(FileSystem.Service, filesystem),
|
Effect.provideService(SkillV2.Service, skill),
|
||||||
Effect.provideService(Global.Service, global),
|
Effect.provideService(Reference.Service, references),
|
||||||
Effect.provideService(SkillV2.Service, skill),
|
Effect.provideService(PluginV2.Service, plugin),
|
||||||
Effect.provideService(Reference.Service, reference),
|
),
|
||||||
),
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const boot = Effect.gen(function* () {
|
const boot = Effect.gen(function* () {
|
||||||
yield* State.batch(
|
yield* add(AgentPlugin.Plugin)
|
||||||
Effect.gen(function* () {
|
yield* add(CommandPlugin.Plugin)
|
||||||
yield* add(AgentPlugin.Plugin)
|
yield* add(SkillPlugin.Plugin)
|
||||||
yield* add(CommandPlugin.Plugin)
|
for (const item of ProviderPlugins) {
|
||||||
yield* add(SkillPlugin.Plugin)
|
yield* add(item)
|
||||||
yield* add(ModelsDevPlugin)
|
}
|
||||||
yield* add(ConfigProviderPlugin.Plugin)
|
yield* add(ModelsDevPlugin)
|
||||||
yield* add(ConfigAgentPlugin.Plugin)
|
yield* add(ConfigProviderPlugin.Plugin)
|
||||||
yield* add(ConfigCommandPlugin.Plugin)
|
yield* add(ConfigAgentPlugin.Plugin)
|
||||||
yield* add(ConfigSkillPlugin.Plugin)
|
yield* add(ConfigCommandPlugin.Plugin)
|
||||||
yield* add(ConfigReferencePlugin.Plugin)
|
yield* add(ConfigSkillPlugin.Plugin)
|
||||||
for (const item of ProviderPlugins) {
|
yield* add(ConfigReferencePlugin.Plugin)
|
||||||
yield* add(item)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}).pipe(Effect.withSpan("PluginBoot.boot"))
|
}).pipe(Effect.withSpan("PluginBoot.boot"))
|
||||||
|
|
||||||
yield* boot.pipe(
|
yield* boot.pipe(
|
||||||
@@ -110,22 +119,12 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
add: (input) =>
|
|
||||||
Deferred.await(done).pipe(
|
|
||||||
Effect.andThen(
|
|
||||||
plugin.add({
|
|
||||||
id: input.id,
|
|
||||||
effect: input.effect(host),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
wait: () => Deferred.await(done),
|
wait: () => Deferred.await(done),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(
|
export const locationLayer = layer.pipe(
|
||||||
Layer.provideMerge(PluginV2.locationLayer),
|
|
||||||
Layer.provideMerge(Integration.locationLayer),
|
Layer.provideMerge(Integration.locationLayer),
|
||||||
Layer.provideMerge(Catalog.locationLayer),
|
Layer.provideMerge(Catalog.locationLayer),
|
||||||
Layer.provideMerge(CommandV2.locationLayer),
|
Layer.provideMerge(CommandV2.locationLayer),
|
||||||
@@ -133,5 +132,4 @@ export const locationLayer = layer.pipe(
|
|||||||
Layer.provideMerge(AgentV2.locationLayer),
|
Layer.provideMerge(AgentV2.locationLayer),
|
||||||
Layer.provideMerge(SkillV2.locationLayer),
|
Layer.provideMerge(SkillV2.locationLayer),
|
||||||
Layer.provideMerge(Reference.locationLayer),
|
Layer.provideMerge(Reference.locationLayer),
|
||||||
Layer.provideMerge(FileSystem.locationLayer),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
export * as CommandPlugin from "./command"
|
export * as CommandPlugin from "./command"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
import { CommandV2 } from "../command"
|
||||||
|
import { Location } from "../location"
|
||||||
|
import { PluginV2 } from "../plugin"
|
||||||
import PROMPT_INITIALIZE from "./command/initialize.txt"
|
import PROMPT_INITIALIZE from "./command/initialize.txt"
|
||||||
import PROMPT_REVIEW from "./command/review.txt"
|
import PROMPT_REVIEW from "./command/review.txt"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = PluginV2.define({
|
||||||
id: "command",
|
id: PluginV2.ID.make("command"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.command.transform((draft) => {
|
const command = yield* CommandV2.Service
|
||||||
draft.update("init", (command) => {
|
const location = yield* Location.Service
|
||||||
command.template = PROMPT_INITIALIZE.replace("${path}", ctx.location.project.directory)
|
const transform = yield* command.transform()
|
||||||
|
|
||||||
|
yield* transform((editor) => {
|
||||||
|
editor.update("init", (command) => {
|
||||||
|
command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory)
|
||||||
command.description = "guided AGENTS.md setup"
|
command.description = "guided AGENTS.md setup"
|
||||||
})
|
})
|
||||||
draft.update("review", (command) => {
|
editor.update("review", (command) => {
|
||||||
command.template = PROMPT_REVIEW.replace("${path}", ctx.location.project.directory)
|
command.template = PROMPT_REVIEW.replace("${path}", location.project.directory)
|
||||||
command.description = "review changes [commit|branch|pr], defaults to uncommitted"
|
command.description = "review changes [commit|branch|pr], defaults to uncommitted"
|
||||||
command.subtask = true
|
command.subtask = true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
export * as PluginHost from "./host"
|
|
||||||
|
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
|
||||||
import type { PluginHost as Interface } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import type { Event as SDKEvent, ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
|
||||||
import { Effect, Schema, Stream } from "effect"
|
|
||||||
import { AgentV2 } from "../agent"
|
|
||||||
import { Catalog } from "../catalog"
|
|
||||||
import { CommandV2 } from "../command"
|
|
||||||
import { EventV2 } from "../event"
|
|
||||||
import { FileSystem } from "../filesystem"
|
|
||||||
import { Global } from "../global"
|
|
||||||
import { Integration } from "../integration"
|
|
||||||
import { Location } from "../location"
|
|
||||||
import { ModelV2 } from "../model"
|
|
||||||
import { Npm } from "../npm"
|
|
||||||
import { PluginV2 } from "../plugin"
|
|
||||||
import { ProviderV2 } from "../provider"
|
|
||||||
import { Reference } from "../reference"
|
|
||||||
import { SkillV2 } from "../skill"
|
|
||||||
|
|
||||||
type EventMap = { [Item in SDKEvent as Item["type"]]: Item }
|
|
||||||
type SDKHook = (event: {
|
|
||||||
readonly model: ModelV2Info
|
|
||||||
readonly package: string
|
|
||||||
readonly options: Record<string, any>
|
|
||||||
sdk?: any
|
|
||||||
}) => Effect.Effect<void> | void
|
|
||||||
type LanguageHook = (event: {
|
|
||||||
readonly model: ModelV2Info
|
|
||||||
readonly sdk: any
|
|
||||||
readonly options: Record<string, any>
|
|
||||||
language?: LanguageModelV3
|
|
||||||
}) => Effect.Effect<void> | void
|
|
||||||
|
|
||||||
export const make = Effect.fn("PluginHost.make")(function* () {
|
|
||||||
const agents = yield* AgentV2.Service
|
|
||||||
const catalog = yield* Catalog.Service
|
|
||||||
const commands = yield* CommandV2.Service
|
|
||||||
const events = yield* EventV2.Service
|
|
||||||
const filesystem = yield* FileSystem.Service
|
|
||||||
const global = yield* Global.Service
|
|
||||||
const integration = yield* Integration.Service
|
|
||||||
const location = yield* Location.Service
|
|
||||||
const npm = yield* Npm.Service
|
|
||||||
const plugin = yield* PluginV2.Service
|
|
||||||
const reference = yield* Reference.Service
|
|
||||||
const skill = yield* SkillV2.Service
|
|
||||||
|
|
||||||
return {
|
|
||||||
agent: {
|
|
||||||
get: (id) => agents.get(AgentV2.ID.make(id)),
|
|
||||||
default: agents.default,
|
|
||||||
list: agents.all,
|
|
||||||
rebuild: agents.rebuild,
|
|
||||||
transform: (callback) =>
|
|
||||||
agents.transform((draft) =>
|
|
||||||
callback({
|
|
||||||
list: draft.list,
|
|
||||||
get: (id) => 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)),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
aisdk: {
|
|
||||||
hook: (name, callback) => {
|
|
||||||
if (name === "sdk") {
|
|
||||||
const run = callback as SDKHook
|
|
||||||
return plugin.hook("aisdk.sdk", (event) => {
|
|
||||||
const output = {
|
|
||||||
model: event.model,
|
|
||||||
package: event.package,
|
|
||||||
options: event.options,
|
|
||||||
sdk: event.sdk,
|
|
||||||
}
|
|
||||||
const result = run(output)
|
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
|
||||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const run = callback as LanguageHook
|
|
||||||
return plugin.hook("aisdk.language", (event) => {
|
|
||||||
const output = {
|
|
||||||
model: event.model,
|
|
||||||
sdk: event.sdk,
|
|
||||||
options: event.options,
|
|
||||||
language: event.language,
|
|
||||||
}
|
|
||||||
const result = run(output)
|
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
|
||||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
catalog: {
|
|
||||||
provider: {
|
|
||||||
get: (id) => catalog.provider.get(ProviderV2.ID.make(id)),
|
|
||||||
list: catalog.provider.all,
|
|
||||||
available: catalog.provider.available,
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
|
||||||
list: catalog.model.all,
|
|
||||||
available: catalog.model.available,
|
|
||||||
default: catalog.model.default,
|
|
||||||
small: (providerID) => catalog.model.small(ProviderV2.ID.make(providerID)),
|
|
||||||
},
|
|
||||||
rebuild: catalog.rebuild,
|
|
||||||
transform: (callback) =>
|
|
||||||
catalog.transform((draft) =>
|
|
||||||
callback({
|
|
||||||
provider: {
|
|
||||||
list: draft.provider.list,
|
|
||||||
get: (id) => 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)),
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
get: (providerID, modelID) => draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
|
||||||
update: (providerID, modelID, update) =>
|
|
||||||
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update),
|
|
||||||
remove: (providerID, modelID) =>
|
|
||||||
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
|
||||||
default: {
|
|
||||||
get: draft.model.default.get,
|
|
||||||
set: (providerID, modelID) =>
|
|
||||||
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
command: {
|
|
||||||
get: commands.get,
|
|
||||||
list: commands.list,
|
|
||||||
rebuild: commands.rebuild,
|
|
||||||
transform: commands.transform,
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
subscribe: <Type extends keyof EventMap>(type: Type): Stream.Stream<EventMap[Type]> =>
|
|
||||||
Stream.unwrap(
|
|
||||||
Effect.sync(() => {
|
|
||||||
const definition = EventV2.registry.get(type)
|
|
||||||
if (!definition) throw new Error(`Unknown event type: ${type}`)
|
|
||||||
const encode = Schema.encodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)
|
|
||||||
return events.subscribe(definition).pipe(
|
|
||||||
Stream.map(
|
|
||||||
(event) =>
|
|
||||||
({
|
|
||||||
id: event.id,
|
|
||||||
type: event.type,
|
|
||||||
properties: encode(event.data),
|
|
||||||
}) as unknown as EventMap[Type],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
filesystem: {
|
|
||||||
read: (input) => filesystem.read(Schema.decodeUnknownSync(FileSystem.ReadInput)(input)),
|
|
||||||
list: (input) => filesystem.list(Schema.decodeUnknownSync(FileSystem.ListInput)(input ?? {})),
|
|
||||||
find: (input) => filesystem.find(Schema.decodeUnknownSync(FileSystem.FindInput)(input)),
|
|
||||||
glob: (input) => filesystem.glob(Schema.decodeUnknownSync(FileSystem.GlobInput)(input)),
|
|
||||||
},
|
|
||||||
integration: {
|
|
||||||
get: (id) => integration.get(Integration.ID.make(id)),
|
|
||||||
list: integration.list,
|
|
||||||
rebuild: integration.rebuild,
|
|
||||||
transform: (callback) =>
|
|
||||||
integration.transform((draft) =>
|
|
||||||
callback({
|
|
||||||
list: draft.list,
|
|
||||||
get: (id) => draft.get(Integration.ID.make(id)),
|
|
||||||
update: (id, update) => draft.update(Integration.ID.make(id), update),
|
|
||||||
remove: (id) => draft.remove(Integration.ID.make(id)),
|
|
||||||
method: {
|
|
||||||
list: (id) => draft.method.list(Integration.ID.make(id)),
|
|
||||||
update: (input) => {
|
|
||||||
if (input.method.type === "env") {
|
|
||||||
draft.method.update({
|
|
||||||
integrationID: Integration.ID.make(input.integrationID),
|
|
||||||
method: { type: "env", names: input.method.names },
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
draft.method.update({
|
|
||||||
integrationID: Integration.ID.make(input.integrationID),
|
|
||||||
method: { type: "key", label: input.method.label },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
remove: (id, method) =>
|
|
||||||
draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
location,
|
|
||||||
npm,
|
|
||||||
path: {
|
|
||||||
home: global.home,
|
|
||||||
data: global.data,
|
|
||||||
cache: global.cache,
|
|
||||||
config: global.config,
|
|
||||||
state: global.state,
|
|
||||||
temp: global.tmp,
|
|
||||||
},
|
|
||||||
reference: {
|
|
||||||
list: reference.list,
|
|
||||||
rebuild: reference.rebuild,
|
|
||||||
transform: (callback) =>
|
|
||||||
reference.transform((draft) =>
|
|
||||||
callback({
|
|
||||||
add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
|
|
||||||
remove: draft.remove,
|
|
||||||
list: draft.list,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
skill: {
|
|
||||||
sources: skill.sources,
|
|
||||||
list: skill.list,
|
|
||||||
rebuild: skill.rebuild,
|
|
||||||
transform: (callback) =>
|
|
||||||
skill.transform((draft) =>
|
|
||||||
callback({
|
|
||||||
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
|
|
||||||
list: draft.list,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
} satisfies Interface
|
|
||||||
})
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { DateTime, Effect, Scope, Stream } from "effect"
|
||||||
import { Effect, Stream } from "effect"
|
import { Catalog } from "../catalog"
|
||||||
|
import { Integration } from "../integration"
|
||||||
|
import { EventV2 } from "../event"
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
import { ModelRequest } from "../model-request"
|
import { ModelRequest } from "../model-request"
|
||||||
import { ModelsDev } from "../models-dev"
|
import { ModelsDev } from "../models-dev"
|
||||||
|
import { PluginV2 } from "../plugin"
|
||||||
import { ProviderV2 } from "../provider"
|
import { ProviderV2 } from "../provider"
|
||||||
|
|
||||||
function released(date: string) {
|
function released(date: string) {
|
||||||
const time = Date.parse(date)
|
const time = Date.parse(date)
|
||||||
return Number.isFinite(time) ? time : 0
|
return DateTime.makeUnsafe(Number.isFinite(time) ? time : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function cost(input: ModelsDev.Model["cost"]) {
|
function cost(input: ModelsDev.Model["cost"]) {
|
||||||
@@ -48,16 +51,22 @@ function variants(model: ModelsDev.Model, packageName?: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelsDevPlugin = define({
|
export const ModelsDevPlugin = PluginV2.define({
|
||||||
id: "models-dev",
|
id: PluginV2.ID.make("models-dev"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
const modelsDev = yield* ModelsDev.Service
|
const modelsDev = yield* ModelsDev.Service
|
||||||
yield* ctx.integration.transform(
|
const events = yield* EventV2.Service
|
||||||
Effect.fn(function* (integrations) {
|
const scope = yield* Scope.Scope
|
||||||
const data = yield* modelsDev.get()
|
const transform = yield* catalog.transform()
|
||||||
|
const integrationTransform = yield* integrations.transform()
|
||||||
|
const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () {
|
||||||
|
const data = yield* modelsDev.get()
|
||||||
|
yield* integrationTransform((integrations) => {
|
||||||
for (const item of Object.values(data)) {
|
for (const item of Object.values(data)) {
|
||||||
if (item.env.length === 0) continue
|
if (item.env.length === 0) continue
|
||||||
const integrationID = item.id
|
const integrationID = Integration.ID.make(item.id)
|
||||||
integrations.update(integrationID, (integration) => (integration.name = item.name))
|
integrations.update(integrationID, (integration) => (integration.name = item.name))
|
||||||
integrations.method.update({
|
integrations.method.update({
|
||||||
integrationID,
|
integrationID,
|
||||||
@@ -68,11 +77,8 @@ export const ModelsDevPlugin = define({
|
|||||||
method: { type: "env", names: [...item.env] },
|
method: { type: "env", names: [...item.env] },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
yield* transform((catalog) => {
|
||||||
yield* ctx.catalog.transform(
|
|
||||||
Effect.fn(function* (catalog) {
|
|
||||||
const data = yield* modelsDev.get()
|
|
||||||
for (const item of Object.values(data)) {
|
for (const item of Object.values(data)) {
|
||||||
const providerID = ProviderV2.ID.make(item.id)
|
const providerID = ProviderV2.ID.make(item.id)
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
@@ -126,10 +132,11 @@ export const ModelsDevPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
})
|
||||||
yield* ctx.event.subscribe("models-dev.refreshed").pipe(
|
yield* refresh()
|
||||||
Stream.runForEach(() => ctx.integration.rebuild().pipe(Effect.andThen(ctx.catalog.rebuild()))),
|
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||||
|
Stream.runForEach(() => refresh()),
|
||||||
Effect.forkScoped({ startImmediately: true }),
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const AlibabaPlugin = define({
|
export const AlibabaPlugin = PluginV2.define({
|
||||||
id: "alibaba",
|
id: PluginV2.ID.make("alibaba"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/alibaba") return
|
if (evt.package !== "@ai-sdk/alibaba") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
||||||
evt.sdk = mod.createAlibaba(evt.options)
|
evt.sdk = mod.createAlibaba(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
type MantleSDK = {
|
type MantleSDK = {
|
||||||
@@ -59,11 +59,11 @@ function selectMantleModel(sdk: MantleSDK, modelID: string) {
|
|||||||
return sdk.responses(modelID)
|
return sdk.responses(modelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AmazonBedrockPlugin = define({
|
export const AmazonBedrockPlugin = PluginV2.define({
|
||||||
id: "amazon-bedrock",
|
id: PluginV2.ID.make("amazon-bedrock"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
|
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
|
||||||
@@ -77,10 +77,7 @@ export const AmazonBedrockPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
||||||
const options = { ...evt.options }
|
const options = { ...evt.options }
|
||||||
const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
|
const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
|
||||||
@@ -111,10 +108,7 @@ export const AmazonBedrockPlugin = define({
|
|||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
|
||||||
evt.sdk = mod.createAmazonBedrock(options)
|
evt.sdk = mod.createAmazonBedrock(options)
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||||
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
|
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
|
||||||
evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
|
evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
|
||||||
@@ -123,6 +117,6 @@ export const AmazonBedrockPlugin = define({
|
|||||||
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
|
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
|
||||||
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
|
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const AnthropicPlugin = define({
|
export const AnthropicPlugin = PluginV2.define({
|
||||||
id: "anthropic",
|
id: PluginV2.ID.make("anthropic"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
|
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
|
||||||
@@ -15,14 +15,11 @@ export const AnthropicPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/anthropic") return
|
if (evt.package !== "@ai-sdk/anthropic") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
|
||||||
evt.sdk = mod.createAnthropic(evt.options)
|
evt.sdk = mod.createAnthropic(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
||||||
@@ -10,11 +10,11 @@ function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
|||||||
return sdk.languageModel(modelID)
|
return sdk.languageModel(modelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AzurePlugin = define({
|
export const AzurePlugin = PluginV2.define({
|
||||||
id: "azure",
|
id: PluginV2.ID.make("azure"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/azure") continue
|
if (item.provider.api.package !== "@ai-sdk/azure") continue
|
||||||
@@ -27,10 +27,7 @@ export const AzurePlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/azure") return
|
if (evt.package !== "@ai-sdk/azure") return
|
||||||
if (evt.model.providerID === ProviderV2.ID.azure) {
|
if (evt.model.providerID === ProviderV2.ID.azure) {
|
||||||
if (
|
if (
|
||||||
@@ -46,22 +43,19 @@ export const AzurePlugin = define({
|
|||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/azure"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/azure"))
|
||||||
evt.sdk = mod.createAzure(evt.options)
|
evt.sdk = mod.createAzure(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
||||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AzureCognitiveServicesPlugin = define({
|
export const AzureCognitiveServicesPlugin = PluginV2.define({
|
||||||
id: "azure-cognitive-services",
|
id: PluginV2.ID.make("azure-cognitive-services"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||||
if (!resourceName) return
|
if (!resourceName) return
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
@@ -73,13 +67,10 @@ export const AzureCognitiveServicesPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const CerebrasPlugin = define({
|
export const CerebrasPlugin = PluginV2.define({
|
||||||
id: "cerebras",
|
id: PluginV2.ID.make("cerebras"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (ctx) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of ctx.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
|
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
ctx.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
|
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/cerebras") return
|
if (evt.package !== "@ai-sdk/cerebras") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
|
||||||
evt.sdk = mod.createCerebras(evt.options)
|
evt.sdk = mod.createCerebras(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
import { Effect, Option, Schema } from "effect"
|
import { Effect, Option, Schema } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const CloudflareAIGatewayPlugin = define({
|
export const CloudflareAIGatewayPlugin = PluginV2.define({
|
||||||
id: "cloudflare-ai-gateway",
|
id: PluginV2.ID.make("cloudflare-ai-gateway"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "ai-gateway-provider") return
|
if (evt.package !== "ai-gateway-provider") return
|
||||||
if (evt.options.baseURL) return
|
if (evt.options.baseURL) return
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ export const CloudflareAIGatewayPlugin = define({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
||||||
|
|
||||||
export const CloudflareWorkersAIPlugin = define({
|
export const CloudflareWorkersAIPlugin = PluginV2.define({
|
||||||
id: "cloudflare-workers-ai",
|
id: PluginV2.ID.make("cloudflare-workers-ai"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
const item = evt.provider.get(providerID)
|
const item = evt.provider.get(providerID)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
@@ -20,10 +20,7 @@ export const CloudflareWorkersAIPlugin = define({
|
|||||||
if (accountId) provider.api.url = workersEndpoint(accountId)
|
if (accountId) provider.api.url = workersEndpoint(accountId)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== providerID) return
|
if (evt.model.providerID !== providerID) return
|
||||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||||
|
|
||||||
@@ -37,14 +34,11 @@ export const CloudflareWorkersAIPlugin = define({
|
|||||||
}) as any,
|
}) as any,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== providerID) return
|
if (evt.model.providerID !== providerID) return
|
||||||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const CoherePlugin = define({
|
export const CoherePlugin = PluginV2.define({
|
||||||
id: "cohere",
|
id: PluginV2.ID.make("cohere"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/cohere") return
|
if (evt.package !== "@ai-sdk/cohere") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
|
||||||
evt.sdk = mod.createCohere(evt.options)
|
evt.sdk = mod.createCohere(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const DeepInfraPlugin = define({
|
export const DeepInfraPlugin = PluginV2.define({
|
||||||
id: "deepinfra",
|
id: PluginV2.ID.make("deepinfra"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/deepinfra") return
|
if (evt.package !== "@ai-sdk/deepinfra") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
|
||||||
evt.sdk = mod.createDeepInfra(evt.options)
|
evt.sdk = mod.createDeepInfra(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { Effect } from "effect"
|
import { Npm } from "../../npm"
|
||||||
|
import { Effect, Option } from "effect"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const DynamicProviderPlugin = define({
|
export const DynamicProviderPlugin = PluginV2.define({
|
||||||
id: "dynamic-provider",
|
id: PluginV2.ID.make("dynamic-provider"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
const npm = yield* Npm.Service
|
||||||
"sdk",
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
if (evt.sdk) return
|
if (evt.sdk) return
|
||||||
|
|
||||||
const installedPath = evt.package.startsWith("file://")
|
const installedPath = evt.package.startsWith("file://")
|
||||||
? evt.package
|
? evt.package
|
||||||
: (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
|
||||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||||
|
|
||||||
const mod = yield* Effect.promise(async () => {
|
const mod = yield* Effect.promise(async () => {
|
||||||
@@ -25,6 +26,6 @@ export const DynamicProviderPlugin = define({
|
|||||||
|
|
||||||
evt.sdk = mod[match](evt.options)
|
evt.sdk = mod[match](evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const GatewayPlugin = define({
|
export const GatewayPlugin = PluginV2.define({
|
||||||
id: "gateway",
|
id: PluginV2.ID.make("gateway"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/gateway") return
|
if (evt.package !== "@ai-sdk/gateway") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
|
||||||
evt.sdk = mod.createGateway(evt.options)
|
evt.sdk = mod.createGateway(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function shouldUseResponses(modelID: string) {
|
function shouldUseResponses(modelID: string) {
|
||||||
@@ -11,31 +11,16 @@ function shouldUseResponses(modelID: string) {
|
|||||||
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GithubCopilotPlugin = define({
|
export const GithubCopilotPlugin = PluginV2.define({
|
||||||
id: "github-copilot",
|
id: PluginV2.ID.make("github-copilot"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
|
|
||||||
if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
|
|
||||||
evt.model.update(item.provider.id, ModelV2.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
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/github-copilot") return
|
if (evt.package !== "@ai-sdk/github-copilot") return
|
||||||
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
|
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
|
||||||
evt.sdk = mod.createOpenaiCompatible(evt.options)
|
evt.sdk = mod.createOpenaiCompatible(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||||
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
||||||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
||||||
@@ -45,6 +30,15 @@ export const GithubCopilotPlugin = define({
|
|||||||
? evt.sdk.responses(evt.model.api.id)
|
? evt.sdk.responses(evt.model.api.id)
|
||||||
: evt.sdk.chat(evt.model.api.id)
|
: evt.sdk.chat(evt.model.api.id)
|
||||||
}),
|
}),
|
||||||
)
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
|
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
|
||||||
|
if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
|
||||||
|
evt.model.update(item.provider.id, ModelV2.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
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const GitLabPlugin = define({
|
export const GitLabPlugin = PluginV2.define({
|
||||||
id: "gitlab",
|
id: PluginV2.ID.make("gitlab"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "gitlab-ai-provider") return
|
if (evt.package !== "gitlab-ai-provider") return
|
||||||
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
|
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
|
||||||
evt.sdk = mod.createGitLab({
|
evt.sdk = mod.createGitLab({
|
||||||
@@ -31,10 +30,7 @@ export const GitLabPlugin = define({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||||
const featureFlags =
|
const featureFlags =
|
||||||
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
|
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
|
||||||
@@ -62,6 +58,6 @@ export const GitLabPlugin = define({
|
|||||||
featureFlags,
|
featureFlags,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function resolveProject(options: Record<string, any>) {
|
function resolveProject(options: Record<string, any>) {
|
||||||
@@ -54,11 +54,11 @@ function authFetch(fetchWithRuntimeOptions?: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GoogleVertexPlugin = define({
|
export const GoogleVertexPlugin = PluginV2.define({
|
||||||
id: "google-vertex",
|
id: PluginV2.ID.make("google-vertex"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (
|
if (
|
||||||
@@ -83,10 +83,7 @@ export const GoogleVertexPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
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 === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
|
||||||
evt.options.fetch = authFetch(evt.options.fetch)
|
evt.options.fetch = authFetch(evt.options.fetch)
|
||||||
return
|
return
|
||||||
@@ -103,22 +100,19 @@ export const GoogleVertexPlugin = define({
|
|||||||
location,
|
location,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
||||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GoogleVertexAnthropicPlugin = define({
|
export const GoogleVertexAnthropicPlugin = PluginV2.define({
|
||||||
id: "google-vertex-anthropic",
|
id: PluginV2.ID.make("google-vertex-anthropic"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
|
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
|
||||||
@@ -138,10 +132,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
||||||
const project =
|
const project =
|
||||||
@@ -165,13 +156,10 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const GooglePlugin = define({
|
export const GooglePlugin = PluginV2.define({
|
||||||
id: "google",
|
id: PluginV2.ID.make("google"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/google") return
|
if (evt.package !== "@ai-sdk/google") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
|
||||||
evt.sdk = mod.createGoogleGenerativeAI(evt.options)
|
evt.sdk = mod.createGoogleGenerativeAI(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const GroqPlugin = define({
|
export const GroqPlugin = PluginV2.define({
|
||||||
id: "groq",
|
id: PluginV2.ID.make("groq"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/groq") return
|
if (evt.package !== "@ai-sdk/groq") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
|
||||||
evt.sdk = mod.createGroq(evt.options)
|
evt.sdk = mod.createGroq(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const KiloPlugin = define({
|
export const KiloPlugin = PluginV2.define({
|
||||||
id: "kilo",
|
id: PluginV2.ID.make("kilo"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||||
@@ -16,6 +16,6 @@ export const KiloPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { Integration } from "../../integration"
|
||||||
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const LLMGatewayPlugin = define({
|
export const LLMGatewayPlugin = PluginV2.define({
|
||||||
id: "llmgateway",
|
id: PluginV2.ID.make("llmgateway"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
const integrations = yield* Integration.Service
|
||||||
Effect.fn(function* (evt) {
|
return {
|
||||||
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.disabled) continue
|
if (item.provider.disabled) continue
|
||||||
|
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||||
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
|
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
|
||||||
if (!(yield* ctx.integration.get(item.provider.id))) continue
|
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||||
provider.request.headers["X-Title"] = "opencode"
|
provider.request.headers["X-Title"] = "opencode"
|
||||||
@@ -19,6 +21,6 @@ export const LLMGatewayPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const MistralPlugin = define({
|
export const MistralPlugin = PluginV2.define({
|
||||||
id: "mistral",
|
id: PluginV2.ID.make("mistral"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/mistral") return
|
if (evt.package !== "@ai-sdk/mistral") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
|
||||||
evt.sdk = mod.createMistral(evt.options)
|
evt.sdk = mod.createMistral(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const NvidiaPlugin = define({
|
export const NvidiaPlugin = PluginV2.define({
|
||||||
id: "nvidia",
|
id: PluginV2.ID.make("nvidia"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||||
@@ -17,6 +17,6 @@ export const NvidiaPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const OpenAICompatiblePlugin = define({
|
export const OpenAICompatiblePlugin = PluginV2.define({
|
||||||
id: "openai-compatible",
|
id: PluginV2.ID.make("openai-compatible"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.sdk) return
|
if (evt.sdk) return
|
||||||
if (!evt.package.includes("@ai-sdk/openai-compatible")) return
|
if (!evt.package.includes("@ai-sdk/openai-compatible")) return
|
||||||
if (evt.options.includeUsage !== false) evt.options.includeUsage = true
|
if (evt.options.includeUsage !== false) evt.options.includeUsage = true
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
||||||
evt.sdk = mod.createOpenAICompatible(evt.options as any)
|
evt.sdk = mod.createOpenAICompatible(evt.options as any)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
import { Integration } from "../../integration"
|
import { Integration } from "../../integration"
|
||||||
import { browser, headless } from "./openai-auth"
|
import { browser, headless } from "./openai-auth"
|
||||||
|
|
||||||
export const OpenAIPlugin = define({
|
export const OpenAIPlugin = PluginV2.define({
|
||||||
id: "openai",
|
id: PluginV2.ID.make("openai"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
yield* integrations.transform((draft) => {
|
yield* integrations.update((editor) => {
|
||||||
draft.method.update(browser)
|
editor.method.update(browser)
|
||||||
draft.method.update(headless)
|
editor.method.update(headless)
|
||||||
})
|
})
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
|
if (evt.package !== "@ai-sdk/openai") return
|
||||||
|
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
|
||||||
|
evt.sdk = mod.createOpenAI(evt.options)
|
||||||
|
}),
|
||||||
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
|
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||||
|
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||||
|
}),
|
||||||
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai") continue
|
if (item.provider.api.package !== "@ai-sdk/openai") continue
|
||||||
@@ -26,21 +35,6 @@ export const OpenAIPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/openai") return
|
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
|
|
||||||
evt.sdk = mod.createOpenAI(evt.options)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
|
||||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { Integration } from "../../integration"
|
||||||
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const OpencodePlugin = define({
|
export const OpencodePlugin = PluginV2.define({
|
||||||
id: "opencode",
|
id: PluginV2.ID.make("opencode"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
let hasKey = false
|
let hasKey = false
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
const item = evt.provider.get(ProviderV2.ID.opencode)
|
const item = evt.provider.get(ProviderV2.ID.opencode)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
const integration = yield* ctx.integration.get(item.provider.id)
|
const integration = yield* integrations.get(Integration.ID.make(item.provider.id))
|
||||||
hasKey = Boolean(
|
hasKey = Boolean(
|
||||||
process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey,
|
process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey,
|
||||||
)
|
)
|
||||||
@@ -25,6 +27,6 @@ export const OpencodePlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const OpenRouterPlugin = define({
|
export const OpenRouterPlugin = PluginV2.define({
|
||||||
id: "openrouter",
|
id: PluginV2.ID.make("openrouter"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
|
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
|
||||||
@@ -24,14 +24,11 @@ export const OpenRouterPlugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@openrouter/ai-sdk-provider") return
|
if (evt.package !== "@openrouter/ai-sdk-provider") return
|
||||||
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
|
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
|
||||||
evt.sdk = mod.createOpenRouter(evt.options)
|
evt.sdk = mod.createOpenRouter(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const PerplexityPlugin = define({
|
export const PerplexityPlugin = PluginV2.define({
|
||||||
id: "perplexity",
|
id: PluginV2.ID.make("perplexity"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/perplexity") return
|
if (evt.package !== "@ai-sdk/perplexity") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
|
||||||
evt.sdk = mod.createPerplexity(evt.options)
|
evt.sdk = mod.createPerplexity(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Npm } from "../../npm"
|
||||||
|
import { Effect, Option } from "effect"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const SapAICorePlugin = define({
|
export const SapAICorePlugin = PluginV2.define({
|
||||||
id: "sap-ai-core",
|
id: PluginV2.ID.make("sap-ai-core"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
const npm = yield* Npm.Service
|
||||||
"sdk",
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||||
const serviceKey =
|
const serviceKey =
|
||||||
process.env.AICORE_SERVICE_KEY ??
|
process.env.AICORE_SERVICE_KEY ??
|
||||||
@@ -17,7 +18,7 @@ export const SapAICorePlugin = define({
|
|||||||
|
|
||||||
const installedPath = evt.package.startsWith("file://")
|
const installedPath = evt.package.startsWith("file://")
|
||||||
? evt.package
|
? evt.package
|
||||||
: (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
|
||||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||||
|
|
||||||
const mod = yield* Effect.promise(async () => {
|
const mod = yield* Effect.promise(async () => {
|
||||||
@@ -34,13 +35,10 @@ export const SapAICorePlugin = define({
|
|||||||
: {},
|
: {},
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||||
evt.language = evt.sdk(evt.model.api.id)
|
evt.language = evt.sdk(evt.model.api.id)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
|
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
|
||||||
@@ -64,12 +64,11 @@ export function cortexFetch(upstream: FetchLike = fetch) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SnowflakeCortexPlugin = define({
|
export const SnowflakeCortexPlugin = PluginV2.define({
|
||||||
id: "snowflake-cortex",
|
id: PluginV2.ID.make("snowflake-cortex"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
|
||||||
const token =
|
const token =
|
||||||
process.env.SNOWFLAKE_CORTEX_TOKEN ??
|
process.env.SNOWFLAKE_CORTEX_TOKEN ??
|
||||||
@@ -85,6 +84,6 @@ export const SnowflakeCortexPlugin = define({
|
|||||||
fetch: cortexFetch(upstream) as typeof fetch,
|
fetch: cortexFetch(upstream) as typeof fetch,
|
||||||
} as any)
|
} as any)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const TogetherAIPlugin = define({
|
export const TogetherAIPlugin = PluginV2.define({
|
||||||
id: "togetherai",
|
id: PluginV2.ID.make("togetherai"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/togetherai") return
|
if (evt.package !== "@ai-sdk/togetherai") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
|
||||||
evt.sdk = mod.createTogetherAI(evt.options)
|
evt.sdk = mod.createTogetherAI(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const VenicePlugin = define({
|
export const VenicePlugin = PluginV2.define({
|
||||||
id: "venice",
|
id: PluginV2.ID.make("venice"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "venice-ai-sdk-provider") return
|
if (evt.package !== "venice-ai-sdk-provider") return
|
||||||
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
|
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
|
||||||
evt.sdk = mod.createVenice(evt.options)
|
evt.sdk = mod.createVenice(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const VercelPlugin = define({
|
export const VercelPlugin = PluginV2.define({
|
||||||
id: "vercel",
|
id: PluginV2.ID.make("vercel"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/vercel") continue
|
if (item.provider.api.package !== "@ai-sdk/vercel") continue
|
||||||
@@ -15,14 +15,11 @@ export const VercelPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/vercel") return
|
if (evt.package !== "@ai-sdk/vercel") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
|
||||||
evt.sdk = mod.createVercel(evt.options)
|
evt.sdk = mod.createVercel(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const XAIPlugin = define({
|
export const XAIPlugin = PluginV2.define({
|
||||||
id: "xai",
|
id: PluginV2.ID.make("xai"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.aisdk.hook(
|
return {
|
||||||
"sdk",
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/xai") return
|
if (evt.package !== "@ai-sdk/xai") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
||||||
evt.sdk = mod.createXai(evt.options)
|
evt.sdk = mod.createXai(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
||||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { PluginV2 } from "../../plugin"
|
||||||
|
|
||||||
export const ZenmuxPlugin = define({
|
export const ZenmuxPlugin = PluginV2.define({
|
||||||
id: "zenmux",
|
id: PluginV2.ID.make("zenmux"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.catalog.transform(
|
return {
|
||||||
Effect.fn(function* (evt) {
|
"catalog.transform": Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||||
@@ -16,6 +16,6 @@ export const ZenmuxPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,19 +2,22 @@
|
|||||||
|
|
||||||
export * as SkillPlugin from "./skill"
|
export * as SkillPlugin from "./skill"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
import { PluginV2 } from "../plugin"
|
||||||
import { AbsolutePath } from "../schema"
|
import { AbsolutePath } from "../schema"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" }
|
import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" }
|
||||||
|
|
||||||
export const CustomizeOpencodeContent = customizeOpencodeContent
|
export const CustomizeOpencodeContent = customizeOpencodeContent
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = PluginV2.define({
|
||||||
id: "skill",
|
id: PluginV2.ID.make("skill"),
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.gen(function* () {
|
||||||
yield* ctx.skill.transform((draft) => {
|
const skill = yield* SkillV2.Service
|
||||||
draft.source(
|
const transform = yield* skill.transform()
|
||||||
|
|
||||||
|
yield* transform((editor) => {
|
||||||
|
editor.source(
|
||||||
new SkillV2.EmbeddedSource({
|
new SkillV2.EmbeddedSource({
|
||||||
type: "embedded",
|
type: "embedded",
|
||||||
skill: new SkillV2.Info({
|
skill: new SkillV2.Info({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as ProviderV2 from "./provider"
|
export * as ProviderV2 from "./provider"
|
||||||
|
|
||||||
import { withStatics } from "./schema"
|
import { withStatics } from "./schema"
|
||||||
import { Schema, Types } from "effect"
|
import { Schema } from "effect"
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(
|
export const ID = Schema.String.pipe(
|
||||||
Schema.brand("ProviderV2.ID"),
|
Schema.brand("ProviderV2.ID"),
|
||||||
@@ -37,9 +37,6 @@ export const Native = Schema.Struct({
|
|||||||
|
|
||||||
export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
|
export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
|
||||||
export type Api = typeof Api.Type
|
export type Api = typeof Api.Type
|
||||||
export type MutableApi<T extends Api = Api> = T extends Api
|
|
||||||
? Omit<Types.DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any })
|
|
||||||
: never
|
|
||||||
|
|
||||||
export const Request = Schema.Struct({
|
export const Request = Schema.Struct({
|
||||||
headers: Schema.Record(Schema.String, Schema.String),
|
headers: Schema.Record(Schema.String, Schema.String),
|
||||||
@@ -69,5 +66,3 @@ export class Info extends Schema.Class<Info>("ProviderV2.Info")({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * as Session from "./session"
|
export * as Session from "./session"
|
||||||
|
|
||||||
import { Effect, Schema, Stream } from "effect"
|
import { Effect, Schema, Stream } from "effect"
|
||||||
|
import { EventV2 } from "../event"
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
import { SessionV2 } from "../session"
|
import { SessionV2 } from "../session"
|
||||||
import { MessageDecodeError } from "../session/error"
|
import { MessageDecodeError } from "../session/error"
|
||||||
@@ -33,7 +34,9 @@ export type Delivery = SessionInput.Delivery
|
|||||||
export const ListInput = SessionV2.ListInput
|
export const ListInput = SessionV2.ListInput
|
||||||
export type ListInput = SessionV2.ListInput
|
export type ListInput = SessionV2.ListInput
|
||||||
|
|
||||||
export type Event = SessionEvent.DurableEvent
|
export const EventCursor = EventV2.Cursor
|
||||||
|
export type EventCursor = EventV2.Cursor
|
||||||
|
export type Event = EventV2.CursorEvent<SessionEvent.DurableEvent>
|
||||||
|
|
||||||
export const NotFoundError = SessionV2.NotFoundError
|
export const NotFoundError = SessionV2.NotFoundError
|
||||||
export type NotFoundError = SessionV2.NotFoundError
|
export type NotFoundError = SessionV2.NotFoundError
|
||||||
@@ -96,7 +99,7 @@ export interface MessageInput {
|
|||||||
|
|
||||||
export interface EventsInput {
|
export interface EventsInput {
|
||||||
readonly sessionID: ID
|
readonly sessionID: ID
|
||||||
readonly after?: number
|
readonly after?: EventCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * as Reference from "./reference"
|
export * as Reference from "./reference"
|
||||||
|
|
||||||
import { Context, Effect, Layer, Schema, Scope, Types } from "effect"
|
import { Context, Effect, Layer, Schema, Scope } from "effect"
|
||||||
|
import { castDraft } from "immer"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Repository } from "./repository"
|
import { Repository } from "./repository"
|
||||||
@@ -39,16 +40,17 @@ export class Info extends Schema.Class<Info>("Reference.Info")({
|
|||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
sources: Map<string, Types.DeepMutable<Source>>
|
sources: Map<string, Source>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Draft = {
|
type Editor = {
|
||||||
add(name: string, source: Source): void
|
add(name: string, source: Source): void
|
||||||
remove(name: string): void
|
remove(name: string): void
|
||||||
list(): readonly [string, Source][]
|
list(): readonly [string, Source][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface extends State.Transformable<Draft> {
|
export interface Interface {
|
||||||
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
readonly list: () => Effect.Effect<Info[]>
|
readonly list: () => Effect.Effect<Info[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,18 +64,18 @@ export const layer = Layer.effect(
|
|||||||
const cache = yield* RepositoryCache.Service
|
const cache = yield* RepositoryCache.Service
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const materialized = new Map<string, Info>()
|
const materialized = new Map<string, Info>()
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Editor>({
|
||||||
initial: () => ({ sources: new Map() }),
|
initial: () => ({ sources: new Map() }),
|
||||||
draft: (draft) => ({
|
editor: (draft) => ({
|
||||||
add: (name, source) => draft.sources.set(name, source as Types.DeepMutable<Source>),
|
add: (name, source) => draft.sources.set(name, castDraft(source)),
|
||||||
remove: (name) => draft.sources.delete(name),
|
remove: (name) => draft.sources.delete(name),
|
||||||
list: () => Array.from(draft.sources.entries()) as [string, Source][],
|
list: () => Array.from(draft.sources.entries()) as [string, Source][],
|
||||||
}),
|
}),
|
||||||
finalize: (draft) =>
|
finalize: (editor) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
materialized.clear()
|
materialized.clear()
|
||||||
const seen = new Map<string, string | undefined>()
|
const seen = new Map<string, string | undefined>()
|
||||||
for (const [name, source] of draft.list()) {
|
for (const [name, source] of editor.list()) {
|
||||||
if (source.type === "local") {
|
if (source.type === "local") {
|
||||||
materialized.set(
|
materialized.set(
|
||||||
name,
|
name,
|
||||||
@@ -126,7 +128,6 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
|
||||||
list: Effect.fn("Reference.list")(function* () {
|
list: Effect.fn("Reference.list")(function* () {
|
||||||
return Array.from(materialized.values())
|
return Array.from(materialized.values())
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ export interface Interface {
|
|||||||
) => Effect.Effect<SessionMessage.Message[], NotFoundError | MessageDecodeError>
|
) => Effect.Effect<SessionMessage.Message[], NotFoundError | MessageDecodeError>
|
||||||
readonly events: (input: {
|
readonly events: (input: {
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
after?: number
|
after?: EventV2.Cursor
|
||||||
}) => Stream.Stream<SessionEvent.DurableEvent, NotFoundError>
|
}) => Stream.Stream<EventV2.CursorEvent<SessionEvent.DurableEvent>, NotFoundError>
|
||||||
readonly switchAgent: (input: {
|
readonly switchAgent: (input: {
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
agent: string
|
agent: string
|
||||||
@@ -339,8 +339,12 @@ export const layer = Layer.effect(
|
|||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
result
|
result
|
||||||
.get(input.sessionID)
|
.get(input.sessionID)
|
||||||
.pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))),
|
.pipe(Effect.as(events.aggregateEvents({ aggregateID: input.sessionID, after: input.after }))),
|
||||||
).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))),
|
).pipe(
|
||||||
|
Stream.filter((event): event is EventV2.CursorEvent<SessionEvent.DurableEvent> =>
|
||||||
|
isDurableSessionEvent(event.event),
|
||||||
|
),
|
||||||
|
),
|
||||||
prompt: Effect.fn("V2Session.prompt")((input) =>
|
prompt: Effect.fn("V2Session.prompt")((input) =>
|
||||||
Effect.uninterruptible(
|
Effect.uninterruptible(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -409,9 +413,9 @@ export const layer = Layer.effect(
|
|||||||
sessionID,
|
sessionID,
|
||||||
timestamp: yield* DateTime.now,
|
timestamp: yield* DateTime.now,
|
||||||
})
|
})
|
||||||
if (event.durable === undefined)
|
if (event.seq === undefined)
|
||||||
return yield* Effect.die("Interrupt request event is missing aggregate sequence")
|
return yield* Effect.die("Interrupt request event is missing aggregate sequence")
|
||||||
yield* execution.interrupt(sessionID, event.durable.seq)
|
yield* execution.interrupt(sessionID, event.seq)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ const Base = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
durable: {
|
sync: {
|
||||||
aggregate: "sessionID",
|
aggregate: "sessionID",
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
const stepSettlementOptions = {
|
const stepSettlementOptions = {
|
||||||
durable: {
|
sync: {
|
||||||
aggregate: "sessionID",
|
aggregate: "sessionID",
|
||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
@@ -456,7 +456,7 @@ export namespace Compaction {
|
|||||||
|
|
||||||
export const Ended = EventV2.define({
|
export const Ended = EventV2.define({
|
||||||
type: "session.next.compaction.ended",
|
type: "session.next.compaction.ended",
|
||||||
durable: { aggregate: "sessionID", version: 2 },
|
sync: { aggregate: "sessionID", version: 2 },
|
||||||
schema: {
|
schema: {
|
||||||
...Base,
|
...Base,
|
||||||
messageID: SessionMessageID.ID,
|
messageID: SessionMessageID.ID,
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
|||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
Effect.flatMap((event) =>
|
Effect.flatMap((event) =>
|
||||||
event.durable === undefined
|
event.seq === undefined
|
||||||
? Effect.die("Prompt admission event is missing aggregate sequence")
|
? Effect.die("Prompt admission event is missing aggregate sequence")
|
||||||
: Effect.succeed(
|
: Effect.succeed(
|
||||||
new Admitted({
|
new Admitted({
|
||||||
admittedSeq: event.durable.seq,
|
admittedSeq: event.seq,
|
||||||
id: input.id,
|
id: input.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
prompt: input.prompt,
|
prompt: input.prompt,
|
||||||
@@ -117,6 +117,13 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
|
|||||||
readonly timeCreated: DateTime.Utc
|
readonly timeCreated: DateTime.Utc
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
const message = yield* db
|
||||||
|
.select({ id: SessionMessageTable.id })
|
||||||
|
.from(SessionMessageTable)
|
||||||
|
.where(eq(SessionMessageTable.id, input.id))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
if (message) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||||
const stored = yield* db
|
const stored = yield* db
|
||||||
.insert(SessionInputTable)
|
.insert(SessionInputTable)
|
||||||
.values({
|
.values({
|
||||||
@@ -201,6 +208,37 @@ const matchesPrompt = (input: Admitted, expected: { readonly sessionID: SessionS
|
|||||||
input.sessionID === expected.sessionID &&
|
input.sessionID === expected.sessionID &&
|
||||||
JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
|
JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
|
||||||
|
|
||||||
|
export const guardReservedID = Effect.fn("SessionInput.guardReservedID")(function* (
|
||||||
|
db: DatabaseService,
|
||||||
|
event: EventV2.Payload,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
Schema.is(SessionEvent.PromptLifecycle.Admitted)(event) ||
|
||||||
|
Schema.is(SessionEvent.PromptLifecycle.Promoted)(event)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
const id = reservedID(event)
|
||||||
|
if (id === undefined) return
|
||||||
|
const admitted = yield* db
|
||||||
|
.select({ id: SessionInputTable.id })
|
||||||
|
.from(SessionInputTable)
|
||||||
|
.where(eq(SessionInputTable.id, id))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
if (admitted === undefined) return
|
||||||
|
return yield* Effect.die(new LifecycleConflict({ id }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const reservedID = (event: EventV2.Payload) => {
|
||||||
|
if (Schema.is(SessionEvent.Step.Started)(event)) return event.data.assistantMessageID
|
||||||
|
if (Schema.is(SessionEvent.AgentSwitched)(event)) return event.data.messageID
|
||||||
|
if (Schema.is(SessionEvent.ModelSwitched)(event)) return event.data.messageID
|
||||||
|
if (Schema.is(SessionEvent.Prompted)(event)) return event.data.messageID
|
||||||
|
if (Schema.is(SessionEvent.Synthetic)(event)) return event.data.messageID
|
||||||
|
if (Schema.is(SessionEvent.Shell.Started)(event)) return event.data.messageID
|
||||||
|
if (Schema.is(SessionEvent.Compaction.Started)(event)) return event.data.messageID
|
||||||
|
}
|
||||||
|
|
||||||
export const projectLegacyPrompted = Effect.fn("SessionInput.projectLegacyPrompted")(function* (
|
export const projectLegacyPrompted = Effect.fn("SessionInput.projectLegacyPrompted")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ function run(db: DatabaseService, event: SessionEvent.Event) {
|
|||||||
const decodeRow = (row: typeof SessionMessageTable.$inferSelect) =>
|
const decodeRow = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||||
decodeMessage({ ...row.data, id: row.id, type: row.type })
|
decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||||
const updateMessage = (message: SessionMessage.Message) => {
|
const updateMessage = (message: SessionMessage.Message) => {
|
||||||
if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.seq === undefined) return Effect.die("Synchronized Session event is missing aggregate sequence")
|
||||||
const encoded = encodeMessage(message)
|
const encoded = encodeMessage(message)
|
||||||
const { id, type, ...data } = encoded
|
const { id, type, ...data } = encoded
|
||||||
return db
|
return db
|
||||||
@@ -192,7 +192,7 @@ function run(db: DatabaseService, event: SessionEvent.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function insertMessage(db: DatabaseService, event: SessionEvent.Event, message: SessionMessage.Message) {
|
function insertMessage(db: DatabaseService, event: SessionEvent.Event, message: SessionMessage.Message) {
|
||||||
if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.seq === undefined) return Effect.die("Synchronized Session event is missing aggregate sequence")
|
||||||
const encoded = encodeMessage(message)
|
const encoded = encodeMessage(message)
|
||||||
const { id, type, ...data } = encoded
|
const { id, type, ...data } = encoded
|
||||||
return db
|
return db
|
||||||
@@ -201,7 +201,7 @@ function insertMessage(db: DatabaseService, event: SessionEvent.Event, message:
|
|||||||
id: SessionMessage.ID.make(id),
|
id: SessionMessage.ID.make(id),
|
||||||
session_id: event.data.sessionID,
|
session_id: event.data.sessionID,
|
||||||
type,
|
type,
|
||||||
seq: event.durable.seq,
|
seq: event.seq,
|
||||||
time_created: DateTime.toEpochMillis(message.time.created),
|
time_created: DateTime.toEpochMillis(message.time.created),
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
@@ -213,6 +213,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
|
yield* events.beforeCommit((event) => SessionInput.guardReservedID(db, event))
|
||||||
yield* events.project(SessionV1.Event.Created, (event) =>
|
yield* events.project(SessionV1.Event.Created, (event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const stored = yield* db
|
const stored = yield* db
|
||||||
@@ -330,7 +331,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.AgentSwitched, (event) => {
|
yield* events.project(SessionEvent.AgentSwitched, (event) => {
|
||||||
if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.seq === undefined) return Effect.die("Synchronized Session event is missing aggregate sequence")
|
||||||
return db
|
return db
|
||||||
.update(SessionTable)
|
.update(SessionTable)
|
||||||
.set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
.set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||||
@@ -339,7 +340,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
.pipe(
|
.pipe(
|
||||||
Effect.orDie,
|
Effect.orDie,
|
||||||
Effect.andThen(run(db, event)),
|
Effect.andThen(run(db, event)),
|
||||||
Effect.andThen(SessionContextEpoch.requestReplacement(db, event.data.sessionID, event.durable.seq)),
|
Effect.andThen(SessionContextEpoch.requestReplacement(db, event.data.sessionID, event.seq)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
yield* events.project(SessionEvent.ModelSwitched, (event) =>
|
yield* events.project(SessionEvent.ModelSwitched, (event) =>
|
||||||
@@ -351,8 +352,9 @@ export const layer = Layer.effectDiscard(
|
|||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
yield* run(db, event)
|
yield* run(db, event)
|
||||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.seq === undefined)
|
||||||
yield* SessionContextEpoch.requestReplacement(db, event.data.sessionID, event.durable.seq)
|
return yield* Effect.die("Synchronized Session event is missing aggregate sequence")
|
||||||
|
yield* SessionContextEpoch.requestReplacement(db, event.data.sessionID, event.seq)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.Prompted, (event) =>
|
yield* events.project(SessionEvent.Prompted, (event) =>
|
||||||
@@ -366,22 +368,24 @@ export const layer = Layer.effectDiscard(
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (existing) return yield* Effect.die(new PromptAlreadyProjected())
|
if (existing) return yield* Effect.die(new PromptAlreadyProjected())
|
||||||
yield* run(db, event)
|
yield* run(db, event)
|
||||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.seq === undefined)
|
||||||
|
return yield* Effect.die("Synchronized Session event is missing aggregate sequence")
|
||||||
yield* SessionInput.projectLegacyPrompted(db, {
|
yield* SessionInput.projectLegacyPrompted(db, {
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID: event.data.sessionID,
|
sessionID: event.data.sessionID,
|
||||||
prompt: event.data.prompt,
|
prompt: event.data.prompt,
|
||||||
delivery: event.data.delivery,
|
delivery: event.data.delivery,
|
||||||
timeCreated: event.data.timestamp,
|
timeCreated: event.data.timestamp,
|
||||||
promotedSeq: event.durable.seq,
|
promotedSeq: event.seq,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.PromptLifecycle.Admitted, (event) =>
|
yield* events.project(SessionEvent.PromptLifecycle.Admitted, (event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.seq === undefined)
|
||||||
|
return yield* Effect.die("Synchronized Session event is missing aggregate sequence")
|
||||||
yield* SessionInput.projectAdmitted(db, {
|
yield* SessionInput.projectAdmitted(db, {
|
||||||
admittedSeq: event.durable.seq,
|
admittedSeq: event.seq,
|
||||||
id: event.data.messageID,
|
id: event.data.messageID,
|
||||||
sessionID: event.data.sessionID,
|
sessionID: event.data.sessionID,
|
||||||
prompt: event.data.prompt,
|
prompt: event.data.prompt,
|
||||||
@@ -392,7 +396,8 @@ export const layer = Layer.effectDiscard(
|
|||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.PromptLifecycle.Promoted, (event) =>
|
yield* events.project(SessionEvent.PromptLifecycle.Promoted, (event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.seq === undefined)
|
||||||
|
return yield* Effect.die("Synchronized Session event is missing aggregate sequence")
|
||||||
yield* insertMessage(
|
yield* insertMessage(
|
||||||
db,
|
db,
|
||||||
event,
|
event,
|
||||||
@@ -401,14 +406,18 @@ export const layer = Layer.effectDiscard(
|
|||||||
sessionID: event.data.sessionID,
|
sessionID: event.data.sessionID,
|
||||||
prompt: event.data.prompt,
|
prompt: event.data.prompt,
|
||||||
timeCreated: event.data.timeCreated,
|
timeCreated: event.data.timeCreated,
|
||||||
promotedSeq: event.durable.seq,
|
promotedSeq: event.seq,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.InterruptRequested, () => Effect.void)
|
yield* events.project(SessionEvent.InterruptRequested, () => Effect.void)
|
||||||
// TODO: Reconstruct context epoch replacement state during replay without adding replay state to every EventV2 payload.
|
yield* events.project(SessionEvent.ContextUpdated, (event) => {
|
||||||
yield* events.project(SessionEvent.ContextUpdated, (event) => run(db, event))
|
if (!event.replay || event.seq === undefined) return run(db, event)
|
||||||
|
return run(db, event).pipe(
|
||||||
|
Effect.andThen(SessionContextEpoch.requestReplacement(db, event.data.sessionID, event.seq)),
|
||||||
|
)
|
||||||
|
})
|
||||||
yield* events.project(SessionEvent.Synthetic, (event) => run(db, event))
|
yield* events.project(SessionEvent.Synthetic, (event) => run(db, event))
|
||||||
yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event))
|
yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event))
|
||||||
yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event))
|
yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event))
|
||||||
@@ -427,9 +436,9 @@ export const layer = Layer.effectDiscard(
|
|||||||
yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event))
|
yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event))
|
||||||
// yield* events.project(SessionEvent.Retried, (event) => run(db, event))
|
// yield* events.project(SessionEvent.Retried, (event) => run(db, event))
|
||||||
yield* events.project(SessionEvent.Compaction.Ended, (event) => {
|
yield* events.project(SessionEvent.Compaction.Ended, (event) => {
|
||||||
if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.version === 1) return Effect.void
|
||||||
if (event.durable.version === 1) return Effect.void
|
const seq = event.seq
|
||||||
const seq = event.durable.seq
|
if (seq === undefined) return Effect.die("Synchronized Session event is missing aggregate sequence")
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* run(db, event)
|
yield* run(db, event)
|
||||||
yield* SessionContextEpoch.requestReplacement(db, event.data.sessionID, seq)
|
yield* SessionContextEpoch.requestReplacement(db, event.data.sessionID, seq)
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiE
|
|||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
export type Error = ModelNotSelectedError | UnsupportedApiError
|
export type Error =
|
||||||
|
| Catalog.ProviderNotFoundError
|
||||||
|
| Catalog.ModelNotFoundError
|
||||||
|
| ModelNotSelectedError
|
||||||
|
| UnsupportedApiError
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Model, Error>
|
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Model, Error>
|
||||||
@@ -145,12 +149,10 @@ export const locationLayer = Layer.effect(
|
|||||||
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
||||||
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
||||||
yield* boot.wait()
|
yield* boot.wait()
|
||||||
const defaultModel = session.model ? undefined : yield* catalog.model.default()
|
|
||||||
const selected = session.model
|
const selected = session.model
|
||||||
? yield* catalog.model.get(session.model.providerID, session.model.id)
|
? yield* catalog.model.get(session.model.providerID, session.model.id)
|
||||||
: defaultModel && supported(defaultModel)
|
: (Option.getOrUndefined((yield* catalog.model.default()).pipe(Option.filter(supported))) ??
|
||||||
? defaultModel
|
(yield* catalog.model.available()).find(supported))
|
||||||
: (yield* catalog.model.available()).find(supported)
|
|
||||||
if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
|
if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
|
||||||
const connection = yield* integrations.connection.forIntegration(Integration.ID.make(selected.providerID))
|
const connection = yield* integrations.connection.forIntegration(Integration.ID.make(selected.providerID))
|
||||||
return yield* fromCatalogModel(
|
return yield* fromCatalogModel(
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export * as SkillV2 from "./skill"
|
export * as SkillV2 from "./skill"
|
||||||
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Context, Effect, Layer, Schema, Types } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
|
import { castDraft } from "immer"
|
||||||
import { AgentV2 } from "./agent"
|
import { AgentV2 } from "./agent"
|
||||||
import { ConfigMarkdown } from "./config/markdown"
|
import { ConfigMarkdown } from "./config/markdown"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
@@ -64,15 +65,16 @@ const Frontmatter = Schema.Struct({
|
|||||||
const decodeFrontmatter = Schema.decodeUnknownOption(Frontmatter)
|
const decodeFrontmatter = Schema.decodeUnknownOption(Frontmatter)
|
||||||
|
|
||||||
export type Data = {
|
export type Data = {
|
||||||
sources: Types.DeepMutable<Source>[]
|
sources: Source[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Draft = {
|
export type Editor = {
|
||||||
source: (source: Source) => void
|
source: (source: Source) => void
|
||||||
list: () => readonly Source[]
|
list: () => readonly Source[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface extends State.Transformable<Draft> {
|
export interface Interface {
|
||||||
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
readonly sources: () => Effect.Effect<Source[]>
|
readonly sources: () => Effect.Effect<Source[]>
|
||||||
readonly list: () => Effect.Effect<Info[]>
|
readonly list: () => Effect.Effect<Info[]>
|
||||||
}
|
}
|
||||||
@@ -85,12 +87,12 @@ export const layer = Layer.effect(
|
|||||||
const discovery = yield* SkillDiscovery.Service
|
const discovery = yield* SkillDiscovery.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
|
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Editor>({
|
||||||
initial: () => ({ sources: [] }),
|
initial: () => ({ sources: [] }),
|
||||||
draft: (draft) => ({
|
editor: (draft) => ({
|
||||||
source: (source) => {
|
source: (source) => {
|
||||||
if (draft.sources.some((item) => Source.equals(item, source))) return
|
if (draft.sources.some((item) => Source.equals(item, source))) return
|
||||||
draft.sources.push(source as Types.DeepMutable<Source>)
|
draft.sources.push(castDraft(source))
|
||||||
},
|
},
|
||||||
list: () => draft.sources as Source[],
|
list: () => draft.sources as Source[],
|
||||||
}),
|
}),
|
||||||
@@ -148,7 +150,6 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
|
||||||
sources: Effect.fn("SkillV2.sources")(function* () {
|
sources: Effect.fn("SkillV2.sources")(function* () {
|
||||||
return state.get().sources
|
return state.get().sources
|
||||||
}),
|
}),
|
||||||
|
|||||||
+72
-88
@@ -1,128 +1,112 @@
|
|||||||
export * as State from "./state"
|
export * as State from "./state"
|
||||||
|
|
||||||
import { Context, Effect, Scope, Semaphore } from "effect"
|
import { Effect, Scope, Semaphore } from "effect"
|
||||||
|
import type { Draft, Objectish } from "immer"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A replayable transform applied to a draft during rebuild.
|
* A replayable transform applied to an editor during rebuild.
|
||||||
*
|
*
|
||||||
* Domain drafts expose readable and writable state while preserving concise
|
* Transforms are intentionally synchronous and mutation-shaped: domain editors
|
||||||
* plugin/config code. Transforms may perform Effects before returning.
|
* hide the draft representation while preserving concise plugin/config code.
|
||||||
*/
|
*/
|
||||||
type TransformCallback<DraftApi> = (draft: DraftApi) => Effect.Effect<void> | void
|
export type Transform<Editor> = (editor: Editor) => void
|
||||||
export type MakeDraft<State, DraftApi> = (state: State) => DraftApi
|
export type MakeEditor<State extends Objectish, Editor> = (draft: Draft<State>) => Editor
|
||||||
|
|
||||||
export interface Registration {
|
export interface Options<State extends Objectish, Editor> {
|
||||||
readonly dispose: Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Transform<DraftApi> = (
|
|
||||||
transform: TransformCallback<DraftApi>,
|
|
||||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
|
||||||
|
|
||||||
export type Rebuild = () => Effect.Effect<void>
|
|
||||||
|
|
||||||
export interface Transformable<DraftApi> {
|
|
||||||
readonly transform: Transform<DraftApi>
|
|
||||||
readonly rebuild: Rebuild
|
|
||||||
}
|
|
||||||
|
|
||||||
const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", {
|
|
||||||
defaultValue: () => undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const current = yield* CurrentBatch
|
|
||||||
if (current) return yield* effect
|
|
||||||
const rebuilds = new Set<Rebuild>()
|
|
||||||
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds))
|
|
||||||
yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true })
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Options<State, DraftApi> {
|
|
||||||
/** Creates the base value for initial state and every scoped-transform rebuild. */
|
/** Creates the base value for initial state and every scoped-transform rebuild. */
|
||||||
readonly initial: () => State
|
readonly initial: () => State
|
||||||
/** Wraps mutable state in a domain-specific draft API. */
|
/** Wraps the mutable draft in a domain-specific editor. */
|
||||||
readonly draft: MakeDraft<State, DraftApi>
|
readonly editor: MakeEditor<State, Editor>
|
||||||
/** Runs after all active transforms and before the rebuilt state becomes visible. */
|
/**
|
||||||
readonly finalize?: (draft: DraftApi) => Effect.Effect<void>
|
* Completes every committed edit.
|
||||||
|
*
|
||||||
|
* For rebuilds, this runs after all active transforms have been replayed and
|
||||||
|
* before the rebuilt state becomes visible. For direct updates, this runs
|
||||||
|
* after the current state has already been edited. The optional reason is
|
||||||
|
* caller-defined metadata for exceptional update origins.
|
||||||
|
*/
|
||||||
|
readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
|
export interface Interface<State extends Objectish, Editor> {
|
||||||
readonly get: () => State
|
readonly get: () => State
|
||||||
/**
|
/**
|
||||||
* Registers and applies a scoped transform. Closing the owning Scope removes
|
* Registers a scoped transform slot and returns the slot updater.
|
||||||
* the transform and rebuilds the materialized state.
|
*
|
||||||
|
* Acquiring the slot has no visible effect until the returned updater is
|
||||||
|
* called. Each updater call replaces that slot's transform, then rebuilds the
|
||||||
|
* materialized state from `initial()` by replaying all active transforms in
|
||||||
|
* registration order. Closing the owning Scope removes the slot and rebuilds.
|
||||||
*/
|
*/
|
||||||
|
readonly transform: () => Effect.Effect<(transform: Transform<Editor>) => Effect.Effect<void>, never, Scope.Scope>
|
||||||
|
/** Registers and applies a replayable transform in the current Scope. */
|
||||||
|
readonly update: (update: Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
|
||||||
|
/**
|
||||||
|
* Mutates the current materialized state directly, once.
|
||||||
|
*
|
||||||
|
* This is not replayable transform state: a later rebuild starts again
|
||||||
|
* from `initial()` plus active transforms, so direct edits must be reserved
|
||||||
|
* for current-state adjustments that are intentionally outside the transform
|
||||||
|
* fold.
|
||||||
|
*/
|
||||||
|
readonly mutate: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function create<State, DraftApi>(options: Options<State, DraftApi>): Interface<State, DraftApi> {
|
export function create<State extends Objectish, Editor>(options: Options<State, Editor>): Interface<State, Editor> {
|
||||||
let state = options.initial()
|
let state = options.initial()
|
||||||
let transforms: { run: TransformCallback<DraftApi> }[] = []
|
let transforms: { update: Transform<Editor> }[] = []
|
||||||
const semaphore = Semaphore.makeUnsafe(1)
|
const semaphore = Semaphore.makeUnsafe(1)
|
||||||
|
|
||||||
const commit = Effect.fn("State.commit")(function* (next: State) {
|
const commit = Effect.fn("State.commit")(function* (next: State, reason?: string) {
|
||||||
const api = options.draft(next)
|
const api = options.editor(next as Draft<State>)
|
||||||
if (options.finalize) yield* options.finalize(api)
|
if (options.finalize) yield* options.finalize(api, reason)
|
||||||
state = next
|
state = next
|
||||||
})
|
})
|
||||||
|
|
||||||
const apply = (transform: TransformCallback<DraftApi>, draft: DraftApi) =>
|
const rebuild = Effect.fnUntraced(function* () {
|
||||||
Effect.suspend(() => {
|
|
||||||
const result = transform(draft)
|
|
||||||
return Effect.isEffect(result) ? Effect.asVoid(result).pipe(Effect.orDie) : Effect.void
|
|
||||||
})
|
|
||||||
|
|
||||||
const materialize = Effect.fnUntraced(function* () {
|
|
||||||
const next = options.initial()
|
const next = options.initial()
|
||||||
const api = options.draft(next)
|
const api = options.editor(next as Draft<State>)
|
||||||
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.rebuild.update"))
|
for (const transform of transforms)
|
||||||
|
yield* Effect.sync(() => transform.update(api)).pipe(Effect.withSpan("State.rebuild.update", {}))
|
||||||
yield* commit(next)
|
yield* commit(next)
|
||||||
})
|
})
|
||||||
|
|
||||||
const rebuild = () => semaphore.withPermit(materialize())
|
const result: Interface<State, Editor> = {
|
||||||
|
|
||||||
const result: Interface<State, DraftApi> = {
|
|
||||||
get: () => state,
|
get: () => state,
|
||||||
transform: Effect.fn("State.transform")(function* (update) {
|
transform: Effect.fn("State.transform")(function* () {
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
return yield* Effect.uninterruptible(
|
return yield* Effect.uninterruptible(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const transform = { run: update }
|
const transform = { update: (_editor: Editor) => {} }
|
||||||
let active = true
|
transforms = [...transforms, transform]
|
||||||
const dispose = Effect.uninterruptible(
|
yield* Scope.addFinalizer(
|
||||||
|
scope,
|
||||||
semaphore.withPermit(
|
semaphore.withPermit(
|
||||||
Effect.suspend(() => {
|
Effect.sync(() => {
|
||||||
if (!active) return Effect.void
|
|
||||||
active = false
|
|
||||||
transforms = transforms.filter((item) => item !== transform)
|
transforms = transforms.filter((item) => item !== transform)
|
||||||
return Effect.gen(function* () {
|
}).pipe(Effect.andThen(rebuild())),
|
||||||
const batch = yield* CurrentBatch
|
|
||||||
if (batch) {
|
|
||||||
batch.add(rebuild)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
yield* materialize()
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
yield* semaphore.withPermit(
|
return (update: Transform<Editor>) =>
|
||||||
Effect.sync(() => {
|
Effect.uninterruptible(
|
||||||
transforms = [...transforms, transform]
|
semaphore.withPermit(
|
||||||
}),
|
Effect.sync(() => {
|
||||||
)
|
transform.update = update
|
||||||
yield* Scope.addFinalizer(scope, dispose)
|
}).pipe(Effect.andThen(rebuild())),
|
||||||
const batch = yield* CurrentBatch
|
),
|
||||||
if (batch) batch.add(rebuild)
|
)
|
||||||
else yield* rebuild()
|
|
||||||
return { dispose }
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
rebuild,
|
update: Effect.fn("State.update")(function* (update) {
|
||||||
|
const transform = yield* result.transform()
|
||||||
|
yield* transform(update)
|
||||||
|
}),
|
||||||
|
mutate: Effect.fn("State.mutate")(function* (update, reason) {
|
||||||
|
const api = options.editor(state as Draft<State>)
|
||||||
|
yield* update(api)
|
||||||
|
if (options.finalize) yield* options.finalize(api, reason)
|
||||||
|
}, semaphore.withPermit),
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * as ApplicationTools from "./application-tools"
|
export * as ApplicationTools from "./application-tools"
|
||||||
|
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
|
import { enableMapSet } from "immer"
|
||||||
import { State } from "../state"
|
import { State } from "../state"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ type Data = {
|
|||||||
readonly entries: Map<string, Entry>
|
readonly entries: Map<string, Entry>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Draft = {
|
type Editor = {
|
||||||
readonly set: (name: string, entry: Entry) => void
|
readonly set: (name: string, entry: Entry) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,12 +27,14 @@ export interface Interface {
|
|||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ApplicationTools") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/ApplicationTools") {}
|
||||||
|
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Editor>({
|
||||||
initial: () => ({ entries: new Map() }),
|
initial: () => ({ entries: new Map() }),
|
||||||
draft: (draft) => ({
|
editor: (draft) => ({
|
||||||
set: (name, tool) => {
|
set: (name, tool) => {
|
||||||
draft.entries.set(name, tool)
|
draft.entries.set(name, tool)
|
||||||
},
|
},
|
||||||
@@ -44,8 +47,9 @@ export const layer = Layer.effect(
|
|||||||
if (entries.length === 0) return
|
if (entries.length === 0) return
|
||||||
yield* Effect.forEach(entries, ([name]) => Tool.validateName(name), { discard: true })
|
yield* Effect.forEach(entries, ([name]) => Tool.validateName(name), { discard: true })
|
||||||
const registrations = entries.map(([name, tool]) => [name, { identity: {}, tool }] as const)
|
const registrations = entries.map(([name, tool]) => [name, { identity: {}, tool }] as const)
|
||||||
yield* state.transform((draft) => {
|
const transform = yield* state.transform()
|
||||||
for (const [name, entry] of registrations) draft.set(name, entry)
|
yield* transform((editor) => {
|
||||||
|
for (const [name, entry] of registrations) editor.set(name, entry)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
entries: () => state.get().entries,
|
entries: () => state.get().entries,
|
||||||
|
|||||||
@@ -502,7 +502,7 @@ export type WithParts = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
durable: {
|
sync: {
|
||||||
aggregate: "sessionID",
|
aggregate: "sessionID",
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
|
|||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { agentHost, host } from "./plugin/host"
|
|
||||||
|
|
||||||
const it = testEffect(AgentV2.locationLayer)
|
const it = testEffect(AgentV2.locationLayer)
|
||||||
|
|
||||||
@@ -24,7 +23,9 @@ describe("AgentV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agent = yield* AgentV2.Service
|
const agent = yield* AgentV2.Service
|
||||||
const id = AgentV2.ID.make("reviewer")
|
const id = AgentV2.ID.make("reviewer")
|
||||||
yield* agent.transform((editor) =>
|
const transform = yield* agent.transform()
|
||||||
|
|
||||||
|
yield* transform((editor) =>
|
||||||
editor.update(id, (info) => {
|
editor.update(id, (info) => {
|
||||||
info.description = "Reviews code"
|
info.description = "Reviews code"
|
||||||
info.mode = "subagent"
|
info.mode = "subagent"
|
||||||
@@ -40,17 +41,19 @@ describe("AgentV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agent = yield* AgentV2.Service
|
const agent = yield* AgentV2.Service
|
||||||
const id = AgentV2.ID.make("reviewer")
|
const id = AgentV2.ID.make("reviewer")
|
||||||
let description = "Old description"
|
const transform = yield* agent.transform()
|
||||||
let hidden = true
|
|
||||||
yield* agent.transform((editor) =>
|
yield* transform((editor) =>
|
||||||
editor.update(id, (info) => {
|
editor.update(id, (info) => {
|
||||||
info.description = description
|
info.description = "Old description"
|
||||||
info.hidden = hidden
|
info.hidden = true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* transform((editor) =>
|
||||||
|
editor.update(id, (info) => {
|
||||||
|
info.description = "New description"
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
description = "New description"
|
|
||||||
hidden = false
|
|
||||||
yield* agent.rebuild()
|
|
||||||
|
|
||||||
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
|
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
|
||||||
}),
|
}),
|
||||||
@@ -61,7 +64,9 @@ describe("AgentV2", () => {
|
|||||||
const agent = yield* AgentV2.Service
|
const agent = yield* AgentV2.Service
|
||||||
const id = AgentV2.ID.make("scoped")
|
const id = AgentV2.ID.make("scoped")
|
||||||
const scope = yield* Scope.make()
|
const scope = yield* Scope.make()
|
||||||
yield* agent.transform((editor) => editor.update(id, () => {})).pipe(Scope.provide(scope))
|
const transform = yield* agent.transform().pipe(Scope.provide(scope))
|
||||||
|
|
||||||
|
yield* transform((editor) => editor.update(id, () => {}))
|
||||||
expect(yield* agent.get(id)).toBeDefined()
|
expect(yield* agent.get(id)).toBeDefined()
|
||||||
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
yield* Scope.close(scope, Exit.void)
|
||||||
@@ -74,7 +79,7 @@ describe("AgentV2", () => {
|
|||||||
const agent = yield* AgentV2.Service
|
const agent = yield* AgentV2.Service
|
||||||
const id = AgentV2.ID.make("build")
|
const id = AgentV2.ID.make("build")
|
||||||
|
|
||||||
yield* agent.transform((editor) =>
|
yield* agent.update((editor) =>
|
||||||
editor.update(id, (info) => {
|
editor.update(id, (info) => {
|
||||||
info.mode = "primary"
|
info.mode = "primary"
|
||||||
info.hidden = true
|
info.hidden = true
|
||||||
@@ -90,10 +95,10 @@ describe("AgentV2", () => {
|
|||||||
const agent = yield* AgentV2.Service
|
const agent = yield* AgentV2.Service
|
||||||
const id = AgentV2.ID.make("custom")
|
const id = AgentV2.ID.make("custom")
|
||||||
|
|
||||||
yield* agent.transform((editor) => editor.update(id, () => {}))
|
yield* agent.update((editor) => editor.update(id, () => {}))
|
||||||
expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id))
|
expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id))
|
||||||
|
|
||||||
yield* agent.transform((editor) => editor.remove(id))
|
yield* agent.update((editor) => editor.remove(id))
|
||||||
expect(yield* agent.get(id)).toBeUndefined()
|
expect(yield* agent.get(id)).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -101,11 +106,11 @@ describe("AgentV2", () => {
|
|||||||
it.effect("does not ambiently opt built-in agents into bash", () =>
|
it.effect("does not ambiently opt built-in agents into bash", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agent = yield* AgentV2.Service
|
const agent = yield* AgentV2.Service
|
||||||
yield* AgentPlugin.Plugin.effect(
|
yield* AgentPlugin.Plugin.effect.pipe(
|
||||||
host({
|
Effect.provideService(
|
||||||
agent: agentHost(agent),
|
Location.Service,
|
||||||
location: location({ directory: AbsolutePath.make("/project") }),
|
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const agents = yield* agent.all()
|
const agents = yield* agent.all()
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Fiber, Layer, Stream } from "effect"
|
import { DateTime, Effect, Fiber, Layer, Option, Stream } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { Policy } from "@opencode-ai/core/policy"
|
import { Policy } from "@opencode-ai/core/policy"
|
||||||
|
import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { required } from "./plugin/provider-helper"
|
|
||||||
|
|
||||||
const locationLayer = Layer.succeed(
|
const locationLayer = Layer.succeed(
|
||||||
Location.Service,
|
Location.Service,
|
||||||
@@ -40,7 +41,7 @@ describe("CatalogV2", () => {
|
|||||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
yield* (yield* catalog.transform())((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
||||||
|
|
||||||
expect((yield* Fiber.join(updated)).length).toBe(1)
|
expect((yield* Fiber.join(updated)).length).toBe(1)
|
||||||
}),
|
}),
|
||||||
@@ -75,13 +76,14 @@ describe("CatalogV2", () => {
|
|||||||
|
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
||||||
|
|
||||||
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
|
||||||
active = second
|
active = second
|
||||||
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
|
||||||
}).pipe(Effect.provide(layer))
|
}).pipe(Effect.provide(layer))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -97,13 +99,13 @@ describe("CatalogV2", () => {
|
|||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
yield* integrations.transform((editor) =>
|
yield* integrations.update((editor) =>
|
||||||
editor.method.update({
|
editor.method.update({
|
||||||
integrationID: Integration.ID.make(providerID),
|
integrationID: Integration.ID.make(providerID),
|
||||||
method: { type: "env", names: ["CATALOG_TEST_API_KEY"] },
|
method: { type: "env", names: ["CATALOG_TEST_API_KEY"] },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* catalog.transform((editor) => editor.provider.update(providerID, () => {}))
|
yield* (yield* catalog.transform())((editor) => editor.provider.update(providerID, () => {}))
|
||||||
|
|
||||||
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
|
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
|
||||||
}),
|
}),
|
||||||
@@ -119,7 +121,9 @@ describe("CatalogV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
yield* catalog.transform((catalog) =>
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* transform((catalog) =>
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.api = {
|
provider.api = {
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
@@ -130,7 +134,7 @@ describe("CatalogV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(required(yield* catalog.provider.get(providerID)).api).toEqual({
|
expect((yield* catalog.provider.get(providerID)).api).toEqual({
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
url: "https://override.example.com",
|
url: "https://override.example.com",
|
||||||
@@ -143,7 +147,9 @@ describe("CatalogV2", () => {
|
|||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
const modelID = ModelV2.ID.make("model")
|
const modelID = ModelV2.ID.make("model")
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.api = {
|
provider.api = {
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
@@ -162,7 +168,7 @@ describe("CatalogV2", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
|
expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({
|
||||||
id: modelID,
|
id: modelID,
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
@@ -177,7 +183,9 @@ describe("CatalogV2", () => {
|
|||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
const modelID = ModelV2.ID.make("model")
|
const modelID = ModelV2.ID.make("model")
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.api = {
|
provider.api = {
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
@@ -188,7 +196,7 @@ describe("CatalogV2", () => {
|
|||||||
catalog.model.update(providerID, modelID, () => {})
|
catalog.model.update(providerID, modelID, () => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
|
expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({
|
||||||
id: modelID,
|
id: modelID,
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
@@ -197,12 +205,106 @@ describe("CatalogV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("runs catalog transform hooks after baseURL is normalized", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
|
const providerID = ProviderV2.ID.make("test")
|
||||||
|
const seen: unknown[] = []
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* plugin.add({
|
||||||
|
id: PluginV2.ID.make("test"),
|
||||||
|
effect: Effect.succeed({
|
||||||
|
"catalog.transform": (evt) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const item = evt.provider.get(providerID)
|
||||||
|
if (!item) return
|
||||||
|
seen.push(item.provider.api.type)
|
||||||
|
if (item?.provider.api.type === "aisdk") seen.push(item.provider.api.url)
|
||||||
|
seen.push(item?.provider.request.body.baseURL)
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
yield* transform((catalog) =>
|
||||||
|
catalog.provider.update(providerID, (provider) => {
|
||||||
|
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
||||||
|
provider.request.body.baseURL = "https://provider.example.com"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(seen).toEqual(["aisdk", "https://provider.example.com", undefined])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("runs catalog transform when a plugin is added", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
|
const providerID = ProviderV2.ID.make("test")
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* transform((catalog) =>
|
||||||
|
catalog.provider.update(providerID, (provider) => {
|
||||||
|
provider.name = "Before"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* plugin.add({
|
||||||
|
id: PluginV2.ID.make("test-transform"),
|
||||||
|
effect: Effect.succeed({
|
||||||
|
"catalog.transform": (evt) =>
|
||||||
|
Effect.sync(() =>
|
||||||
|
evt.provider.update(providerID, (provider) => {
|
||||||
|
provider.name = "After"
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
|
expect((yield* catalog.provider.get(providerID)).name).toBe("After")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("ignores plugin additions from another location", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
|
let invoked = 0
|
||||||
|
|
||||||
|
yield* plugin.add({
|
||||||
|
id: PluginV2.ID.make("test-transform"),
|
||||||
|
effect: Effect.succeed({
|
||||||
|
"catalog.transform": () => Effect.sync(() => invoked++),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
yield* Effect.yieldNow
|
||||||
|
expect(invoked).toBe(1)
|
||||||
|
|
||||||
|
yield* events.publish(
|
||||||
|
PluginV2.Event.Added,
|
||||||
|
{ id: PluginV2.ID.make("test-transform") },
|
||||||
|
{
|
||||||
|
location: new Location.Info({
|
||||||
|
directory: AbsolutePath.make("other"),
|
||||||
|
project: { id: Project.ID.global, directory: AbsolutePath.make("other") },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
|
expect(invoked).toBe(1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("resolves provider and model request merges", () =>
|
it.effect("resolves provider and model request merges", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
const modelID = ModelV2.ID.make("model")
|
const modelID = ModelV2.ID.make("model")
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.request.headers.provider = "provider"
|
provider.request.headers.provider = "provider"
|
||||||
provider.request.headers.shared = "provider"
|
provider.request.headers.shared = "provider"
|
||||||
@@ -219,7 +321,7 @@ describe("CatalogV2", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
const model = yield* catalog.model.get(providerID, modelID)
|
||||||
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
|
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
|
||||||
expect(model.request.body).toEqual({ provider: true, model: true, request: true })
|
expect(model.request.body).toEqual({ provider: true, model: true, request: true })
|
||||||
expect(model.request.options).toEqual({ shared: "model", model: true })
|
expect(model.request.options).toEqual({ shared: "model", model: true })
|
||||||
@@ -230,17 +332,19 @@ describe("CatalogV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(providerID, () => {})
|
catalog.provider.update(providerID, () => {})
|
||||||
catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => {
|
catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => {
|
||||||
model.time.released = 1000
|
model.time.released = DateTime.makeUnsafe(1000)
|
||||||
})
|
})
|
||||||
catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => {
|
catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => {
|
||||||
model.time.released = 2000
|
model.time.released = DateTime.makeUnsafe(2000)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect((yield* catalog.model.default())?.id).toMatch("new")
|
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toMatch("new")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -250,26 +354,26 @@ describe("CatalogV2", () => {
|
|||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
const old = ModelV2.ID.make("old")
|
const old = ModelV2.ID.make("old")
|
||||||
const newest = ModelV2.ID.make("new")
|
const newest = ModelV2.ID.make("new")
|
||||||
const models = (catalog: Catalog.Draft) => {
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
const models = (catalog: Catalog.Editor) => {
|
||||||
catalog.provider.update(providerID, () => {})
|
catalog.provider.update(providerID, () => {})
|
||||||
catalog.model.update(providerID, old, (model) => {
|
catalog.model.update(providerID, old, (model) => {
|
||||||
model.time.released = 1000
|
model.time.released = DateTime.makeUnsafe(1000)
|
||||||
})
|
})
|
||||||
catalog.model.update(providerID, newest, (model) => {
|
catalog.model.update(providerID, newest, (model) => {
|
||||||
model.time.released = 2000
|
model.time.released = DateTime.makeUnsafe(2000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let configured = true
|
yield* transform((catalog) => {
|
||||||
yield* catalog.transform((catalog) => {
|
|
||||||
models(catalog)
|
models(catalog)
|
||||||
if (configured) catalog.model.default.set(providerID, old)
|
catalog.model.default.set(providerID, old)
|
||||||
})
|
})
|
||||||
expect((yield* catalog.model.default())?.id).toBe(old)
|
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(old)
|
||||||
|
|
||||||
configured = false
|
yield* transform(models)
|
||||||
yield* catalog.rebuild()
|
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(newest)
|
||||||
expect((yield* catalog.model.default())?.id).toBe(newest)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -280,7 +384,9 @@ describe("CatalogV2", () => {
|
|||||||
const enabledProvider = ProviderV2.ID.make("enabled")
|
const enabledProvider = ProviderV2.ID.make("enabled")
|
||||||
const disabledModel = ModelV2.ID.make("configured")
|
const disabledModel = ModelV2.ID.make("configured")
|
||||||
const fallbackModel = ModelV2.ID.make("fallback")
|
const fallbackModel = ModelV2.ID.make("fallback")
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(disabledProvider, (provider) => {
|
catalog.provider.update(disabledProvider, (provider) => {
|
||||||
provider.disabled = true
|
provider.disabled = true
|
||||||
})
|
})
|
||||||
@@ -290,7 +396,7 @@ describe("CatalogV2", () => {
|
|||||||
catalog.model.default.set(disabledProvider, disabledModel)
|
catalog.model.default.set(disabledProvider, disabledModel)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(yield* catalog.model.default()).toMatchObject({
|
expect(Option.getOrUndefined(yield* catalog.model.default())).toMatchObject({
|
||||||
providerID: enabledProvider,
|
providerID: enabledProvider,
|
||||||
id: fallbackModel,
|
id: fallbackModel,
|
||||||
})
|
})
|
||||||
@@ -301,23 +407,25 @@ describe("CatalogV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(providerID, () => {})
|
catalog.provider.update(providerID, () => {})
|
||||||
catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
|
catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
|
||||||
model.capabilities.input = ["text"]
|
model.capabilities.input = ["text"]
|
||||||
model.capabilities.output = ["text"]
|
model.capabilities.output = ["text"]
|
||||||
model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }]
|
model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }]
|
||||||
model.time.released = Date.now()
|
model.time.released = DateTime.makeUnsafe(Date.now())
|
||||||
})
|
})
|
||||||
catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => {
|
catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => {
|
||||||
model.capabilities.input = ["text"]
|
model.capabilities.input = ["text"]
|
||||||
model.capabilities.output = ["text"]
|
model.capabilities.output = ["text"]
|
||||||
model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }]
|
model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }]
|
||||||
model.time.released = Date.now()
|
model.time.released = DateTime.makeUnsafe(Date.now())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect((yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
|
expect(Option.getOrUndefined(yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -326,15 +434,17 @@ describe("CatalogV2", () => {
|
|||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const policy = yield* Policy.Service
|
const policy = yield* Policy.Service
|
||||||
const providerID = ProviderV2.ID.make("blocked")
|
const providerID = ProviderV2.ID.make("blocked")
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
|
||||||
yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })])
|
yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })])
|
||||||
yield* catalog.transform((catalog) => {
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(providerID, () => {})
|
catalog.provider.update(providerID, () => {})
|
||||||
catalog.model.update(providerID, ModelV2.ID.make("model"), () => {})
|
catalog.model.update(providerID, ModelV2.ID.make("model"), () => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(yield* catalog.provider.all()).toEqual([])
|
expect(yield* catalog.provider.all()).toEqual([])
|
||||||
expect(yield* catalog.model.all()).toEqual([])
|
expect(yield* catalog.model.all()).toEqual([])
|
||||||
expect(yield* catalog.provider.get(providerID)).toBeUndefined()
|
expect(yield* catalog.provider.get(providerID).pipe(Effect.option)).toEqual(Option.none())
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ describe("CommandV2", () => {
|
|||||||
it.effect("applies command transforms and preserves later overrides", () =>
|
it.effect("applies command transforms and preserves later overrides", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const command = yield* CommandV2.Service
|
const command = yield* CommandV2.Service
|
||||||
yield* command.transform((editor) => {
|
const transform = yield* command.transform()
|
||||||
|
yield* transform((editor) => {
|
||||||
editor.update("review", (command) => {
|
editor.update("review", (command) => {
|
||||||
command.template = "First"
|
command.template = "First"
|
||||||
command.description = "Review code"
|
command.description = "Review code"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { PermissionV2 } from "@opencode-ai/core/permission"
|
|||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { tmpdir } from "../fixture/tmpdir"
|
import { tmpdir } from "../fixture/tmpdir"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { agentHost, host } from "../plugin/host"
|
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer))
|
const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer))
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
@@ -20,7 +19,9 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
const build = AgentV2.ID.make("build")
|
const build = AgentV2.ID.make("build")
|
||||||
yield* agents.transform((editor) =>
|
const defaults = yield* agents.transform()
|
||||||
|
|
||||||
|
yield* defaults((editor) =>
|
||||||
editor.update(build, (agent) => {
|
editor.update(build, (agent) => {
|
||||||
agent.mode = "primary"
|
agent.mode = "primary"
|
||||||
agent.permissions.push({ action: "bash", resource: "*", effect: "allow" })
|
agent.permissions.push({ action: "bash", resource: "*", effect: "allow" })
|
||||||
@@ -67,8 +68,9 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
yield* ConfigAgentPlugin.Plugin.effect.pipe(
|
||||||
Effect.provideService(Config.Service, config),
|
Effect.provideService(Config.Service, config),
|
||||||
|
Effect.provideService(AgentV2.Service, agents),
|
||||||
)
|
)
|
||||||
|
|
||||||
const buildAgent = yield* agents.get(build)
|
const buildAgent = yield* agents.get(build)
|
||||||
@@ -148,8 +150,9 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
yield* ConfigAgentPlugin.Plugin.effect.pipe(
|
||||||
Effect.provideService(Config.Service, config),
|
Effect.provideService(Config.Service, config),
|
||||||
|
Effect.provideService(AgentV2.Service, agents),
|
||||||
)
|
)
|
||||||
|
|
||||||
const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
|
const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
|
||||||
@@ -174,7 +177,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
const build = AgentV2.ID.make("build")
|
const build = AgentV2.ID.make("build")
|
||||||
yield* agents.transform((editor) => editor.update(build, () => {}))
|
const defaults = yield* agents.transform()
|
||||||
|
yield* defaults((editor) => editor.update(build, () => {}))
|
||||||
|
|
||||||
const config = Config.Service.of({
|
const config = Config.Service.of({
|
||||||
entries: () =>
|
entries: () =>
|
||||||
@@ -186,8 +190,9 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
yield* ConfigAgentPlugin.Plugin.effect.pipe(
|
||||||
Effect.provideService(Config.Service, config),
|
Effect.provideService(Config.Service, config),
|
||||||
|
Effect.provideService(AgentV2.Service, agents),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* agents.get(build)).toBeUndefined()
|
expect(yield* agents.get(build)).toBeUndefined()
|
||||||
@@ -246,8 +251,9 @@ Use native v2 fields.`,
|
|||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
yield* ConfigAgentPlugin.Plugin.effect.pipe(
|
||||||
Effect.provideService(Config.Service, config),
|
Effect.provideService(Config.Service, config),
|
||||||
|
Effect.provideService(AgentV2.Service, agents),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
|
|||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { tmpdir } from "../fixture/tmpdir"
|
import { tmpdir } from "../fixture/tmpdir"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { host } from "../plugin/host"
|
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer))
|
const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer))
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
@@ -42,7 +41,8 @@ Review files`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
const command = yield* CommandV2.Service
|
const command = yield* CommandV2.Service
|
||||||
yield* ConfigCommandPlugin.Plugin.effect(host({ command })).pipe(
|
yield* ConfigCommandPlugin.Plugin.effect.pipe(
|
||||||
|
Effect.provideService(CommandV2.Service, command),
|
||||||
Effect.provideService(
|
Effect.provideService(
|
||||||
Config.Service,
|
Config.Service,
|
||||||
Config.Service.of({
|
Config.Service.of({
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { Integration } from "@opencode-ai/core/integration"
|
|||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { it, required, withEnv } from "../plugin/provider-helper"
|
import { it, withEnv } from "../plugin/provider-helper"
|
||||||
import { catalogHost, host, integrationHost } from "../plugin/host"
|
|
||||||
|
|
||||||
function request(headers: Record<string, string>, variant?: string) {
|
function request(headers: Record<string, string>, variant?: string) {
|
||||||
return {
|
return {
|
||||||
@@ -59,12 +58,14 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||||||
|
|
||||||
yield* plugin.add({
|
yield* plugin.add({
|
||||||
...ConfigProviderPlugin.Plugin,
|
...ConfigProviderPlugin.Plugin,
|
||||||
effect: ConfigProviderPlugin.Plugin.effect(
|
effect: ConfigProviderPlugin.Plugin.effect.pipe(
|
||||||
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
|
Effect.provideService(Config.Service, config),
|
||||||
).pipe(Effect.provideService(Config.Service, config)),
|
Effect.provideService(Catalog.Service, catalog),
|
||||||
|
Effect.provideService(Integration.Service, integrations),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
const model = yield* catalog.model.get(providerID, modelID)
|
||||||
expect(model.variants).toMatchObject([
|
expect(model.variants).toMatchObject([
|
||||||
{
|
{
|
||||||
id: "high",
|
id: "high",
|
||||||
@@ -118,12 +119,14 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||||||
|
|
||||||
yield* plugin.add({
|
yield* plugin.add({
|
||||||
...ConfigProviderPlugin.Plugin,
|
...ConfigProviderPlugin.Plugin,
|
||||||
effect: ConfigProviderPlugin.Plugin.effect(
|
effect: ConfigProviderPlugin.Plugin.effect.pipe(
|
||||||
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
|
Effect.provideService(Config.Service, config),
|
||||||
).pipe(Effect.provideService(Config.Service, config)),
|
Effect.provideService(Catalog.Service, catalog),
|
||||||
|
Effect.provideService(Integration.Service, integrations),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
const model = yield* catalog.model.get(providerID, modelID)
|
||||||
expect(model.variants[0]).toMatchObject({
|
expect(model.variants[0]).toMatchObject({
|
||||||
id: "high",
|
id: "high",
|
||||||
body: {},
|
body: {},
|
||||||
@@ -219,14 +222,16 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||||||
|
|
||||||
yield* plugin.add({
|
yield* plugin.add({
|
||||||
...ConfigProviderPlugin.Plugin,
|
...ConfigProviderPlugin.Plugin,
|
||||||
effect: ConfigProviderPlugin.Plugin.effect(
|
effect: ConfigProviderPlugin.Plugin.effect.pipe(
|
||||||
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
|
Effect.provideService(Config.Service, config),
|
||||||
).pipe(Effect.provideService(Config.Service, config)),
|
Effect.provideService(Catalog.Service, catalog),
|
||||||
|
Effect.provideService(Integration.Service, integrations),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const provider = required(yield* catalog.provider.get(providerID))
|
const provider = yield* catalog.provider.get(providerID)
|
||||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
const model = yield* catalog.model.get(providerID, modelID)
|
||||||
expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
|
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
|
||||||
expect(provider.name).toBe("Renamed")
|
expect(provider.name).toBe("Renamed")
|
||||||
expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({
|
expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({
|
||||||
type: "env",
|
type: "env",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
|||||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||||
import { location } from "../fixture/location"
|
import { location } from "../fixture/location"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { host } from "../plugin/host"
|
|
||||||
|
|
||||||
const it = testEffect(Layer.empty)
|
const it = testEffect(Layer.empty)
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
@@ -19,33 +18,16 @@ describe("ConfigSkillPlugin.Plugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const directory = AbsolutePath.make("/repo/packages/app")
|
const directory = AbsolutePath.make("/repo/packages/app")
|
||||||
const sources: SkillV2.Source[] = []
|
const sources: SkillV2.Source[] = []
|
||||||
const transform = Effect.fnUntraced(function* (update: (draft: SkillV2.Draft) => void | Effect.Effect<void>) {
|
const transform = Effect.fnUntraced(function* () {
|
||||||
const result = update({
|
return Effect.fnUntraced(function* (update: (editor: SkillV2.Editor) => void) {
|
||||||
source: (source) => {
|
update({
|
||||||
sources.push(source)
|
source: (source) => sources.push(source),
|
||||||
},
|
list: () => sources,
|
||||||
list: () => sources,
|
})
|
||||||
})
|
})
|
||||||
if (Effect.isEffect(result)) yield* result
|
|
||||||
const dispose = Effect.sync(() => {
|
|
||||||
sources.length = 0
|
|
||||||
})
|
|
||||||
yield* Effect.addFinalizer(() => dispose)
|
|
||||||
return { dispose }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* ConfigSkillPlugin.Plugin.effect(
|
yield* ConfigSkillPlugin.Plugin.effect.pipe(
|
||||||
host({
|
|
||||||
location: location({ directory }),
|
|
||||||
path: { ...host().path, home: "/home/test" },
|
|
||||||
skill: SkillV2.Service.of({
|
|
||||||
transform,
|
|
||||||
rebuild: () => Effect.void,
|
|
||||||
sources: () => Effect.succeed(sources),
|
|
||||||
list: () => Effect.succeed([]),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
).pipe(
|
|
||||||
Effect.provideService(
|
Effect.provideService(
|
||||||
Config.Service,
|
Config.Service,
|
||||||
Config.Service.of({
|
Config.Service.of({
|
||||||
@@ -61,6 +43,16 @@ describe("ConfigSkillPlugin.Plugin", () => {
|
|||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
Effect.provideService(Global.Service, Global.Service.of(Global.make({ home: "/home/test" }))),
|
||||||
|
Effect.provideService(Location.Service, Location.Service.of(location({ directory }))),
|
||||||
|
Effect.provideService(
|
||||||
|
SkillV2.Service,
|
||||||
|
SkillV2.Service.of({
|
||||||
|
transform,
|
||||||
|
sources: () => Effect.succeed(sources),
|
||||||
|
list: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(sources).toEqual([
|
expect(sources).toEqual([
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const Message = EventV2.define({
|
|||||||
|
|
||||||
const SyncMessage = EventV2.define({
|
const SyncMessage = EventV2.define({
|
||||||
type: "test.sync",
|
type: "test.sync",
|
||||||
durable: {
|
sync: {
|
||||||
version: 1,
|
version: 1,
|
||||||
aggregate: "id",
|
aggregate: "id",
|
||||||
},
|
},
|
||||||
@@ -42,7 +42,7 @@ const SyncMessage = EventV2.define({
|
|||||||
|
|
||||||
const SyncSent = EventV2.define({
|
const SyncSent = EventV2.define({
|
||||||
type: "test.sent",
|
type: "test.sent",
|
||||||
durable: {
|
sync: {
|
||||||
version: 1,
|
version: 1,
|
||||||
aggregate: "messageID",
|
aggregate: "messageID",
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@ const GlobalMessage = EventV2.define({
|
|||||||
|
|
||||||
const VersionedMessage = EventV2.define({
|
const VersionedMessage = EventV2.define({
|
||||||
type: "test.versioned",
|
type: "test.versioned",
|
||||||
durable: {
|
sync: {
|
||||||
version: 2,
|
version: 2,
|
||||||
aggregate: "id",
|
aggregate: "id",
|
||||||
},
|
},
|
||||||
@@ -73,7 +73,7 @@ const VersionedMessage = EventV2.define({
|
|||||||
|
|
||||||
const SyncTimestamp = EventV2.define({
|
const SyncTimestamp = EventV2.define({
|
||||||
type: "test.timestamp",
|
type: "test.timestamp",
|
||||||
durable: {
|
sync: {
|
||||||
version: 1,
|
version: 1,
|
||||||
aggregate: "id",
|
aggregate: "id",
|
||||||
},
|
},
|
||||||
@@ -132,7 +132,7 @@ describe("EventV2", () => {
|
|||||||
const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" })
|
const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" })
|
||||||
|
|
||||||
expect(event.type).toBe("test.versioned")
|
expect(event.type).toBe("test.versioned")
|
||||||
expect(event.durable?.version).toBe(2)
|
expect(event.version).toBe(2)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -146,12 +146,12 @@ describe("EventV2", () => {
|
|||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const latest = EventV2.define({
|
const latest = EventV2.define({
|
||||||
type: "test.out-of-order",
|
type: "test.out-of-order",
|
||||||
durable: { version: 2, aggregate: "id" },
|
sync: { version: 2, aggregate: "id" },
|
||||||
schema: { id: Schema.String },
|
schema: { id: Schema.String },
|
||||||
})
|
})
|
||||||
EventV2.define({
|
EventV2.define({
|
||||||
type: "test.out-of-order",
|
type: "test.out-of-order",
|
||||||
durable: { version: 1, aggregate: "id" },
|
sync: { version: 1, aggregate: "id" },
|
||||||
schema: { id: Schema.String },
|
schema: { id: Schema.String },
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ describe("EventV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("commits local operational state inside a new durable event transaction", () =>
|
it.effect("commits local operational state inside a new synchronized event transaction", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const received = new Array<string>()
|
const received = new Array<string>()
|
||||||
@@ -207,7 +207,7 @@ describe("EventV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("rolls back the durable event and projector when the local commit fails", () =>
|
it.effect("rolls back the synchronized event and projector when the local commit fails", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
@@ -236,7 +236,7 @@ describe("EventV2", () => {
|
|||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const exit = yield* events.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit)
|
const exit = yield* events.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit)
|
||||||
|
|
||||||
expect(String(exit)).toContain("Local commit hooks require a durable event")
|
expect(String(exit)).toContain("Local commit hooks require a synchronized event")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -290,6 +290,7 @@ describe("EventV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const received = new Array<string>()
|
const received = new Array<string>()
|
||||||
|
yield* events.sync(() => Effect.die("sync defect"))
|
||||||
yield* events.listen(() => {
|
yield* events.listen(() => {
|
||||||
throw new Error("listener defect")
|
throw new Error("listener defect")
|
||||||
})
|
})
|
||||||
@@ -302,7 +303,7 @@ describe("EventV2", () => {
|
|||||||
const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" })
|
const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" })
|
||||||
|
|
||||||
expect(received).toEqual([SyncMessage.type])
|
expect(received).toEqual([SyncMessage.type])
|
||||||
expect(event.durable?.seq).toBeNumber()
|
expect(event.seq).toBeNumber()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -335,7 +336,49 @@ describe("EventV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("inserts durable event rows on publish", () =>
|
it.effect("does not synchronize live-only events", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const synchronized = new Array<string>()
|
||||||
|
const unsubscribe = yield* events.sync((event) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
synchronized.push(event.type)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* Effect.addFinalizer(() => unsubscribe)
|
||||||
|
|
||||||
|
yield* events.publish(Message, { text: "live only" })
|
||||||
|
yield* events.publish(SyncMessage, { id: "one", text: "durable" })
|
||||||
|
|
||||||
|
expect(synchronized).toEqual([SyncMessage.type])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("synchronizes only after the durable event commits", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const { db } = yield* Database.Service
|
||||||
|
const synchronized = new Array<boolean>()
|
||||||
|
yield* events.sync((event) =>
|
||||||
|
db
|
||||||
|
.select({ id: EventTable.id })
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.id, event.id))
|
||||||
|
.get()
|
||||||
|
.pipe(
|
||||||
|
Effect.orDie,
|
||||||
|
Effect.map((row) => synchronized.push(row !== undefined)),
|
||||||
|
Effect.asVoid,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* events.publish(SyncMessage, { id: EventV2.ID.create(), text: "durable" })
|
||||||
|
|
||||||
|
expect(synchronized).toEqual([true])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("inserts sync event rows on publish", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
@@ -355,7 +398,7 @@ describe("EventV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("increments durable event seq per aggregate", () =>
|
it.effect("increments sync event seq per aggregate", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
@@ -374,22 +417,22 @@ describe("EventV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("replays durable aggregate events after a sequence and tails new events", () =>
|
it.effect("replays durable aggregate events after a cursor and tails new events", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
||||||
const fiber = yield* events
|
const fiber = yield* events
|
||||||
.durable({ aggregateID, after: 0 })
|
.aggregateEvents({ aggregateID, after: EventV2.Cursor.make(0) })
|
||||||
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "two" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "two" })
|
||||||
|
|
||||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
|
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual([
|
||||||
[1, { id: aggregateID, text: "one" }],
|
[EventV2.Cursor.make(1), { id: aggregateID, text: "one" }],
|
||||||
[2, { id: aggregateID, text: "two" }],
|
[EventV2.Cursor.make(2), { id: aggregateID, text: "two" }],
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -399,18 +442,20 @@ describe("EventV2", () => {
|
|||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
||||||
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* events
|
||||||
|
.aggregateEvents({ aggregateID })
|
||||||
|
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
||||||
|
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
Array.from(yield* Fiber.join(fiber)).map((event) => [
|
Array.from(yield* Fiber.join(fiber)).map((event) => [
|
||||||
event.durable?.seq,
|
event.cursor,
|
||||||
(event.data as { text: string }).text,
|
(event.event.data as { text: string }).text,
|
||||||
]),
|
]),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
[0, "zero"],
|
[EventV2.Cursor.make(0), "zero"],
|
||||||
[1, "one"],
|
[EventV2.Cursor.make(1), "one"],
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -431,15 +476,17 @@ describe("EventV2", () => {
|
|||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* events
|
||||||
|
.aggregateEvents({ aggregateID })
|
||||||
|
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Deferred.await(readStarted)
|
yield* Deferred.await(readStarted)
|
||||||
|
|
||||||
pause = false
|
pause = false
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "during handoff" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "during handoff" })
|
||||||
yield* Deferred.succeed(continueRead, undefined)
|
yield* Deferred.succeed(continueRead, undefined)
|
||||||
|
|
||||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
|
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual([
|
||||||
[0, { id: aggregateID, text: "during handoff" }],
|
[EventV2.Cursor.make(0), { id: aggregateID, text: "during handoff" }],
|
||||||
])
|
])
|
||||||
}).pipe(Effect.provide(Layer.mergeAll(database, eventLayer)))
|
}).pipe(Effect.provide(Layer.mergeAll(database, eventLayer)))
|
||||||
}),
|
}),
|
||||||
@@ -451,7 +498,7 @@ describe("EventV2", () => {
|
|||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
const count = 64
|
const count = 64
|
||||||
const fiber = yield* events
|
const fiber = yield* events
|
||||||
.durable({ aggregateID })
|
.aggregateEvents({ aggregateID })
|
||||||
.pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
|
.pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
@@ -459,8 +506,11 @@ describe("EventV2", () => {
|
|||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: String(index) })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: String(index) })
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual(
|
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual(
|
||||||
Array.from({ length: count }, (_, index) => [index, { id: aggregateID, text: String(index) }]),
|
Array.from({ length: count }, (_, index) => [
|
||||||
|
EventV2.Cursor.make(index),
|
||||||
|
{ id: aggregateID, text: String(index) },
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -469,13 +519,15 @@ describe("EventV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* events
|
||||||
|
.aggregateEvents({ aggregateID })
|
||||||
|
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
yield* events.publish(Message, { text: "live only" })
|
yield* events.publish(Message, { text: "live only" })
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "durable" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "durable" })
|
||||||
|
|
||||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([SyncMessage.type])
|
expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.event.type)).toEqual([SyncMessage.type])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -498,7 +550,7 @@ describe("EventV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("replays durable events through projectors", () =>
|
it.effect("replays sync events through projectors", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const received = new Array<EventV2.Payload>()
|
const received = new Array<EventV2.Payload>()
|
||||||
@@ -654,7 +706,7 @@ describe("EventV2", () => {
|
|||||||
})
|
})
|
||||||
.pipe(Effect.exit)
|
.pipe(Effect.exit)
|
||||||
|
|
||||||
expect(String(exit)).toContain("Unknown durable event type")
|
expect(String(exit)).toContain("Unknown sync event type")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -791,7 +843,7 @@ describe("EventV2", () => {
|
|||||||
const replayed = {
|
const replayed = {
|
||||||
id: published.id,
|
id: published.id,
|
||||||
type: EventV2.versionedType(SyncMessage.type, 1),
|
type: EventV2.versionedType(SyncMessage.type, 1),
|
||||||
seq: published.durable!.seq,
|
seq: published.seq!,
|
||||||
aggregateID,
|
aggregateID,
|
||||||
data: published.data,
|
data: published.data,
|
||||||
}
|
}
|
||||||
@@ -936,7 +988,7 @@ describe("EventV2", () => {
|
|||||||
yield* events.replay(replayed, { publish: true })
|
yield* events.replay(replayed, { publish: true })
|
||||||
yield* events.replay(replayed, { publish: true })
|
yield* events.replay(replayed, { publish: true })
|
||||||
|
|
||||||
expect(received).toMatchObject([{ id: replayed.id, durable: { seq: 0, version: 1 }, data: replayed.data }])
|
expect(received).toMatchObject([{ id: replayed.id, seq: 0, data: replayed.data }])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1058,7 +1110,7 @@ describe("EventV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("remove clears durable event sequence", () =>
|
it.effect("remove clears sync event sequence", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const received = new Array<EventV2.Payload>()
|
const received = new Array<EventV2.Payload>()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import fs from "fs/promises"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { ConfigProvider, Deferred, Duration, Effect, Fiber, Layer, Option, Stream } from "effect"
|
import { ConfigProvider, Deferred, Duration, Effect, Fiber, Layer, Option, Stream } from "effect"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
|
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
|
||||||
@@ -18,7 +19,8 @@ const describeWatcher = Watcher.hasNativeBinding() && !process.env.CI ? describe
|
|||||||
|
|
||||||
type WatcherEvent = { file: string; event: "add" | "change" | "unlink" }
|
type WatcherEvent = { file: string; event: "add" | "change" | "unlink" }
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(FSUtil.defaultLayer, EventV2.defaultLayer))
|
const root = LayerNode.group([FSUtil.node, EventV2.node])
|
||||||
|
const it = testEffect(LayerNode.buildLayer(root))
|
||||||
|
|
||||||
const configLayer = Layer.succeed(
|
const configLayer = Layer.succeed(
|
||||||
Config.Service,
|
Config.Service,
|
||||||
@@ -26,6 +28,7 @@ const configLayer = Layer.succeed(
|
|||||||
entries: () => Effect.succeed([]),
|
entries: () => Effect.succeed([]),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
const configNode = LayerNode.make(configLayer, [])
|
||||||
|
|
||||||
const flagsLayer = ConfigProvider.layer(
|
const flagsLayer = ConfigProvider.layer(
|
||||||
ConfigProvider.fromUnknown({
|
ConfigProvider.fromUnknown({
|
||||||
@@ -35,15 +38,21 @@ const flagsLayer = ConfigProvider.layer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
function provide(directory: string, vcs?: Location.Interface["vcs"]) {
|
function provide(directory: string, vcs?: Location.Interface["vcs"]) {
|
||||||
const locationLayer = Layer.succeed(
|
const locationNode = LayerNode.make(
|
||||||
Location.Service,
|
Layer.succeed(
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make(directory) }, { vcs })),
|
Location.Service,
|
||||||
|
Location.Service.of(location({ directory: AbsolutePath.make(directory) }, { vcs })),
|
||||||
|
),
|
||||||
|
[],
|
||||||
)
|
)
|
||||||
return Effect.provide(
|
return Effect.provide(
|
||||||
Watcher.layer.pipe(
|
LayerNode.buildLayer(
|
||||||
Layer.provide(configLayer),
|
LayerNode.group([
|
||||||
Layer.provide(Git.defaultLayer),
|
LayerNode.make(Watcher.layer, [configNode, EventV2.node, FSUtil.node, Git.node, locationNode]),
|
||||||
Layer.provide(locationLayer),
|
root,
|
||||||
|
]),
|
||||||
|
{ replacements: [LayerNode.replace(configNode, configLayer)] },
|
||||||
|
).pipe(
|
||||||
Layer.provide(flagsLayer),
|
Layer.provide(flagsLayer),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -196,7 +205,7 @@ describeWatcher("Watcher", () => {
|
|||||||
yield* noUpdate((event) => event.file === file, fs.writeFileString(file, "gone")).pipe(
|
yield* noUpdate((event) => event.file === file, fs.writeFileString(file, "gone")).pipe(
|
||||||
Effect.provideService(EventV2.Service, events),
|
Effect.provideService(EventV2.Service, events),
|
||||||
)
|
)
|
||||||
}).pipe(Effect.provide(Layer.mergeAll(FSUtil.defaultLayer, EventV2.defaultLayer))),
|
}).pipe(Effect.provide(LayerNode.buildLayer(root))),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("ignores .git/index changes", () =>
|
it.live("ignores .git/index changes", () =>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe("Integration", () => {
|
|||||||
const openai = Integration.ID.make("openai")
|
const openai = Integration.ID.make("openai")
|
||||||
|
|
||||||
yield* integrations
|
yield* integrations
|
||||||
.transform((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
|
.update((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
|
||||||
.pipe(Scope.provide(scope))
|
.pipe(Scope.provide(scope))
|
||||||
expect(yield* integrations.get(openai)).toEqual(
|
expect(yield* integrations.get(openai)).toEqual(
|
||||||
new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }),
|
new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }),
|
||||||
@@ -70,10 +70,10 @@ describe("Integration", () => {
|
|||||||
const second = yield* Scope.fork(yield* Scope.Scope)
|
const second = yield* Scope.fork(yield* Scope.Scope)
|
||||||
|
|
||||||
yield* integrations
|
yield* integrations
|
||||||
.transform((editor) => editor.update(id, (integration) => (integration.name = "OpenAI")))
|
.update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI")))
|
||||||
.pipe(Scope.provide(first))
|
.pipe(Scope.provide(first))
|
||||||
yield* integrations
|
yield* integrations
|
||||||
.transform((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override")))
|
.update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override")))
|
||||||
.pipe(Scope.provide(second))
|
.pipe(Scope.provide(second))
|
||||||
expect((yield* integrations.get(id))?.name).toBe("OpenAI Override")
|
expect((yield* integrations.get(id))?.name).toBe("OpenAI Override")
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ describe("Integration", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
yield* integrations
|
yield* integrations
|
||||||
.transform((editor) =>
|
.update((editor) =>
|
||||||
editor.method.update({
|
editor.method.update({
|
||||||
integrationID,
|
integrationID,
|
||||||
method: { id: methodID, type: "oauth", label: "ChatGPT" },
|
method: { id: methodID, type: "oauth", label: "ChatGPT" },
|
||||||
@@ -108,7 +108,7 @@ describe("Integration", () => {
|
|||||||
)
|
)
|
||||||
.pipe(Scope.provide(first))
|
.pipe(Scope.provide(first))
|
||||||
yield* integrations
|
yield* integrations
|
||||||
.transform((editor) => {
|
.update((editor) => {
|
||||||
expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" })
|
expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" })
|
||||||
expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }])
|
expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }])
|
||||||
expect(editor.method.list(integrationID)).toEqual([
|
expect(editor.method.list(integrationID)).toEqual([
|
||||||
@@ -141,7 +141,7 @@ describe("Integration", () => {
|
|||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const integrationID = Integration.ID.make("openai")
|
const integrationID = Integration.ID.make("openai")
|
||||||
yield* integrations.transform((editor) =>
|
yield* integrations.update((editor) =>
|
||||||
editor.method.update({
|
editor.method.update({
|
||||||
integrationID,
|
integrationID,
|
||||||
method: { type: "key", label: "API key" },
|
method: { type: "key", label: "API key" },
|
||||||
@@ -179,7 +179,7 @@ describe("Integration", () => {
|
|||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
const integrationID = Integration.ID.make("openai")
|
const integrationID = Integration.ID.make("openai")
|
||||||
const methodID = Integration.MethodID.make("chatgpt")
|
const methodID = Integration.MethodID.make("chatgpt")
|
||||||
yield* integrations.transform((editor) =>
|
yield* integrations.update((editor) =>
|
||||||
editor.method.update({
|
editor.method.update({
|
||||||
integrationID,
|
integrationID,
|
||||||
method: { id: methodID, type: "oauth", label: "ChatGPT" },
|
method: { id: methodID, type: "oauth", label: "ChatGPT" },
|
||||||
@@ -238,7 +238,7 @@ describe("Integration", () => {
|
|||||||
const integrationID = Integration.ID.make("openai")
|
const integrationID = Integration.ID.make("openai")
|
||||||
const methodID = Integration.MethodID.make("chatgpt")
|
const methodID = Integration.MethodID.make("chatgpt")
|
||||||
let closed = false
|
let closed = false
|
||||||
yield* integrations.transform((editor) =>
|
yield* integrations.update((editor) =>
|
||||||
editor.method.update({
|
editor.method.update({
|
||||||
integrationID,
|
integrationID,
|
||||||
method: { id: methodID, type: "oauth", label: "ChatGPT" },
|
method: { id: methodID, type: "oauth", label: "ChatGPT" },
|
||||||
@@ -275,7 +275,7 @@ describe("Integration", () => {
|
|||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
const integrationID = Integration.ID.make("openai")
|
const integrationID = Integration.ID.make("openai")
|
||||||
const methodID = Integration.MethodID.make("browser")
|
const methodID = Integration.MethodID.make("browser")
|
||||||
yield* integrations.transform((editor) =>
|
yield* integrations.update((editor) =>
|
||||||
editor.method.update({
|
editor.method.update({
|
||||||
integrationID,
|
integrationID,
|
||||||
method: { id: methodID, type: "oauth", label: "Browser" },
|
method: { id: methodID, type: "oauth", label: "Browser" },
|
||||||
@@ -312,7 +312,7 @@ describe("Integration", () => {
|
|||||||
const integrationID = Integration.ID.make("openai")
|
const integrationID = Integration.ID.make("openai")
|
||||||
const methodID = Integration.MethodID.make("browser")
|
const methodID = Integration.MethodID.make("browser")
|
||||||
let closed = false
|
let closed = false
|
||||||
yield* integrations.transform((editor) =>
|
yield* integrations.update((editor) =>
|
||||||
editor.method.update({
|
editor.method.update({
|
||||||
integrationID,
|
integrationID,
|
||||||
method: { id: methodID, type: "oauth", label: "Browser" },
|
method: { id: methodID, type: "oauth", label: "Browser" },
|
||||||
@@ -375,7 +375,7 @@ describe("Integration", () => {
|
|||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
yield* integrations.transform((editor) =>
|
yield* integrations.update((editor) =>
|
||||||
editor.method.update({
|
editor.method.update({
|
||||||
integrationID,
|
integrationID,
|
||||||
method: {
|
method: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Cause, Effect, Exit, Layer } from "effect"
|
import { Effect, Exit, Layer } from "effect"
|
||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
@@ -31,13 +31,6 @@ const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
|
|||||||
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
|
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
|
||||||
|
|
||||||
describe("FileSystem", () => {
|
describe("FileSystem", () => {
|
||||||
const expectFail = (exit: Exit.Exit<unknown, unknown>) => {
|
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
|
||||||
if (Exit.isSuccess(exit)) return
|
|
||||||
expect(Cause.hasFails(exit.cause)).toBe(true)
|
|
||||||
expect(Cause.hasDies(exit.cause)).toBe(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.live("reads text and binary files", () =>
|
it.live("reads text and binary files", () =>
|
||||||
withTmp((directory) =>
|
withTmp((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -67,39 +60,13 @@ describe("FileSystem", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("fails for missing paths", () =>
|
it.live("rejects lexical escapes", () =>
|
||||||
withTmp((directory) =>
|
withTmp((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const exit = yield* (yield* FileSystem.Service)
|
const result = yield* (yield* FileSystem.Service)
|
||||||
.read({ path: RelativePath.make("missing.txt") })
|
.read({ path: RelativePath.make("../outside.txt") })
|
||||||
.pipe(Effect.exit)
|
.pipe(Effect.exit)
|
||||||
expectFail(exit)
|
expect(Exit.isFailure(result)).toBe(true)
|
||||||
}).pipe(provide(directory)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("fails for wrong path kinds", () =>
|
|
||||||
withTmp((directory) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* Effect.promise(() => fs.mkdir(path.join(directory, "src")))
|
|
||||||
yield* Effect.promise(() => fs.writeFile(path.join(directory, "README.md"), "# Test"))
|
|
||||||
const service = yield* FileSystem.Service
|
|
||||||
expectFail(yield* service.read({ path: RelativePath.make("src") }).pipe(Effect.exit))
|
|
||||||
expectFail(yield* service.list({ path: RelativePath.make("README.md") }).pipe(Effect.exit))
|
|
||||||
}).pipe(provide(directory)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("fails for lexical and symlink escapes", () =>
|
|
||||||
withTmp((directory) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const outside = path.join(directory, "..", "outside.txt")
|
|
||||||
yield* Effect.promise(() => fs.writeFile(outside, "outside"))
|
|
||||||
yield* Effect.promise(() => fs.symlink(outside, path.join(directory, "linked.txt")))
|
|
||||||
const service = yield* FileSystem.Service
|
|
||||||
expectFail(yield* service.read({ path: RelativePath.make("../outside.txt") }).pipe(Effect.exit))
|
|
||||||
expectFail(yield* service.read({ path: RelativePath.make("linked.txt") }).pipe(Effect.exit))
|
|
||||||
yield* Effect.promise(() => fs.rm(outside))
|
|
||||||
}).pipe(provide(directory)),
|
}).pipe(provide(directory)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Deferred, Effect, Equal, Hash, Layer, Schema, Stream } from "effect"
|
import { Effect, Equal, Hash, Layer, Schema } from "effect"
|
||||||
import { Tool } from "@opencode-ai/core/public"
|
import { Tool } from "@opencode-ai/core/public"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
@@ -88,7 +86,8 @@ describe("LocationServiceMap", () => {
|
|||||||
yield* PluginBoot.Service.use((boot) => boot.wait())
|
yield* PluginBoot.Service.use((boot) => boot.wait())
|
||||||
yield* Reference.Service
|
yield* Reference.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
||||||
return {
|
return {
|
||||||
providers: yield* catalog.provider.all(),
|
providers: yield* catalog.provider.all(),
|
||||||
tools: yield* toolDefinitions(yield* ToolRegistry.Service),
|
tools: yield* toolDefinitions(yield* ToolRegistry.Service),
|
||||||
@@ -136,53 +135,4 @@ describe("LocationServiceMap", () => {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("installs public plugins into a location", () =>
|
|
||||||
Effect.acquireRelease(
|
|
||||||
Effect.promise(() => tmpdir()),
|
|
||||||
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
|
||||||
).pipe(
|
|
||||||
Effect.flatMap((dir) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const boot = yield* PluginBoot.Service
|
|
||||||
const catalogUpdated = yield* Deferred.make<void>()
|
|
||||||
const seen: string[] = []
|
|
||||||
yield* boot.add(
|
|
||||||
define({
|
|
||||||
id: "reviewer",
|
|
||||||
effect: (ctx) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* ctx.event.subscribe("catalog.updated").pipe(
|
|
||||||
Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)),
|
|
||||||
Effect.forkScoped({ startImmediately: true }),
|
|
||||||
)
|
|
||||||
yield* ctx.agent.transform((agent) => {
|
|
||||||
agent.update("reviewer", (item) => {
|
|
||||||
item.description = "Reviews code"
|
|
||||||
item.mode = "subagent"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
seen.push((yield* ctx.agent.get("reviewer"))?.description ?? "")
|
|
||||||
yield* ctx.catalog.transform((catalog) => {
|
|
||||||
catalog.provider.update("public", (provider) => {
|
|
||||||
provider.name = "Public provider"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* Deferred.await(catalogUpdated)
|
|
||||||
expect(seen).toEqual(["Reviews code"])
|
|
||||||
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
|
||||||
description: "Reviews code",
|
|
||||||
mode: "subagent",
|
|
||||||
})
|
|
||||||
}).pipe(
|
|
||||||
Effect.scoped,
|
|
||||||
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ describe("Npm.add", () => {
|
|||||||
return yield* npm.add(spec)
|
return yield* npm.add(spec)
|
||||||
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
|
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
|
||||||
|
|
||||||
expect(entry.entrypoint).toBeDefined()
|
expect(Option.isSome(entry.entrypoint)).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ function setup(rules: PermissionV2.Ruleset = []) {
|
|||||||
function setRules(rules: PermissionV2.Ruleset) {
|
function setRules(rules: PermissionV2.Ruleset) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
yield* agents.transform((editor) =>
|
const update = yield* agents.transform()
|
||||||
|
yield* update((editor) =>
|
||||||
editor.update(AgentV2.ID.make("test"), (agent) => {
|
editor.update(AgentV2.ID.make("test"), (agent) => {
|
||||||
agent.permissions = [...rules]
|
agent.permissions = [...rules]
|
||||||
}),
|
}),
|
||||||
@@ -129,7 +130,7 @@ describe("PermissionV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup([{ action: "read", resource: "*", effect: "allow" }])
|
yield* setup([{ action: "read", resource: "*", effect: "allow" }])
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
yield* agents.transform((editor) =>
|
yield* agents.update((editor) =>
|
||||||
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
|
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
|
||||||
agent.permissions.push({ action: "read", resource: "*", effect: "deny" })
|
agent.permissions.push({ action: "read", resource: "*", effect: "deny" })
|
||||||
}),
|
}),
|
||||||
@@ -138,7 +139,7 @@ describe("PermissionV2", () => {
|
|||||||
|
|
||||||
expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" })
|
expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" })
|
||||||
expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" })
|
expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" })
|
||||||
yield* agents.transform((editor) =>
|
yield* agents.update((editor) =>
|
||||||
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
|
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
|
||||||
agent.permissions = []
|
agent.permissions = []
|
||||||
}),
|
}),
|
||||||
@@ -186,7 +187,8 @@ describe("PermissionV2", () => {
|
|||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
yield* agents.transform((editor) =>
|
const update = yield* agents.transform()
|
||||||
|
yield* update((editor) =>
|
||||||
editor.update(AgentV2.ID.make("build"), (agent) => {
|
editor.update(AgentV2.ID.make("build"), (agent) => {
|
||||||
agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }]
|
agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }]
|
||||||
}),
|
}),
|
||||||
@@ -212,7 +214,7 @@ describe("PermissionV2", () => {
|
|||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
yield* agents.transform((editor) => {
|
yield* agents.update((editor) => {
|
||||||
editor.remove(AgentV2.ID.make("test"))
|
editor.remove(AgentV2.ID.make("test"))
|
||||||
editor.remove(AgentV2.ID.make("build"))
|
editor.remove(AgentV2.ID.make("build"))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const plugins = PluginV2.layer.pipe(Layer.provide(events))
|
|||||||
function state() {
|
function state() {
|
||||||
return State.create({
|
return State.create({
|
||||||
initial: () => ({ values: [] as string[] }),
|
initial: () => ({ values: [] as string[] }),
|
||||||
draft: (draft) => ({
|
editor: (draft) => ({
|
||||||
add: (value: string) => draft.values.push(value),
|
add: (value: string) => draft.values.push(value),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -34,9 +34,8 @@ describe("PluginV2", () => {
|
|||||||
yield* plugin.add({
|
yield* plugin.add({
|
||||||
id: PluginV2.ID.make("scoped"),
|
id: PluginV2.ID.make("scoped"),
|
||||||
effect: Effect.gen(function* () {
|
effect: Effect.gen(function* () {
|
||||||
yield* values.transform((editor) => {
|
const transform = yield* values.transform()
|
||||||
editor.add("scoped")
|
yield* transform((editor) => editor.add("scoped"))
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
expect(values.get().values).toEqual(["scoped"])
|
expect(values.get().values).toEqual(["scoped"])
|
||||||
@@ -59,9 +58,8 @@ describe("PluginV2", () => {
|
|||||||
.add({
|
.add({
|
||||||
id,
|
id,
|
||||||
effect: Effect.gen(function* () {
|
effect: Effect.gen(function* () {
|
||||||
yield* values.transform((editor) => {
|
const transform = yield* values.transform()
|
||||||
editor.add("first")
|
yield* transform((editor) => editor.add("first"))
|
||||||
})
|
|
||||||
yield* Deferred.succeed(firstStarted, undefined)
|
yield* Deferred.succeed(firstStarted, undefined)
|
||||||
yield* Deferred.await(releaseFirst)
|
yield* Deferred.await(releaseFirst)
|
||||||
}),
|
}),
|
||||||
@@ -73,9 +71,8 @@ describe("PluginV2", () => {
|
|||||||
.add({
|
.add({
|
||||||
id,
|
id,
|
||||||
effect: Effect.gen(function* () {
|
effect: Effect.gen(function* () {
|
||||||
yield* values.transform((editor) => {
|
const transform = yield* values.transform()
|
||||||
editor.add("second")
|
yield* transform((editor) => editor.add("second"))
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.pipe(Effect.forkChild({ startImmediately: true }))
|
.pipe(Effect.forkChild({ startImmediately: true }))
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { CommandPlugin } from "@opencode-ai/core/plugin/command"
|
|||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { location } from "../fixture/location"
|
import { location } from "../fixture/location"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { host } from "./host"
|
|
||||||
|
|
||||||
const directory = AbsolutePath.make("/repo/packages/app")
|
const directory = AbsolutePath.make("/repo/packages/app")
|
||||||
const project = AbsolutePath.make("/repo")
|
const project = AbsolutePath.make("/repo")
|
||||||
@@ -22,11 +21,12 @@ describe("CommandPlugin.Plugin", () => {
|
|||||||
it.effect("registers built-in init and review commands", () =>
|
it.effect("registers built-in init and review commands", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const command = yield* CommandV2.Service
|
const command = yield* CommandV2.Service
|
||||||
yield* CommandPlugin.Plugin.effect(
|
yield* CommandPlugin.Plugin.effect.pipe(
|
||||||
host({
|
Effect.provideService(CommandV2.Service, command),
|
||||||
command,
|
Effect.provideService(
|
||||||
location: location({ directory }, { projectDirectory: project }),
|
Location.Service,
|
||||||
}),
|
Location.Service.of(location({ directory }, { projectDirectory: project })),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* command.get("init")).toMatchObject({
|
expect(yield* command.get("init")).toMatchObject({
|
||||||
|
|||||||
@@ -1,319 +0,0 @@
|
|||||||
import type { AISDKHooks, PluginHost } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
||||||
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
|
|
||||||
import { Effect, Stream } from "effect"
|
|
||||||
|
|
||||||
export function host(overrides: Partial<PluginHost> = {}): PluginHost {
|
|
||||||
return {
|
|
||||||
aisdk: {
|
|
||||||
hook: () => Effect.die("unused aisdk.hook"),
|
|
||||||
},
|
|
||||||
agent: {
|
|
||||||
get: () => Effect.die("unused agent.get"),
|
|
||||||
default: () => Effect.die("unused agent.default"),
|
|
||||||
list: () => Effect.die("unused agent.list"),
|
|
||||||
rebuild: () => Effect.die("unused agent.rebuild"),
|
|
||||||
transform: () => Effect.die("unused agent.transform"),
|
|
||||||
},
|
|
||||||
catalog: {
|
|
||||||
provider: {
|
|
||||||
get: () => Effect.die("unused catalog.provider.get"),
|
|
||||||
list: () => Effect.die("unused catalog.provider.list"),
|
|
||||||
available: () => Effect.die("unused catalog.provider.available"),
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
get: () => Effect.die("unused catalog.model.get"),
|
|
||||||
list: () => Effect.die("unused catalog.model.list"),
|
|
||||||
available: () => Effect.die("unused catalog.model.available"),
|
|
||||||
default: () => Effect.die("unused catalog.model.default"),
|
|
||||||
small: () => Effect.die("unused catalog.model.small"),
|
|
||||||
},
|
|
||||||
rebuild: () => Effect.die("unused catalog.rebuild"),
|
|
||||||
transform: () => Effect.die("unused catalog.transform"),
|
|
||||||
},
|
|
||||||
command: {
|
|
||||||
get: () => Effect.die("unused command.get"),
|
|
||||||
list: () => Effect.die("unused command.list"),
|
|
||||||
rebuild: () => Effect.die("unused command.rebuild"),
|
|
||||||
transform: () => Effect.die("unused command.transform"),
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
subscribe: () => Stream.die("unused event.subscribe"),
|
|
||||||
},
|
|
||||||
filesystem: {
|
|
||||||
read: () => Effect.die("unused filesystem.read"),
|
|
||||||
list: () => Effect.die("unused filesystem.list"),
|
|
||||||
find: () => Effect.die("unused filesystem.find"),
|
|
||||||
glob: () => Effect.die("unused filesystem.glob"),
|
|
||||||
},
|
|
||||||
integration: {
|
|
||||||
get: () => Effect.die("unused integration.get"),
|
|
||||||
list: () => Effect.die("unused integration.list"),
|
|
||||||
rebuild: () => Effect.die("unused integration.rebuild"),
|
|
||||||
transform: () => Effect.die("unused integration.transform"),
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
directory: "/unused/location",
|
|
||||||
project: { directory: "/unused/project" },
|
|
||||||
},
|
|
||||||
npm: {
|
|
||||||
add: () => Effect.die("unused npm.add"),
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
home: "/unused/home",
|
|
||||||
data: "/unused/data",
|
|
||||||
cache: "/unused/cache",
|
|
||||||
config: "/unused/config",
|
|
||||||
state: "/unused/state",
|
|
||||||
temp: "/unused/temp",
|
|
||||||
},
|
|
||||||
reference: {
|
|
||||||
list: () => Effect.die("unused reference.list"),
|
|
||||||
rebuild: () => Effect.die("unused reference.rebuild"),
|
|
||||||
transform: () => Effect.die("unused reference.transform"),
|
|
||||||
},
|
|
||||||
skill: {
|
|
||||||
sources: () => Effect.die("unused skill.sources"),
|
|
||||||
list: () => Effect.die("unused skill.list"),
|
|
||||||
rebuild: () => Effect.die("unused skill.rebuild"),
|
|
||||||
transform: () => Effect.die("unused skill.transform"),
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function aisdkHost(plugin: PluginV2.Interface): PluginHost["aisdk"] {
|
|
||||||
return {
|
|
||||||
hook: (name, callback) => {
|
|
||||||
if (name === "sdk") {
|
|
||||||
const run = callback as AISDKHooks["sdk"]
|
|
||||||
return plugin.hook("aisdk.sdk", (event) => {
|
|
||||||
const output = { ...event }
|
|
||||||
const result = run(output)
|
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
|
||||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const run = callback as AISDKHooks["language"]
|
|
||||||
return plugin.hook("aisdk.language", (event) => {
|
|
||||||
const output = { ...event }
|
|
||||||
const result = run(output)
|
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
|
||||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
|
|
||||||
return {
|
|
||||||
...host().agent,
|
|
||||||
transform: (callback) =>
|
|
||||||
agent.transform((draft) =>
|
|
||||||
callback({
|
|
||||||
list: () => draft.list().map(agentInfo),
|
|
||||||
get: (id) => {
|
|
||||||
const value = draft.get(AgentV2.ID.make(id))
|
|
||||||
return value && agentInfo(value)
|
|
||||||
},
|
|
||||||
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
|
|
||||||
update: (id, update) =>
|
|
||||||
draft.update(AgentV2.ID.make(id), (value) => {
|
|
||||||
const current = agentInfo(value)
|
|
||||||
update(current)
|
|
||||||
Object.assign(value, current, { id: AgentV2.ID.make(current.id) })
|
|
||||||
}),
|
|
||||||
remove: (id) => draft.remove(AgentV2.ID.make(id)),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
|
|
||||||
return {
|
|
||||||
...host().catalog,
|
|
||||||
rebuild: catalog.rebuild,
|
|
||||||
transform: (callback) =>
|
|
||||||
catalog.transform((draft) =>
|
|
||||||
callback({
|
|
||||||
provider: {
|
|
||||||
list: () =>
|
|
||||||
draft.provider.list().map((value) => ({
|
|
||||||
provider: providerInfo(value.provider),
|
|
||||||
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
|
|
||||||
})),
|
|
||||||
get: (id) => {
|
|
||||||
const value = draft.provider.get(ProviderV2.ID.make(id))
|
|
||||||
return (
|
|
||||||
value && {
|
|
||||||
provider: providerInfo(value.provider),
|
|
||||||
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
update: (id, update) =>
|
|
||||||
draft.provider.update(ProviderV2.ID.make(id), (value) => {
|
|
||||||
const current = providerInfo(value)
|
|
||||||
update(current)
|
|
||||||
Object.assign(value, current, { id: ProviderV2.ID.make(current.id) })
|
|
||||||
}),
|
|
||||||
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
get: (providerID, modelID) => {
|
|
||||||
const value = draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))
|
|
||||||
return value && modelInfo(value)
|
|
||||||
},
|
|
||||||
update: (providerID, modelID, update) =>
|
|
||||||
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), (value) => {
|
|
||||||
const current = modelInfo(value)
|
|
||||||
update(current)
|
|
||||||
Object.assign(value, current, {
|
|
||||||
id: ModelV2.ID.make(current.id),
|
|
||||||
providerID: ProviderV2.ID.make(current.providerID),
|
|
||||||
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
|
|
||||||
variants: current.variants.map((variant) => ({
|
|
||||||
...variant,
|
|
||||||
id: ModelV2.VariantID.make(variant.id),
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
remove: (providerID, modelID) =>
|
|
||||||
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
|
||||||
default: {
|
|
||||||
get: () => {
|
|
||||||
const value = draft.model.default.get()
|
|
||||||
return value && { providerID: value.providerID, modelID: value.modelID }
|
|
||||||
},
|
|
||||||
set: (providerID, modelID) =>
|
|
||||||
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function integrationHost(integration: Integration.Interface): PluginHost["integration"] {
|
|
||||||
const info = (value: Integration.Info) => ({
|
|
||||||
id: value.id,
|
|
||||||
name: value.name,
|
|
||||||
methods: value.methods.map(method),
|
|
||||||
connections: value.connections.map((item) => ({ ...item })),
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))),
|
|
||||||
list: () => integration.list().pipe(Effect.map((items) => items.map(info))),
|
|
||||||
rebuild: integration.rebuild,
|
|
||||||
transform: (callback) =>
|
|
||||||
integration.transform((draft) =>
|
|
||||||
callback({
|
|
||||||
list: () => draft.list().map((value) => ({ id: value.id, name: value.name })),
|
|
||||||
get: (id) => {
|
|
||||||
const value = draft.get(Integration.ID.make(id))
|
|
||||||
return value && { id: value.id, name: value.name }
|
|
||||||
},
|
|
||||||
update: (id, update) => draft.update(Integration.ID.make(id), update),
|
|
||||||
remove: (id) => draft.remove(Integration.ID.make(id)),
|
|
||||||
method: {
|
|
||||||
list: (id) => draft.method.list(Integration.ID.make(id)).map(method),
|
|
||||||
update: (input) =>
|
|
||||||
input.method.type === "env"
|
|
||||||
? draft.method.update({
|
|
||||||
integrationID: Integration.ID.make(input.integrationID),
|
|
||||||
method: { ...input.method, names: [...input.method.names] },
|
|
||||||
})
|
|
||||||
: draft.method.update({
|
|
||||||
integrationID: Integration.ID.make(input.integrationID),
|
|
||||||
method: input.method,
|
|
||||||
}),
|
|
||||||
remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function method(value: Integration.Method) {
|
|
||||||
if (value.type === "env") return { type: value.type, names: [...value.names] }
|
|
||||||
if (value.type === "key") return { type: value.type, label: value.label }
|
|
||||||
return {
|
|
||||||
type: value.type,
|
|
||||||
id: value.id,
|
|
||||||
label: value.label,
|
|
||||||
prompts: value.prompts?.map((prompt) => {
|
|
||||||
if (prompt.type === "text") return { ...prompt }
|
|
||||||
return { ...prompt, options: prompt.options.map((option) => ({ ...option })) }
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function internalMethod(
|
|
||||||
value: IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod,
|
|
||||||
): Integration.Method {
|
|
||||||
if (value.type === "env") return value
|
|
||||||
if (value.type === "key") return value
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
id: Integration.MethodID.make(value.id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function agentInfo(value: AgentV2.Info) {
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
model: value.model && { ...value.model },
|
|
||||||
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
|
|
||||||
permissions: value.permissions.map((permission) => ({ ...permission })),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function providerInfo(value: ProviderV2.MutableInfo) {
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
|
|
||||||
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
|
|
||||||
capabilities: {
|
|
||||||
...value.capabilities,
|
|
||||||
input: [...value.capabilities.input],
|
|
||||||
output: [...value.capabilities.output],
|
|
||||||
},
|
|
||||||
request: {
|
|
||||||
...value.request,
|
|
||||||
headers: { ...value.request.headers },
|
|
||||||
body: { ...value.request.body },
|
|
||||||
generation: value.request.generation && {
|
|
||||||
...value.request.generation,
|
|
||||||
stop: value.request.generation.stop && [...value.request.generation.stop],
|
|
||||||
},
|
|
||||||
options: value.request.options && { ...value.request.options },
|
|
||||||
},
|
|
||||||
variants: value.variants.map((variant) => ({
|
|
||||||
...variant,
|
|
||||||
headers: { ...variant.headers },
|
|
||||||
body: { ...variant.body },
|
|
||||||
generation: variant.generation && {
|
|
||||||
...variant.generation,
|
|
||||||
stop: variant.generation.stop && [...variant.generation.stop],
|
|
||||||
},
|
|
||||||
options: variant.options && { ...variant.options },
|
|
||||||
})),
|
|
||||||
time: { ...value.time },
|
|
||||||
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
|
|
||||||
limit: { ...value.limit },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer, Stream } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
@@ -15,7 +15,6 @@ import { Policy } from "@opencode-ai/core/policy"
|
|||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { location } from "../fixture/location"
|
import { location } from "../fixture/location"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { catalogHost, host, integrationHost } from "./host"
|
|
||||||
|
|
||||||
const events = EventV2.defaultLayer
|
const events = EventV2.defaultLayer
|
||||||
const locationLayer = Layer.succeed(
|
const locationLayer = Layer.succeed(
|
||||||
@@ -57,15 +56,8 @@ describe("ModelsDevPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
yield* ModelsDevPlugin.effect
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
const catalog = yield* Catalog.Service
|
|
||||||
yield* ModelsDevPlugin.effect(
|
|
||||||
host({
|
|
||||||
catalog: catalogHost(catalog),
|
|
||||||
event: { subscribe: () => Stream.never },
|
|
||||||
integration: integrationHost(integrations),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
expect(yield* integrations.list()).toEqual([
|
expect(yield* integrations.list()).toEqual([
|
||||||
new Integration.Info({
|
new Integration.Info({
|
||||||
id: Integration.ID.make("acme"),
|
id: Integration.ID.make("acme"),
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { Effect } from "effect"
|
|||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
|
import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
|
||||||
import { addPlugin, it, model } from "./provider-helper"
|
import { it, model } from "./provider-helper"
|
||||||
|
|
||||||
describe("AlibabaPlugin", () => {
|
describe("AlibabaPlugin", () => {
|
||||||
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
|
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AlibabaPlugin)
|
yield* plugin.add(AlibabaPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{ model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
|
{ model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
|
||||||
@@ -23,7 +23,7 @@ describe("AlibabaPlugin", () => {
|
|||||||
it.effect("ignores non-Alibaba SDK packages", () =>
|
it.effect("ignores non-Alibaba SDK packages", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AlibabaPlugin)
|
yield* plugin.add(AlibabaPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{ model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
|
{ model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
|
||||||
@@ -36,7 +36,7 @@ describe("AlibabaPlugin", () => {
|
|||||||
it.effect("matches the old bundled Alibaba SDK provider naming", () =>
|
it.effect("matches the old bundled Alibaba SDK provider naming", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AlibabaPlugin)
|
yield* plugin.add(AlibabaPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -56,7 +56,7 @@ describe("AlibabaPlugin", () => {
|
|||||||
it.effect("uses the old default languageModel(api.id) behavior", () =>
|
it.effect("uses the old default languageModel(api.id) behavior", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AlibabaPlugin)
|
yield* plugin.add(AlibabaPlugin)
|
||||||
const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } })
|
const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } })
|
||||||
const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
|
const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
|
||||||
const language = result.sdk?.languageModel(item.api.id)
|
const language = result.sdk?.languageModel(item.api.id)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
|||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
|
import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
|
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
|
||||||
|
|
||||||
function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
|
function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
|
||||||
const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
|
const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
|
||||||
@@ -30,8 +30,9 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
const bedrock = provider("amazon-bedrock", {
|
const bedrock = provider("amazon-bedrock", {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
|
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
|
||||||
request: {
|
request: {
|
||||||
@@ -44,7 +45,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
item.request = bedrock.request
|
item.request = bedrock.request
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
|
const result = yield* catalog.provider.get(ProviderV2.ID.amazonBedrock)
|
||||||
expect(result.api).toEqual({
|
expect(result.api).toEqual({
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
@@ -58,7 +59,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -83,7 +84,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -117,7 +118,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -137,7 +138,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
|
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -156,7 +157,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
|
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -175,7 +176,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
|
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -195,7 +196,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const headers: Array<string | null> = []
|
const headers: Array<string | null> = []
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -224,7 +225,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const headers: Array<string | null> = []
|
const headers: Array<string | null> = []
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -252,7 +253,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -281,7 +282,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
@@ -311,7 +312,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
it.effect("ignores other Bedrock provider subpaths", () =>
|
it.effect("ignores other Bedrock provider subpaths", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -340,7 +341,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const headers: Array<string | null> = []
|
const headers: Array<string | null> = []
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -371,7 +372,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
@@ -432,7 +433,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
@@ -517,7 +518,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
expected: "au.anthropic.claude-sonnet-4-5",
|
expected: "au.anthropic.claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
for (const item of cases) {
|
for (const item of cases) {
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
@@ -537,7 +538,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
yield* plugin.add(AmazonBedrockPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
|||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
|
import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { addPlugin, it, model, provider, required } from "./provider-helper"
|
import { it, model, provider } from "./provider-helper"
|
||||||
|
|
||||||
describe("AnthropicPlugin", () => {
|
describe("AnthropicPlugin", () => {
|
||||||
it.effect("applies legacy beta headers", () =>
|
it.effect("applies legacy beta headers", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* addPlugin(plugin, AnthropicPlugin)
|
yield* plugin.add(AnthropicPlugin)
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
const item = provider("anthropic", {
|
const item = provider("anthropic", {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/anthropic" },
|
api: { type: "aisdk", package: "@ai-sdk/anthropic" },
|
||||||
request: { headers: { Existing: "1" }, body: {} },
|
request: { headers: { Existing: "1" }, body: {} },
|
||||||
@@ -22,10 +23,10 @@ describe("AnthropicPlugin", () => {
|
|||||||
draft.request = item.request
|
draft.request = item.request
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
|
expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
|
||||||
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
||||||
)
|
)
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
|
expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,11 +34,10 @@ describe("AnthropicPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* addPlugin(plugin, AnthropicPlugin)
|
yield* plugin.add(AnthropicPlugin)
|
||||||
yield* catalog.transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
|
const transform = yield* catalog.transform()
|
||||||
expect(
|
yield* transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
|
||||||
required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
|
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"]).toBeUndefined()
|
||||||
).toBeUndefined()
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ describe("AnthropicPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const providers: string[] = []
|
const providers: string[] = []
|
||||||
yield* addPlugin(plugin, AnthropicPlugin)
|
yield* plugin.add(AnthropicPlugin)
|
||||||
yield* plugin.add({
|
yield* plugin.add({
|
||||||
id: PluginV2.ID.make("anthropic-sdk-inspector"),
|
id: PluginV2.ID.make("anthropic-sdk-inspector"),
|
||||||
effect: Effect.succeed({
|
effect: Effect.succeed({
|
||||||
@@ -72,7 +72,7 @@ describe("AnthropicPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const providers: string[] = []
|
const providers: string[] = []
|
||||||
yield* addPlugin(plugin, AnthropicPlugin)
|
yield* plugin.add(AnthropicPlugin)
|
||||||
yield* plugin.add({
|
yield* plugin.add({
|
||||||
id: PluginV2.ID.make("anthropic-sdk-inspector"),
|
id: PluginV2.ID.make("anthropic-sdk-inspector"),
|
||||||
effect: Effect.succeed({
|
effect: Effect.succeed({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
|||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
|
import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
|
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
|
||||||
|
|
||||||
describe("AzureCognitiveServicesPlugin", () => {
|
describe("AzureCognitiveServicesPlugin", () => {
|
||||||
it.effect("maps the resource env var to the Azure SDK baseURL", () =>
|
it.effect("maps the resource env var to the Azure SDK baseURL", () =>
|
||||||
@@ -12,13 +12,14 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
yield* plugin.add(AzureCognitiveServicesPlugin)
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
|
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
|
||||||
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
|
const result = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))
|
||||||
expect(result.api).toEqual({
|
expect(result.api).toEqual({
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
@@ -35,8 +36,9 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
yield* plugin.add(AzureCognitiveServicesPlugin)
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
const azure = provider("azure-cognitive-services", {
|
const azure = provider("azure-cognitive-services", {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
|
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
|
||||||
})
|
})
|
||||||
@@ -48,8 +50,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
item.api = openai.api
|
item.api = openai.api
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
|
const azure = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))
|
||||||
const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
|
const openai = yield* catalog.provider.get(ProviderV2.ID.openai)
|
||||||
expect(azure.request.body.baseURL).toBeUndefined()
|
expect(azure.request.body.baseURL).toBeUndefined()
|
||||||
expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
|
expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
|
||||||
expect(openai.request.body.baseURL).toBeUndefined()
|
expect(openai.request.body.baseURL).toBeUndefined()
|
||||||
@@ -62,7 +64,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
yield* plugin.add(AzureCognitiveServicesPlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
@@ -80,7 +82,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
yield* plugin.add(AzureCognitiveServicesPlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{ model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
|
{ model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||||
@@ -101,7 +103,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const sdk = fakeSelectorSdk(calls)
|
const sdk = fakeSelectorSdk(calls)
|
||||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
yield* plugin.add(AzureCognitiveServicesPlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,35 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
|
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { location } from "../fixture/location"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
|
||||||
|
|
||||||
|
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
|
||||||
|
const preferences = Credential.layer.pipe(Layer.provide(database))
|
||||||
|
const accounts = Layer.merge(
|
||||||
|
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
|
||||||
|
preferences,
|
||||||
|
)
|
||||||
|
const itWithAccount = testEffect(
|
||||||
|
Catalog.locationLayer.pipe(
|
||||||
|
Layer.provideMerge(accounts),
|
||||||
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
|
Layer.provideMerge(
|
||||||
|
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
||||||
|
),
|
||||||
|
Layer.provideMerge(npmLayer),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
describe("AzurePlugin", () => {
|
describe("AzurePlugin", () => {
|
||||||
it.effect("resolves resourceName from env", () =>
|
it.effect("resolves resourceName from env", () =>
|
||||||
@@ -12,13 +37,14 @@ describe("AzurePlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* plugin.add(AzurePlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.azure, (item) => {
|
catalog.provider.update(ProviderV2.ID.azure, (item) => {
|
||||||
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
|
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -28,7 +54,9 @@ describe("AzurePlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* plugin.add(AzurePlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
const azure = provider("azure", {
|
const azure = provider("azure", {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||||
request: { headers: {}, body: { resourceName: "from-config" } },
|
request: { headers: {}, body: { resourceName: "from-config" } },
|
||||||
@@ -39,19 +67,50 @@ describe("AzurePlugin", () => {
|
|||||||
})
|
})
|
||||||
catalog.provider.update(ProviderV2.ID.openai, () => {})
|
catalog.provider.update(ProviderV2.ID.openai, () => {})
|
||||||
})
|
})
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
|
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
itWithAccount.effect("prefers account resourceName over env", () =>
|
||||||
|
withEnv(
|
||||||
|
{
|
||||||
|
AZURE_RESOURCE_NAME: "from-env",
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
|
const credentials = yield* Credential.Service
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
yield* credentials.create({
|
||||||
|
integrationID: Integration.ID.make("azure"),
|
||||||
|
value: new Credential.Key({
|
||||||
|
type: "key",
|
||||||
|
key: "key",
|
||||||
|
metadata: { resourceName: "from-account" },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
yield* plugin.add(AzurePlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
|
catalog.provider.update(ProviderV2.ID.azure, (item) => {
|
||||||
|
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-account")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("falls back to env when configured resourceName is blank", () =>
|
it.effect("falls back to env when configured resourceName is blank", () =>
|
||||||
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
|
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* plugin.add(AzurePlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
const azure = provider("azure", {
|
const azure = provider("azure", {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||||
request: { headers: {}, body: { resourceName: "" } },
|
request: { headers: {}, body: { resourceName: "" } },
|
||||||
@@ -61,8 +120,7 @@ describe("AzurePlugin", () => {
|
|||||||
item.request = azure.request
|
item.request = azure.request
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -72,7 +130,9 @@ describe("AzurePlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* plugin.add(AzurePlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
const azure = provider("azure", {
|
const azure = provider("azure", {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||||
request: { headers: {}, body: { resourceName: " " } },
|
request: { headers: {}, body: { resourceName: " " } },
|
||||||
@@ -82,8 +142,7 @@ describe("AzurePlugin", () => {
|
|||||||
item.request = azure.request
|
item.request = azure.request
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -92,7 +151,7 @@ describe("AzurePlugin", () => {
|
|||||||
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
yield* plugin.add(AzurePlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -111,7 +170,7 @@ describe("AzurePlugin", () => {
|
|||||||
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
yield* plugin.add(AzurePlugin)
|
||||||
const exit = yield* plugin
|
const exit = yield* plugin
|
||||||
.trigger(
|
.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -128,7 +187,7 @@ describe("AzurePlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
yield* plugin.add(AzurePlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
|
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
|
||||||
@@ -142,7 +201,7 @@ describe("AzurePlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
yield* plugin.add(AzurePlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
|
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
|
||||||
@@ -156,7 +215,7 @@ describe("AzurePlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
yield* plugin.add(AzurePlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
@@ -176,7 +235,7 @@ describe("AzurePlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
yield* plugin.add(AzurePlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
|
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||||
@@ -200,7 +259,7 @@ describe("AzurePlugin", () => {
|
|||||||
calls.push(`${method}:${id}`)
|
calls.push(`${method}:${id}`)
|
||||||
return { modelId: id, provider: method, specificationVersion: "v3" }
|
return { modelId: id, provider: method, specificationVersion: "v3" }
|
||||||
}
|
}
|
||||||
yield* addPlugin(plugin, AzurePlugin)
|
yield* plugin.add(AzurePlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
|||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
|
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { addPlugin, it, model, required } from "./provider-helper"
|
import { it, model } from "./provider-helper"
|
||||||
|
|
||||||
const cerebrasOptions: Record<string, unknown>[] = []
|
const cerebrasOptions: Record<string, unknown>[] = []
|
||||||
|
|
||||||
@@ -23,14 +23,15 @@ describe("CerebrasPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* addPlugin(plugin, CerebrasPlugin)
|
yield* plugin.add(CerebrasPlugin)
|
||||||
yield* catalog.transform((catalog) => {
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
|
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
|
||||||
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
|
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
|
||||||
item.request.headers.Existing = "1"
|
item.request.headers.Existing = "1"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
|
||||||
Existing: "1",
|
Existing: "1",
|
||||||
"X-Cerebras-3rd-Party-Integration": "opencode",
|
"X-Cerebras-3rd-Party-Integration": "opencode",
|
||||||
})
|
})
|
||||||
@@ -41,9 +42,10 @@ describe("CerebrasPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* addPlugin(plugin, CerebrasPlugin)
|
yield* plugin.add(CerebrasPlugin)
|
||||||
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
|
const transform = yield* catalog.transform()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
|
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
|
||||||
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ describe("CerebrasPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
cerebrasOptions.length = 0
|
cerebrasOptions.length = 0
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CerebrasPlugin)
|
yield* plugin.add(CerebrasPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -70,7 +72,7 @@ describe("CerebrasPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
cerebrasOptions.length = 0
|
cerebrasOptions.length = 0
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CerebrasPlugin)
|
yield* plugin.add(CerebrasPlugin)
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -88,7 +90,7 @@ describe("CerebrasPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
cerebrasOptions.length = 0
|
cerebrasOptions.length = 0
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CerebrasPlugin)
|
yield* plugin.add(CerebrasPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, mock } from "bun:test"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway"
|
import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway"
|
||||||
import { addPlugin, it, model, withEnv } from "./provider-helper"
|
import { it, model, withEnv } from "./provider-helper"
|
||||||
|
|
||||||
const aiGatewayCalls: Record<string, unknown>[] = []
|
const aiGatewayCalls: Record<string, unknown>[] = []
|
||||||
const unifiedCalls: string[] = []
|
const unifiedCalls: string[] = []
|
||||||
@@ -78,7 +78,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -98,7 +98,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -142,7 +142,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -171,7 +171,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -208,7 +208,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -239,7 +239,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
yield* plugin.trigger(
|
yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -261,7 +261,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -284,7 +284,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -313,7 +313,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -336,7 +336,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -364,7 +364,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetCalls()
|
resetCalls()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
yield* plugin.add(CloudflareAIGatewayPlugin)
|
||||||
|
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
|
|||||||
@@ -1,11 +1,36 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
|
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { location } from "../fixture/location"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper"
|
||||||
|
|
||||||
|
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
|
||||||
|
const preferences = Credential.layer.pipe(Layer.provide(database))
|
||||||
|
const accounts = Layer.merge(
|
||||||
|
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
|
||||||
|
preferences,
|
||||||
|
)
|
||||||
|
const itWithAccount = testEffect(
|
||||||
|
Catalog.locationLayer.pipe(
|
||||||
|
Layer.provideMerge(accounts),
|
||||||
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
|
Layer.provideMerge(
|
||||||
|
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
||||||
|
),
|
||||||
|
Layer.provideMerge(npmLayer),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
|
function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
|
||||||
return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
|
return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
|
||||||
@@ -32,13 +57,14 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "test-provider" }
|
provider.api = { type: "aisdk", package: "test-provider" }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
const provider = yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))
|
||||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
|
|
||||||
const sdk = yield* plugin.trigger(
|
const sdk = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -63,13 +89,14 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
|
provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
|
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
package: "test-provider",
|
package: "test-provider",
|
||||||
url: "https://proxy.example/v1",
|
url: "https://proxy.example/v1",
|
||||||
@@ -82,7 +109,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
|
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -99,19 +126,56 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
itWithAccount.effect("falls back to account metadata when account env is absent", () =>
|
||||||
|
withEnv(
|
||||||
|
{
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: undefined,
|
||||||
|
CLOUDFLARE_API_KEY: undefined,
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
|
const credentials = yield* Credential.Service
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
yield* credentials.create({
|
||||||
|
integrationID: Integration.ID.make("cloudflare-workers-ai"),
|
||||||
|
value: new Credential.Key({
|
||||||
|
type: "key",
|
||||||
|
key: "account-key",
|
||||||
|
metadata: { accountId: "account-acct" },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) =>
|
||||||
|
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||||
|
provider.api = { type: "aisdk", package: "test-provider" }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).request.body).toMatchObject(
|
||||||
|
{
|
||||||
|
apiKey: "account-key",
|
||||||
|
accountId: "account-acct",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("uses env account ID over configured account ID", () =>
|
it.effect("uses env account ID over configured account ID", () =>
|
||||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
|
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
|
const transform = yield* catalog.transform()
|
||||||
|
yield* transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "test-provider" }
|
provider.api = { type: "aisdk", package: "test-provider" }
|
||||||
provider.request.body.accountId = "configured-acct"
|
provider.request.body.accountId = "configured-acct"
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
|
|
||||||
type: "aisdk",
|
type: "aisdk",
|
||||||
package: "test-provider",
|
package: "test-provider",
|
||||||
url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
|
url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
|
||||||
@@ -124,7 +188,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
|
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -153,7 +217,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -183,7 +247,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
@@ -202,7 +266,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Effect } from "effect"
|
|||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
|
import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
|
||||||
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
|
import { fakeSelectorSdk, it, model } from "./provider-helper"
|
||||||
|
|
||||||
const cohereOptions: Record<string, any>[] = []
|
const cohereOptions: Record<string, any>[] = []
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ describe("CoherePlugin", () => {
|
|||||||
it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
|
it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CoherePlugin)
|
yield* plugin.add(CoherePlugin)
|
||||||
|
|
||||||
const ignored = yield* plugin.trigger(
|
const ignored = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
@@ -45,7 +45,7 @@ describe("CoherePlugin", () => {
|
|||||||
it.effect("uses the model provider ID as the bundled SDK name", () =>
|
it.effect("uses the model provider ID as the bundled SDK name", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, CoherePlugin)
|
yield* plugin.add(CoherePlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -70,7 +70,7 @@ describe("CoherePlugin", () => {
|
|||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const sdk = fakeSelectorSdk(calls)
|
const sdk = fakeSelectorSdk(calls)
|
||||||
yield* addPlugin(plugin, CoherePlugin)
|
yield* plugin.add(CoherePlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{ model: model("cohere", "alias", { api: { id: ModelV2.ID.make("command-r-plus") } }), sdk, options: {} },
|
{ model: model("cohere", "alias", { api: { id: ModelV2.ID.make("command-r-plus") } }), sdk, options: {} },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { EventV2 } from "@opencode-ai/core/event"
|
|||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
|
import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { addPlugin, it, model } from "./provider-helper"
|
import { it, model } from "./provider-helper"
|
||||||
|
|
||||||
const itAISDK = testEffect(
|
const itAISDK = testEffect(
|
||||||
Layer.provideMerge(AISDK.layer, PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
|
Layer.provideMerge(AISDK.layer, PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
|
||||||
@@ -36,7 +36,7 @@ describe("DeepInfraPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetDeepInfraMock()
|
resetDeepInfraMock()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
yield* plugin.add(DeepInfraPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{ model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
|
{ model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
|
||||||
@@ -50,7 +50,7 @@ describe("DeepInfraPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetDeepInfraMock()
|
resetDeepInfraMock()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
yield* plugin.add(DeepInfraPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ describe("DeepInfraPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetDeepInfraMock()
|
resetDeepInfraMock()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
yield* plugin.add(DeepInfraPlugin)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -88,7 +88,7 @@ describe("DeepInfraPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resetDeepInfraMock()
|
resetDeepInfraMock()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
yield* plugin.add(DeepInfraPlugin)
|
||||||
const packages = [
|
const packages = [
|
||||||
"unmatched-package",
|
"unmatched-package",
|
||||||
"@ai-sdk/deepinfra-compatible",
|
"@ai-sdk/deepinfra-compatible",
|
||||||
@@ -119,7 +119,7 @@ describe("DeepInfraPlugin", () => {
|
|||||||
resetDeepInfraMock()
|
resetDeepInfraMock()
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
yield* plugin.add(DeepInfraPlugin)
|
||||||
const language = yield* aisdk.language(
|
const language = yield* aisdk.language(
|
||||||
model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", {
|
model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/deepinfra" },
|
api: { type: "aisdk", package: "@ai-sdk/deepinfra" },
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { ModelV2 } from "@opencode-ai/core/model"
|
|||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic"
|
import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { host } from "./host"
|
|
||||||
import { fixtureProvider, it, model, npmLayer } from "./provider-helper"
|
import { fixtureProvider, it, model, npmLayer } from "./provider-helper"
|
||||||
|
|
||||||
const fixtureProviderPath = fileURLToPath(fixtureProvider)
|
const fixtureProviderPath = fileURLToPath(fixtureProvider)
|
||||||
@@ -19,24 +18,19 @@ const itWithAISDK = testEffect(
|
|||||||
AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
|
AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
|
||||||
)
|
)
|
||||||
|
|
||||||
function npmEntrypointLayer(entrypoint?: string) {
|
function npmEntrypointLayer(entrypoint: Option.Option<string>) {
|
||||||
return Layer.succeed(
|
return Layer.succeed(
|
||||||
Npm.Service,
|
Npm.Service,
|
||||||
Npm.Service.of({
|
Npm.Service.of({
|
||||||
add: () => Effect.succeed({ directory: "", entrypoint }),
|
add: () => Effect.succeed({ directory: "", entrypoint }),
|
||||||
install: () => Effect.void,
|
install: () => Effect.void,
|
||||||
which: () => Effect.succeed(undefined),
|
which: () => Effect.succeed(Option.none<string>()),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function dynamicPlugin(layer = npmLayer) {
|
function dynamicPlugin(layer = npmLayer) {
|
||||||
return {
|
return { id: DynamicProviderPlugin.id, effect: DynamicProviderPlugin.effect.pipe(Effect.provide(layer)) }
|
||||||
id: DynamicProviderPlugin.id,
|
|
||||||
effect: Effect.gen(function* () {
|
|
||||||
yield* DynamicProviderPlugin.effect(host({ npm: yield* Npm.Service }))
|
|
||||||
}).pipe(Effect.provide(layer)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tempEntrypoint(source: string) {
|
function tempEntrypoint(source: string) {
|
||||||
@@ -108,7 +102,7 @@ describe("DynamicProviderPlugin", () => {
|
|||||||
it.effect("loads npm packages through their resolved import entrypoint", () =>
|
it.effect("loads npm packages through their resolved import entrypoint", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(fixtureProviderPath)))
|
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(fixtureProviderPath))))
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -126,7 +120,7 @@ describe("DynamicProviderPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
yield* plugin.add(dynamicPlugin(npmEntrypointLayer()))
|
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.none<string>())))
|
||||||
const exit = yield* aisdk
|
const exit = yield* aisdk
|
||||||
.language(model("missing-entrypoint", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
|
.language(model("missing-entrypoint", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
|
||||||
.pipe(Effect.exit)
|
.pipe(Effect.exit)
|
||||||
@@ -155,7 +149,7 @@ describe("DynamicProviderPlugin", () => {
|
|||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n")
|
const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n")
|
||||||
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(tmp.entrypoint)))
|
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(tmp.entrypoint))))
|
||||||
const exit = yield* aisdk
|
const exit = yield* aisdk
|
||||||
.language(model("missing-factory", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
|
.language(model("missing-factory", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
|
||||||
.pipe(Effect.exit)
|
.pipe(Effect.exit)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user