|
@@ -26,6 +26,7 @@ import {
|
|
|
type ToolCallContent,
|
|
type ToolCallContent,
|
|
|
type ToolKind,
|
|
type ToolKind,
|
|
|
} from "@agentclientprotocol/sdk"
|
|
} from "@agentclientprotocol/sdk"
|
|
|
|
|
+
|
|
|
import { Log } from "../util/log"
|
|
import { Log } from "../util/log"
|
|
|
import { ACPSessionManager } from "./session"
|
|
import { ACPSessionManager } from "./session"
|
|
|
import type { ACPConfig } from "./types"
|
|
import type { ACPConfig } from "./types"
|
|
@@ -40,6 +41,11 @@ import { LoadAPIKeyError } from "ai"
|
|
|
import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
|
|
import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
|
|
|
import { applyPatch } from "diff"
|
|
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 {
|
|
export namespace ACP {
|
|
|
const log = Log.create({ service: "acp-agent" })
|
|
const log = Log.create({ service: "acp-agent" })
|
|
|
|
|
|
|
@@ -476,7 +482,7 @@ export namespace ACP {
|
|
|
sessionId,
|
|
sessionId,
|
|
|
models: load.models,
|
|
models: load.models,
|
|
|
modes: load.modes,
|
|
modes: load.modes,
|
|
|
- _meta: {},
|
|
|
|
|
|
|
+ _meta: load._meta,
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
const error = MessageV2.fromError(e, {
|
|
const error = MessageV2.fromError(e, {
|
|
@@ -529,7 +535,7 @@ export namespace ACP {
|
|
|
providerID: lastUser.model.providerID,
|
|
providerID: lastUser.model.providerID,
|
|
|
modelID: lastUser.model.modelID,
|
|
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
|
|
result.modes.currentModeId = lastUser.agent
|
|
|
this.sessionManager.setMode(sessionId, 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
|
|
const agents = await this.config.sdk.app
|
|
|
.agents(
|
|
.agents(
|
|
|
{
|
|
{
|
|
@@ -986,6 +972,56 @@ export namespace ACP {
|
|
|
)
|
|
)
|
|
|
.then((resp) => resp.data!)
|
|
.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
|
|
const commands = await this.config.sdk.command
|
|
|
.list(
|
|
.list(
|
|
|
{
|
|
{
|
|
@@ -1006,20 +1042,6 @@ export namespace ACP {
|
|
|
description: "compact the session",
|
|
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> = {}
|
|
const mcpServers: Record<string, Config.Mcp> = {}
|
|
|
for (const server of params.mcpServers) {
|
|
for (const server of params.mcpServers) {
|
|
|
if ("type" in server) {
|
|
if ("type" in server) {
|
|
@@ -1073,40 +1095,46 @@ export namespace ACP {
|
|
|
return {
|
|
return {
|
|
|
sessionId,
|
|
sessionId,
|
|
|
models: {
|
|
models: {
|
|
|
- currentModelId: `${model.providerID}/${model.modelID}`,
|
|
|
|
|
|
|
+ currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
|
|
|
availableModels,
|
|
availableModels,
|
|
|
},
|
|
},
|
|
|
- modes: {
|
|
|
|
|
- availableModes,
|
|
|
|
|
- currentModeId,
|
|
|
|
|
- },
|
|
|
|
|
- _meta: {},
|
|
|
|
|
|
|
+ modes,
|
|
|
|
|
+ _meta: buildVariantMeta({
|
|
|
|
|
+ model,
|
|
|
|
|
+ variant: this.sessionManager.getVariant(sessionId),
|
|
|
|
|
+ availableVariants,
|
|
|
|
|
+ }),
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async unstable_setSessionModel(params: SetSessionModelRequest) {
|
|
async unstable_setSessionModel(params: SetSessionModelRequest) {
|
|
|
const session = this.sessionManager.get(params.sessionId)
|
|
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 {
|
|
return {
|
|
|
- _meta: {},
|
|
|
|
|
|
|
+ _meta: buildVariantMeta({
|
|
|
|
|
+ model: selection.model,
|
|
|
|
|
+ variant: selection.variant,
|
|
|
|
|
+ availableVariants,
|
|
|
|
|
+ }),
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
|
|
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)
|
|
this.sessionManager.setMode(params.sessionId, params.modeId)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1223,6 +1251,7 @@ export namespace ACP {
|
|
|
providerID: model.providerID,
|
|
providerID: model.providerID,
|
|
|
modelID: model.modelID,
|
|
modelID: model.modelID,
|
|
|
},
|
|
},
|
|
|
|
|
+ variant: this.sessionManager.getVariant(sessionID),
|
|
|
parts,
|
|
parts,
|
|
|
agent,
|
|
agent,
|
|
|
directory,
|
|
directory,
|
|
@@ -1434,4 +1463,105 @@ export namespace ACP {
|
|
|
}
|
|
}
|
|
|
return result
|
|
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 }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|