diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/opencode/package.json | 2 | ||||
| -rw-r--r-- | packages/opencode/src/acp/agent.ts | 242 | ||||
| -rw-r--r-- | packages/opencode/src/acp/session.ts | 12 | ||||
| -rw-r--r-- | packages/opencode/src/acp/types.ts | 1 |
4 files changed, 200 insertions, 57 deletions
diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0818122b3..25193c0c0 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -49,7 +49,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.12.0", + "@agentclientprotocol/sdk": "0.13.0", "@ai-sdk/amazon-bedrock": "3.0.73", "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/azure": "2.0.91", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index d4d556485..cc9a029a0 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -26,6 +26,7 @@ import { type ToolCallContent, type ToolKind, } from "@agentclientprotocol/sdk" + import { Log } from "../util/log" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" @@ -40,6 +41,11 @@ import { LoadAPIKeyError } from "ai" import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import { applyPatch } from "diff" +type ModeOption = { id: string; name: string; description?: string } +type ModelOption = { modelId: string; name: string } + +const DEFAULT_VARIANT_VALUE = "default" + export namespace ACP { const log = Log.create({ service: "acp-agent" }) @@ -476,7 +482,7 @@ export namespace ACP { sessionId, models: load.models, modes: load.modes, - _meta: {}, + _meta: load._meta, } } catch (e) { const error = MessageV2.fromError(e, { @@ -529,7 +535,7 @@ export namespace ACP { providerID: lastUser.model.providerID, modelID: lastUser.model.modelID, }) - if (result.modes.availableModes.some((m) => m.id === lastUser.agent)) { + if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) { result.modes.currentModeId = lastUser.agent this.sessionManager.setMode(sessionId, lastUser.agent) } @@ -956,27 +962,7 @@ export namespace ACP { } } - private async loadSessionMode(params: LoadSessionRequest) { - const directory = params.cwd - const model = await defaultModel(this.config, directory) - const sessionId = params.sessionId - - const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) - const entries = providers.sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) return -1 - if (nameA > nameB) return 1 - return 0 - }) - const availableModels = entries.flatMap((provider) => { - const models = Provider.sort(Object.values(provider.models)) - return models.map((model) => ({ - modelId: `${provider.id}/${model.id}`, - name: `${provider.name}/${model.name}`, - })) - }) - + private async loadAvailableModes(directory: string): Promise<ModeOption[]> { const agents = await this.config.sdk.app .agents( { @@ -986,6 +972,56 @@ export namespace ACP { ) .then((resp) => resp.data!) + return agents + .filter((agent) => agent.mode !== "subagent" && !agent.hidden) + .map((agent) => ({ + id: agent.name, + name: agent.name, + description: agent.description, + })) + } + + private async resolveModeState( + directory: string, + sessionId: string, + ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> { + const availableModes = await this.loadAvailableModes(directory) + const currentModeId = + this.sessionManager.get(sessionId).modeId || + (await (async () => { + if (!availableModes.length) return undefined + const defaultAgentName = await AgentModule.defaultAgent() + const resolvedModeId = + availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id + this.sessionManager.setMode(sessionId, resolvedModeId) + return resolvedModeId + })()) + + return { availableModes, currentModeId } + } + + private async loadSessionMode(params: LoadSessionRequest) { + const directory = params.cwd + const model = await defaultModel(this.config, directory) + const sessionId = params.sessionId + + const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) + const entries = sortProvidersByName(providers) + const availableVariants = modelVariantsFromProviders(entries, model) + const currentVariant = this.sessionManager.getVariant(sessionId) + if (currentVariant && !availableVariants.includes(currentVariant)) { + this.sessionManager.setVariant(sessionId, undefined) + } + const availableModels = buildAvailableModels(entries, { includeVariants: true }) + const modeState = await this.resolveModeState(directory, sessionId) + const currentModeId = modeState.currentModeId + const modes = currentModeId + ? { + availableModes: modeState.availableModes, + currentModeId, + } + : undefined + const commands = await this.config.sdk.command .list( { @@ -1006,20 +1042,6 @@ export namespace ACP { description: "compact the session", }) - const availableModes = agents - .filter((agent) => agent.mode !== "subagent" && !agent.hidden) - .map((agent) => ({ - id: agent.name, - name: agent.name, - description: agent.description, - })) - - const defaultAgentName = await AgentModule.defaultAgent() - const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id - - // Persist the default mode so prompt() uses it immediately - this.sessionManager.setMode(sessionId, currentModeId) - const mcpServers: Record<string, Config.Mcp> = {} for (const server of params.mcpServers) { if ("type" in server) { @@ -1073,40 +1095,46 @@ export namespace ACP { return { sessionId, models: { - currentModelId: `${model.providerID}/${model.modelID}`, + currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), availableModels, }, - modes: { - availableModes, - currentModeId, - }, - _meta: {}, + modes, + _meta: buildVariantMeta({ + model, + variant: this.sessionManager.getVariant(sessionId), + availableVariants, + }), } } async unstable_setSessionModel(params: SetSessionModelRequest) { const session = this.sessionManager.get(params.sessionId) + const providers = await this.sdk.config + .providers({ directory: session.cwd }, { throwOnError: true }) + .then((x) => x.data!.providers) - const model = Provider.parseModel(params.modelId) + const selection = parseModelSelection(params.modelId, providers) + this.sessionManager.setModel(session.id, selection.model) + this.sessionManager.setVariant(session.id, selection.variant) - this.sessionManager.setModel(session.id, { - providerID: model.providerID, - modelID: model.modelID, - }) + const entries = sortProvidersByName(providers) + const availableVariants = modelVariantsFromProviders(entries, selection.model) return { - _meta: {}, + _meta: buildVariantMeta({ + model: selection.model, + variant: selection.variant, + availableVariants, + }), } } async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> { - this.sessionManager.get(params.sessionId) - await this.config.sdk.app - .agents({}, { throwOnError: true }) - .then((x) => x.data) - .then((agent) => { - if (!agent) throw new Error(`Agent not found: ${params.modeId}`) - }) + const session = this.sessionManager.get(params.sessionId) + const availableModes = await this.loadAvailableModes(session.cwd) + if (!availableModes.some((mode) => mode.id === params.modeId)) { + throw new Error(`Agent not found: ${params.modeId}`) + } this.sessionManager.setMode(params.sessionId, params.modeId) } @@ -1223,6 +1251,7 @@ export namespace ACP { providerID: model.providerID, modelID: model.modelID, }, + variant: this.sessionManager.getVariant(sessionID), parts, agent, directory, @@ -1434,4 +1463,105 @@ export namespace ACP { } return result } + + function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] { + return [...providers].sort((a, b) => { + const nameA = a.name.toLowerCase() + const nameB = b.name.toLowerCase() + if (nameA < nameB) return -1 + if (nameA > nameB) return 1 + return 0 + }) + } + + function modelVariantsFromProviders( + providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>, + model: { providerID: string; modelID: string }, + ): string[] { + const provider = providers.find((entry) => entry.id === model.providerID) + if (!provider) return [] + const modelInfo = provider.models[model.modelID] + if (!modelInfo?.variants) return [] + return Object.keys(modelInfo.variants) + } + + function buildAvailableModels( + providers: Array<{ id: string; name: string; models: Record<string, any> }>, + options: { includeVariants?: boolean } = {}, + ): ModelOption[] { + const includeVariants = options.includeVariants ?? false + return providers.flatMap((provider) => { + const models = Provider.sort(Object.values(provider.models) as any) + return models.flatMap((model) => { + const base: ModelOption = { + modelId: `${provider.id}/${model.id}`, + name: `${provider.name}/${model.name}`, + } + if (!includeVariants || !model.variants) return [base] + const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE) + const variantOptions = variants.map((variant) => ({ + modelId: `${provider.id}/${model.id}/${variant}`, + name: `${provider.name}/${model.name} (${variant})`, + })) + return [base, ...variantOptions] + }) + }) + } + + function formatModelIdWithVariant( + model: { providerID: string; modelID: string }, + variant: string | undefined, + availableVariants: string[], + includeVariant: boolean, + ) { + const base = `${model.providerID}/${model.modelID}` + if (!includeVariant || !variant || !availableVariants.includes(variant)) return base + return `${base}/${variant}` + } + + function buildVariantMeta(input: { + model: { providerID: string; modelID: string } + variant?: string + availableVariants: string[] + }) { + return { + opencode: { + modelId: `${input.model.providerID}/${input.model.modelID}`, + variant: input.variant ?? null, + availableVariants: input.availableVariants, + }, + } + } + + function parseModelSelection( + modelId: string, + providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>, + ): { model: { providerID: string; modelID: string }; variant?: string } { + const parsed = Provider.parseModel(modelId) + const provider = providers.find((p) => p.id === parsed.providerID) + if (!provider) { + return { model: parsed, variant: undefined } + } + + // Check if modelID exists directly + if (provider.models[parsed.modelID]) { + return { model: parsed, variant: undefined } + } + + // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high") + const segments = parsed.modelID.split("/") + if (segments.length > 1) { + const candidateVariant = segments[segments.length - 1] + const baseModelId = segments.slice(0, -1).join("/") + const baseModelInfo = provider.models[baseModelId] + if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) { + return { + model: { providerID: parsed.providerID, modelID: baseModelId }, + variant: candidateVariant, + } + } + } + + return { model: parsed, variant: undefined } + } } diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 151fa5646..18aa42313 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -96,6 +96,18 @@ export class ACPSessionManager { return session } + getVariant(sessionId: string) { + const session = this.get(sessionId) + return session.variant + } + + setVariant(sessionId: string, variant?: string) { + const session = this.get(sessionId) + session.variant = variant + this.sessions.set(sessionId, session) + return session + } + setMode(sessionId: string, modeId: string) { const session = this.get(sessionId) session.modeId = modeId diff --git a/packages/opencode/src/acp/types.ts b/packages/opencode/src/acp/types.ts index 42b230912..de8ac5081 100644 --- a/packages/opencode/src/acp/types.ts +++ b/packages/opencode/src/acp/types.ts @@ -10,6 +10,7 @@ export interface ACPSessionState { providerID: string modelID: string } + variant?: string modeId?: string } |
