210 lines
7.0 KiB
TypeScript
210 lines
7.0 KiB
TypeScript
import { Agent } from "@/agent/agent"
|
|
import { Command } from "@/command"
|
|
import { InstanceRef } from "@/effect/instance-ref"
|
|
import { InstanceStore } from "@/project/instance-store"
|
|
import { ModelID, ProviderID } from "@/provider/schema"
|
|
import { Provider } from "@/provider/provider"
|
|
import { Context, Effect, Layer, SynchronizedRef } from "effect"
|
|
import type * as ACPNextError from "./error"
|
|
|
|
export type ModelOption = {
|
|
readonly providerID: ProviderID
|
|
readonly providerName: string
|
|
readonly modelID: ModelID
|
|
readonly modelName: string
|
|
}
|
|
|
|
export type ModeOption = {
|
|
readonly id: string
|
|
readonly name: string
|
|
readonly description?: string
|
|
}
|
|
|
|
export type ModelVariants = NonNullable<Provider.Model["variants"]>
|
|
|
|
export type DefaultModel = {
|
|
readonly providerID: ProviderID
|
|
readonly modelID: ModelID
|
|
}
|
|
|
|
export type Snapshot = {
|
|
readonly directory: string
|
|
readonly providers: Record<ProviderID, Provider.Info>
|
|
readonly modelOptions: readonly ModelOption[]
|
|
readonly variantsByModel: Readonly<Record<string, ModelVariants>>
|
|
readonly availableModes: readonly ModeOption[]
|
|
readonly defaultModeID: string
|
|
readonly availableCommands: readonly Command.Info[]
|
|
readonly defaultModel?: DefaultModel
|
|
}
|
|
|
|
export interface LoaderInterface {
|
|
readonly load: (directory: string) => Effect.Effect<Snapshot, ACPNextError.Error>
|
|
}
|
|
|
|
export interface Interface {
|
|
readonly get: (directory: string) => Effect.Effect<Snapshot, ACPNextError.Error>
|
|
readonly refresh: (directory: string) => Effect.Effect<Snapshot, ACPNextError.Error>
|
|
readonly variants: (snapshot: Snapshot, model: DefaultModel) => ModelVariants | undefined
|
|
}
|
|
|
|
export class Loader extends Context.Service<Loader, LoaderInterface>()("@opencode/ACPNextDirectoryLoader") {}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/ACPNextDirectory") {}
|
|
|
|
export const modelKey = (model: DefaultModel) => `${model.providerID}/${model.modelID}`
|
|
|
|
export const variants = (snapshot: Snapshot, model: DefaultModel) => snapshot.variantsByModel[modelKey(model)]
|
|
|
|
export const build = (input: {
|
|
readonly directory: string
|
|
readonly providers: Record<ProviderID, Provider.Info>
|
|
readonly modes: readonly ModeOption[]
|
|
readonly defaultModeID: string
|
|
readonly commands: readonly Command.Info[]
|
|
readonly defaultModel?: DefaultModel
|
|
}): Snapshot => {
|
|
const modelOptions = Provider.sort(
|
|
Object.values(input.providers).flatMap((provider) =>
|
|
Object.values(provider.models).map((model) => ({
|
|
id: model.id,
|
|
providerID: provider.id,
|
|
providerName: provider.name,
|
|
modelID: model.id,
|
|
modelName: model.name,
|
|
})),
|
|
),
|
|
).map((model) => ({
|
|
providerID: model.providerID,
|
|
providerName: model.providerName,
|
|
modelID: model.modelID,
|
|
modelName: model.modelName,
|
|
}))
|
|
|
|
return {
|
|
directory: input.directory,
|
|
providers: input.providers,
|
|
modelOptions,
|
|
variantsByModel: Object.fromEntries(
|
|
Object.values(input.providers).flatMap((provider) =>
|
|
Object.values(provider.models).flatMap((model) =>
|
|
model.variants ? [[modelKey({ providerID: provider.id, modelID: model.id }), model.variants]] : [],
|
|
),
|
|
),
|
|
),
|
|
availableModes: input.modes,
|
|
defaultModeID: input.modes.some((mode) => mode.id === input.defaultModeID)
|
|
? input.defaultModeID
|
|
: (input.modes[0]?.id ?? input.defaultModeID),
|
|
availableCommands: input.commands,
|
|
...(input.defaultModel ? { defaultModel: input.defaultModel } : {}),
|
|
}
|
|
}
|
|
|
|
export const loaderLayer = Layer.effect(
|
|
Loader,
|
|
Effect.gen(function* () {
|
|
const store = yield* InstanceStore.Service
|
|
const provider = yield* Provider.Service
|
|
const agent = yield* Agent.Service
|
|
const command = yield* Command.Service
|
|
|
|
return Loader.of({
|
|
load: Effect.fn("ACPNextDirectoryLoader.load")(function* (directory) {
|
|
const ctx = yield* store.load({ directory })
|
|
return yield* Effect.gen(function* () {
|
|
const providers = yield* provider.list()
|
|
const [agents, defaultAgent, commands, defaultModel] = yield* Effect.all(
|
|
[agent.list(), agent.defaultInfo(), command.list(), provider.defaultModel().pipe(Effect.option)],
|
|
{ concurrency: "unbounded" },
|
|
)
|
|
return build({
|
|
directory,
|
|
providers,
|
|
modes: agents
|
|
.filter((item) => item.mode !== "subagent" && item.hidden !== true)
|
|
.map((item) => ({
|
|
id: item.name,
|
|
name: item.name,
|
|
...(item.description ? { description: item.description } : {}),
|
|
})),
|
|
defaultModeID: defaultAgent.name,
|
|
commands: commands.toSorted((a, b) => a.name.localeCompare(b.name)),
|
|
...(defaultModel._tag === "Some" ? { defaultModel: defaultModel.value } : {}),
|
|
})
|
|
}).pipe(Effect.provideService(InstanceRef, ctx))
|
|
}),
|
|
})
|
|
}),
|
|
)
|
|
|
|
export const layer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const loader = yield* Loader
|
|
const snapshots = yield* SynchronizedRef.make(new Map<string, Effect.Effect<Snapshot, ACPNextError.Error>>())
|
|
|
|
const cached = Effect.fnUntraced(function* (directory: string) {
|
|
return yield* SynchronizedRef.modifyEffect(
|
|
snapshots,
|
|
Effect.fnUntraced(function* (items) {
|
|
const current = items.get(directory)
|
|
if (current) return [current, items] as const
|
|
const next = yield* Effect.cached(
|
|
loader.load(directory).pipe(
|
|
Effect.tapError(() =>
|
|
SynchronizedRef.update(snapshots, (state) => {
|
|
const next = new Map(state)
|
|
next.delete(directory)
|
|
return next
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
return [next, new Map(items).set(directory, next)] as const
|
|
}),
|
|
)
|
|
})
|
|
|
|
const get = Effect.fn("ACPNextDirectory.get")(function* (directory: string) {
|
|
return yield* yield* cached(directory)
|
|
})
|
|
|
|
const refresh = Effect.fn("ACPNextDirectory.refresh")(function* (directory: string) {
|
|
return yield* SynchronizedRef.modifyEffect(
|
|
snapshots,
|
|
Effect.fnUntraced(function* (items) {
|
|
const next = yield* Effect.cached(
|
|
loader.load(directory).pipe(
|
|
Effect.tapError(() =>
|
|
SynchronizedRef.update(snapshots, (state) => {
|
|
const next = new Map(state)
|
|
next.delete(directory)
|
|
return next
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
return [next, new Map(items).set(directory, next)] as const
|
|
}),
|
|
).pipe(Effect.flatten)
|
|
})
|
|
|
|
return Service.of({
|
|
get,
|
|
refresh,
|
|
variants,
|
|
})
|
|
}),
|
|
)
|
|
|
|
export const defaultLayer = layer.pipe(
|
|
Layer.provide(loaderLayer),
|
|
Layer.provide(Provider.defaultLayer),
|
|
Layer.provide(Agent.defaultLayer),
|
|
Layer.provide(Command.defaultLayer),
|
|
Layer.provide(InstanceStore.defaultLayer),
|
|
)
|
|
|
|
export * as Directory from "./directory"
|