feat(acp-next): add directory snapshot service (#29241)
This commit is contained in:
@@ -0,0 +1,193 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly get: (directory: string) => Effect.Effect<Snapshot>
|
||||||
|
readonly refresh: (directory: string) => Effect.Effect<Snapshot>
|
||||||
|
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>>())
|
||||||
|
|
||||||
|
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))
|
||||||
|
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))
|
||||||
|
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"
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Directory } from "@/acp-next/directory"
|
||||||
|
import { Command } from "@/command"
|
||||||
|
import { ModelID, ProviderID } from "@/provider/schema"
|
||||||
|
import { Provider } from "@/provider/provider"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { it } from "../lib/effect"
|
||||||
|
|
||||||
|
const command = (name: string): Command.Info => ({
|
||||||
|
name,
|
||||||
|
source: "command",
|
||||||
|
template: `run ${name}`,
|
||||||
|
hints: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const model = (providerID: ProviderID, id: string, variants?: Directory.ModelVariants): Provider.Model => ({
|
||||||
|
id: ModelID.make(id),
|
||||||
|
providerID,
|
||||||
|
api: {
|
||||||
|
id,
|
||||||
|
url: "https://example.com",
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
},
|
||||||
|
name: id,
|
||||||
|
family: "test",
|
||||||
|
capabilities: {
|
||||||
|
temperature: true,
|
||||||
|
reasoning: Boolean(variants),
|
||||||
|
attachment: false,
|
||||||
|
toolcall: true,
|
||||||
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
|
},
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
context: 128000,
|
||||||
|
output: 4096,
|
||||||
|
},
|
||||||
|
status: "active",
|
||||||
|
options: {},
|
||||||
|
headers: {},
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
...(variants ? { variants } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const snapshot = (directory: string) => {
|
||||||
|
const providerID = ProviderID.make(`provider-${directory}`)
|
||||||
|
const modelID = ModelID.make(`model-${directory}`)
|
||||||
|
const providers = {
|
||||||
|
[providerID]: {
|
||||||
|
id: providerID,
|
||||||
|
name: `Provider ${directory}`,
|
||||||
|
source: "config",
|
||||||
|
env: [],
|
||||||
|
options: {},
|
||||||
|
models: {
|
||||||
|
[modelID]: model(providerID, modelID, {
|
||||||
|
low: { reasoningEffort: "low" },
|
||||||
|
high: { reasoningEffort: "high" },
|
||||||
|
}),
|
||||||
|
[ModelID.make(`plain-${directory}`)]: model(providerID, `plain-${directory}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Record<ProviderID, Provider.Info>
|
||||||
|
|
||||||
|
return Directory.build({
|
||||||
|
directory,
|
||||||
|
providers,
|
||||||
|
modes: [
|
||||||
|
{ id: "build", name: `build-${directory}` },
|
||||||
|
{ id: "plan", name: `plan-${directory}`, description: "plan first" },
|
||||||
|
],
|
||||||
|
defaultModeID: "build",
|
||||||
|
commands: [command(`init-${directory}`), command(`review-${directory}`)],
|
||||||
|
defaultModel: { providerID, modelID },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fakeLayer = (calls: string[]) =>
|
||||||
|
Directory.layer.pipe(
|
||||||
|
Layer.provide(
|
||||||
|
Layer.succeed(
|
||||||
|
Directory.Loader,
|
||||||
|
Directory.Loader.of({
|
||||||
|
load: (directory) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
calls.push(directory)
|
||||||
|
return snapshot(directory)
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe("ACP next directory snapshot", () => {
|
||||||
|
it.effect("two concurrent callers share one load", () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const directory = yield* Directory.Service
|
||||||
|
const [first, second] = yield* Effect.all([directory.get("alpha"), directory.get("alpha")], {
|
||||||
|
concurrency: "unbounded",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls).toEqual(["alpha"])
|
||||||
|
expect(first).toBe(second)
|
||||||
|
}).pipe(Effect.provide(fakeLayer(calls)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("warm calls use cached data", () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const directory = yield* Directory.Service
|
||||||
|
const first = yield* directory.get("alpha")
|
||||||
|
const second = yield* directory.get("alpha")
|
||||||
|
|
||||||
|
expect(calls).toEqual(["alpha"])
|
||||||
|
expect(first).toBe(second)
|
||||||
|
}).pipe(Effect.provide(fakeLayer(calls)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("different directories get different snapshots", () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const directory = yield* Directory.Service
|
||||||
|
const [alpha, beta] = yield* Effect.all([directory.get("alpha"), directory.get("beta")], {
|
||||||
|
concurrency: "unbounded",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls.toSorted()).toEqual(["alpha", "beta"])
|
||||||
|
expect(alpha.directory).toBe("alpha")
|
||||||
|
expect(beta.directory).toBe("beta")
|
||||||
|
expect(alpha.defaultModel?.providerID).not.toBe(beta.defaultModel?.providerID)
|
||||||
|
}).pipe(Effect.provide(fakeLayer(calls)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("model variant lookup works", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const directory = yield* Directory.Service
|
||||||
|
const alpha = yield* directory.get("alpha")
|
||||||
|
const model = alpha.defaultModel!
|
||||||
|
|
||||||
|
expect(directory.variants(alpha, model)).toEqual({
|
||||||
|
low: { reasoningEffort: "low" },
|
||||||
|
high: { reasoningEffort: "high" },
|
||||||
|
})
|
||||||
|
expect(directory.variants(alpha, { ...model, modelID: ModelID.make("missing") })).toBeUndefined()
|
||||||
|
}).pipe(Effect.provide(fakeLayer([]))),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("commands and modes are included", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const directory = yield* Directory.Service
|
||||||
|
const alpha = yield* directory.get("alpha")
|
||||||
|
|
||||||
|
expect(alpha.availableCommands.map((item) => item.name)).toEqual(["init-alpha", "review-alpha"])
|
||||||
|
expect(alpha.availableModes).toEqual([
|
||||||
|
{ id: "build", name: "build-alpha" },
|
||||||
|
{ id: "plan", name: "plan-alpha", description: "plan first" },
|
||||||
|
])
|
||||||
|
expect(alpha.defaultModeID).toBe("build")
|
||||||
|
}).pipe(Effect.provide(fakeLayer([]))),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("falls back when the default mode is not available", () =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
expect(
|
||||||
|
Directory.build({
|
||||||
|
directory: "alpha",
|
||||||
|
providers: {},
|
||||||
|
modes: [
|
||||||
|
{ id: "build", name: "Build" },
|
||||||
|
{ id: "plan", name: "Plan" },
|
||||||
|
],
|
||||||
|
defaultModeID: "hidden",
|
||||||
|
commands: [],
|
||||||
|
}).defaultModeID,
|
||||||
|
).toBe("build")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user