fix(acp-next): add config switch fast paths (#29255)
This commit is contained in:
@@ -8,6 +8,9 @@ import {
|
|||||||
type LoadSessionRequest,
|
type LoadSessionRequest,
|
||||||
type NewSessionRequest,
|
type NewSessionRequest,
|
||||||
type PromptRequest,
|
type PromptRequest,
|
||||||
|
type SetSessionConfigOptionRequest,
|
||||||
|
type SetSessionModelRequest,
|
||||||
|
type SetSessionModeRequest,
|
||||||
} from "@agentclientprotocol/sdk"
|
} from "@agentclientprotocol/sdk"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
@@ -41,6 +44,18 @@ export class Agent implements ACPAgent {
|
|||||||
return run(this.service.loadSession(params))
|
return run(this.service.loadSession(params))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSessionConfigOption(params: SetSessionConfigOptionRequest) {
|
||||||
|
return run(this.service.setSessionConfigOption(params))
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionMode(params: SetSessionModeRequest) {
|
||||||
|
return run(this.service.setSessionMode(params))
|
||||||
|
}
|
||||||
|
|
||||||
|
unstable_setSessionModel(params: SetSessionModelRequest) {
|
||||||
|
return run(this.service.setSessionModel(params))
|
||||||
|
}
|
||||||
|
|
||||||
prompt(params: PromptRequest) {
|
prompt(params: PromptRequest) {
|
||||||
return run(this.service.prompt(params))
|
return run(this.service.prompt(params))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,18 @@ import {
|
|||||||
type NewSessionResponse,
|
type NewSessionResponse,
|
||||||
type PromptRequest,
|
type PromptRequest,
|
||||||
type PromptResponse,
|
type PromptResponse,
|
||||||
|
type SetSessionConfigOptionRequest,
|
||||||
|
type SetSessionConfigOptionResponse,
|
||||||
|
type SetSessionModelRequest,
|
||||||
|
type SetSessionModelResponse,
|
||||||
|
type SetSessionModeRequest,
|
||||||
|
type SetSessionModeResponse,
|
||||||
} from "@agentclientprotocol/sdk"
|
} from "@agentclientprotocol/sdk"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { Context, Effect, Layer, ManagedRuntime } from "effect"
|
import { Context, Effect, Layer, ManagedRuntime } from "effect"
|
||||||
import * as ACPNextError from "./error"
|
import * as ACPNextError from "./error"
|
||||||
import { buildConfigOptions } from "./config-option"
|
import { buildConfigOptions, parseModelSelection } from "./config-option"
|
||||||
import { Directory } from "./directory"
|
import { Directory } from "./directory"
|
||||||
import { ACPNextSession } from "./session"
|
import { ACPNextSession } from "./session"
|
||||||
import { ModelID, ProviderID } from "@/provider/schema"
|
import { ModelID, ProviderID } from "@/provider/schema"
|
||||||
@@ -34,6 +40,11 @@ export type Interface = {
|
|||||||
readonly authenticate: (input: AuthenticateRequest) => Effect.Effect<AuthenticateResponse, Error>
|
readonly authenticate: (input: AuthenticateRequest) => Effect.Effect<AuthenticateResponse, Error>
|
||||||
readonly newSession: (input: NewSessionRequest) => Effect.Effect<NewSessionResponse, Error>
|
readonly newSession: (input: NewSessionRequest) => Effect.Effect<NewSessionResponse, Error>
|
||||||
readonly loadSession: (input: LoadSessionRequest) => Effect.Effect<LoadSessionResponse, Error>
|
readonly loadSession: (input: LoadSessionRequest) => Effect.Effect<LoadSessionResponse, Error>
|
||||||
|
readonly setSessionConfigOption: (
|
||||||
|
input: SetSessionConfigOptionRequest,
|
||||||
|
) => Effect.Effect<SetSessionConfigOptionResponse, Error>
|
||||||
|
readonly setSessionMode: (input: SetSessionModeRequest) => Effect.Effect<SetSessionModeResponse, Error>
|
||||||
|
readonly setSessionModel: (input: SetSessionModelRequest) => Effect.Effect<SetSessionModelResponse, Error>
|
||||||
readonly prompt: (input: PromptRequest) => Effect.Effect<PromptResponse, Error>
|
readonly prompt: (input: PromptRequest) => Effect.Effect<PromptResponse, Error>
|
||||||
readonly cancel: (input: CancelNotification) => Effect.Effect<void, Error>
|
readonly cancel: (input: CancelNotification) => Effect.Effect<void, Error>
|
||||||
}
|
}
|
||||||
@@ -180,11 +191,96 @@ export function make(input: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const setSessionConfigOption = Effect.fn("ACPNext.setSessionConfigOption")(function* (
|
||||||
|
params: SetSessionConfigOptionRequest,
|
||||||
|
) {
|
||||||
|
const current = yield* session.get(params.sessionId)
|
||||||
|
const snapshot = yield* directorySnapshot(current.cwd)
|
||||||
|
if (typeof params.value !== "string") {
|
||||||
|
return yield* new ACPNextError.InvalidConfigOptionError({ configId: params.configId })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.configId === "model") {
|
||||||
|
const selected = yield* parseSelectedModel(snapshot, params.value)
|
||||||
|
const variant = selected.variant ?? selectVariant(snapshot, selected.model)
|
||||||
|
const state = yield* session
|
||||||
|
.setVariant(params.sessionId, Directory.variants(snapshot, selected.model) ? variant : undefined)
|
||||||
|
.pipe(Effect.andThen(session.setModel(params.sessionId, selected.model)))
|
||||||
|
return {
|
||||||
|
configOptions: configOptions(snapshot, {
|
||||||
|
model: state.model ?? selected.model,
|
||||||
|
variant: state.variant,
|
||||||
|
modeId: state.modeId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.configId === "effort") {
|
||||||
|
const model = current.model ?? selectDefaultModel(snapshot)
|
||||||
|
const variants = Directory.variants(snapshot, model)
|
||||||
|
if (!variants || !Object.keys(variants).includes(params.value)) {
|
||||||
|
return yield* new ACPNextError.InvalidEffortError({ effort: params.value })
|
||||||
|
}
|
||||||
|
const state = yield* session.setVariant(params.sessionId, params.value)
|
||||||
|
return {
|
||||||
|
configOptions: configOptions(snapshot, {
|
||||||
|
model: state.model ?? model,
|
||||||
|
variant: state.variant,
|
||||||
|
modeId: state.modeId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.configId === "mode") {
|
||||||
|
if (!snapshot.availableModes.some((mode) => mode.id === params.value)) {
|
||||||
|
return yield* new ACPNextError.InvalidModeError({ mode: params.value })
|
||||||
|
}
|
||||||
|
const state = yield* session.setMode(params.sessionId, params.value)
|
||||||
|
return {
|
||||||
|
configOptions: configOptions(snapshot, {
|
||||||
|
model: state.model ?? selectDefaultModel(snapshot),
|
||||||
|
variant: state.variant,
|
||||||
|
modeId: state.modeId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return yield* new ACPNextError.InvalidConfigOptionError({ configId: params.configId })
|
||||||
|
})
|
||||||
|
|
||||||
|
const setSessionMode = Effect.fn("ACPNext.setSessionMode")(function* (params: SetSessionModeRequest) {
|
||||||
|
const current = yield* session.get(params.sessionId)
|
||||||
|
const snapshot = yield* directorySnapshot(current.cwd)
|
||||||
|
if (!snapshot.availableModes.some((mode) => mode.id === params.modeId)) {
|
||||||
|
return yield* new ACPNextError.InvalidModeError({ mode: params.modeId })
|
||||||
|
}
|
||||||
|
yield* session.setMode(params.sessionId, params.modeId)
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const setSessionModel = Effect.fn("ACPNext.setSessionModel")(function* (params: SetSessionModelRequest) {
|
||||||
|
const current = yield* session.get(params.sessionId)
|
||||||
|
const snapshot = yield* directorySnapshot(current.cwd)
|
||||||
|
const selected = yield* parseSelectedModel(snapshot, params.modelId)
|
||||||
|
yield* session
|
||||||
|
.setVariant(
|
||||||
|
params.sessionId,
|
||||||
|
Directory.variants(snapshot, selected.model)
|
||||||
|
? (selected.variant ?? selectVariant(snapshot, selected.model))
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
.pipe(Effect.andThen(session.setModel(params.sessionId, selected.model)))
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialize,
|
initialize,
|
||||||
authenticate,
|
authenticate,
|
||||||
newSession,
|
newSession,
|
||||||
loadSession,
|
loadSession,
|
||||||
|
setSessionConfigOption,
|
||||||
|
setSessionMode,
|
||||||
|
setSessionModel,
|
||||||
prompt: Effect.fn("ACPNext.prompt")(function* (_input: PromptRequest) {
|
prompt: Effect.fn("ACPNext.prompt")(function* (_input: PromptRequest) {
|
||||||
return yield* new ACPNextError.UnsupportedOperationError({ method: "session/prompt" })
|
return yield* new ACPNextError.UnsupportedOperationError({ method: "session/prompt" })
|
||||||
}),
|
}),
|
||||||
@@ -371,6 +467,30 @@ function configOptions(snapshot: Directory.Snapshot, session: ConfigState) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSelectedModel(snapshot: Directory.Snapshot, modelId: string) {
|
||||||
|
const selected = parseModelSelection(modelId, Object.values(snapshot.providers))
|
||||||
|
const provider = snapshot.providers[ProviderID.make(selected.model.providerID)]
|
||||||
|
const model = provider?.models[ModelID.make(selected.model.modelID)]
|
||||||
|
if (!model) {
|
||||||
|
return Effect.fail(
|
||||||
|
new ACPNextError.InvalidModelError({
|
||||||
|
providerId: selected.model.providerID,
|
||||||
|
modelId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (selected.variant && !model.variants?.[selected.variant]) {
|
||||||
|
return Effect.fail(new ACPNextError.InvalidEffortError({ effort: selected.variant }))
|
||||||
|
}
|
||||||
|
return Effect.succeed({
|
||||||
|
model: {
|
||||||
|
providerID: provider.id,
|
||||||
|
modelID: model.id,
|
||||||
|
},
|
||||||
|
variant: selected.variant,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function sendAvailableCommands(
|
function sendAvailableCommands(
|
||||||
connection: Pick<AgentSideConnection, "sessionUpdate"> | undefined,
|
connection: Pick<AgentSideConnection, "sessionUpdate"> | undefined,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { describe, expect, it } from "bun:test"
|
import { describe, expect, it } from "bun:test"
|
||||||
import type { AgentSideConnection, LoadSessionResponse, NewSessionResponse } from "@agentclientprotocol/sdk"
|
import type {
|
||||||
|
AgentSideConnection,
|
||||||
|
LoadSessionResponse,
|
||||||
|
NewSessionResponse,
|
||||||
|
SessionConfigOption,
|
||||||
|
SessionConfigSelectOption,
|
||||||
|
SetSessionConfigOptionResponse,
|
||||||
|
} from "@agentclientprotocol/sdk"
|
||||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import * as ACPNextService from "@/acp-next/service"
|
import * as ACPNextService from "@/acp-next/service"
|
||||||
@@ -10,6 +17,7 @@ import type { Provider } from "@/provider/provider"
|
|||||||
const providerID = ProviderID.make("test")
|
const providerID = ProviderID.make("test")
|
||||||
const modelID = ModelID.make("test-model")
|
const modelID = ModelID.make("test-model")
|
||||||
const configuredModelID = ModelID.make("configured-model")
|
const configuredModelID = ModelID.make("configured-model")
|
||||||
|
const secondModelID = ModelID.make("second-model")
|
||||||
|
|
||||||
const provider: Provider.Info = {
|
const provider: Provider.Info = {
|
||||||
id: providerID,
|
id: providerID,
|
||||||
@@ -88,6 +96,43 @@ const provider: Provider.Info = {
|
|||||||
headers: {},
|
headers: {},
|
||||||
release_date: "2026-01-01",
|
release_date: "2026-01-01",
|
||||||
},
|
},
|
||||||
|
[secondModelID]: {
|
||||||
|
id: secondModelID,
|
||||||
|
providerID,
|
||||||
|
api: {
|
||||||
|
id: secondModelID,
|
||||||
|
url: "https://example.com",
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
},
|
||||||
|
name: "Second Model",
|
||||||
|
family: "test",
|
||||||
|
capabilities: {
|
||||||
|
temperature: true,
|
||||||
|
reasoning: true,
|
||||||
|
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: {
|
||||||
|
low: { reasoningEffort: "low" },
|
||||||
|
medium: { reasoningEffort: "medium" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +404,131 @@ describe("ACP next service sessions", () => {
|
|||||||
expect(result.sessionId).toBe("configured-model")
|
expect(result.sessionId).toBe("configured-model")
|
||||||
expect(result.configOptions?.find((option) => option.id === "model")?.currentValue).toBe("test/configured-model")
|
expect(result.configOptions?.find((option) => option.id === "model")?.currentValue).toBe("test/configured-model")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("switches model and returns updated model and effort options", async () => {
|
||||||
|
const { service } = makeService()
|
||||||
|
const session = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] }))
|
||||||
|
const updated = await Effect.runPromise(
|
||||||
|
service.setSessionConfigOption({
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
configId: "model",
|
||||||
|
value: "test/second-model",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(select(updated, "model")?.currentValue).toBe("test/second-model")
|
||||||
|
expect(select(updated, "effort")?.currentValue).toBe("low")
|
||||||
|
expect(flattenSelectOptions(select(updated, "effort")).map((option) => option.value)).toEqual(["low", "medium"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("switches effort and returns the updated effort current value", async () => {
|
||||||
|
const { service } = makeService()
|
||||||
|
const session = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] }))
|
||||||
|
const updated = await Effect.runPromise(
|
||||||
|
service.setSessionConfigOption({
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
configId: "effort",
|
||||||
|
value: "high",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(select(updated, "effort")?.currentValue).toBe("high")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("switches mode and returns the updated mode current value", async () => {
|
||||||
|
const { service } = makeService()
|
||||||
|
const session = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] }))
|
||||||
|
const updated = await Effect.runPromise(
|
||||||
|
service.setSessionConfigOption({
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
configId: "mode",
|
||||||
|
value: "plan",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(select(updated, "mode")?.currentValue).toBe("plan")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("maps invalid model effort mode and config id to invalid params", async () => {
|
||||||
|
const { service } = makeService()
|
||||||
|
const session = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] }))
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
[
|
||||||
|
{ configId: "model", value: "test/missing-model" },
|
||||||
|
{ configId: "effort", value: "max" },
|
||||||
|
{ configId: "mode", value: "missing-mode" },
|
||||||
|
{ configId: "missing", value: "value" },
|
||||||
|
].map((input) =>
|
||||||
|
Effect.runPromise(
|
||||||
|
service
|
||||||
|
.setSessionConfigOption({ sessionId: session.sessionId, ...input })
|
||||||
|
.pipe(Effect.mapError(ACPNextError.toRequestError), Effect.flip),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expect(results.map((error) => error.code)).toEqual([-32602, -32602, -32602, -32602])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not reload providers or commands when switching effort from a warm snapshot", async () => {
|
||||||
|
let providersCalls = 0
|
||||||
|
let commandCalls = 0
|
||||||
|
const sdk = {
|
||||||
|
config: {
|
||||||
|
providers: () => {
|
||||||
|
providersCalls++
|
||||||
|
return Promise.resolve({ data: { providers: [provider], default: { test: modelID } } })
|
||||||
|
},
|
||||||
|
get: () => Promise.resolve({ data: {} }),
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
agents: () => Promise.resolve({ data: [{ name: "build", mode: "primary", permission: [], options: {} }] }),
|
||||||
|
skills: () => Promise.resolve({ data: [] }),
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
list: () => {
|
||||||
|
commandCalls++
|
||||||
|
return Promise.resolve({ data: [] })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
create: () => Promise.resolve({ data: { id: "ses_fast" } }),
|
||||||
|
list: () => Promise.resolve({ data: [] }),
|
||||||
|
},
|
||||||
|
mcp: {
|
||||||
|
add: () => Promise.resolve({ data: {} }),
|
||||||
|
},
|
||||||
|
} as unknown as OpencodeClient
|
||||||
|
const service = ACPNextService.make({ sdk })
|
||||||
|
const session = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] }))
|
||||||
|
|
||||||
|
expect(providersCalls).toBe(1)
|
||||||
|
expect(commandCalls).toBe(1)
|
||||||
|
|
||||||
|
await Effect.runPromise(
|
||||||
|
service.setSessionConfigOption({
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
configId: "effort",
|
||||||
|
value: "high",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(providersCalls).toBe(1)
|
||||||
|
expect(commandCalls).toBe(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function categories(result: NewSessionResponse | LoadSessionResponse) {
|
function categories(result: NewSessionResponse | LoadSessionResponse) {
|
||||||
return result.configOptions?.map((option) => option.category) ?? []
|
return result.configOptions?.map((option) => option.category) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function select(result: SetSessionConfigOptionResponse, id: string) {
|
||||||
|
return result.configOptions.find(
|
||||||
|
(option): option is Extract<SessionConfigOption, { type: "select" }> =>
|
||||||
|
option.id === id && option.type === "select",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenSelectOptions(option: Extract<SessionConfigOption, { type: "select" }> | undefined) {
|
||||||
|
return option?.options.flatMap((item): SessionConfigSelectOption[] => ("value" in item ? [item] : item.options)) ?? []
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import type {
|
|||||||
LoadSessionResponse,
|
LoadSessionResponse,
|
||||||
NewSessionResponse,
|
NewSessionResponse,
|
||||||
SessionNotification,
|
SessionNotification,
|
||||||
|
SetSessionConfigOptionResponse,
|
||||||
} from "@agentclientprotocol/sdk"
|
} from "@agentclientprotocol/sdk"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { cliIt } from "../../lib/cli-process"
|
import { cliIt } from "../../lib/cli-process"
|
||||||
import { testProviderConfig } from "../../lib/test-provider"
|
import { testProviderConfig } from "../../lib/test-provider"
|
||||||
import { createAcpClient, expectOk, selectConfigOption } from "../acp/acp-test-client"
|
import { createAcpClient, expectOk, firstAlternateValue, selectConfigOption } from "../acp/acp-test-client"
|
||||||
|
|
||||||
describe("opencode acp-next (subprocess)", () => {
|
describe("opencode acp-next (subprocess)", () => {
|
||||||
cliIt.live(
|
cliIt.live(
|
||||||
@@ -98,6 +99,66 @@ describe("opencode acp-next (subprocess)", () => {
|
|||||||
60_000,
|
60_000,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cliIt.live(
|
||||||
|
"switches model through config options behind OPENCODE_ACP_NEXT",
|
||||||
|
({ home, llm, opencode }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const acp = createAcpClient(
|
||||||
|
yield* opencode.acp({
|
||||||
|
env: {
|
||||||
|
OPENCODE_ACP_NEXT: "1",
|
||||||
|
OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 })
|
||||||
|
const session = expectOk(yield* acp.request<NewSessionResponse>("session/new", { cwd: home, mcpServers: [] }))
|
||||||
|
|
||||||
|
const updated = expectOk(
|
||||||
|
yield* acp.request<SetSessionConfigOptionResponse>("session/set_config_option", {
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
configId: "model",
|
||||||
|
value: "test/second-model",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(selectConfigOption(updated.configOptions, "model")?.currentValue).toBe("test/second-model")
|
||||||
|
}),
|
||||||
|
60_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
cliIt.live(
|
||||||
|
"switches effort through config options behind OPENCODE_ACP_NEXT",
|
||||||
|
({ home, llm, opencode }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const acp = createAcpClient(
|
||||||
|
yield* opencode.acp({
|
||||||
|
env: {
|
||||||
|
OPENCODE_ACP_NEXT: "1",
|
||||||
|
OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 })
|
||||||
|
const session = expectOk(yield* acp.request<NewSessionResponse>("session/new", { cwd: home, mcpServers: [] }))
|
||||||
|
const effort = selectConfigOption(session.configOptions, "effort")
|
||||||
|
expect(effort?.category).toBe("thought_level")
|
||||||
|
const nextEffort = effort ? firstAlternateValue(effort) : undefined
|
||||||
|
expect(nextEffort).toBe("high")
|
||||||
|
|
||||||
|
const updated = expectOk(
|
||||||
|
yield* acp.request<SetSessionConfigOptionResponse>("session/set_config_option", {
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
configId: "effort",
|
||||||
|
value: nextEffort,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(selectConfigOption(updated.configOptions, "effort")?.currentValue).toBe(nextEffort)
|
||||||
|
}),
|
||||||
|
60_000,
|
||||||
|
)
|
||||||
|
|
||||||
cliIt.live(
|
cliIt.live(
|
||||||
"exits cleanly when flagged stdin is closed",
|
"exits cleanly when flagged stdin is closed",
|
||||||
({ opencode }) =>
|
({ opencode }) =>
|
||||||
@@ -134,3 +195,34 @@ function errorCode(error: unknown) {
|
|||||||
if (!("code" in error)) return undefined
|
if (!("code" in error)) return undefined
|
||||||
return typeof error.code === "number" ? error.code : undefined
|
return typeof error.code === "number" ? error.code : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifierConfig(llmUrl: string) {
|
||||||
|
const config = testProviderConfig(llmUrl)
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
model: "test/test-model",
|
||||||
|
provider: {
|
||||||
|
test: {
|
||||||
|
...config.provider.test,
|
||||||
|
models: {
|
||||||
|
"test-model": {
|
||||||
|
...config.provider.test.models["test-model"],
|
||||||
|
variants: {
|
||||||
|
low: {},
|
||||||
|
high: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"second-model": {
|
||||||
|
...config.provider.test.models["test-model"],
|
||||||
|
id: "second-model",
|
||||||
|
name: "Second Test Model",
|
||||||
|
variants: {
|
||||||
|
medium: {},
|
||||||
|
max: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user