mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 09:28:27 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 975b1132f1 | |||
| cd97de7391 | |||
| 23fd5907be | |||
| 49d3f86802 | |||
| 909a1a6d78 | |||
| dc468bdcfd | |||
| f48f24ec4e | |||
| 34b3d59a23 | |||
| a0a500316e | |||
| 1787fa4261 | |||
| 130957288e |
@@ -137,7 +137,7 @@ const table = sqliteTable("session", {
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Avoid mocks as much as possible
|
- Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option.
|
||||||
- Test actual implementation, do not duplicate logic into tests
|
- Test actual implementation, do not duplicate logic into tests
|
||||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
|
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ const table = sqliteTable("session", {
|
|||||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
|
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
|
||||||
- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
|
- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
|
||||||
- Preserve one explicit `llm.stream(request)` call per provider turn and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
|
- Preserve one explicit `llm.stream(request)` call per provider turn and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
|
||||||
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash activity recovery requires a separate explicit design before it may retry provider work.
|
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
|
||||||
- Keep delivery vocabulary explicit. Prompts steer by default and coalesce into the active activity at the next safe provider-turn boundary. Explicit `queue` inputs open FIFO future activities one at a time after the active activity settles.
|
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
|
||||||
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
||||||
- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned.
|
- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned.
|
||||||
|
|||||||
+17
@@ -39,6 +39,18 @@ An expected temporary inability to observe a **Context Source** value; the runti
|
|||||||
**Safe Provider-Turn Boundary**:
|
**Safe Provider-Turn Boundary**:
|
||||||
The point immediately before a provider call, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically.
|
The point immediately before a provider call, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically.
|
||||||
|
|
||||||
|
**Admitted Prompt**:
|
||||||
|
A durable user input accepted into the Session inbox but not yet included in **Session History**.
|
||||||
|
|
||||||
|
**Prompt Promotion**:
|
||||||
|
The durable transition that removes an **Admitted Prompt** from pending input and appends its user message to **Session History**.
|
||||||
|
|
||||||
|
**Provider Turn**:
|
||||||
|
One request to a model provider and the response projected from that request.
|
||||||
|
|
||||||
|
**Session Drain**:
|
||||||
|
One process-local execution span that promotes eligible input and runs required **Provider Turns** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary.
|
||||||
|
|
||||||
**Model Tool Output**:
|
**Model Tool Output**:
|
||||||
The bounded projection of a Core-executed tool result persisted in Session history and replayed to the model. A tool may shape this projection semantically, but the Tool Registry enforces the final size limit.
|
The bounded projection of a Core-executed tool result persisted in Session history and replayed to the model. A tool may shape this projection semantically, but the Tool Registry enforces the final size limit.
|
||||||
|
|
||||||
@@ -67,6 +79,11 @@ The host-supplied environment overlay applied by the server when creating a PTY,
|
|||||||
- Changes from multiple **Context Sources** admitted at one safe boundary combine into one **Mid-Conversation System Message**.
|
- Changes from multiple **Context Sources** admitted at one safe boundary combine into one **Mid-Conversation System Message**.
|
||||||
- Context changes are sampled and admitted lazily at a **Safe Provider-Turn Boundary**, never pushed asynchronously when their source changes.
|
- Context changes are sampled and admitted lazily at a **Safe Provider-Turn Boundary**, never pushed asynchronously when their source changes.
|
||||||
- At a **Safe Provider-Turn Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**.
|
- At a **Safe Provider-Turn Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**.
|
||||||
|
- An **Admitted Prompt** is replayable pending input, not yet model-visible **Session History**.
|
||||||
|
- **Prompt Promotion** atomically consumes the pending inbox entry and appends its model-visible user message.
|
||||||
|
- Steering prompts promote at the next **Safe Provider-Turn Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's provider-turn allowance; multiple prompts promoted at one boundary reset it once.
|
||||||
|
- A queued prompt does not promote while the current **Session Drain** requires continuation. The runner promotes one queued prompt when the Session would otherwise become idle, then reevaluates continuation before promoting another.
|
||||||
|
- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, provider attempts, and tool state rather than inventing an enclosing execution identity.
|
||||||
- The first provider turn renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the turn instead of persisting an incomplete baseline.
|
- The first provider turn renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the turn instead of persisting an incomplete baseline.
|
||||||
- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion.
|
- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion.
|
||||||
- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history.
|
- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history.
|
||||||
|
|||||||
@@ -196,6 +196,13 @@ export async function handler(
|
|||||||
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
|
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
|
||||||
headers.set(k, headers.get(v)!)
|
headers.set(k, headers.get(v)!)
|
||||||
})
|
})
|
||||||
|
Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => {
|
||||||
|
if (v === "$ip") return headers.set(k, ip)
|
||||||
|
if (v === "$session") return headers.set(k, sessionId)
|
||||||
|
if (v === "$model") return headers.set(k, model)
|
||||||
|
if (v === "$request") return headers.set(k, requestId)
|
||||||
|
headers.set(k, v)
|
||||||
|
})
|
||||||
headers.delete("host")
|
headers.delete("host")
|
||||||
headers.delete("content-length")
|
headers.delete("content-length")
|
||||||
headers.delete("x-opencode-request")
|
headers.delete("x-opencode-request")
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export namespace ZenData {
|
|||||||
apiKey: z.union([z.string(), z.record(z.string(), z.string())]),
|
apiKey: z.union([z.string(), z.record(z.string(), z.string())]),
|
||||||
format: FormatSchema.optional(),
|
format: FormatSchema.optional(),
|
||||||
headerMappings: z.record(z.string(), z.string()).optional(),
|
headerMappings: z.record(z.string(), z.string()).optional(),
|
||||||
|
headerModifier: z.record(z.string(), z.any()).optional(),
|
||||||
payloadModifier: z.record(z.string(), z.any()).optional(),
|
payloadModifier: z.record(z.string(), z.any()).optional(),
|
||||||
payloadMappings: z.record(z.string(), z.string()).optional(),
|
payloadMappings: z.record(z.string(), z.string()).optional(),
|
||||||
adjustCacheUsage: z.boolean().optional(),
|
adjustCacheUsage: z.boolean().optional(),
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
reload: state.reload,
|
||||||
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)
|
||||||
}),
|
}),
|
||||||
|
|||||||
+74
-21
@@ -1,14 +1,27 @@
|
|||||||
export * as AISDK from "./aisdk"
|
export * as AISDK from "./aisdk"
|
||||||
|
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
import { Cause, Context, Effect, Layer, Schema } from "effect"
|
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { EventV2 } from "./event"
|
|
||||||
import { PluginV2 } from "./plugin"
|
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
|
import { State } from "./state"
|
||||||
|
|
||||||
type SDK = any
|
type SDK = any
|
||||||
|
|
||||||
|
export interface SDKEvent {
|
||||||
|
readonly model: ModelV2.Info
|
||||||
|
readonly package: string
|
||||||
|
readonly options: Record<string, any>
|
||||||
|
sdk?: SDK
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanguageEvent {
|
||||||
|
readonly model: ModelV2.Info
|
||||||
|
readonly sdk: SDK
|
||||||
|
readonly options: Record<string, any>
|
||||||
|
language?: LanguageModelV3
|
||||||
|
}
|
||||||
|
|
||||||
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
||||||
if (typeof ms !== "number" || ms <= 0) return res
|
if (typeof ms !== "number" || ms <= 0) return res
|
||||||
if (!res.body) return res
|
if (!res.body) return res
|
||||||
@@ -117,19 +130,70 @@ function initError(providerID: ProviderV2.ID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
|
readonly hook: {
|
||||||
|
readonly sdk: (
|
||||||
|
callback: (event: SDKEvent) => Effect.Effect<void> | void,
|
||||||
|
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||||
|
readonly language: (
|
||||||
|
callback: (event: LanguageEvent) => Effect.Effect<void> | void,
|
||||||
|
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||||
|
}
|
||||||
|
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
|
||||||
|
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
|
||||||
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
|
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const locationLayer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
|
||||||
|
let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
|
||||||
const languages = new Map<string, LanguageModelV3>()
|
const languages = new Map<string, LanguageModelV3>()
|
||||||
const sdks = new Map<string, SDK>()
|
const sdks = new Map<string, SDK>()
|
||||||
|
|
||||||
return Service.of({
|
const register = <Event>(
|
||||||
|
hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
|
||||||
|
update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void,
|
||||||
|
) =>
|
||||||
|
Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) {
|
||||||
|
const scope = yield* Scope.Scope
|
||||||
|
let active = true
|
||||||
|
update([...hooks(), callback])
|
||||||
|
const dispose = Effect.sync(() => {
|
||||||
|
if (!active) return
|
||||||
|
active = false
|
||||||
|
update(hooks().filter((item) => item !== callback))
|
||||||
|
})
|
||||||
|
yield* Scope.addFinalizer(scope, dispose)
|
||||||
|
return { dispose }
|
||||||
|
})
|
||||||
|
|
||||||
|
const run = Effect.fnUntraced(function* <Event>(
|
||||||
|
hooks: readonly ((event: Event) => Effect.Effect<void> | void)[],
|
||||||
|
event: Event,
|
||||||
|
) {
|
||||||
|
for (const hook of hooks) {
|
||||||
|
const result = hook(event)
|
||||||
|
if (Effect.isEffect(result)) yield* result
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
const service = Service.of({
|
||||||
|
hook: {
|
||||||
|
sdk: register(
|
||||||
|
() => sdkHooks,
|
||||||
|
(next) => (sdkHooks = next),
|
||||||
|
),
|
||||||
|
language: register(
|
||||||
|
() => languageHooks,
|
||||||
|
(next) => (languageHooks = next),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
runSDK: (event) => run(sdkHooks, event),
|
||||||
|
runLanguage: (event) => run(languageHooks, event),
|
||||||
language: Effect.fn("AISDK.language")(function* (model) {
|
language: Effect.fn("AISDK.language")(function* (model) {
|
||||||
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
|
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
|
||||||
const existing = languages.get(key)
|
const existing = languages.get(key)
|
||||||
@@ -148,26 +212,14 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
const sdk =
|
const sdk =
|
||||||
sdks.get(sdkKey) ??
|
sdks.get(sdkKey) ??
|
||||||
(yield* plugin
|
(yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
|
||||||
.trigger("aisdk.sdk", { model, package: model.api.package, options }, {})
|
|
||||||
.pipe(initError(model.providerID))).sdk
|
|
||||||
if (!sdk)
|
if (!sdk)
|
||||||
return yield* new InitError({
|
return yield* new InitError({
|
||||||
providerID: model.providerID,
|
providerID: model.providerID,
|
||||||
cause: new Error("No AISDK provider plugin returned an SDK"),
|
cause: new Error("No AISDK provider plugin returned an SDK"),
|
||||||
})
|
})
|
||||||
sdks.set(sdkKey, sdk)
|
sdks.set(sdkKey, sdk)
|
||||||
const result = yield* plugin
|
const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
|
||||||
.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model,
|
|
||||||
sdk,
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
.pipe(initError(model.providerID))
|
|
||||||
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
|
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
|
||||||
initError(model.providerID),
|
initError(model.providerID),
|
||||||
)
|
)
|
||||||
@@ -175,7 +227,8 @@ export const layer = Layer.effect(
|
|||||||
return language
|
return language
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
return service
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))
|
export const defaultLayer = locationLayer
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
const result: Interface = {
|
const result: Interface = {
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
reload: state.reload,
|
||||||
|
|
||||||
provider: {
|
provider: {
|
||||||
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
rebuild: state.rebuild,
|
reload: state.reload,
|
||||||
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,6 @@
|
|||||||
export * as ConfigAgentPlugin from "./agent"
|
export * as ConfigAgentPlugin from "./agent"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../../plugin/internal"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * as ConfigCommandPlugin from "./command"
|
export * as ConfigCommandPlugin from "./command"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../../plugin/internal"
|
||||||
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"
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
export * as ConfigExternalPlugin from "./external"
|
||||||
|
|
||||||
|
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
|
||||||
|
import { Effect, Schema } from "effect"
|
||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath, pathToFileURL } from "url"
|
||||||
|
import { Config } from "../../config"
|
||||||
|
import { FSUtil } from "../../fs-util"
|
||||||
|
import { Location } from "../../location"
|
||||||
|
import { Npm } from "../../npm"
|
||||||
|
import { define } from "../../plugin/internal"
|
||||||
|
import { PluginPromise } from "../../plugin/promise"
|
||||||
|
|
||||||
|
const PluginModule = Schema.Struct({
|
||||||
|
default: Schema.Union([
|
||||||
|
Schema.Struct({
|
||||||
|
id: Schema.String,
|
||||||
|
effect: Schema.declare<EffectPlugin["effect"]>(
|
||||||
|
(input): input is EffectPlugin["effect"] => typeof input === "function",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
id: Schema.String,
|
||||||
|
setup: Schema.declare<PromisePlugin["setup"]>(
|
||||||
|
(input): input is PromisePlugin["setup"] => typeof input === "function",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Plugin = define({
|
||||||
|
id: "config-plugin",
|
||||||
|
effect: Effect.fn(function* (ctx) {
|
||||||
|
const config = yield* Config.Service
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const configured: { package: string; options?: Record<string, any> }[] = []
|
||||||
|
|
||||||
|
for (const entry of yield* config.entries()) {
|
||||||
|
if (entry.type === "document") {
|
||||||
|
const directory = entry.path ? path.dirname(entry.path) : location.directory
|
||||||
|
for (const item of entry.info.plugins ?? []) {
|
||||||
|
const ref = typeof item === "string" ? { package: item } : item
|
||||||
|
const packageName = (() => {
|
||||||
|
if (ref.package.startsWith("file://")) return fileURLToPath(ref.package)
|
||||||
|
if (ref.package.startsWith("./") || ref.package.startsWith("../")) {
|
||||||
|
return path.resolve(directory, ref.package)
|
||||||
|
}
|
||||||
|
return ref.package
|
||||||
|
})()
|
||||||
|
configured.push({ package: packageName, options: ref.options })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.type === "directory") {
|
||||||
|
const files = yield* fs
|
||||||
|
.glob("{plugin,plugins}/*.{ts,js}", {
|
||||||
|
cwd: entry.path,
|
||||||
|
absolute: true,
|
||||||
|
include: "file",
|
||||||
|
dot: true,
|
||||||
|
symlink: true,
|
||||||
|
})
|
||||||
|
.pipe(Effect.orElseSucceed(() => []))
|
||||||
|
files.sort()
|
||||||
|
for (const file of files) configured.push({ package: file })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ref of configured) {
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const entrypoint = path.isAbsolute(ref.package)
|
||||||
|
? pathToFileURL(ref.package).href
|
||||||
|
: (yield* npm.add(ref.package)).entrypoint
|
||||||
|
if (!entrypoint) return
|
||||||
|
|
||||||
|
const mod = yield* Effect.promise(() => import(entrypoint))
|
||||||
|
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||||
|
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||||
|
yield* ctx.plugin.add({
|
||||||
|
id: plugin.id,
|
||||||
|
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
|
||||||
|
})
|
||||||
|
}).pipe(Effect.ignoreCause)
|
||||||
|
}
|
||||||
|
}).pipe(Effect.forkScoped({ startImmediately: true }))
|
||||||
|
}),
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export * as ConfigProviderPlugin from "./provider"
|
export * as ConfigProviderPlugin from "./provider"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../../plugin/internal"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
export * as ConfigReferencePlugin from "./reference"
|
export * as ConfigReferencePlugin from "./reference"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../../plugin/internal"
|
||||||
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 { Reference } from "../../reference"
|
import { Reference } from "../../reference"
|
||||||
import { AbsolutePath } from "../../schema"
|
import { AbsolutePath } from "../../schema"
|
||||||
|
import { Global } from "../../global"
|
||||||
|
import { Location } from "../../location"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "core/config-reference",
|
id: "core/config-reference",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const global = yield* Global.Service
|
||||||
yield* ctx.reference.transform(
|
yield* ctx.reference.transform(
|
||||||
Effect.fn(function* (draft) {
|
Effect.fn(function* (draft) {
|
||||||
const entries = new Map<string, Reference.Source>()
|
const entries = new Map<string, Reference.Source>()
|
||||||
for (const doc of (yield* config.entries()).filter(
|
for (const doc of (yield* config.entries()).filter(
|
||||||
(entry): entry is Config.Document => entry.type === "document",
|
(entry): entry is Config.Document => entry.type === "document",
|
||||||
)) {
|
)) {
|
||||||
const directory = doc.path ? path.dirname(doc.path) : ctx.location.directory
|
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
||||||
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
||||||
if (!validAlias(name)) continue
|
if (!validAlias(name)) continue
|
||||||
entries.set(
|
entries.set(
|
||||||
@@ -27,7 +31,7 @@ export const Plugin = define({
|
|||||||
? new Reference.LocalSource({
|
? new Reference.LocalSource({
|
||||||
type: "local",
|
type: "local",
|
||||||
path: AbsolutePath.make(
|
path: AbsolutePath.make(
|
||||||
localPath(directory, ctx.path.home, typeof entry === "string" ? entry : entry.path),
|
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
|
||||||
),
|
),
|
||||||
description: typeof entry === "string" ? undefined : entry.description,
|
description: typeof entry === "string" ? undefined : entry.description,
|
||||||
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
export * as ConfigSkillPlugin from "./skill"
|
export * as ConfigSkillPlugin from "./skill"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../../plugin/internal"
|
||||||
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 { AbsolutePath } from "../../schema"
|
import { AbsolutePath } from "../../schema"
|
||||||
import { SkillV2 } from "../../skill"
|
import { SkillV2 } from "../../skill"
|
||||||
|
import { Global } from "../../global"
|
||||||
|
import { Location } from "../../location"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "config-skill",
|
id: "config-skill",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
|
const global = yield* Global.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
yield* ctx.skill.transform(
|
yield* ctx.skill.transform(
|
||||||
Effect.fn(function* (draft) {
|
Effect.fn(function* (draft) {
|
||||||
const entries = yield* config.entries()
|
const entries = yield* config.entries()
|
||||||
@@ -29,13 +33,11 @@ export const Plugin = define({
|
|||||||
draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
|
draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const expanded = item.startsWith("~/") ? path.join(ctx.path.home, item.slice(2)) : item
|
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
|
||||||
draft.source(
|
draft.source(
|
||||||
new SkillV2.DirectorySource({
|
new SkillV2.DirectorySource({
|
||||||
type: "directory",
|
type: "directory",
|
||||||
path: AbsolutePath.make(
|
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
|
||||||
path.isAbsolute(expanded) ? expanded : path.join(ctx.location.directory, expanded),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -38,5 +38,7 @@ export const migrations = (
|
|||||||
import("./migration/20260611192811_lush_chimera"),
|
import("./migration/20260611192811_lush_chimera"),
|
||||||
import("./migration/20260612174303_project_dir_strategy"),
|
import("./migration/20260612174303_project_dir_strategy"),
|
||||||
import("./migration/20260622142730_simplify_session_context_epoch"),
|
import("./migration/20260622142730_simplify_session_context_epoch"),
|
||||||
|
import("./migration/20260622170816_reset_v2_session_state"),
|
||||||
|
import("./migration/20260622202450_simplify_session_input"),
|
||||||
])
|
])
|
||||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260622170816_reset_v2_session_state",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`DELETE FROM \`session_context_epoch\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`session_input\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||||
|
yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL WHERE \`workspace_id\` IS NOT NULL;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`workspace\`;`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260622202450_simplify_session_input",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`DELETE FROM \`session_context_epoch\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`session_input\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||||
|
yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL WHERE \`workspace_id\` IS NOT NULL;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`workspace\`;`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
@@ -46,6 +46,19 @@ export type Payload<D extends Definition = Definition> = {
|
|||||||
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
||||||
export type Unsubscribe = Effect.Effect<void>
|
export type Unsubscribe = Effect.Effect<void>
|
||||||
|
|
||||||
|
export const latestSequence = Effect.fn("EventV2.latestSequence")(function* (
|
||||||
|
db: Database.Interface["db"],
|
||||||
|
aggregateID: string,
|
||||||
|
) {
|
||||||
|
const row = yield* db
|
||||||
|
.select({ seq: EventSequenceTable.seq })
|
||||||
|
.from(EventSequenceTable)
|
||||||
|
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
return row?.seq ?? -1
|
||||||
|
})
|
||||||
|
|
||||||
export type SerializedEvent = {
|
export type SerializedEvent = {
|
||||||
readonly id: ID
|
readonly id: ID
|
||||||
readonly type: string
|
readonly type: string
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ export const locationLayer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
reload: state.reload,
|
||||||
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
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Catalog } from "./catalog"
|
|||||||
import { Integration } from "./integration"
|
import { Integration } from "./integration"
|
||||||
import { CommandV2 } from "./command"
|
import { CommandV2 } from "./command"
|
||||||
import { AgentV2 } from "./agent"
|
import { AgentV2 } from "./agent"
|
||||||
import { PluginBoot } from "./plugin/boot"
|
import { PluginInternal } from "./plugin/internal"
|
||||||
import { Project } from "./project"
|
import { Project } from "./project"
|
||||||
import { ProjectCopy } from "./project/copy"
|
import { ProjectCopy } from "./project/copy"
|
||||||
import { ProjectDirectories } from "./project/directories"
|
import { ProjectDirectories } from "./project/directories"
|
||||||
@@ -65,7 +65,7 @@ export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("
|
|||||||
Integration.locationLayer,
|
Integration.locationLayer,
|
||||||
CommandV2.locationLayer,
|
CommandV2.locationLayer,
|
||||||
AgentV2.locationLayer,
|
AgentV2.locationLayer,
|
||||||
PluginBoot.locationLayer,
|
PluginInternal.locationLayer,
|
||||||
ProjectCopy.locationLayer,
|
ProjectCopy.locationLayer,
|
||||||
FileSystem.locationLayer,
|
FileSystem.locationLayer,
|
||||||
Watcher.locationLayer,
|
Watcher.locationLayer,
|
||||||
|
|||||||
+70
-175
@@ -1,12 +1,17 @@
|
|||||||
export * as PluginV2 from "./plugin"
|
export * as PluginV2 from "./plugin"
|
||||||
|
|
||||||
import { createDraft, finishDraft, type Draft } from "immer"
|
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
|
||||||
import { Context, Effect, Exit, Layer, Schema, Scope } from "effect"
|
import { Context, Effect, Exit, Layer, Schema, Scope } from "effect"
|
||||||
import type { ModelV2 } from "./model"
|
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
import type { Catalog } from "./catalog"
|
import { AgentV2 } from "./agent"
|
||||||
|
import { AISDK } from "./aisdk"
|
||||||
|
import { Catalog } from "./catalog"
|
||||||
|
import { CommandV2 } from "./command"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
|
import { Integration } from "./integration"
|
||||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||||
|
import { PluginHost } from "./plugin/host"
|
||||||
|
import { Reference } from "./reference"
|
||||||
|
import { SkillV2 } from "./skill"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
||||||
@@ -21,69 +26,9 @@ export const Event = {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
type HookSpec = {
|
|
||||||
"catalog.transform": {
|
|
||||||
input: Catalog.Draft
|
|
||||||
output: {}
|
|
||||||
}
|
|
||||||
"aisdk.language": {
|
|
||||||
input: {
|
|
||||||
model: ModelV2.Info
|
|
||||||
sdk: any
|
|
||||||
options: Record<string, any>
|
|
||||||
}
|
|
||||||
output: {
|
|
||||||
language?: LanguageModelV3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"aisdk.sdk": {
|
|
||||||
input: {
|
|
||||||
model: ModelV2.Info
|
|
||||||
package: string
|
|
||||||
options: Record<string, any>
|
|
||||||
}
|
|
||||||
output: {
|
|
||||||
sdk?: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Hooks = {
|
|
||||||
[Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
|
|
||||||
-readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
|
|
||||||
? Draft<HookSpec[Name]["output"][Field]>
|
|
||||||
: HookSpec[Name]["output"][Field]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HookFunctions = {
|
|
||||||
[key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
|
|
||||||
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
|
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly add: (input: {
|
readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void>
|
||||||
id: string
|
|
||||||
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
|
|
||||||
}) => 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>(
|
|
||||||
id: ID,
|
|
||||||
name: Name,
|
|
||||||
input: HookInput<Name>,
|
|
||||||
output: HookOutput<Name>,
|
|
||||||
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
|
|
||||||
readonly trigger: <Name extends keyof Hooks>(
|
|
||||||
name: Name,
|
|
||||||
input: HookInput<Name>,
|
|
||||||
output: HookOutput<Name>,
|
|
||||||
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
|
||||||
@@ -91,127 +36,77 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
let hooks: {
|
|
||||||
id: ID
|
|
||||||
hooks: HookFunctions
|
|
||||||
scope: Scope.Closeable
|
|
||||||
}[] = []
|
|
||||||
let registrations: {
|
|
||||||
[Name in keyof Hooks]: {
|
|
||||||
name: Name
|
|
||||||
callback: (input: Hooks[Name]) => Effect.Effect<void> | void
|
|
||||||
}
|
|
||||||
}[keyof Hooks][] = []
|
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const locks = KeyedMutex.makeUnsafe<ID>()
|
const locks = KeyedMutex.makeUnsafe<ID>()
|
||||||
const scope = yield* Scope.make()
|
const scope = yield* Scope.make()
|
||||||
|
const active = new Map<ID, Scope.Closeable>()
|
||||||
|
const loading = new Set<ID>()
|
||||||
|
let host: Parameters<Plugin["effect"]>[0]
|
||||||
|
|
||||||
|
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: Plugin["effect"]) {
|
||||||
|
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
|
||||||
|
|
||||||
|
yield* locks.withLock(id)(
|
||||||
|
Effect.sync(() => loading.add(id)).pipe(
|
||||||
|
Effect.andThen(
|
||||||
|
State.batch(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const existing = active.get(id)
|
||||||
|
active.delete(id)
|
||||||
|
if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore)
|
||||||
|
|
||||||
|
const child = yield* Scope.fork(scope)
|
||||||
|
yield* effect(host).pipe(
|
||||||
|
Scope.provide(child),
|
||||||
|
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }),
|
||||||
|
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
||||||
|
)
|
||||||
|
active.set(id, child)
|
||||||
|
yield* events.publish(Event.Added, { id })
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.ensuring(Effect.sync(() => loading.delete(id))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const remove = Effect.fn("Plugin.remove")(function* (id: ID) {
|
||||||
|
if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`)
|
||||||
|
|
||||||
|
yield* locks.withLock(id)(
|
||||||
|
State.batch(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const current = active.get(id)
|
||||||
|
active.delete(id)
|
||||||
|
if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// One registry-owned scope lets shutdown remove every plugin transform in one batch.
|
|
||||||
yield* Effect.addFinalizer((exit) =>
|
yield* Effect.addFinalizer((exit) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
hooks = []
|
active.clear()
|
||||||
yield* State.batch(Scope.close(scope, exit))
|
yield* State.batch(Scope.close(scope, exit))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const svc = Service.of({
|
const service = Service.of({
|
||||||
add: Effect.fn("Plugin.add")(function* (input) {
|
add,
|
||||||
const id = ID.make(input.id)
|
remove,
|
||||||
yield* locks.withLock(id)(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const existing = hooks.find((item) => item.id === id)
|
|
||||||
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
|
|
||||||
const childScope = yield* Scope.fork(scope)
|
|
||||||
const result = yield* input.effect.pipe(
|
|
||||||
Scope.provide(childScope),
|
|
||||||
Effect.withSpan("Plugin.load", {
|
|
||||||
attributes: {
|
|
||||||
"plugin.id": id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
|
|
||||||
)
|
|
||||||
const next = {
|
|
||||||
id,
|
|
||||||
hooks: result ?? {},
|
|
||||||
scope: childScope,
|
|
||||||
}
|
|
||||||
hooks = existing ? hooks.with(hooks.indexOf(existing), next) : [...hooks, next]
|
|
||||||
yield* events.publish(Event.Added, { id })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
|
|
||||||
return yield* svc.triggerFor(ID.make("*"), name, input, output)
|
|
||||||
}),
|
|
||||||
triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) {
|
|
||||||
const draftEntries = new Map<string, ReturnType<typeof createDraft>>()
|
|
||||||
const event = {
|
|
||||||
...input,
|
|
||||||
...output,
|
|
||||||
} as Record<string, unknown>
|
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(output)) {
|
|
||||||
if (value && typeof value === "object") {
|
|
||||||
draftEntries.set(field, createDraft(value))
|
|
||||||
event[field] = draftEntries.get(field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of hooks) {
|
|
||||||
if (id !== ID.make("*") && item.id !== id) continue
|
|
||||||
const match = item.hooks[name]
|
|
||||||
if (!match) continue
|
|
||||||
yield* match(event as any).pipe(
|
|
||||||
Effect.withSpan(`Plugin.hook.${name}`, {
|
|
||||||
attributes: {
|
|
||||||
plugin: item.id,
|
|
||||||
hook: name,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of registrations) {
|
|
||||||
if (item.name !== name) continue
|
|
||||||
const result = item.callback(event as never)
|
|
||||||
if (Effect.isEffect(result)) yield* result
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [field, draft] of draftEntries) {
|
|
||||||
event[field] = finishDraft(draft)
|
|
||||||
}
|
|
||||||
|
|
||||||
return event as any
|
|
||||||
}),
|
|
||||||
remove: Effect.fn("Plugin.remove")(function* (id) {
|
|
||||||
yield* locks.withLock(id)(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const existing = hooks.find((item) => item.id === id)
|
|
||||||
hooks = hooks.filter((item) => item.id !== id)
|
|
||||||
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
hook: Effect.fn("Plugin.hook")(function* (name, callback) {
|
|
||||||
const scope = yield* Scope.Scope
|
|
||||||
const registration = { name, callback } as (typeof registrations)[number]
|
|
||||||
let active = true
|
|
||||||
registrations = [...registrations, registration]
|
|
||||||
const dispose = Effect.sync(() => {
|
|
||||||
if (!active) return
|
|
||||||
active = false
|
|
||||||
registrations = registrations.filter((item) => item !== registration)
|
|
||||||
})
|
})
|
||||||
yield* Scope.addFinalizer(scope, dispose)
|
host = yield* PluginHost.make(service)
|
||||||
return { dispose }
|
return service
|
||||||
}),
|
|
||||||
})
|
|
||||||
return svc
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer.pipe(
|
||||||
|
Layer.provideMerge(AgentV2.locationLayer),
|
||||||
// opencode
|
Layer.provideMerge(AISDK.locationLayer),
|
||||||
// sdcok
|
Layer.provideMerge(Catalog.locationLayer),
|
||||||
|
Layer.provideMerge(CommandV2.locationLayer),
|
||||||
|
Layer.provideMerge(Integration.locationLayer),
|
||||||
|
Layer.provideMerge(Reference.locationLayer),
|
||||||
|
Layer.provideMerge(SkillV2.locationLayer),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
export * as AgentPlugin from "./agent"
|
export * as AgentPlugin from "./agent"
|
||||||
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "./internal"
|
||||||
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"
|
||||||
|
|
||||||
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
|
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
|
||||||
@@ -99,7 +100,8 @@ Rules:
|
|||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "agent",
|
id: "agent",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const worktree = ctx.location.directory
|
const location = yield* Location.Service
|
||||||
|
const worktree = location.directory
|
||||||
const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
|
const 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" },
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
export * as PluginBoot from "./boot"
|
|
||||||
|
|
||||||
import type { Plugin as PublicPlugin } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { Context, Deferred, Effect, Layer } from "effect"
|
|
||||||
import { Integration } from "../integration"
|
|
||||||
import { AgentV2 } from "../agent"
|
|
||||||
import { Catalog } from "../catalog"
|
|
||||||
import { CommandV2 } from "../command"
|
|
||||||
import { Config } from "../config"
|
|
||||||
import { ConfigAgentPlugin } from "../config/plugin/agent"
|
|
||||||
import { ConfigCommandPlugin } from "../config/plugin/command"
|
|
||||||
import { ConfigSkillPlugin } from "../config/plugin/skill"
|
|
||||||
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
|
||||||
import { EventV2 } from "../event"
|
|
||||||
import { FSUtil } from "../fs-util"
|
|
||||||
import { FileSystem } from "../filesystem"
|
|
||||||
import { Global } from "../global"
|
|
||||||
import { Location } from "../location"
|
|
||||||
import { ModelsDev } from "../models-dev"
|
|
||||||
import { Npm } from "../npm"
|
|
||||||
import { PluginV2 } from "../plugin"
|
|
||||||
import { AgentPlugin } from "./agent"
|
|
||||||
import { CommandPlugin } from "./command"
|
|
||||||
import { SkillPlugin } from "./skill"
|
|
||||||
import { ConfigProviderPlugin } from "../config/plugin/provider"
|
|
||||||
import { ModelsDevPlugin } from "./models-dev"
|
|
||||||
import { ProviderPlugins } from "./provider"
|
|
||||||
import { SkillV2 } from "../skill"
|
|
||||||
import { Reference } from "../reference"
|
|
||||||
import { State } from "../state"
|
|
||||||
import { PluginHost } from "./host"
|
|
||||||
|
|
||||||
type InternalPlugin = PublicPlugin<any>
|
|
||||||
|
|
||||||
export interface Interface {
|
|
||||||
readonly add: (plugin: PublicPlugin<any>) => Effect.Effect<void>
|
|
||||||
readonly wait: () => Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginBoot") {}
|
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
|
||||||
Service,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const catalog = yield* Catalog.Service
|
|
||||||
const commands = yield* CommandV2.Service
|
|
||||||
const plugin = yield* PluginV2.Service
|
|
||||||
const integration = yield* Integration.Service
|
|
||||||
const agents = yield* AgentV2.Service
|
|
||||||
const config = yield* Config.Service
|
|
||||||
const location = yield* Location.Service
|
|
||||||
const modelsDev = yield* ModelsDev.Service
|
|
||||||
const npm = yield* Npm.Service
|
|
||||||
const events = yield* EventV2.Service
|
|
||||||
const fs = yield* FSUtil.Service
|
|
||||||
const filesystem = yield* FileSystem.Service
|
|
||||||
const global = yield* Global.Service
|
|
||||||
const skill = yield* SkillV2.Service
|
|
||||||
const reference = yield* Reference.Service
|
|
||||||
const host = yield* PluginHost.make()
|
|
||||||
const done = yield* Deferred.make<void>()
|
|
||||||
|
|
||||||
const add = Effect.fn("PluginBoot.add")(function* (input: InternalPlugin) {
|
|
||||||
yield* plugin.add({
|
|
||||||
id: input.id,
|
|
||||||
effect: input
|
|
||||||
.effect(host)
|
|
||||||
.pipe(
|
|
||||||
Effect.provideService(Catalog.Service, catalog),
|
|
||||||
Effect.provideService(CommandV2.Service, commands),
|
|
||||||
Effect.provideService(Integration.Service, integration),
|
|
||||||
Effect.provideService(AgentV2.Service, agents),
|
|
||||||
Effect.provideService(Config.Service, config),
|
|
||||||
Effect.provideService(Location.Service, location),
|
|
||||||
Effect.provideService(ModelsDev.Service, modelsDev),
|
|
||||||
Effect.provideService(Npm.Service, npm),
|
|
||||||
Effect.provideService(EventV2.Service, events),
|
|
||||||
Effect.provideService(FSUtil.Service, fs),
|
|
||||||
Effect.provideService(FileSystem.Service, filesystem),
|
|
||||||
Effect.provideService(Global.Service, global),
|
|
||||||
Effect.provideService(SkillV2.Service, skill),
|
|
||||||
Effect.provideService(Reference.Service, reference),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const boot = Effect.gen(function* () {
|
|
||||||
yield* State.batch(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* add(AgentPlugin.Plugin)
|
|
||||||
yield* add(CommandPlugin.Plugin)
|
|
||||||
yield* add(SkillPlugin.Plugin)
|
|
||||||
yield* add(ModelsDevPlugin)
|
|
||||||
yield* add(ConfigProviderPlugin.Plugin)
|
|
||||||
yield* add(ConfigAgentPlugin.Plugin)
|
|
||||||
yield* add(ConfigCommandPlugin.Plugin)
|
|
||||||
yield* add(ConfigSkillPlugin.Plugin)
|
|
||||||
yield* add(ConfigReferencePlugin.Plugin)
|
|
||||||
for (const item of ProviderPlugins) {
|
|
||||||
yield* add(item)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}).pipe(Effect.withSpan("PluginBoot.boot"))
|
|
||||||
|
|
||||||
yield* boot.pipe(
|
|
||||||
Effect.exit,
|
|
||||||
Effect.flatMap((exit) => Deferred.done(done, exit)),
|
|
||||||
Effect.forkScoped,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Service.of({
|
|
||||||
add: (input) =>
|
|
||||||
Deferred.await(done).pipe(
|
|
||||||
Effect.andThen(
|
|
||||||
plugin.add({
|
|
||||||
id: input.id,
|
|
||||||
effect: input.effect(host),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
wait: () => Deferred.await(done),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(
|
|
||||||
Layer.provideMerge(PluginV2.locationLayer),
|
|
||||||
Layer.provideMerge(Integration.locationLayer),
|
|
||||||
Layer.provideMerge(Catalog.locationLayer),
|
|
||||||
Layer.provideMerge(CommandV2.locationLayer),
|
|
||||||
Layer.provideMerge(Config.locationLayer),
|
|
||||||
Layer.provideMerge(AgentV2.locationLayer),
|
|
||||||
Layer.provideMerge(SkillV2.locationLayer),
|
|
||||||
Layer.provideMerge(Reference.locationLayer),
|
|
||||||
Layer.provideMerge(FileSystem.locationLayer),
|
|
||||||
)
|
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
export * as CommandPlugin from "./command"
|
export * as CommandPlugin from "./command"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "./internal"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
import { Location } from "../location"
|
||||||
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 = define({
|
||||||
id: "command",
|
id: "command",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
|
const location = yield* Location.Service
|
||||||
yield* ctx.command.transform((draft) => {
|
yield* ctx.command.transform((draft) => {
|
||||||
draft.update("init", (command) => {
|
draft.update("init", (command) => {
|
||||||
command.template = PROMPT_INITIALIZE.replace("${path}", ctx.location.project.directory)
|
command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory)
|
||||||
command.description = "guided AGENTS.md setup"
|
command.description = "guided AGENTS.md setup"
|
||||||
})
|
})
|
||||||
draft.update("review", (command) => {
|
draft.update("review", (command) => {
|
||||||
command.template = PROMPT_REVIEW.replace("${path}", ctx.location.project.directory)
|
command.template = PROMPT_REVIEW.replace("${path}", location.project.directory)
|
||||||
command.description = "review changes [commit|branch|pr], defaults to uncommitted"
|
command.description = "review changes [commit|branch|pr], defaults to uncommitted"
|
||||||
command.subtask = true
|
command.subtask = true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,58 +1,31 @@
|
|||||||
export * as PluginHost from "./host"
|
export * as PluginHost from "./host"
|
||||||
|
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect"
|
||||||
import type { PluginHost as Interface } from "@opencode-ai/plugin/v2/effect"
|
import { Effect, Schema } from "effect"
|
||||||
import type { Event as SDKEvent, ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
|
||||||
import { Effect, Schema, Stream } from "effect"
|
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
|
import { AISDK } from "../aisdk"
|
||||||
import { Catalog } from "../catalog"
|
import { Catalog } from "../catalog"
|
||||||
import { CommandV2 } from "../command"
|
import { CommandV2 } from "../command"
|
||||||
import { EventV2 } from "../event"
|
|
||||||
import { FileSystem } from "../filesystem"
|
|
||||||
import { Global } from "../global"
|
|
||||||
import { Integration } from "../integration"
|
import { Integration } from "../integration"
|
||||||
import { Location } from "../location"
|
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
import { Npm } from "../npm"
|
|
||||||
import { PluginV2 } from "../plugin"
|
import { PluginV2 } from "../plugin"
|
||||||
import { ProviderV2 } from "../provider"
|
import { ProviderV2 } from "../provider"
|
||||||
import { Reference } from "../reference"
|
import { Reference } from "../reference"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
|
|
||||||
type EventMap = { [Item in SDKEvent as Item["type"]]: Item }
|
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
||||||
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 agents = yield* AgentV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const commands = yield* CommandV2.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 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 reference = yield* Reference.Service
|
||||||
const skill = yield* SkillV2.Service
|
const skill = yield* SkillV2.Service
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
options: {},
|
||||||
agent: {
|
agent: {
|
||||||
get: (id) => agents.get(AgentV2.ID.make(id)),
|
reload: agents.reload,
|
||||||
default: agents.default,
|
|
||||||
list: agents.all,
|
|
||||||
rebuild: agents.rebuild,
|
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
agents.transform((draft) =>
|
agents.transform((draft) =>
|
||||||
callback({
|
callback({
|
||||||
@@ -65,51 +38,35 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
aisdk: {
|
aisdk: {
|
||||||
hook: (name, callback) => {
|
sdk: (callback) =>
|
||||||
if (name === "sdk") {
|
aisdk.hook.sdk((event) => {
|
||||||
const run = callback as SDKHook
|
|
||||||
return plugin.hook("aisdk.sdk", (event) => {
|
|
||||||
const output = {
|
const output = {
|
||||||
model: event.model,
|
model: event.model,
|
||||||
package: event.package,
|
package: event.package,
|
||||||
options: event.options,
|
options: event.options,
|
||||||
sdk: event.sdk,
|
sdk: event.sdk,
|
||||||
}
|
}
|
||||||
const result = run(output)
|
const result = callback(output)
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
}
|
language: (callback) =>
|
||||||
const run = callback as LanguageHook
|
aisdk.hook.language((event) => {
|
||||||
return plugin.hook("aisdk.language", (event) => {
|
|
||||||
const output = {
|
const output = {
|
||||||
model: event.model,
|
model: event.model,
|
||||||
sdk: event.sdk,
|
sdk: event.sdk,
|
||||||
options: event.options,
|
options: event.options,
|
||||||
language: event.language,
|
language: event.language,
|
||||||
}
|
}
|
||||||
const result = run(output)
|
const result = callback(output)
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
catalog: {
|
catalog: {
|
||||||
provider: {
|
reload: catalog.reload,
|
||||||
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) =>
|
transform: (callback) =>
|
||||||
catalog.transform((draft) =>
|
catalog.transform((draft) =>
|
||||||
callback({
|
callback({
|
||||||
@@ -135,41 +92,11 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
get: commands.get,
|
reload: commands.reload,
|
||||||
list: commands.list,
|
|
||||||
rebuild: commands.rebuild,
|
|
||||||
transform: commands.transform,
|
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: {
|
integration: {
|
||||||
get: (id) => integration.get(Integration.ID.make(id)),
|
reload: integration.reload,
|
||||||
list: integration.list,
|
|
||||||
rebuild: integration.rebuild,
|
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
integration.transform((draft) =>
|
integration.transform((draft) =>
|
||||||
callback({
|
callback({
|
||||||
@@ -198,19 +125,12 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
location,
|
plugin: {
|
||||||
npm,
|
add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect),
|
||||||
path: {
|
remove: (id) => plugin.remove(PluginV2.ID.make(id)),
|
||||||
home: global.home,
|
|
||||||
data: global.data,
|
|
||||||
cache: global.cache,
|
|
||||||
config: global.config,
|
|
||||||
state: global.state,
|
|
||||||
temp: global.tmp,
|
|
||||||
},
|
},
|
||||||
reference: {
|
reference: {
|
||||||
list: reference.list,
|
reload: reference.reload,
|
||||||
rebuild: reference.rebuild,
|
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
reference.transform((draft) =>
|
reference.transform((draft) =>
|
||||||
callback({
|
callback({
|
||||||
@@ -221,9 +141,7 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
skill: {
|
skill: {
|
||||||
sources: skill.sources,
|
reload: skill.reload,
|
||||||
list: skill.list,
|
|
||||||
rebuild: skill.rebuild,
|
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
skill.transform((draft) =>
|
skill.transform((draft) =>
|
||||||
callback({
|
callback({
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
export * as PluginInternal from "./internal"
|
||||||
|
|
||||||
|
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Effect, Layer, Scope } from "effect"
|
||||||
|
import { AgentV2 } from "../agent"
|
||||||
|
import { Catalog } from "../catalog"
|
||||||
|
import { CommandV2 } from "../command"
|
||||||
|
import { Config } from "../config"
|
||||||
|
import { ConfigAgentPlugin } from "../config/plugin/agent"
|
||||||
|
import { ConfigCommandPlugin } from "../config/plugin/command"
|
||||||
|
import { ConfigExternalPlugin } from "../config/plugin/external"
|
||||||
|
import { ConfigProviderPlugin } from "../config/plugin/provider"
|
||||||
|
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
||||||
|
import { ConfigSkillPlugin } from "../config/plugin/skill"
|
||||||
|
import { EventV2 } from "../event"
|
||||||
|
import { FileSystem } from "../filesystem"
|
||||||
|
import { FSUtil } from "../fs-util"
|
||||||
|
import { Global } from "../global"
|
||||||
|
import { Integration } from "../integration"
|
||||||
|
import { Location } from "../location"
|
||||||
|
import { ModelsDev } from "../models-dev"
|
||||||
|
import { Npm } from "../npm"
|
||||||
|
import { PluginV2 } from "../plugin"
|
||||||
|
import { Reference } from "../reference"
|
||||||
|
import { SkillV2 } from "../skill"
|
||||||
|
import { AgentPlugin } from "./agent"
|
||||||
|
import { CommandPlugin } from "./command"
|
||||||
|
import { ModelsDevPlugin } from "./models-dev"
|
||||||
|
import { ProviderPlugins } from "./provider"
|
||||||
|
import { SkillPlugin } from "./skill"
|
||||||
|
|
||||||
|
export type Requirements =
|
||||||
|
| AgentV2.Service
|
||||||
|
| Catalog.Service
|
||||||
|
| CommandV2.Service
|
||||||
|
| Config.Service
|
||||||
|
| EventV2.Service
|
||||||
|
| FileSystem.Service
|
||||||
|
| FSUtil.Service
|
||||||
|
| Global.Service
|
||||||
|
| Integration.Service
|
||||||
|
| Location.Service
|
||||||
|
| ModelsDev.Service
|
||||||
|
| Npm.Service
|
||||||
|
| Reference.Service
|
||||||
|
| SkillV2.Service
|
||||||
|
|
||||||
|
export interface Plugin<R = never> {
|
||||||
|
readonly id: string
|
||||||
|
readonly effect: (context: PluginContext) => Effect.Effect<void, never, R | Scope.Scope>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function define<R>(plugin: Plugin<R>) {
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
export const locationLayer = Layer.effectDiscard(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const commands = yield* CommandV2.Service
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
|
const integration = yield* Integration.Service
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const config = yield* Config.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const modelsDev = yield* ModelsDev.Service
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const filesystem = yield* FileSystem.Service
|
||||||
|
const global = yield* Global.Service
|
||||||
|
const skill = yield* SkillV2.Service
|
||||||
|
const reference = yield* Reference.Service
|
||||||
|
const add = <R>(input: Plugin<R>) => {
|
||||||
|
const loaded = {
|
||||||
|
id: input.id,
|
||||||
|
effect: (context: PluginContext) =>
|
||||||
|
input
|
||||||
|
.effect(context)
|
||||||
|
.pipe(
|
||||||
|
Effect.provideService(Catalog.Service, catalog),
|
||||||
|
Effect.provideService(CommandV2.Service, commands),
|
||||||
|
Effect.provideService(Integration.Service, integration),
|
||||||
|
Effect.provideService(AgentV2.Service, agents),
|
||||||
|
Effect.provideService(Config.Service, config),
|
||||||
|
Effect.provideService(Location.Service, location),
|
||||||
|
Effect.provideService(ModelsDev.Service, modelsDev),
|
||||||
|
Effect.provideService(Npm.Service, npm),
|
||||||
|
Effect.provideService(EventV2.Service, events),
|
||||||
|
Effect.provideService(FSUtil.Service, fs),
|
||||||
|
Effect.provideService(FileSystem.Service, filesystem),
|
||||||
|
Effect.provideService(Global.Service, global),
|
||||||
|
Effect.provideService(SkillV2.Service, skill),
|
||||||
|
Effect.provideService(Reference.Service, reference),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect)
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
yield* add(AgentPlugin.Plugin)
|
||||||
|
yield* add(CommandPlugin.Plugin)
|
||||||
|
yield* add(SkillPlugin.Plugin)
|
||||||
|
yield* add(ModelsDevPlugin)
|
||||||
|
yield* add(ConfigProviderPlugin.Plugin)
|
||||||
|
yield* add(ConfigAgentPlugin.Plugin)
|
||||||
|
yield* add(ConfigCommandPlugin.Plugin)
|
||||||
|
yield* add(ConfigSkillPlugin.Plugin)
|
||||||
|
yield* add(ConfigReferencePlugin.Plugin)
|
||||||
|
for (const item of ProviderPlugins) yield* add(item)
|
||||||
|
yield* add(ConfigExternalPlugin.Plugin)
|
||||||
|
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
Layer.provideMerge(PluginV2.locationLayer),
|
||||||
|
Layer.provideMerge(Config.locationLayer),
|
||||||
|
Layer.provideMerge(FileSystem.locationLayer),
|
||||||
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "./internal"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
|
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"
|
||||||
@@ -52,6 +53,7 @@ export const ModelsDevPlugin = define({
|
|||||||
id: "models-dev",
|
id: "models-dev",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const modelsDev = yield* ModelsDev.Service
|
const modelsDev = yield* ModelsDev.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
yield* ctx.integration.transform(
|
yield* ctx.integration.transform(
|
||||||
Effect.fn(function* (integrations) {
|
Effect.fn(function* (integrations) {
|
||||||
const data = yield* modelsDev.get()
|
const data = yield* modelsDev.get()
|
||||||
@@ -128,8 +130,8 @@ export const ModelsDevPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.event.subscribe("models-dev.refreshed").pipe(
|
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||||
Stream.runForEach(() => ctx.integration.rebuild().pipe(Effect.andThen(ctx.catalog.rebuild()))),
|
Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))),
|
||||||
Effect.forkScoped({ startImmediately: true }),
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
export * as PluginPromise from "./promise"
|
||||||
|
|
||||||
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import type { Plugin, PluginContext, Registration } from "@opencode-ai/plugin/v2/promise"
|
||||||
|
import { Effect, Scope } from "effect"
|
||||||
|
|
||||||
|
// The Effect host hands back this registration shape; mirror it structurally so
|
||||||
|
// we do not have to alias the Effect package's `Registration` against the Promise one.
|
||||||
|
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
||||||
|
* loader (`PluginV2` / `PluginInternal`) can run it unchanged.
|
||||||
|
*
|
||||||
|
* Hook registrations created during the async `setup` attach to the plugin's
|
||||||
|
* scope, so unloading the plugin disposes them. The captured fiber context
|
||||||
|
* preserves boot-time batching, so Promise-plugin transforms still coalesce
|
||||||
|
* into one reload per domain.
|
||||||
|
*/
|
||||||
|
export function fromPromise(plugin: Plugin) {
|
||||||
|
return define({
|
||||||
|
id: plugin.id,
|
||||||
|
effect: (host) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const scope = yield* Scope.Scope
|
||||||
|
const context = yield* Effect.context<Scope.Scope>()
|
||||||
|
|
||||||
|
// Run a hook registration on the plugin scope and resolve once it is registered.
|
||||||
|
const register = (effect: Effect.Effect<HostRegistration, never, Scope.Scope>): Promise<Registration> =>
|
||||||
|
Effect.runPromiseWith(context)(Scope.provide(scope)(effect)).then((registration) => ({
|
||||||
|
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const run = (effect: Effect.Effect<void>) => Effect.runPromiseWith(context)(effect)
|
||||||
|
|
||||||
|
const transform =
|
||||||
|
<Draft>(domain: {
|
||||||
|
transform: (
|
||||||
|
callback: (draft: Draft) => Effect.Effect<void> | void,
|
||||||
|
) => Effect.Effect<HostRegistration, never, Scope.Scope>
|
||||||
|
}) =>
|
||||||
|
(callback: (draft: Draft) => Promise<void> | void) =>
|
||||||
|
register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft)))))
|
||||||
|
|
||||||
|
const context2: PluginContext = {
|
||||||
|
options: host.options,
|
||||||
|
agent: {
|
||||||
|
transform: transform(host.agent),
|
||||||
|
reload: () => run(host.agent.reload()),
|
||||||
|
},
|
||||||
|
aisdk: {
|
||||||
|
sdk: (callback) =>
|
||||||
|
register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
|
language: (callback) =>
|
||||||
|
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
|
},
|
||||||
|
catalog: {
|
||||||
|
transform: transform(host.catalog),
|
||||||
|
reload: () => run(host.catalog.reload()),
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
transform: transform(host.command),
|
||||||
|
reload: () => run(host.command.reload()),
|
||||||
|
},
|
||||||
|
integration: {
|
||||||
|
transform: transform(host.integration),
|
||||||
|
reload: () => run(host.integration.reload()),
|
||||||
|
},
|
||||||
|
plugin: {
|
||||||
|
add: (input) => {
|
||||||
|
const child = fromPromise(input)
|
||||||
|
return run(host.plugin.add(child))
|
||||||
|
},
|
||||||
|
remove: (id) => run(host.plugin.remove(id)),
|
||||||
|
},
|
||||||
|
reference: {
|
||||||
|
transform: transform(host.reference),
|
||||||
|
reload: () => run(host.reference.reload()),
|
||||||
|
},
|
||||||
|
skill: {
|
||||||
|
transform: transform(host.skill),
|
||||||
|
reload: () => run(host.skill.reload()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -30,8 +30,10 @@ import { VercelPlugin } from "./provider/vercel"
|
|||||||
import { VenicePlugin } from "./provider/venice"
|
import { VenicePlugin } from "./provider/venice"
|
||||||
import { XAIPlugin } from "./provider/xai"
|
import { XAIPlugin } from "./provider/xai"
|
||||||
import { ZenmuxPlugin } from "./provider/zenmux"
|
import { ZenmuxPlugin } from "./provider/zenmux"
|
||||||
|
import type { PluginInternal } from "./internal"
|
||||||
|
import type { Scope } from "effect"
|
||||||
|
|
||||||
export const ProviderPlugins = [
|
export const ProviderPlugins: PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>[] = [
|
||||||
AlibabaPlugin,
|
AlibabaPlugin,
|
||||||
AmazonBedrockPlugin,
|
AmazonBedrockPlugin,
|
||||||
AnthropicPlugin,
|
AnthropicPlugin,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const AlibabaPlugin = define({
|
export const AlibabaPlugin = define({
|
||||||
id: "alibaba",
|
id: "alibaba",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -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 { define } from "../internal"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
type MantleSDK = {
|
type MantleSDK = {
|
||||||
@@ -78,8 +78,7 @@ export const AmazonBedrockPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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 }
|
||||||
@@ -112,8 +111,7 @@ export const AmazonBedrockPlugin = define({
|
|||||||
evt.sdk = mod.createAmazonBedrock(options)
|
evt.sdk = mod.createAmazonBedrock(options)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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") {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const AnthropicPlugin = define({
|
export const AnthropicPlugin = define({
|
||||||
id: "anthropic",
|
id: "anthropic",
|
||||||
@@ -16,8 +16,7 @@ export const AnthropicPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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"))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
||||||
@@ -28,8 +28,7 @@ export const AzurePlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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) {
|
||||||
@@ -47,8 +46,7 @@ export const AzurePlugin = define({
|
|||||||
evt.sdk = mod.createAzure(evt.options)
|
evt.sdk = mod.createAzure(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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))
|
||||||
@@ -74,8 +72,7 @@ export const AzureCognitiveServicesPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const CerebrasPlugin = define({
|
export const CerebrasPlugin = define({
|
||||||
id: "cerebras",
|
id: "cerebras",
|
||||||
@@ -15,8 +15,7 @@ export const CerebrasPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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"))
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
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 { define } from "../internal"
|
||||||
|
|
||||||
export const CloudflareAIGatewayPlugin = define({
|
export const CloudflareAIGatewayPlugin = define({
|
||||||
id: "cloudflare-ai-gateway",
|
id: "cloudflare-ai-gateway",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { define } from "../internal"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
||||||
@@ -21,8 +21,7 @@ export const CloudflareWorkersAIPlugin = define({
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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
|
||||||
@@ -38,8 +37,7 @@ export const CloudflareWorkersAIPlugin = define({
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const CoherePlugin = define({
|
export const CoherePlugin = define({
|
||||||
id: "cohere",
|
id: "cohere",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const DeepInfraPlugin = define({
|
export const DeepInfraPlugin = define({
|
||||||
id: "deepinfra",
|
id: "deepinfra",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
import { Npm } from "../../npm"
|
||||||
|
|
||||||
export const DynamicProviderPlugin = define({
|
export const DynamicProviderPlugin = define({
|
||||||
id: "dynamic-provider",
|
id: "dynamic-provider",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
const npm = yield* Npm.Service
|
||||||
"sdk",
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
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
|
: (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 () => {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const GatewayPlugin = define({
|
export const GatewayPlugin = define({
|
||||||
id: "gateway",
|
id: "gateway",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -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 { define } from "../internal"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function shouldUseResponses(modelID: string) {
|
function shouldUseResponses(modelID: string) {
|
||||||
@@ -25,16 +25,14 @@ export const GithubCopilotPlugin = define({
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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) {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
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 { define } from "../internal"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const GitLabPlugin = define({
|
export const GitLabPlugin = define({
|
||||||
id: "gitlab",
|
id: "gitlab",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
@@ -32,8 +31,7 @@ export const GitLabPlugin = define({
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||||
const featureFlags =
|
const featureFlags =
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function resolveProject(options: Record<string, any>) {
|
function resolveProject(options: Record<string, any>) {
|
||||||
@@ -84,8 +84,7 @@ export const GoogleVertexPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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)
|
||||||
@@ -104,8 +103,7 @@ export const GoogleVertexPlugin = define({
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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())
|
||||||
@@ -139,8 +137,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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"))
|
||||||
@@ -166,8 +163,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const GooglePlugin = define({
|
export const GooglePlugin = define({
|
||||||
id: "google",
|
id: "google",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const GroqPlugin = define({
|
export const GroqPlugin = define({
|
||||||
id: "groq",
|
id: "groq",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const KiloPlugin = define({
|
export const KiloPlugin = define({
|
||||||
id: "kilo",
|
id: "kilo",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
import { Integration } from "../../integration"
|
||||||
|
|
||||||
export const LLMGatewayPlugin = define({
|
export const LLMGatewayPlugin = define({
|
||||||
id: "llmgateway",
|
id: "llmgateway",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
yield* ctx.catalog.transform(
|
yield* ctx.catalog.transform(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
@@ -11,7 +13,7 @@ export const LLMGatewayPlugin = define({
|
|||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.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
|
if (!(yield* integrations.get(Integration.ID.make(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"
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const MistralPlugin = define({
|
export const MistralPlugin = define({
|
||||||
id: "mistral",
|
id: "mistral",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const NvidiaPlugin = define({
|
export const NvidiaPlugin = define({
|
||||||
id: "nvidia",
|
id: "nvidia",
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const OpenAICompatiblePlugin = define({
|
export const OpenAICompatiblePlugin = define({
|
||||||
id: "openai-compatible",
|
id: "openai-compatible",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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
|
||||||
|
|||||||
@@ -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 { define } from "../internal"
|
||||||
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"
|
||||||
@@ -27,16 +27,14 @@ export const OpenAIPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.package !== "@ai-sdk/openai") return
|
if (evt.package !== "@ai-sdk/openai") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
|
||||||
evt.sdk = mod.createOpenAI(evt.options)
|
evt.sdk = mod.createOpenAI(evt.options)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
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 { define } from "../internal"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
import { Integration } from "../../integration"
|
||||||
|
|
||||||
export const OpencodePlugin = define({
|
export const OpencodePlugin = define({
|
||||||
id: "opencode",
|
id: "opencode",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
let hasKey = false
|
let hasKey = false
|
||||||
yield* ctx.catalog.transform(
|
yield* ctx.catalog.transform(
|
||||||
Effect.fn(function* (evt) {
|
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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 { define } from "../internal"
|
||||||
|
|
||||||
export const OpenRouterPlugin = define({
|
export const OpenRouterPlugin = define({
|
||||||
id: "openrouter",
|
id: "openrouter",
|
||||||
@@ -25,8 +25,7 @@ export const OpenRouterPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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"))
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const PerplexityPlugin = define({
|
export const PerplexityPlugin = define({
|
||||||
id: "perplexity",
|
id: "perplexity",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
import { Npm } from "../../npm"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const SapAICorePlugin = define({
|
export const SapAICorePlugin = define({
|
||||||
id: "sap-ai-core",
|
id: "sap-ai-core",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
const npm = yield* Npm.Service
|
||||||
"sdk",
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
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 =
|
||||||
@@ -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
|
: (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 () => {
|
||||||
@@ -35,8 +36,7 @@ export const SapAICorePlugin = define({
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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 { define } from "../internal"
|
||||||
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>
|
||||||
@@ -67,8 +67,7 @@ export function cortexFetch(upstream: FetchLike = fetch) {
|
|||||||
export const SnowflakeCortexPlugin = define({
|
export const SnowflakeCortexPlugin = define({
|
||||||
id: "snowflake-cortex",
|
id: "snowflake-cortex",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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 =
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const TogetherAIPlugin = define({
|
export const TogetherAIPlugin = define({
|
||||||
id: "togetherai",
|
id: "togetherai",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const VenicePlugin = define({
|
export const VenicePlugin = define({
|
||||||
id: "venice",
|
id: "venice",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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"))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const VercelPlugin = define({
|
export const VercelPlugin = define({
|
||||||
id: "vercel",
|
id: "vercel",
|
||||||
@@ -16,8 +16,7 @@ export const VercelPlugin = define({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
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"))
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const XAIPlugin = define({
|
export const XAIPlugin = define({
|
||||||
id: "xai",
|
id: "xai",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.sdk(
|
||||||
"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)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.hook(
|
yield* ctx.aisdk.language(
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
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,5 +1,5 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "../internal"
|
||||||
|
|
||||||
export const ZenmuxPlugin = define({
|
export const ZenmuxPlugin = define({
|
||||||
id: "zenmux",
|
id: "zenmux",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
export * as SkillPlugin from "./skill"
|
export * as SkillPlugin from "./skill"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "./internal"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { AbsolutePath } from "../schema"
|
import { AbsolutePath } from "../schema"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Slug } from "../util/slug"
|
|||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { PluginBoot } from "../plugin/boot"
|
|
||||||
|
|
||||||
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
|
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
|
||||||
export type StrategyID = typeof StrategyID.Type
|
export type StrategyID = typeof StrategyID.Type
|
||||||
@@ -125,10 +124,8 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
|
|||||||
|
|
||||||
export const refreshAfterBoot = Effect.gen(function* () {
|
export const refreshAfterBoot = Effect.gen(function* () {
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const boot = yield* PluginBoot.Service
|
|
||||||
const copies = yield* Service
|
const copies = yield* Service
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
yield* boot.wait()
|
|
||||||
yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id })
|
yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id })
|
||||||
const result = yield* copies.refresh({ projectID: location.project.id })
|
const result = yield* copies.refresh({ projectID: location.project.id })
|
||||||
yield* Effect.logInfo("project copy refresh done", {
|
yield* Effect.logInfo("project copy refresh done", {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
reload: state.reload,
|
||||||
list: Effect.fn("Reference.list")(function* () {
|
list: Effect.fn("Reference.list")(function* () {
|
||||||
return Array.from(materialized.values())
|
return Array.from(materialized.values())
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export * as ReferenceGuidance from "./guidance"
|
export * as ReferenceGuidance from "./guidance"
|
||||||
|
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { PluginBoot } from "../plugin/boot"
|
|
||||||
import { Reference } from "../reference"
|
import { Reference } from "../reference"
|
||||||
import { SystemContext } from "../system-context/index"
|
import { SystemContext } from "../system-context/index"
|
||||||
|
|
||||||
@@ -34,12 +33,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const boot = yield* PluginBoot.Service
|
|
||||||
const references = yield* Reference.Service
|
const references = yield* Reference.Service
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
load: Effect.fn("ReferenceGuidance.load")(function* () {
|
load: Effect.fn("ReferenceGuidance.load")(function* () {
|
||||||
yield* boot.wait()
|
|
||||||
const available = (yield* references.list())
|
const available = (yield* references.list())
|
||||||
.filter((reference) => reference.description !== undefined)
|
.filter((reference) => reference.description !== undefined)
|
||||||
.map((reference) => ({
|
.map((reference) => ({
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const prepareOnce = Effect.fnUntraced(function* (
|
|||||||
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
||||||
}
|
}
|
||||||
if (result._tag === "ReplacementReady") {
|
if (result._tag === "ReplacementReady") {
|
||||||
const baselineSeq = replacementSeq ?? (yield* SessionInput.latestSeq(db, sessionID))
|
const baselineSeq = replacementSeq ?? (yield* EventV2.latestSequence(db, sessionID))
|
||||||
yield* replace(db, sessionID, baselineSeq, result.generation)
|
yield* replace(db, sessionID, baselineSeq, result.generation)
|
||||||
return { baseline: result.generation.baseline, baselineSeq }
|
return { baseline: result.generation.baseline, baselineSeq }
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ const insert = Effect.fnUntraced(function* (
|
|||||||
sessionID: SessionSchema.ID,
|
sessionID: SessionSchema.ID,
|
||||||
generation: SystemContext.Generation,
|
generation: SystemContext.Generation,
|
||||||
) {
|
) {
|
||||||
const baselineSeq = yield* SessionInput.latestSeq(db, sessionID)
|
const baselineSeq = yield* EventV2.latestSequence(db, sessionID)
|
||||||
yield* db
|
yield* db
|
||||||
.insert(SessionContextEpochTable)
|
.insert(SessionContextEpochTable)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ const Base = {
|
|||||||
timestamp: V2Schema.DateTimeUtcFromMillis,
|
timestamp: V2Schema.DateTimeUtcFromMillis,
|
||||||
sessionID: SessionSchema.ID,
|
sessionID: SessionSchema.ID,
|
||||||
}
|
}
|
||||||
|
const PromptFields = {
|
||||||
|
...Base,
|
||||||
|
messageID: SessionMessageID.ID,
|
||||||
|
prompt: Prompt,
|
||||||
|
delivery: Schema.Literals(["steer", "queue"]),
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
durable: {
|
durable: {
|
||||||
@@ -83,40 +89,16 @@ export type Moved = typeof Moved.Type
|
|||||||
export const Prompted = EventV2.define({
|
export const Prompted = EventV2.define({
|
||||||
type: "session.next.prompted",
|
type: "session.next.prompted",
|
||||||
...options,
|
...options,
|
||||||
schema: {
|
schema: PromptFields,
|
||||||
...Base,
|
|
||||||
messageID: SessionMessageID.ID,
|
|
||||||
prompt: Prompt,
|
|
||||||
delivery: Schema.Literals(["steer", "queue"]),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
export type Prompted = typeof Prompted.Type
|
export type Prompted = typeof Prompted.Type
|
||||||
|
|
||||||
export namespace PromptLifecycle {
|
export const PromptAdmitted = EventV2.define({
|
||||||
export const Admitted = EventV2.define({
|
|
||||||
type: "session.next.prompt.admitted",
|
type: "session.next.prompt.admitted",
|
||||||
...options,
|
...options,
|
||||||
schema: {
|
schema: PromptFields,
|
||||||
...Base,
|
})
|
||||||
messageID: SessionMessageID.ID,
|
export type PromptAdmitted = typeof PromptAdmitted.Type
|
||||||
prompt: Prompt,
|
|
||||||
delivery: Schema.Literals(["steer", "queue"]),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export type Admitted = typeof Admitted.Type
|
|
||||||
|
|
||||||
export const Promoted = EventV2.define({
|
|
||||||
type: "session.next.prompt.promoted",
|
|
||||||
...options,
|
|
||||||
schema: {
|
|
||||||
...Base,
|
|
||||||
messageID: SessionMessageID.ID,
|
|
||||||
prompt: Prompt,
|
|
||||||
timeCreated: V2Schema.DateTimeUtcFromMillis,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export type Promoted = typeof Promoted.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContextUpdated = EventV2.define({
|
export const ContextUpdated = EventV2.define({
|
||||||
type: "session.next.context.updated",
|
type: "session.next.context.updated",
|
||||||
@@ -436,20 +418,9 @@ export namespace Compaction {
|
|||||||
})
|
})
|
||||||
export type Delta = typeof Delta.Type
|
export type Delta = typeof Delta.Type
|
||||||
|
|
||||||
// Retain the unpublished v1 decoder so stored beta events remain replayable.
|
|
||||||
export const EndedV1 = EventV2.define({
|
|
||||||
type: "session.next.compaction.ended",
|
|
||||||
...options,
|
|
||||||
schema: {
|
|
||||||
...Base,
|
|
||||||
text: Schema.String,
|
|
||||||
include: Schema.String.pipe(Schema.optional),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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 },
|
...options,
|
||||||
schema: {
|
schema: {
|
||||||
...Base,
|
...Base,
|
||||||
messageID: SessionMessageID.ID,
|
messageID: SessionMessageID.ID,
|
||||||
@@ -466,8 +437,7 @@ const DurableDefinitions = [
|
|||||||
ModelSwitched,
|
ModelSwitched,
|
||||||
Moved,
|
Moved,
|
||||||
Prompted,
|
Prompted,
|
||||||
PromptLifecycle.Admitted,
|
PromptAdmitted,
|
||||||
PromptLifecycle.Promoted,
|
|
||||||
ContextUpdated,
|
ContextUpdated,
|
||||||
Synthetic,
|
Synthetic,
|
||||||
Shell.Started,
|
Shell.Started,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { and, asc, eq, isNull, lte } from "drizzle-orm"
|
|||||||
import { DateTime, Effect, Schema } from "effect"
|
import { DateTime, Effect, Schema } from "effect"
|
||||||
import type { Database } from "../database/database"
|
import type { Database } from "../database/database"
|
||||||
import type { EventV2 } from "../event"
|
import type { EventV2 } from "../event"
|
||||||
import { EventSequenceTable } from "../event/sql"
|
|
||||||
import { NonNegativeInt } from "../schema"
|
import { NonNegativeInt } from "../schema"
|
||||||
import { V2Schema } from "../v2-schema"
|
import { V2Schema } from "../v2-schema"
|
||||||
import { SessionEvent } from "./event"
|
import { SessionEvent } from "./event"
|
||||||
@@ -65,7 +64,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
|||||||
if (existing !== undefined) return existing
|
if (existing !== undefined) return existing
|
||||||
const timestamp = yield* DateTime.now
|
const timestamp = yield* DateTime.now
|
||||||
return yield* events
|
return yield* events
|
||||||
.publish(SessionEvent.PromptLifecycle.Admitted, {
|
.publish(SessionEvent.PromptAdmitted, {
|
||||||
messageID: input.id,
|
messageID: input.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -93,19 +92,6 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const latestSeq = Effect.fn("SessionInput.latestSeq")(function* (
|
|
||||||
db: DatabaseService,
|
|
||||||
sessionID: SessionSchema.ID,
|
|
||||||
) {
|
|
||||||
const row = yield* db
|
|
||||||
.select({ seq: EventSequenceTable.seq })
|
|
||||||
.from(EventSequenceTable)
|
|
||||||
.where(eq(EventSequenceTable.aggregate_id, sessionID))
|
|
||||||
.get()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
return row?.seq ?? -1
|
|
||||||
})
|
|
||||||
|
|
||||||
export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(function* (
|
export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
input: {
|
input: {
|
||||||
@@ -117,6 +103,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 !== undefined) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||||
const stored = yield* db
|
const stored = yield* db
|
||||||
.insert(SessionInputTable)
|
.insert(SessionInputTable)
|
||||||
.values({
|
.values({
|
||||||
@@ -134,12 +127,13 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
|
|||||||
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const projectPromoted = Effect.fn("SessionInput.projectPromoted")(function* (
|
export const projectPrompted = Effect.fn("SessionInput.projectPrompted")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
input: {
|
input: {
|
||||||
readonly id: SessionMessage.ID
|
readonly id: SessionMessage.ID
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
readonly prompt: Prompt
|
readonly prompt: Prompt
|
||||||
|
readonly delivery: Delivery
|
||||||
readonly timeCreated: DateTime.Utc
|
readonly timeCreated: DateTime.Utc
|
||||||
readonly promotedSeq: number
|
readonly promotedSeq: number
|
||||||
},
|
},
|
||||||
@@ -157,14 +151,32 @@ export const projectPromoted = Effect.fn("SessionInput.projectPromoted")(functio
|
|||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (!updated) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
if (updated) {
|
||||||
const stored = fromRow(updated)
|
const stored = fromRow(updated)
|
||||||
if (
|
if (!matchesProjection(stored, input)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||||
!matchesPrompt(stored, input) ||
|
return
|
||||||
DateTime.toEpochMillis(stored.timeCreated) !== DateTime.toEpochMillis(input.timeCreated)
|
}
|
||||||
)
|
|
||||||
|
const stored = yield* find(db, input.id)
|
||||||
|
if (stored) {
|
||||||
|
if (!matchesProjection(stored, input) || stored.promotedSeq !== input.promotedSeq)
|
||||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||||
return toMessage(stored)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* db
|
||||||
|
.insert(SessionInputTable)
|
||||||
|
.values({
|
||||||
|
id: input.id,
|
||||||
|
session_id: input.sessionID,
|
||||||
|
prompt: encodePrompt(input.prompt),
|
||||||
|
delivery: input.delivery,
|
||||||
|
admitted_seq: input.promotedSeq,
|
||||||
|
promoted_seq: input.promotedSeq,
|
||||||
|
time_created: DateTime.toEpochMillis(input.timeCreated),
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const hasPending = Effect.fn("SessionInput.hasPending")(function* (
|
export const hasPending = Effect.fn("SessionInput.hasPending")(function* (
|
||||||
@@ -201,35 +213,17 @@ 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 projectLegacyPrompted = Effect.fn("SessionInput.projectLegacyPrompted")(function* (
|
const matchesProjection = (
|
||||||
db: DatabaseService,
|
input: Admitted,
|
||||||
input: {
|
expected: {
|
||||||
readonly id: SessionMessage.ID
|
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
readonly prompt: Prompt
|
readonly prompt: Prompt
|
||||||
readonly delivery: Delivery
|
readonly delivery: Delivery
|
||||||
readonly timeCreated: DateTime.Utc
|
readonly timeCreated: DateTime.Utc
|
||||||
readonly promotedSeq: number
|
|
||||||
},
|
},
|
||||||
) {
|
) =>
|
||||||
const inserted = yield* db
|
equivalent(input, expected) &&
|
||||||
.insert(SessionInputTable)
|
DateTime.toEpochMillis(input.timeCreated) === DateTime.toEpochMillis(expected.timeCreated)
|
||||||
.values({
|
|
||||||
id: input.id,
|
|
||||||
session_id: input.sessionID,
|
|
||||||
admitted_seq: input.promotedSeq,
|
|
||||||
prompt: encodePrompt(input.prompt),
|
|
||||||
delivery: input.delivery,
|
|
||||||
promoted_seq: input.promotedSeq,
|
|
||||||
time_created: DateTime.toEpochMillis(input.timeCreated),
|
|
||||||
})
|
|
||||||
.onConflictDoNothing()
|
|
||||||
.returning()
|
|
||||||
.get()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
if (!inserted) return yield* Effect.die("Prompt projection conflicts with admitted input")
|
|
||||||
return fromRow(inserted)
|
|
||||||
})
|
|
||||||
|
|
||||||
const publish = Effect.fn("SessionInput.publish")(function* (
|
const publish = Effect.fn("SessionInput.publish")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
@@ -238,18 +232,19 @@ const publish = Effect.fn("SessionInput.publish")(function* (
|
|||||||
rows: ReadonlyArray<typeof SessionInputTable.$inferSelect>,
|
rows: ReadonlyArray<typeof SessionInputTable.$inferSelect>,
|
||||||
) {
|
) {
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
const id = SessionMessage.ID.make(row.id)
|
||||||
yield* events
|
yield* events
|
||||||
.publish(SessionEvent.PromptLifecycle.Promoted, {
|
.publish(SessionEvent.Prompted, {
|
||||||
sessionID,
|
sessionID,
|
||||||
timestamp: yield* DateTime.now,
|
timestamp: DateTime.makeUnsafe(row.time_created),
|
||||||
messageID: SessionMessage.ID.make(row.id),
|
messageID: id,
|
||||||
prompt: decodePrompt(row.prompt),
|
prompt: decodePrompt(row.prompt),
|
||||||
timeCreated: DateTime.makeUnsafe(row.time_created),
|
delivery: row.delivery,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
Effect.catchDefect((defect) =>
|
Effect.catchDefect((defect) =>
|
||||||
defect instanceof LifecycleConflict
|
defect instanceof LifecycleConflict
|
||||||
? find(db, SessionMessage.ID.make(row.id)).pipe(
|
? find(db, id).pipe(
|
||||||
Effect.flatMap((stored) => (stored?.promotedSeq === undefined ? Effect.die(defect) : Effect.void)),
|
Effect.flatMap((stored) => (stored?.promotedSeq === undefined ? Effect.die(defect) : Effect.void)),
|
||||||
)
|
)
|
||||||
: Effect.die(defect),
|
: Effect.die(defect),
|
||||||
@@ -303,13 +298,3 @@ export const promoteNextQueued = Effect.fn("SessionInput.promoteNextQueued")(fun
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
return row === undefined ? false : yield* publish(db, events, sessionID, [row]).pipe(Effect.as(true))
|
return row === undefined ? false : yield* publish(db, events, sessionID, [row]).pipe(Effect.as(true))
|
||||||
})
|
})
|
||||||
|
|
||||||
const toMessage = (input: Admitted) =>
|
|
||||||
new SessionMessage.User({
|
|
||||||
id: input.id,
|
|
||||||
type: "user",
|
|
||||||
text: input.prompt.text,
|
|
||||||
files: input.prompt.files,
|
|
||||||
agents: input.prompt.agents,
|
|
||||||
time: { created: input.timeCreated },
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
"session.next.prompt.admitted": () => Effect.void,
|
"session.next.prompt.admitted": () => Effect.void,
|
||||||
"session.next.prompt.promoted": () => Effect.void,
|
|
||||||
"session.next.context.updated": (event) =>
|
"session.next.context.updated": (event) =>
|
||||||
adapter.appendMessage(
|
adapter.appendMessage(
|
||||||
new SessionMessage.System({
|
new SessionMessage.System({
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ type DatabaseService = Database.Interface["db"]
|
|||||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
|
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
|
||||||
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
|
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
|
||||||
|
|
||||||
class PromptAlreadyProjected extends Error {}
|
|
||||||
export class SessionAlreadyProjected extends Error {}
|
export class SessionAlreadyProjected extends Error {}
|
||||||
|
|
||||||
type Usage = {
|
type Usage = {
|
||||||
@@ -350,27 +349,19 @@ export const layer = Layer.effectDiscard(
|
|||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.Prompted, (event) =>
|
yield* events.project(SessionEvent.Prompted, (event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const messageID = event.data.messageID
|
|
||||||
const existing = yield* db
|
|
||||||
.select({ id: SessionMessageTable.id })
|
|
||||||
.from(SessionMessageTable)
|
|
||||||
.where(eq(SessionMessageTable.id, messageID))
|
|
||||||
.get()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
if (existing) return yield* Effect.die(new PromptAlreadyProjected())
|
|
||||||
yield* run(db, event)
|
|
||||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
||||||
yield* SessionInput.projectLegacyPrompted(db, {
|
yield* SessionInput.projectPrompted(db, {
|
||||||
id: messageID,
|
id: event.data.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.durable.seq,
|
||||||
})
|
})
|
||||||
|
yield* run(db, event)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.PromptLifecycle.Admitted, (event) =>
|
yield* events.project(SessionEvent.PromptAdmitted, (event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
||||||
yield* SessionInput.projectAdmitted(db, {
|
yield* SessionInput.projectAdmitted(db, {
|
||||||
@@ -383,22 +374,6 @@ export const layer = Layer.effectDiscard(
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.PromptLifecycle.Promoted, (event) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
|
||||||
yield* insertMessage(
|
|
||||||
db,
|
|
||||||
event,
|
|
||||||
yield* SessionInput.projectPromoted(db, {
|
|
||||||
id: event.data.messageID,
|
|
||||||
sessionID: event.data.sessionID,
|
|
||||||
prompt: event.data.prompt,
|
|
||||||
timeCreated: event.data.timeCreated,
|
|
||||||
promotedSeq: event.durable.seq,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
yield* events.project(SessionEvent.ContextUpdated, (event) => run(db, event))
|
yield* events.project(SessionEvent.ContextUpdated, (event) => run(db, event))
|
||||||
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))
|
||||||
@@ -417,9 +392,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event))
|
yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event))
|
||||||
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) => run(db, event))
|
||||||
event.durable?.version === 1 ? Effect.void : run(db, event),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ import { MAX_STEPS_PROMPT } from "./max-steps"
|
|||||||
* - [ ] Update title, summaries, compaction state, and cleanup in bounded background work.
|
* - [ ] Update title, summaries, compaction state, and cleanup in bounded background work.
|
||||||
*
|
*
|
||||||
* Use `llm.stream(request)` for each provider turn. Keep tool execution and continuation here.
|
* Use `llm.stream(request)` for each provider turn. Keep tool execution and continuation here.
|
||||||
* Durable activity recovery remains a separate future slice with an explicit retry policy.
|
* Durable continuation recovery remains a separate future slice with an explicit retry policy.
|
||||||
*
|
*
|
||||||
* The current slice loads V2 history, translates it, resolves a model through a core service, and persists one
|
* The current slice loads V2 history, translates it, resolves a model through a core service, and persists one
|
||||||
* provider turn. Registry definitions are advertised, local tool calls are settled durably, and an
|
* provider turn. Registry definitions are advertised, local tool calls are settled durably, and an
|
||||||
@@ -142,9 +142,9 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
type TurnTransition =
|
type TurnTransition =
|
||||||
// Automatic compaction completed; rebuild the request from compacted history.
|
// Automatic compaction completed; rebuild the request from compacted history.
|
||||||
| { readonly _tag: "ContinueAfterCompaction" }
|
| { readonly _tag: "ContinueAfterCompaction"; readonly step: number }
|
||||||
// Overflow compaction completed; rebuild once through the path without overflow recovery.
|
// Overflow compaction completed; rebuild once through the path without overflow recovery.
|
||||||
| { readonly _tag: "ContinueAfterOverflowCompaction" }
|
| { readonly _tag: "ContinueAfterOverflowCompaction"; readonly step: number }
|
||||||
|
|
||||||
class TurnTransitionError extends Error {
|
class TurnTransitionError extends Error {
|
||||||
constructor(readonly transition: TurnTransition) {
|
constructor(readonly transition: TurnTransition) {
|
||||||
@@ -152,10 +152,9 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const continueAfterCompaction = new TurnTransitionError({ _tag: "ContinueAfterCompaction" })
|
const continueAfterCompaction = (step: number) => new TurnTransitionError({ _tag: "ContinueAfterCompaction", step })
|
||||||
const continueAfterOverflowCompaction = new TurnTransitionError({
|
const continueAfterOverflowCompaction = (step: number) =>
|
||||||
_tag: "ContinueAfterOverflowCompaction",
|
new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step })
|
||||||
})
|
|
||||||
|
|
||||||
const loadSystemContext = (agent: AgentV2.Selection) =>
|
const loadSystemContext = (agent: AgentV2.Selection) =>
|
||||||
Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load()], {
|
Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load()], {
|
||||||
@@ -175,20 +174,23 @@ export const layer = Layer.effect(
|
|||||||
const initialized = yield* SessionContextEpoch.initialize(db, loadSystemContext(agent), session.id)
|
const initialized = yield* SessionContextEpoch.initialize(db, loadSystemContext(agent), session.id)
|
||||||
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
|
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
|
||||||
let needsContinuation = false
|
let needsContinuation = false
|
||||||
|
let currentStep = step
|
||||||
if (promotion) {
|
if (promotion) {
|
||||||
const cutoff = yield* SessionInput.latestSeq(db, session.id)
|
const cutoff = yield* EventV2.latestSequence(db, session.id)
|
||||||
if (promotion === "steer") yield* SessionInput.promoteSteers(db, events, session.id, cutoff)
|
let promoted = 0
|
||||||
|
if (promotion === "steer") promoted = yield* SessionInput.promoteSteers(db, events, session.id, cutoff)
|
||||||
if (promotion === "queue") {
|
if (promotion === "queue") {
|
||||||
yield* SessionInput.promoteNextQueued(db, events, session.id)
|
promoted += Number(yield* SessionInput.promoteNextQueued(db, events, session.id))
|
||||||
yield* SessionInput.promoteSteers(db, events, session.id, cutoff)
|
promoted += yield* SessionInput.promoteSteers(db, events, session.id, cutoff)
|
||||||
}
|
}
|
||||||
|
if (promoted > 0) currentStep = 1
|
||||||
}
|
}
|
||||||
const system =
|
const system =
|
||||||
initialized ?? (yield* SessionContextEpoch.prepare(db, events, loadSystemContext(agent), session.id))
|
initialized ?? (yield* SessionContextEpoch.prepare(db, events, loadSystemContext(agent), session.id))
|
||||||
const model = yield* models.resolve(session)
|
const model = yield* models.resolve(session)
|
||||||
const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq)
|
const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq)
|
||||||
const context = entries.map((entry) => entry.message)
|
const context = entries.map((entry) => entry.message)
|
||||||
const isLastStep = agent.info?.steps !== undefined && step >= agent.info.steps
|
const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps
|
||||||
const toolMaterialization = isLastStep ? undefined : yield* tools.materialize(agent.info?.permissions)
|
const toolMaterialization = isLastStep ? undefined : yield* tools.materialize(agent.info?.permissions)
|
||||||
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
|
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
|
||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
@@ -202,7 +204,7 @@ export const layer = Layer.effect(
|
|||||||
toolChoice: isLastStep ? "none" : undefined,
|
toolChoice: isLastStep ? "none" : undefined,
|
||||||
})
|
})
|
||||||
if (yield* compaction.compactIfNeeded({ sessionID: session.id, entries, model, request }))
|
if (yield* compaction.compactIfNeeded({ sessionID: session.id, entries, model, request }))
|
||||||
return yield* Effect.die(continueAfterCompaction)
|
return yield* Effect.die(continueAfterCompaction(currentStep))
|
||||||
const publisher = createLLMEventPublisher(events, {
|
const publisher = createLLMEventPublisher(events, {
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
@@ -272,7 +274,7 @@ export const layer = Layer.effect(
|
|||||||
isContextOverflowFailure(overflowFailure ?? failure) &&
|
isContextOverflowFailure(overflowFailure ?? failure) &&
|
||||||
(yield* restore(recoverOverflow({ sessionID: session.id, entries, model, request })))
|
(yield* restore(recoverOverflow({ sessionID: session.id, entries, model, request })))
|
||||||
)
|
)
|
||||||
return yield* Effect.die(continueAfterOverflowCompaction)
|
return yield* Effect.die(continueAfterOverflowCompaction(currentStep))
|
||||||
if (overflowFailure) yield* publish(overflowFailure)
|
if (overflowFailure) yield* publish(overflowFailure)
|
||||||
const llmFailure = failure instanceof LLMError ? failure : undefined
|
const llmFailure = failure instanceof LLMError ? failure : undefined
|
||||||
if (llmFailure && !publisher.hasProviderError()) {
|
if (llmFailure && !publisher.hasProviderError()) {
|
||||||
@@ -306,7 +308,7 @@ export const layer = Layer.effect(
|
|||||||
yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true))
|
yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true))
|
||||||
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
|
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
|
||||||
if (settled._tag === "Failure") return yield* Effect.failCause(settled.cause)
|
if (settled._tag === "Failure") return yield* Effect.failCause(settled.cause)
|
||||||
return !publisher.hasProviderError() && needsContinuation
|
return { needsContinuation: !publisher.hasProviderError() && needsContinuation, step: currentStep }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}, Effect.scoped)
|
}, Effect.scoped)
|
||||||
@@ -314,7 +316,7 @@ export const layer = Layer.effect(
|
|||||||
sessionID: SessionSchema.ID,
|
sessionID: SessionSchema.ID,
|
||||||
promotion: SessionInput.Delivery | undefined,
|
promotion: SessionInput.Delivery | undefined,
|
||||||
step: number,
|
step: number,
|
||||||
) => Effect.Effect<boolean, RunError>
|
) => Effect.Effect<{ readonly needsContinuation: boolean; readonly step: number }, RunError>
|
||||||
|
|
||||||
const runAfterOverflowCompaction: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) {
|
const runAfterOverflowCompaction: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) {
|
||||||
return yield* runTurnAttempt(sessionID, promotion, step).pipe(
|
return yield* runTurnAttempt(sessionID, promotion, step).pipe(
|
||||||
@@ -324,7 +326,7 @@ export const layer = Layer.effect(
|
|||||||
if (defect.transition._tag === "ContinueAfterOverflowCompaction")
|
if (defect.transition._tag === "ContinueAfterOverflowCompaction")
|
||||||
return yield* Effect.die("Post-compaction provider attempt cannot recover another overflow")
|
return yield* Effect.die("Post-compaction provider attempt cannot recover another overflow")
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
return yield* runAfterOverflowCompaction(sessionID, undefined, step)
|
return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -337,8 +339,8 @@ export const layer = Layer.effect(
|
|||||||
if (!(defect instanceof TurnTransitionError)) return yield* Effect.die(defect)
|
if (!(defect instanceof TurnTransitionError)) return yield* Effect.die(defect)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
if (defect.transition._tag === "ContinueAfterOverflowCompaction")
|
if (defect.transition._tag === "ContinueAfterOverflowCompaction")
|
||||||
return yield* runAfterOverflowCompaction(sessionID, undefined, step)
|
return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step)
|
||||||
return yield* runTurn(sessionID, undefined, step)
|
return yield* runTurn(sessionID, undefined, defect.transition.step)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -353,16 +355,19 @@ export const layer = Layer.effect(
|
|||||||
if (!input.force && !hasSteer && !hasQueue) return
|
if (!input.force && !hasSteer && !hasQueue) return
|
||||||
yield* failInterruptedTools(input.sessionID)
|
yield* failInterruptedTools(input.sessionID)
|
||||||
let promotion: SessionInput.Delivery | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefined
|
let promotion: SessionInput.Delivery | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefined
|
||||||
let openActivity = input.force || hasSteer || hasQueue
|
let shouldRun = input.force || hasSteer || hasQueue
|
||||||
while (openActivity) {
|
while (shouldRun) {
|
||||||
let needsContinuation = true
|
let needsContinuation = true
|
||||||
for (let step = 1; needsContinuation; step++) {
|
let step = 1
|
||||||
needsContinuation = yield* runTurn(input.sessionID, promotion, step)
|
while (needsContinuation) {
|
||||||
|
const result = yield* runTurn(input.sessionID, promotion, step)
|
||||||
|
needsContinuation = result.needsContinuation
|
||||||
|
step = result.step + 1
|
||||||
promotion = "steer"
|
promotion = "steer"
|
||||||
if (!needsContinuation) needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer")
|
if (!needsContinuation) needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer")
|
||||||
}
|
}
|
||||||
openActivity = yield* SessionInput.hasPending(db, input.sessionID, "queue")
|
shouldRun = yield* SessionInput.hasPending(db, input.sessionID, "queue")
|
||||||
promotion = openActivity ? "queue" : undefined
|
promotion = shouldRun ? "queue" : undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Integration } from "../../integration"
|
|||||||
import { IntegrationConnection } from "../../integration/connection"
|
import { IntegrationConnection } from "../../integration/connection"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { ModelRequest } from "../../model-request"
|
import { ModelRequest } from "../../model-request"
|
||||||
import { PluginBoot } from "../../plugin/boot"
|
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
import { SessionSchema } from "../schema"
|
import { SessionSchema } from "../schema"
|
||||||
|
|
||||||
@@ -178,11 +177,9 @@ export const locationLayer = Layer.effect(
|
|||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const credentials = yield* Credential.Service
|
const credentials = yield* Credential.Service
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
const boot = yield* PluginBoot.Service
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
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()
|
|
||||||
const defaultModel = session.model ? undefined : yield* catalog.model.default()
|
const defaultModel = session.model ? undefined : yield* catalog.model.default()
|
||||||
const selected = session.model
|
const selected = session.model
|
||||||
? (yield* catalog.model.available()).find(
|
? (yield* catalog.model.available()).find(
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
rebuild: state.rebuild,
|
reload: state.reload,
|
||||||
sources: Effect.fn("SkillV2.sources")(function* () {
|
sources: Effect.fn("SkillV2.sources")(function* () {
|
||||||
return state.get().sources
|
return state.get().sources
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export * as SkillGuidance from "./guidance"
|
|||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { PluginBoot } from "../plugin/boot"
|
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
import { SystemContext } from "../system-context/index"
|
import { SystemContext } from "../system-context/index"
|
||||||
|
|
||||||
@@ -40,12 +39,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const boot = yield* PluginBoot.Service
|
|
||||||
const skills = yield* SkillV2.Service
|
const skills = yield* SkillV2.Service
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
load: Effect.fn("SkillGuidance.load")(function* (selection) {
|
load: Effect.fn("SkillGuidance.load")(function* (selection) {
|
||||||
yield* boot.wait()
|
|
||||||
const agent = selection.info
|
const agent = selection.info
|
||||||
if (!agent) return SystemContext.empty
|
if (!agent) return SystemContext.empty
|
||||||
const permitted = SkillV2.available(yield* skills.list(), agent)
|
const permitted = SkillV2.available(yield* skills.list(), agent)
|
||||||
|
|||||||
+15
-15
@@ -3,7 +3,7 @@ export * as State from "./state"
|
|||||||
import { Context, Effect, Scope, Semaphore } from "effect"
|
import { Context, Effect, Scope, Semaphore } from "effect"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A replayable transform applied to a draft during rebuild.
|
* A replayable transform applied to a draft during reload.
|
||||||
*
|
*
|
||||||
* Domain drafts expose readable and writable state while preserving concise
|
* Domain drafts expose readable and writable state while preserving concise
|
||||||
* plugin/config code. Transforms may perform Effects before returning.
|
* plugin/config code. Transforms may perform Effects before returning.
|
||||||
@@ -19,14 +19,14 @@ export type Transform<DraftApi> = (
|
|||||||
transform: TransformCallback<DraftApi>,
|
transform: TransformCallback<DraftApi>,
|
||||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
) => Effect.Effect<Registration, never, Scope.Scope>
|
||||||
|
|
||||||
export type Rebuild = () => Effect.Effect<void>
|
export type Reload = () => Effect.Effect<void>
|
||||||
|
|
||||||
export interface Transformable<DraftApi> {
|
export interface Transformable<DraftApi> {
|
||||||
readonly transform: Transform<DraftApi>
|
readonly transform: Transform<DraftApi>
|
||||||
readonly rebuild: Rebuild
|
readonly reload: Reload
|
||||||
}
|
}
|
||||||
|
|
||||||
const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", {
|
const CurrentBatch = Context.Reference<Set<Reload> | undefined>("@opencode/State/CurrentBatch", {
|
||||||
defaultValue: () => undefined,
|
defaultValue: () => undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -34,15 +34,15 @@ export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const current = yield* CurrentBatch
|
const current = yield* CurrentBatch
|
||||||
if (current) return yield* effect
|
if (current) return yield* effect
|
||||||
const rebuilds = new Set<Rebuild>()
|
const reloads = new Set<Reload>()
|
||||||
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds))
|
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, reloads))
|
||||||
yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true })
|
yield* Effect.forEach(reloads, (reload) => reload(), { discard: true })
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Options<State, DraftApi> {
|
export interface Options<State, DraftApi> {
|
||||||
/** Creates the base value for initial state and every scoped-transform rebuild. */
|
/** Creates the base value for initial state and every scoped-transform reload. */
|
||||||
readonly initial: () => State
|
readonly initial: () => State
|
||||||
/** Wraps mutable state in a domain-specific draft API. */
|
/** Wraps mutable state in a domain-specific draft API. */
|
||||||
readonly draft: MakeDraft<State, DraftApi>
|
readonly draft: MakeDraft<State, DraftApi>
|
||||||
@@ -54,7 +54,7 @@ export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
|
|||||||
readonly get: () => State
|
readonly get: () => State
|
||||||
/**
|
/**
|
||||||
* Registers and applies a scoped transform. Closing the owning Scope removes
|
* Registers and applies a scoped transform. Closing the owning Scope removes
|
||||||
* the transform and rebuilds the materialized state.
|
* the transform and reloads the materialized state.
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +78,11 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
|||||||
const materialize = Effect.fnUntraced(function* () {
|
const materialize = Effect.fnUntraced(function* () {
|
||||||
const next = options.initial()
|
const next = options.initial()
|
||||||
const api = options.draft(next)
|
const api = options.draft(next)
|
||||||
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.rebuild.update"))
|
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.reload.update"))
|
||||||
yield* commit(next)
|
yield* commit(next)
|
||||||
})
|
})
|
||||||
|
|
||||||
const rebuild = () => semaphore.withPermit(materialize())
|
const reload = () => semaphore.withPermit(materialize())
|
||||||
|
|
||||||
const result: Interface<State, DraftApi> = {
|
const result: Interface<State, DraftApi> = {
|
||||||
get: () => state,
|
get: () => state,
|
||||||
@@ -101,7 +101,7 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
|||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const batch = yield* CurrentBatch
|
const batch = yield* CurrentBatch
|
||||||
if (batch) {
|
if (batch) {
|
||||||
batch.add(rebuild)
|
batch.add(reload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
yield* materialize()
|
yield* materialize()
|
||||||
@@ -116,13 +116,13 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
|||||||
)
|
)
|
||||||
yield* Scope.addFinalizer(scope, dispose)
|
yield* Scope.addFinalizer(scope, dispose)
|
||||||
const batch = yield* CurrentBatch
|
const batch = yield* CurrentBatch
|
||||||
if (batch) batch.add(rebuild)
|
if (batch) batch.add(reload)
|
||||||
else yield* rebuild()
|
else yield* reload()
|
||||||
return { dispose }
|
return { dispose }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
rebuild,
|
reload,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { pathToFileURL } from "url"
|
|||||||
import { ToolFailure } from "@opencode-ai/llm"
|
import { ToolFailure } from "@opencode-ai/llm"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Layer, Schema } from "effect"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { PluginBoot } from "../plugin/boot"
|
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
@@ -58,10 +57,8 @@ export const layer = Layer.effectDiscard(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tools = yield* Tools.Service
|
const tools = yield* Tools.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const boot = yield* PluginBoot.Service
|
|
||||||
const skills = yield* SkillV2.Service
|
const skills = yield* SkillV2.Service
|
||||||
const permission = yield* PermissionV2.Service
|
const permission = yield* PermissionV2.Service
|
||||||
yield* boot.wait()
|
|
||||||
yield* tools
|
yield* tools
|
||||||
.register({
|
.register({
|
||||||
[name]: Tool.make({
|
[name]: Tool.make({
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe("AgentV2", () => {
|
|||||||
)
|
)
|
||||||
description = "New description"
|
description = "New description"
|
||||||
hidden = false
|
hidden = false
|
||||||
yield* agent.rebuild()
|
yield* agent.reload()
|
||||||
|
|
||||||
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
|
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
|
||||||
}),
|
}),
|
||||||
@@ -104,8 +104,12 @@ describe("AgentV2", () => {
|
|||||||
yield* AgentPlugin.Plugin.effect(
|
yield* AgentPlugin.Plugin.effect(
|
||||||
host({
|
host({
|
||||||
agent: agentHost(agent),
|
agent: agentHost(agent),
|
||||||
location: location({ directory: AbsolutePath.make("/project") }),
|
|
||||||
}),
|
}),
|
||||||
|
).pipe(
|
||||||
|
Effect.provideService(
|
||||||
|
Location.Service,
|
||||||
|
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const agents = yield* agent.all()
|
const agents = yield* agent.all()
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ describe("CatalogV2", () => {
|
|||||||
expect((yield* catalog.model.default())?.id).toBe(old)
|
expect((yield* catalog.model.default())?.id).toBe(old)
|
||||||
|
|
||||||
configured = false
|
configured = false
|
||||||
yield* catalog.rebuild()
|
yield* catalog.reload()
|
||||||
expect((yield* catalog.model.default())?.id).toBe(newest)
|
expect((yield* catalog.model.default())?.id).toBe(newest)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Review files`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
const command = yield* CommandV2.Service
|
const command = yield* CommandV2.Service
|
||||||
yield* ConfigCommandPlugin.Plugin.effect(host({ command })).pipe(
|
yield* ConfigCommandPlugin.Plugin.effect(host({ command: { ...command, reload: command.reload } })).pipe(
|
||||||
Effect.provideService(
|
Effect.provideService(
|
||||||
Config.Service,
|
Config.Service,
|
||||||
Config.Service.of({
|
Config.Service.of({
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||||
|
|
||||||
|
export default define({
|
||||||
|
id: "directory-plugin",
|
||||||
|
setup: async (ctx) => {
|
||||||
|
await ctx.agent.transform((agents) => {
|
||||||
|
agents.update("directory", (agent) => {
|
||||||
|
agent.description = "Loaded from plugin directory"
|
||||||
|
agent.mode = "subagent"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Effect, Schema } from "effect"
|
||||||
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
|
import { Config } from "@opencode-ai/core/config"
|
||||||
|
import { ConfigExternalPlugin } from "@opencode-ai/core/config/plugin/external"
|
||||||
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
import { Npm } from "@opencode-ai/core/npm"
|
||||||
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
|
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||||
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
import { PluginTestLayer } from "../plugin/fixture"
|
||||||
|
|
||||||
|
const it = testEffect(PluginTestLayer)
|
||||||
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
|
|
||||||
|
describe("ConfigExternalPlugin", () => {
|
||||||
|
it.live("resolves and loads a configured Promise plugin with options", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
const host = yield* PluginHost.make(plugins)
|
||||||
|
const document = path.join(import.meta.dir, "config.json")
|
||||||
|
|
||||||
|
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||||
|
Effect.provideService(PluginV2.Service, plugins),
|
||||||
|
Effect.provideService(FSUtil.Service, fs),
|
||||||
|
Effect.provideService(Location.Service, location),
|
||||||
|
Effect.provideService(Npm.Service, npm),
|
||||||
|
Effect.provideService(
|
||||||
|
Config.Service,
|
||||||
|
Config.Service.of({
|
||||||
|
entries: () =>
|
||||||
|
Effect.succeed([
|
||||||
|
new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
path: document,
|
||||||
|
info: decode({
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
package: "../plugin/fixtures/config-promise-plugin.ts",
|
||||||
|
options: { description: "Loaded from config" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
||||||
|
description: "Loaded from config",
|
||||||
|
mode: "subagent",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("loads a configured Effect plugin with options", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
const host = yield* PluginHost.make(plugins)
|
||||||
|
|
||||||
|
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||||
|
Effect.provideService(PluginV2.Service, plugins),
|
||||||
|
Effect.provideService(FSUtil.Service, fs),
|
||||||
|
Effect.provideService(Location.Service, location),
|
||||||
|
Effect.provideService(Npm.Service, npm),
|
||||||
|
Effect.provideService(
|
||||||
|
Config.Service,
|
||||||
|
Config.Service.of({
|
||||||
|
entries: () =>
|
||||||
|
Effect.succeed([
|
||||||
|
new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
path: path.join(import.meta.dir, "config.json"),
|
||||||
|
info: decode({
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
package: "../plugin/fixtures/config-effect-plugin.ts",
|
||||||
|
options: { description: "Effect plugin from config" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(yield* waitForAgent(agents, "effect-configured")).toMatchObject({
|
||||||
|
description: "Effect plugin from config",
|
||||||
|
mode: "subagent",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("ignores invalid plugins and continues loading", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
const host = yield* PluginHost.make(plugins)
|
||||||
|
|
||||||
|
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||||
|
Effect.provideService(PluginV2.Service, plugins),
|
||||||
|
Effect.provideService(FSUtil.Service, fs),
|
||||||
|
Effect.provideService(Location.Service, location),
|
||||||
|
Effect.provideService(Npm.Service, npm),
|
||||||
|
Effect.provideService(
|
||||||
|
Config.Service,
|
||||||
|
Config.Service.of({
|
||||||
|
entries: () =>
|
||||||
|
Effect.succeed([
|
||||||
|
new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
path: path.join(import.meta.dir, "config.json"),
|
||||||
|
info: decode({
|
||||||
|
plugins: [
|
||||||
|
"../plugin/fixtures/missing-plugin.ts",
|
||||||
|
"../plugin/fixtures/invalid-plugin.ts",
|
||||||
|
{
|
||||||
|
package: "../plugin/fixtures/config-promise-plugin.ts",
|
||||||
|
options: { description: "Loaded after invalid plugins" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
||||||
|
description: "Loaded after invalid plugins",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("installs and resolves npm plugin packages", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const host = yield* PluginHost.make(plugins)
|
||||||
|
let installed: string | undefined
|
||||||
|
const npm = Npm.Service.of({
|
||||||
|
add: (spec) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
installed = spec
|
||||||
|
return {
|
||||||
|
directory: import.meta.dir,
|
||||||
|
entrypoint: path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
install: () => Effect.void,
|
||||||
|
which: () => Effect.succeed(undefined),
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||||
|
Effect.provideService(PluginV2.Service, plugins),
|
||||||
|
Effect.provideService(FSUtil.Service, fs),
|
||||||
|
Effect.provideService(Location.Service, location),
|
||||||
|
Effect.provideService(Npm.Service, npm),
|
||||||
|
Effect.provideService(
|
||||||
|
Config.Service,
|
||||||
|
Config.Service.of({
|
||||||
|
entries: () =>
|
||||||
|
Effect.succeed([
|
||||||
|
new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
info: decode({
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
package: "example-plugin@1.0.0",
|
||||||
|
options: { description: "Installed from npm" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
||||||
|
description: "Installed from npm",
|
||||||
|
})
|
||||||
|
expect(installed).toBe("example-plugin@1.0.0")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("loads plugin files from config directories", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
const host = yield* PluginHost.make(plugins)
|
||||||
|
|
||||||
|
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||||
|
Effect.provideService(PluginV2.Service, plugins),
|
||||||
|
Effect.provideService(FSUtil.Service, fs),
|
||||||
|
Effect.provideService(Location.Service, location),
|
||||||
|
Effect.provideService(Npm.Service, npm),
|
||||||
|
Effect.provideService(
|
||||||
|
Config.Service,
|
||||||
|
Config.Service.of({
|
||||||
|
entries: () =>
|
||||||
|
Effect.succeed([
|
||||||
|
new Config.Directory({
|
||||||
|
type: "directory",
|
||||||
|
path: AbsolutePath.make(path.join(import.meta.dir, "fixtures")),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(yield* waitForAgent(agents, "directory")).toMatchObject({
|
||||||
|
description: "Loaded from plugin directory",
|
||||||
|
mode: "subagent",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const waitForAgent = Effect.fnUntraced(function* (agents: AgentV2.Interface, id: string) {
|
||||||
|
for (let attempt = 0; attempt < 100; attempt++) {
|
||||||
|
const agent = yield* agents.get(AgentV2.ID.make(id))
|
||||||
|
if (agent) return agent
|
||||||
|
yield* Effect.sleep("10 millis")
|
||||||
|
}
|
||||||
|
return yield* Effect.die(`Timed out waiting for agent ${id}`)
|
||||||
|
})
|
||||||
@@ -15,11 +15,8 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* (config: Config.Interface) {
|
const addPlugin = Effect.fn(function* (config: Config.Interface) {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const host = yield* PluginHost.make()
|
const host = yield* PluginHost.make(plugin)
|
||||||
yield* plugin.add({
|
yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config))
|
||||||
...ConfigProviderPlugin.Plugin,
|
|
||||||
effect: ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function required<T>(value: T | undefined): T {
|
function required<T>(value: T | undefined): T {
|
||||||
|
|||||||
@@ -36,16 +36,11 @@ describe("ConfigSkillPlugin.Plugin", () => {
|
|||||||
|
|
||||||
yield* ConfigSkillPlugin.Plugin.effect(
|
yield* ConfigSkillPlugin.Plugin.effect(
|
||||||
host({
|
host({
|
||||||
location: location({ directory }),
|
skill: { transform, reload: () => Effect.void },
|
||||||
path: { ...host().path, home: "/home/test" },
|
|
||||||
skill: SkillV2.Service.of({
|
|
||||||
transform,
|
|
||||||
rebuild: () => Effect.void,
|
|
||||||
sources: () => Effect.succeed(sources),
|
|
||||||
list: () => Effect.succeed([]),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
).pipe(
|
).pipe(
|
||||||
|
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home: "/home/test" })),
|
||||||
|
Effect.provideService(Location.Service, Location.Service.of(location({ directory }))),
|
||||||
Effect.provideService(
|
Effect.provideService(
|
||||||
Config.Service,
|
Config.Service,
|
||||||
Config.Service.of({
|
Config.Service.of({
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/m
|
|||||||
import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
|
import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
|
||||||
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
|
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
|
||||||
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
|
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
|
||||||
|
import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
@@ -22,6 +24,8 @@ import { SessionTable } from "@opencode-ai/core/session/sql"
|
|||||||
import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata"
|
import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata"
|
||||||
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
|
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
||||||
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
||||||
@@ -226,6 +230,94 @@ describe("DatabaseMigration", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("preserves canonical V1 state and restarts its event stream", async () => {
|
||||||
|
await run(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const db = yield* makeDb
|
||||||
|
yield* db.run(sql`PRAGMA foreign_keys = ON`)
|
||||||
|
yield* DatabaseMigration.apply(db)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('global', '/project', 1, 1, '[]')`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO workspace (id, type, project_id, time_used) VALUES ('workspace', 'local', 'global', 1)`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO session (id, project_id, workspace_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'global', 'workspace', 'session', '/project', 'Before', 'test', 1, 1)`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('message', 'session', 1, 1, '{}')`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('part', 'message', 'session', 1, 1, '{}')`,
|
||||||
|
)
|
||||||
|
yield* db.run(sql`INSERT INTO event_sequence (aggregate_id, seq) VALUES ('session', 9)`)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO event (id, aggregate_id, seq, type, data) VALUES ('event', 'session', 9, 'session.updated.1', '{}')`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO session_input (id, session_id, prompt, delivery, admitted_seq, time_created) VALUES ('input', 'session', '{}', 'steer', 9, 1)`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}')`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
|
||||||
|
)
|
||||||
|
yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionInputMigration.id}`)
|
||||||
|
yield* DatabaseMigration.applyOnly(db, [simplifySessionInputMigration])
|
||||||
|
|
||||||
|
const database = Layer.succeed(Database.Service, { db })
|
||||||
|
const events = EventV2.layer.pipe(Layer.provide(database))
|
||||||
|
yield* EventV2.Service.use((service) =>
|
||||||
|
service.publish(SessionV1.Event.Updated, {
|
||||||
|
sessionID: SessionSchema.ID.make("session"),
|
||||||
|
info: {
|
||||||
|
id: SessionSchema.ID.make("session"),
|
||||||
|
slug: "session",
|
||||||
|
projectID: ProjectV2.ID.global,
|
||||||
|
directory: "/project",
|
||||||
|
title: "After",
|
||||||
|
version: "test",
|
||||||
|
time: { created: 1, updated: 2 },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
Layer.merge(events, SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
yield* db.get(sql`
|
||||||
|
SELECT
|
||||||
|
(SELECT title FROM session WHERE id = 'session') AS title,
|
||||||
|
(SELECT workspace_id FROM session WHERE id = 'session') AS workspaceID,
|
||||||
|
(SELECT COUNT(*) FROM message WHERE id = 'message') AS messages,
|
||||||
|
(SELECT COUNT(*) FROM part WHERE id = 'part') AS parts,
|
||||||
|
(SELECT COUNT(*) FROM workspace) AS workspaces,
|
||||||
|
(SELECT COUNT(*) FROM session_input) AS sessionInputs,
|
||||||
|
(SELECT COUNT(*) FROM session_message) AS sessionMessages,
|
||||||
|
(SELECT COUNT(*) FROM session_context_epoch) AS contextEpochs,
|
||||||
|
(SELECT seq FROM event_sequence WHERE aggregate_id = 'session') AS seq,
|
||||||
|
(SELECT type FROM event WHERE aggregate_id = 'session') AS eventType
|
||||||
|
`),
|
||||||
|
).toEqual({
|
||||||
|
title: "After",
|
||||||
|
workspaceID: null,
|
||||||
|
messages: 1,
|
||||||
|
parts: 1,
|
||||||
|
workspaces: 0,
|
||||||
|
sessionInputs: 0,
|
||||||
|
sessionMessages: 0,
|
||||||
|
contextEpochs: 0,
|
||||||
|
seq: 0,
|
||||||
|
eventType: "session.updated.1",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test("resets incompatible projected Session messages before adding sequence order", async () => {
|
test("resets incompatible projected Session messages before adding sequence order", async () => {
|
||||||
await run(
|
await run(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
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 { DateTime, Deferred, Effect, Equal, Hash, Layer, Schema, Stream } from "effect"
|
import { DateTime, 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 { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
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"
|
||||||
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
|
||||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
import { ProjectV2 } 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"
|
||||||
@@ -88,7 +88,6 @@ describe("LocationServiceMap", () => {
|
|||||||
|
|
||||||
const update = (directory: string) =>
|
const update = (directory: string) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
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"), () => {}))
|
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
||||||
@@ -197,36 +196,21 @@ describe("LocationServiceMap", () => {
|
|||||||
).pipe(
|
).pipe(
|
||||||
Effect.flatMap((dir) =>
|
Effect.flatMap((dir) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const boot = yield* PluginBoot.Service
|
const plugins = yield* PluginV2.Service
|
||||||
const catalogUpdated = yield* Deferred.make<void>()
|
const reviewer = define({
|
||||||
const seen: string[] = []
|
|
||||||
yield* boot.add(
|
|
||||||
define({
|
|
||||||
id: "reviewer",
|
id: "reviewer",
|
||||||
effect: (ctx) =>
|
effect: (ctx) =>
|
||||||
Effect.gen(function* () {
|
ctx.agent
|
||||||
yield* ctx.event.subscribe("catalog.updated").pipe(
|
.transform((agent) => {
|
||||||
Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)),
|
|
||||||
Effect.forkScoped({ startImmediately: true }),
|
|
||||||
)
|
|
||||||
yield* ctx.agent.transform((agent) => {
|
|
||||||
agent.update("reviewer", (item) => {
|
agent.update("reviewer", (item) => {
|
||||||
item.description = "Reviews code"
|
item.description = "Reviews code"
|
||||||
item.mode = "subagent"
|
item.mode = "subagent"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
seen.push((yield* ctx.agent.get("reviewer"))?.description ?? "")
|
.pipe(Effect.asVoid),
|
||||||
yield* ctx.catalog.transform((catalog) => {
|
|
||||||
catalog.provider.update("public", (provider) => {
|
|
||||||
provider.name = "Public provider"
|
|
||||||
})
|
})
|
||||||
})
|
yield* plugins.add(PluginV2.ID.make(reviewer.id), reviewer.effect)
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* Deferred.await(catalogUpdated)
|
|
||||||
expect(seen).toEqual(["Reviews code"])
|
|
||||||
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
||||||
description: "Reviews code",
|
description: "Reviews code",
|
||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
|
|||||||
@@ -1,127 +1,43 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
|
import { Effect } from "effect"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { State } from "@opencode-ai/core/state"
|
import { testEffect } from "./lib/effect"
|
||||||
import { it } from "./lib/effect"
|
import { PluginTestLayer } from "./plugin/fixture"
|
||||||
|
|
||||||
const events = Layer.mock(EventV2.Service)({
|
const it = testEffect(PluginTestLayer)
|
||||||
publish: (definition, data) =>
|
|
||||||
Effect.succeed({
|
|
||||||
id: EventV2.ID.make("evt_plugin_test"),
|
|
||||||
type: definition.type,
|
|
||||||
data,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
const plugins = PluginV2.layer.pipe(Layer.provide(events))
|
|
||||||
|
|
||||||
function state() {
|
|
||||||
return State.create({
|
|
||||||
initial: () => ({ values: [] as string[] }),
|
|
||||||
draft: (draft) => ({
|
|
||||||
add: (value: string) => draft.values.push(value),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("PluginV2", () => {
|
describe("PluginV2", () => {
|
||||||
it.effect("closes plugin-owned scopes when the registry layer finalizes", () =>
|
it.effect("adds, replaces, and removes plugins", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const values = state()
|
const plugins = yield* PluginV2.Service
|
||||||
const layerScope = yield* Scope.fork(yield* Scope.Scope)
|
const agents = yield* AgentV2.Service
|
||||||
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
|
let description = "first"
|
||||||
|
|
||||||
yield* plugin.add({
|
const managed = () =>
|
||||||
id: PluginV2.ID.make("scoped"),
|
define({
|
||||||
effect: Effect.gen(function* () {
|
id: "managed",
|
||||||
yield* values.transform((editor) => {
|
effect: (ctx) =>
|
||||||
editor.add("scoped")
|
ctx.agent
|
||||||
})
|
.transform((agents) =>
|
||||||
}),
|
agents.update("configured", (agent) => {
|
||||||
})
|
agent.description = description
|
||||||
expect(values.get().values).toEqual(["scoped"])
|
|
||||||
|
|
||||||
yield* Scope.close(layerScope, Exit.void)
|
|
||||||
expect(values.get().values).toEqual([])
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("batches plugin state rebuilds when the registry layer finalizes", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
let finalized = 0
|
|
||||||
const values = State.create({
|
|
||||||
initial: () => ({ values: [] as string[] }),
|
|
||||||
draft: (draft) => ({ add: (value: string) => draft.values.push(value) }),
|
|
||||||
finalize: () => Effect.sync(() => finalized++),
|
|
||||||
})
|
|
||||||
const layerScope = yield* Scope.fork(yield* Scope.Scope)
|
|
||||||
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
|
|
||||||
|
|
||||||
yield* State.batch(
|
|
||||||
Effect.forEach(
|
|
||||||
["first", "second"],
|
|
||||||
(id) =>
|
|
||||||
plugin.add({
|
|
||||||
id: PluginV2.ID.make(id),
|
|
||||||
effect: values
|
|
||||||
.transform((editor) => {
|
|
||||||
editor.add(id)
|
|
||||||
})
|
|
||||||
.pipe(Effect.asVoid),
|
.pipe(Effect.asVoid),
|
||||||
}),
|
|
||||||
{ discard: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
finalized = 0
|
|
||||||
|
|
||||||
yield* Scope.close(layerScope, Exit.void)
|
|
||||||
expect(values.get().values).toEqual([])
|
|
||||||
expect(finalized).toBe(1)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("serializes same-ID additions and leaves one removable attachment", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const values = state()
|
|
||||||
const layerScope = yield* Scope.fork(yield* Scope.Scope)
|
|
||||||
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
|
|
||||||
const id = PluginV2.ID.make("shared")
|
|
||||||
const firstStarted = yield* Deferred.make<void>()
|
|
||||||
const releaseFirst = yield* Deferred.make<void>()
|
|
||||||
|
|
||||||
const first = yield* plugin
|
|
||||||
.add({
|
|
||||||
id,
|
|
||||||
effect: Effect.gen(function* () {
|
|
||||||
yield* values.transform((editor) => {
|
|
||||||
editor.add("first")
|
|
||||||
})
|
})
|
||||||
yield* Deferred.succeed(firstStarted, undefined)
|
|
||||||
yield* Deferred.await(releaseFirst)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.pipe(Effect.forkChild)
|
|
||||||
yield* Deferred.await(firstStarted)
|
|
||||||
|
|
||||||
const second = yield* plugin
|
yield* plugins.add(PluginV2.ID.make("managed"), managed().effect)
|
||||||
.add({
|
|
||||||
id,
|
|
||||||
effect: Effect.gen(function* () {
|
|
||||||
yield* values.transform((editor) => {
|
|
||||||
editor.add("second")
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.pipe(Effect.forkChild({ startImmediately: true }))
|
|
||||||
expect(values.get().values).toEqual(["first"])
|
|
||||||
|
|
||||||
yield* Deferred.succeed(releaseFirst, undefined)
|
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
|
||||||
yield* Fiber.join(first)
|
|
||||||
yield* Fiber.join(second)
|
|
||||||
expect(values.get().values).toEqual(["second"])
|
|
||||||
|
|
||||||
yield* plugin.remove(id)
|
description = "second"
|
||||||
expect(values.get().values).toEqual([])
|
yield* plugins.add(PluginV2.ID.make("managed"), managed().effect)
|
||||||
|
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
|
||||||
|
|
||||||
|
yield* plugins.remove(PluginV2.ID.make("managed"))
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ describe("CommandPlugin.Plugin", () => {
|
|||||||
const command = yield* CommandV2.Service
|
const command = yield* CommandV2.Service
|
||||||
yield* CommandPlugin.Plugin.effect(
|
yield* CommandPlugin.Plugin.effect(
|
||||||
host({
|
host({
|
||||||
command,
|
command: { transform: command.transform, reload: command.reload },
|
||||||
location: location({ directory }, { projectDirectory: project }),
|
|
||||||
}),
|
}),
|
||||||
|
).pipe(
|
||||||
|
Effect.provideService(
|
||||||
|
Location.Service,
|
||||||
|
Location.Service.of(location({ directory }, { projectDirectory: project })),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* command.get("init")).toMatchObject({
|
expect(yield* command.get("init")).toMatchObject({
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
|
||||||
import { CommandV2 } from "@opencode-ai/core/command"
|
|
||||||
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 { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||||
@@ -8,23 +5,13 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
|
|||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Npm } from "@opencode-ai/core/npm"
|
import { Npm } from "@opencode-ai/core/npm"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { Reference } from "@opencode-ai/core/reference"
|
|
||||||
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
||||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
|
||||||
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { tempLocationLayer } from "../fixture/location"
|
import { tempLocationLayer } from "../fixture/location"
|
||||||
|
|
||||||
export const PluginTestLayer = Layer.mergeAll(
|
export const PluginTestLayer = Layer.mergeAll(FileSystem.locationLayer, PluginV2.locationLayer).pipe(
|
||||||
AgentV2.locationLayer,
|
|
||||||
CommandV2.locationLayer,
|
|
||||||
Catalog.locationLayer,
|
|
||||||
FileSystem.locationLayer,
|
|
||||||
PluginV2.locationLayer,
|
|
||||||
Reference.locationLayer,
|
|
||||||
SkillV2.locationLayer,
|
|
||||||
).pipe(
|
|
||||||
Layer.provideMerge(
|
Layer.provideMerge(
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
Credential.defaultLayer,
|
Credential.defaultLayer,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
|
||||||
|
export default define({
|
||||||
|
id: "config-effect-plugin",
|
||||||
|
effect: (ctx) =>
|
||||||
|
ctx.agent
|
||||||
|
.transform((agents) => {
|
||||||
|
agents.update("effect-configured", (agent) => {
|
||||||
|
agent.description = ctx.options.description
|
||||||
|
agent.mode = "subagent"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.pipe(Effect.asVoid),
|
||||||
|
})
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||||
|
|
||||||
|
export default define({
|
||||||
|
id: "config-promise-plugin",
|
||||||
|
setup: async (ctx) => {
|
||||||
|
await ctx.agent.transform((agents) => {
|
||||||
|
agents.update("configured", (agent) => {
|
||||||
|
agent.description = ctx.options.description
|
||||||
|
agent.mode = "subagent"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default {}
|
||||||
@@ -1,120 +1,55 @@
|
|||||||
import type { AISDKHooks, PluginHost } from "@opencode-ai/plugin/v2/effect"
|
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
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 { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
|
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect } from "effect"
|
||||||
|
|
||||||
export function host(overrides: Partial<PluginHost> = {}): PluginHost {
|
type Overrides = Partial<Omit<PluginContext, "options">>
|
||||||
|
|
||||||
|
export function host(overrides: Overrides = {}): PluginContext {
|
||||||
return {
|
return {
|
||||||
aisdk: {
|
options: {},
|
||||||
hook: () => Effect.die("unused aisdk.hook"),
|
agent: overrides.agent ?? {
|
||||||
},
|
|
||||||
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"),
|
transform: () => Effect.die("unused agent.transform"),
|
||||||
|
reload: () => Effect.die("unused agent.reload"),
|
||||||
},
|
},
|
||||||
catalog: {
|
aisdk: overrides.aisdk ?? {
|
||||||
provider: {
|
sdk: () => Effect.die("unused aisdk.sdk"),
|
||||||
get: () => Effect.die("unused catalog.provider.get"),
|
language: () => Effect.die("unused aisdk.language"),
|
||||||
list: () => Effect.die("unused catalog.provider.list"),
|
|
||||||
available: () => Effect.die("unused catalog.provider.available"),
|
|
||||||
},
|
},
|
||||||
model: {
|
catalog: overrides.catalog ?? {
|
||||||
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"),
|
transform: () => Effect.die("unused catalog.transform"),
|
||||||
|
reload: () => Effect.die("unused catalog.reload"),
|
||||||
},
|
},
|
||||||
command: {
|
command: overrides.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"),
|
transform: () => Effect.die("unused command.transform"),
|
||||||
|
reload: () => Effect.die("unused command.reload"),
|
||||||
},
|
},
|
||||||
event: {
|
integration: overrides.integration ?? {
|
||||||
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"),
|
transform: () => Effect.die("unused integration.transform"),
|
||||||
|
reload: () => Effect.die("unused integration.reload"),
|
||||||
},
|
},
|
||||||
location: {
|
plugin: overrides.plugin ?? {
|
||||||
directory: "/unused/location",
|
add: () => Effect.die("unused plugin.add"),
|
||||||
project: { directory: "/unused/project" },
|
remove: () => Effect.die("unused plugin.remove"),
|
||||||
},
|
},
|
||||||
npm: {
|
reference: overrides.reference ?? {
|
||||||
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"),
|
transform: () => Effect.die("unused reference.transform"),
|
||||||
|
reload: () => Effect.die("unused reference.reload"),
|
||||||
},
|
},
|
||||||
skill: {
|
skill: overrides.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"),
|
transform: () => Effect.die("unused skill.transform"),
|
||||||
},
|
reload: () => Effect.die("unused skill.reload"),
|
||||||
...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"] {
|
export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
|
||||||
return {
|
return {
|
||||||
...host().agent,
|
reload: agent.reload,
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
agent.transform((draft) =>
|
agent.transform((draft) =>
|
||||||
callback({
|
callback({
|
||||||
@@ -136,10 +71,9 @@ export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
|
export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"] {
|
||||||
return {
|
return {
|
||||||
...host().catalog,
|
reload: catalog.reload,
|
||||||
rebuild: catalog.rebuild,
|
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
catalog.transform((draft) =>
|
catalog.transform((draft) =>
|
||||||
callback({
|
callback({
|
||||||
@@ -201,17 +135,9 @@ export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrationHost(integration: Integration.Interface): PluginHost["integration"] {
|
export function integrationHost(integration: Integration.Interface): PluginContext["integration"] {
|
||||||
const info = (value: Integration.Info) => ({
|
|
||||||
id: value.id,
|
|
||||||
name: value.name,
|
|
||||||
methods: value.methods.map(method),
|
|
||||||
connections: value.connections.map((item) => ({ ...item })),
|
|
||||||
})
|
|
||||||
return {
|
return {
|
||||||
get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))),
|
reload: integration.reload,
|
||||||
list: () => integration.list().pipe(Effect.map((items) => items.map(info))),
|
|
||||||
rebuild: integration.rebuild,
|
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
integration.transform((draft) =>
|
integration.transform((draft) =>
|
||||||
callback({
|
callback({
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
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"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
|
||||||
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
|
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
|
||||||
import { Policy } from "@opencode-ai/core/policy"
|
import { Policy } from "@opencode-ai/core/policy"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
@@ -22,21 +20,13 @@ const locationLayer = Layer.succeed(
|
|||||||
Location.Service,
|
Location.Service,
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
||||||
)
|
)
|
||||||
const plugins = PluginV2.layer.pipe(Layer.provide(events))
|
|
||||||
const policy = Policy.layer.pipe(Layer.provide(locationLayer))
|
const policy = Policy.layer.pipe(Layer.provide(locationLayer))
|
||||||
const connections = Credential.defaultLayer.pipe(Layer.fresh)
|
const connections = Credential.defaultLayer.pipe(Layer.fresh)
|
||||||
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
|
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
|
||||||
const catalog = Catalog.layer.pipe(
|
const catalog = Catalog.layer.pipe(
|
||||||
Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, connections, integrations)),
|
Layer.provide(Layer.mergeAll(events, locationLayer, policy, connections, integrations)),
|
||||||
)
|
|
||||||
const layer = Layer.mergeAll(
|
|
||||||
catalog.pipe(Layer.provide(connections)),
|
|
||||||
integrations,
|
|
||||||
connections,
|
|
||||||
events,
|
|
||||||
locationLayer,
|
|
||||||
plugins,
|
|
||||||
)
|
)
|
||||||
|
const layer = Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer)
|
||||||
const it = testEffect(layer)
|
const it = testEffect(layer)
|
||||||
|
|
||||||
describe("ModelsDevPlugin", () => {
|
describe("ModelsDevPlugin", () => {
|
||||||
@@ -58,7 +48,6 @@ describe("ModelsDevPlugin", () => {
|
|||||||
yield* ModelsDevPlugin.effect(
|
yield* ModelsDevPlugin.effect(
|
||||||
host({
|
host({
|
||||||
catalog: catalogHost(catalog),
|
catalog: catalogHost(catalog),
|
||||||
event: { subscribe: () => Stream.never },
|
|
||||||
integration: integrationHost(integrations),
|
integration: integrationHost(integrations),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
|
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||||
|
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
|
||||||
|
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
import { PluginTestLayer } from "./fixture"
|
||||||
|
|
||||||
|
const it = testEffect(PluginTestLayer)
|
||||||
|
|
||||||
|
describe("fromPromise", () => {
|
||||||
|
it.effect("loads a promise plugin and registers a transform hook", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
|
||||||
|
const promisePlugin = define({
|
||||||
|
id: "promise-example",
|
||||||
|
setup: async (ctx) => {
|
||||||
|
expect(ctx.options.mode).toBe("strict")
|
||||||
|
await ctx.agent.transform((draft) => {
|
||||||
|
draft.update("reviewer", (item) => {
|
||||||
|
item.description = "Reviews code"
|
||||||
|
item.mode = "subagent"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const adapted = PluginPromise.fromPromise(promisePlugin)
|
||||||
|
yield* adapted.effect({ ...host, options: { mode: "strict" } })
|
||||||
|
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
||||||
|
description: "Reviews code",
|
||||||
|
mode: "subagent",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("disposes a hook registration on request", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
|
||||||
|
const promisePlugin = define({
|
||||||
|
id: "promise-dispose",
|
||||||
|
setup: async (ctx) => {
|
||||||
|
const registration = await ctx.agent.transform((draft) => {
|
||||||
|
draft.update("temp", (item) => {
|
||||||
|
item.description = "temporary"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await registration.dispose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const adapted = PluginPromise.fromPromise(promisePlugin)
|
||||||
|
yield* adapted.effect(host)
|
||||||
|
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { createAlibaba } from "@ai-sdk/alibaba"
|
import { createAlibaba } from "@ai-sdk/alibaba"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
@@ -13,27 +14,25 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
const addPlugin = Effect.fn(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const host = yield* PluginHost.make()
|
const aisdk = yield* AISDK.Service
|
||||||
yield* plugin.add({ id: AlibabaPlugin.id, effect: AlibabaPlugin.effect(host) })
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
yield* AlibabaPlugin.effect(host)
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
||||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/alibaba",
|
package: "@ai-sdk/alibaba",
|
||||||
options: { name: "alibaba" },
|
options: { name: "alibaba" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(result.sdk).toBeDefined()
|
expect(result.sdk).toBeDefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -41,19 +40,16 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
||||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "alibaba" },
|
options: { name: "alibaba" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(result.sdk).toBeUndefined()
|
expect(result.sdk).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -61,19 +57,16 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
|
||||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/alibaba",
|
package: "@ai-sdk/alibaba",
|
||||||
options: { name: "custom-alibaba", apiKey: "test" },
|
options: { name: "custom-alibaba", apiKey: "test" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen")
|
const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen")
|
||||||
const actual = result.sdk?.languageModel("qwen")
|
const actual = result.sdk?.languageModel("qwen")
|
||||||
expect(actual?.provider).toBe(expected.provider)
|
expect(actual?.provider).toBe(expected.provider)
|
||||||
@@ -84,12 +77,13 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const item = new ModelV2.Info({
|
const item = new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
|
||||||
})
|
})
|
||||||
const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
|
const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
|
||||||
const language = result.sdk?.languageModel(item.api.id)
|
const language = result.sdk?.languageModel(item.api.id)
|
||||||
expect(language?.modelId).toBe("qwen-plus")
|
expect(language?.modelId).toBe("qwen-plus")
|
||||||
expect(language?.provider).toBe("alibaba.chat")
|
expect(language?.provider).toBe("alibaba.chat")
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
const addPlugin = Effect.fn(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const host = yield* PluginHost.make()
|
const aisdk = yield* AISDK.Service
|
||||||
yield* plugin.add({ id: AmazonBedrockPlugin.id, effect: AmazonBedrockPlugin.effect(host) })
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
yield* AmazonBedrockPlugin.effect(host)
|
||||||
})
|
})
|
||||||
|
|
||||||
function required<T>(value: T | undefined): T {
|
function required<T>(value: T | undefined): T {
|
||||||
@@ -109,10 +111,9 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
@@ -125,9 +126,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
endpoint: "https://endpoint.example",
|
endpoint: "https://endpoint.example",
|
||||||
region: "us-east-1",
|
region: "us-east-1",
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(bedrockBaseURL(result.sdk)).toBe("https://endpoint.example")
|
expect(bedrockBaseURL(result.sdk)).toBe("https://endpoint.example")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -137,10 +136,9 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
@@ -152,9 +150,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
baseURL: "https://base.example",
|
baseURL: "https://base.example",
|
||||||
region: "us-east-1",
|
region: "us-east-1",
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(bedrockBaseURL(result.sdk)).toBe("https://base.example")
|
expect(bedrockBaseURL(result.sdk)).toBe("https://base.example")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -174,10 +170,9 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: {
|
api: {
|
||||||
@@ -188,9 +183,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: { name: "amazon-bedrock" },
|
options: { name: "amazon-bedrock" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(result.sdk).toBeDefined()
|
expect(result.sdk).toBeDefined()
|
||||||
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
|
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
|
||||||
}),
|
}),
|
||||||
@@ -201,19 +194,16 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: { name: "amazon-bedrock", region: "eu-west-1" },
|
options: { name: "amazon-bedrock", region: "eu-west-1" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
|
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -223,19 +213,16 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: { name: "amazon-bedrock" },
|
options: { name: "amazon-bedrock" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
|
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -245,19 +232,16 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: { name: "amazon-bedrock" },
|
options: { name: "amazon-bedrock" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
|
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -267,11 +251,10 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () =>
|
withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const headers: Array<string | null> = []
|
const headers: Array<string | null> = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
@@ -285,9 +268,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
return new Response("{}")
|
return new Response("{}")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
|
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
|
||||||
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token")
|
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token")
|
||||||
expect(headers).toEqual(["Bearer option-token"])
|
expect(headers).toEqual(["Bearer option-token"])
|
||||||
@@ -299,11 +280,10 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () =>
|
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const headers: Array<string | null> = []
|
const headers: Array<string | null> = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
@@ -317,9 +297,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
return new Response("{}")
|
return new Response("{}")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
|
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
|
||||||
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("env-token")
|
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("env-token")
|
||||||
expect(headers).toEqual(["Bearer env-token"])
|
expect(headers).toEqual(["Bearer env-token"])
|
||||||
@@ -331,10 +309,9 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
||||||
api: {
|
api: {
|
||||||
@@ -350,9 +327,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
|
baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
|
||||||
region: "us-east-2",
|
region: "us-east-2",
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
const language = result.sdk.responses("openai.gpt-5.5")
|
const language = result.sdk.responses("openai.gpt-5.5")
|
||||||
expect(openAIUrl(language, "/responses", "openai.gpt-5.5")).toBe(
|
expect(openAIUrl(language, "/responses", "openai.gpt-5.5")).toBe(
|
||||||
"https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses",
|
"https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses",
|
||||||
@@ -364,11 +339,10 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () =>
|
it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
||||||
api: {
|
api: {
|
||||||
@@ -379,12 +353,8 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
|
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
|
||||||
},
|
})
|
||||||
{},
|
yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
|
||||||
api: {
|
api: {
|
||||||
@@ -395,9 +365,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { region: "us-east-1" },
|
options: { region: "us-east-1" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["responses:openai.gpt-5.5", "chat:openai.gpt-oss-safeguard-120b"])
|
expect(calls).toEqual(["responses:openai.gpt-5.5", "chat:openai.gpt-oss-safeguard-120b"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -405,10 +373,9 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: {
|
api: {
|
||||||
@@ -419,9 +386,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock/anthropic",
|
package: "@ai-sdk/amazon-bedrock/anthropic",
|
||||||
options: { name: "amazon-bedrock" },
|
options: { name: "amazon-bedrock" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(result.sdk).toBeUndefined()
|
expect(result.sdk).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -438,11 +403,10 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const headers: Array<string | null> = []
|
const headers: Array<string | null> = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: {
|
api: {
|
||||||
@@ -459,9 +423,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
return new Response("{}")
|
return new Response("{}")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", {
|
bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", {
|
||||||
body: "{}",
|
body: "{}",
|
||||||
@@ -476,35 +438,26 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
it.effect("applies legacy cross-region inference prefixes", () =>
|
it.effect("applies legacy cross-region inference prefixes", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "eu-west-1" },
|
options: { region: "eu-west-1" },
|
||||||
},
|
})
|
||||||
{},
|
yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
|
||||||
api: {
|
api: {
|
||||||
@@ -515,33 +468,23 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "eu-west-1" },
|
options: { region: "eu-west-1" },
|
||||||
},
|
})
|
||||||
{},
|
yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "ap-northeast-1" },
|
options: { region: "ap-northeast-1" },
|
||||||
},
|
})
|
||||||
{},
|
yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "ap-southeast-2" },
|
options: { region: "ap-southeast-2" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([
|
||||||
"languageModel:us.anthropic.claude-sonnet-4-5",
|
"languageModel:us.anthropic.claude-sonnet-4-5",
|
||||||
"languageModel:eu.anthropic.claude-sonnet-4-5",
|
"languageModel:eu.anthropic.claude-sonnet-4-5",
|
||||||
@@ -556,20 +499,17 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
withEnv({ AWS_REGION: "eu-west-1" }, () =>
|
withEnv({ AWS_REGION: "eu-west-1" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["languageModel:eu.anthropic.claude-sonnet-4-5"])
|
expect(calls).toEqual(["languageModel:eu.anthropic.claude-sonnet-4-5"])
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -578,6 +518,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
it.effect("applies the full legacy cross-region prefix matrix", () =>
|
it.effect("applies the full legacy cross-region prefix matrix", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const cases = [
|
const cases = [
|
||||||
{ region: "us-east-1", modelID: "amazon.nova-micro-v1:0", expected: "us.amazon.nova-micro-v1:0" },
|
{ region: "us-east-1", modelID: "amazon.nova-micro-v1:0", expected: "us.amazon.nova-micro-v1:0" },
|
||||||
@@ -647,18 +588,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
]
|
]
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
for (const item of cases) {
|
for (const item of cases) {
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
|
||||||
api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: item.region },
|
options: { region: item.region },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`))
|
expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`))
|
||||||
}),
|
}),
|
||||||
@@ -667,20 +604,17 @@ describe("AmazonBedrockPlugin", () => {
|
|||||||
it.effect("ignores non-Bedrock providers for language selection", () =>
|
it.effect("ignores non-Bedrock providers for language selection", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "eu-west-1" },
|
options: { region: "eu-west-1" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual([])
|
expect(calls).toEqual([])
|
||||||
expect(result.language).toBeUndefined()
|
expect(result.language).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
@@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
const addPlugin = Effect.fn(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const host = yield* PluginHost.make()
|
const aisdk = yield* AISDK.Service
|
||||||
yield* plugin.add({ id: AnthropicPlugin.id, effect: AnthropicPlugin.effect(host) })
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
yield* AnthropicPlugin.effect(host)
|
||||||
})
|
})
|
||||||
|
|
||||||
function required<T>(value: T | undefined): T {
|
function required<T>(value: T | undefined): T {
|
||||||
@@ -59,19 +61,16 @@ describe("AnthropicPlugin", () => {
|
|||||||
it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () =>
|
it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
|
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/anthropic",
|
package: "@ai-sdk/anthropic",
|
||||||
options: { name: "custom-anthropic", apiKey: "test" },
|
options: { name: "custom-anthropic", apiKey: "test" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("custom-anthropic")
|
expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("custom-anthropic")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -79,19 +78,16 @@ describe("AnthropicPlugin", () => {
|
|||||||
it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () =>
|
it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
|
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/anthropic",
|
package: "@ai-sdk/anthropic",
|
||||||
options: { name: "anthropic", apiKey: "test" },
|
options: { name: "anthropic", apiKey: "test" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("anthropic")
|
expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("anthropic")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
const addPlugin = Effect.fn(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const host = yield* PluginHost.make()
|
const aisdk = yield* AISDK.Service
|
||||||
yield* plugin.add({ id: AzureCognitiveServicesPlugin.id, effect: AzureCognitiveServicesPlugin.effect(host) })
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
yield* AzureCognitiveServicesPlugin.effect(host)
|
||||||
})
|
})
|
||||||
|
|
||||||
function required<T>(value: T | undefined): T {
|
function required<T>(value: T | undefined): T {
|
||||||
@@ -114,20 +116,17 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
it.effect("selects chat only for completion URLs", () =>
|
it.effect("selects chat only for completion URLs", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { useCompletionUrls: true },
|
options: { useCompletionUrls: true },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["chat:deployment"])
|
expect(calls).toEqual(["chat:deployment"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -135,32 +134,25 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
it.effect("uses the legacy Azure selector order and provider guard", () =>
|
it.effect("uses the legacy Azure selector order and provider guard", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
const ignored = yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
const ignored = yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["responses:deployment"])
|
expect(calls).toEqual(["responses:deployment"])
|
||||||
expect(ignored.language).toBeUndefined()
|
expect(ignored.language).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
@@ -169,51 +161,34 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||||||
it.effect("falls back from responses to messages, chat, then languageModel", () =>
|
it.effect("falls back from responses to messages, chat, then languageModel", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const sdk = fakeSelectorSdk(calls)
|
const sdk = fakeSelectorSdk(calls)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
|
||||||
ProviderV2.ID.make("azure-cognitive-services"),
|
|
||||||
ModelV2.ID.make("messages-deployment"),
|
|
||||||
),
|
|
||||||
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
|
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
|
||||||
api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
|
sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
|
||||||
ProviderV2.ID.make("azure-cognitive-services"),
|
|
||||||
ModelV2.ID.make("language-deployment"),
|
|
||||||
),
|
|
||||||
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: sdk.languageModel },
|
sdk: { languageModel: sdk.languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([
|
||||||
"messages:messages-deployment",
|
"messages:messages-deployment",
|
||||||
"chat:chat-deployment",
|
"chat:chat-deployment",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
const addPlugin = Effect.fn(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const host = yield* PluginHost.make()
|
const aisdk = yield* AISDK.Service
|
||||||
yield* plugin.add({ id: AzurePlugin.id, effect: AzurePlugin.effect(host) })
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
yield* AzurePlugin.effect(host)
|
||||||
})
|
})
|
||||||
|
|
||||||
function required<T>(value: T | undefined): T {
|
function required<T>(value: T | undefined): T {
|
||||||
@@ -142,19 +144,16 @@ 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
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/azure",
|
package: "@ai-sdk/azure",
|
||||||
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
|
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(result.sdk).toBeDefined()
|
expect(result.sdk).toBeDefined()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -163,21 +162,17 @@ describe("AzurePlugin", () => {
|
|||||||
it.effect("rejects missing resourceName when baseURL is not configured", () =>
|
it.effect("rejects missing resourceName when baseURL is not configured", () =>
|
||||||
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const exit = yield* plugin
|
const exit = yield* aisdk
|
||||||
.trigger(
|
.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/azure",
|
package: "@ai-sdk/azure",
|
||||||
options: { name: "azure" },
|
options: { name: "azure" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
.pipe(Effect.exit)
|
.pipe(Effect.exit)
|
||||||
expect(exit._tag).toBe("Failure")
|
expect(exit._tag).toBe("Failure")
|
||||||
}),
|
}),
|
||||||
@@ -187,20 +182,17 @@ describe("AzurePlugin", () => {
|
|||||||
it.effect("selects chat only for completion URLs", () =>
|
it.effect("selects chat only for completion URLs", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { useCompletionUrls: true },
|
options: { useCompletionUrls: true },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["chat:deployment"])
|
expect(calls).toEqual(["chat:deployment"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -208,20 +200,17 @@ describe("AzurePlugin", () => {
|
|||||||
it.effect("selects chat from per-call useCompletionUrls", () =>
|
it.effect("selects chat from per-call useCompletionUrls", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { useCompletionUrls: true },
|
options: { useCompletionUrls: true },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["chat:deployment"])
|
expect(calls).toEqual(["chat:deployment"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -229,11 +218,10 @@ describe("AzurePlugin", () => {
|
|||||||
it.effect("ignores model useCompletionUrls when per-call option is unset", () =>
|
it.effect("ignores model useCompletionUrls when per-call option is unset", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
@@ -241,9 +229,7 @@ describe("AzurePlugin", () => {
|
|||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["responses:deployment"])
|
expect(calls).toEqual(["responses:deployment"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -251,32 +237,25 @@ describe("AzurePlugin", () => {
|
|||||||
it.effect("uses the legacy Azure selector order and provider guard", () =>
|
it.effect("uses the legacy Azure selector order and provider guard", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
const ignored = yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
const ignored = yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["responses:deployment"])
|
expect(calls).toEqual(["responses:deployment"])
|
||||||
expect(ignored.language).toBeUndefined()
|
expect(ignored.language).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
@@ -285,36 +264,29 @@ describe("AzurePlugin", () => {
|
|||||||
it.effect("falls back through the legacy Azure selector order", () =>
|
it.effect("falls back through the legacy Azure selector order", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const make = (method: string) => (id: string) => {
|
const make = (method: string) => (id: string) => {
|
||||||
calls.push(`${method}:${id}`)
|
calls.push(`${method}:${id}`)
|
||||||
return { modelId: id, provider: method, specificationVersion: "v3" }
|
return { modelId: id, provider: method, specificationVersion: "v3" }
|
||||||
}
|
}
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runLanguage({
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
|
||||||
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
|
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
yield* aisdk.runLanguage({
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"aisdk.language",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
|
||||||
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
|
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: make("languageModel") },
|
sdk: { languageModel: make("languageModel") },
|
||||||
options: {},
|
options: {},
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"])
|
expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
import { describe, expect, mock } from "bun:test"
|
import { describe, expect, mock } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
const addPlugin = Effect.fn(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const host = yield* PluginHost.make()
|
const aisdk = yield* AISDK.Service
|
||||||
yield* plugin.add({ id: CerebrasPlugin.id, effect: CerebrasPlugin.effect(host) })
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
yield* CerebrasPlugin.effect(host)
|
||||||
})
|
})
|
||||||
|
|
||||||
void mock.module("@ai-sdk/cerebras", () => ({
|
void mock.module("@ai-sdk/cerebras", () => ({
|
||||||
@@ -59,10 +61,9 @@ describe("CerebrasPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
cerebrasOptions.length = 0
|
cerebrasOptions.length = 0
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.empty(
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
@@ -76,9 +77,7 @@ describe("CerebrasPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
package: "@ai-sdk/cerebras",
|
package: "@ai-sdk/cerebras",
|
||||||
options: { name: "custom-cerebras", apiKey: "test" },
|
options: { name: "custom-cerebras", apiKey: "test" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(cerebrasOptions).toEqual([{ name: "custom-cerebras", apiKey: "test" }])
|
expect(cerebrasOptions).toEqual([{ name: "custom-cerebras", apiKey: "test" }])
|
||||||
expect(result.sdk.languageModel("llama-4-scout-17b-16e-instruct").provider).toBe("custom-cerebras")
|
expect(result.sdk.languageModel("llama-4-scout-17b-16e-instruct").provider).toBe("custom-cerebras")
|
||||||
}),
|
}),
|
||||||
@@ -88,10 +87,9 @@ describe("CerebrasPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
cerebrasOptions.length = 0
|
cerebrasOptions.length = 0
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* plugin.trigger(
|
yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.empty(
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
@@ -105,9 +103,7 @@ describe("CerebrasPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
package: "@ai-sdk/cerebras",
|
package: "@ai-sdk/cerebras",
|
||||||
options: { name: "configured-cerebras", apiKey: "test" },
|
options: { name: "configured-cerebras", apiKey: "test" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(cerebrasOptions).toEqual([{ name: "configured-cerebras", apiKey: "test" }])
|
expect(cerebrasOptions).toEqual([{ name: "configured-cerebras", apiKey: "test" }])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -116,10 +112,9 @@ describe("CerebrasPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
cerebrasOptions.length = 0
|
cerebrasOptions.length = 0
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* aisdk.runSDK({
|
||||||
"aisdk.sdk",
|
|
||||||
{
|
|
||||||
model: new ModelV2.Info({
|
model: new ModelV2.Info({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.empty(
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
@@ -133,9 +128,7 @@ describe("CerebrasPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
package: "@ai-sdk/groq",
|
package: "@ai-sdk/groq",
|
||||||
options: { name: "custom-cerebras", apiKey: "test" },
|
options: { name: "custom-cerebras", apiKey: "test" },
|
||||||
},
|
})
|
||||||
{},
|
|
||||||
)
|
|
||||||
expect(cerebrasOptions).toEqual([])
|
expect(cerebrasOptions).toEqual([])
|
||||||
expect(result.sdk).toBeUndefined()
|
expect(result.sdk).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user