mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-27 00:31:00 +00:00
feat(acp-next): add directory snapshot service (#29241)
This commit is contained in:
parent
d18eab5b85
commit
b2d76434ba
2 changed files with 378 additions and 0 deletions
193
packages/opencode/src/acp-next/directory.ts
Normal file
193
packages/opencode/src/acp-next/directory.ts
Normal file
|
|
@ -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"
|
||||
185
packages/opencode/test/acp-next/directory.test.ts
Normal file
185
packages/opencode/test/acp-next/directory.test.ts
Normal file
|
|
@ -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")
|
||||
}),
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue