Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
87bb973
chore: upgrade acp sdk and implement session config options
validatedev Jan 17, 2026
ab54916
chore: upgrade bun to 1.3.6
validatedev Jan 17, 2026
508bd4c
Merge branch 'anomalyco:dev' into feat/expose-acp-thinking-variants
validatedev Jan 17, 2026
c510b55
refactor: reorganize helper functions in acp agent module
validatedev Jan 17, 2026
5311624
refactor: remove unused ACP config options fallback flag
validatedev Jan 17, 2026
2ab0cbd
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 17, 2026
109bb9e
refactor: simplify mode and model state resolution logic
validatedev Jan 17, 2026
ca65724
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 18, 2026
546a460
chore: remove trailing whitespace in flag.ts
validatedev Jan 18, 2026
8ee7fc0
fix: add optional chaining for modes in agent restoration
validatedev Jan 18, 2026
f71e8af
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 18, 2026
cb7ab64
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 18, 2026
e6baf42
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 18, 2026
937bf6a
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 18, 2026
c00bc3b
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 20, 2026
51315d1
fix: merge remote-tracking branch 'upstream/dev' into feat/expose-acp…
validatedev Jan 23, 2026
0fce5d9
fix: use sync.set instead of setSyncStore in prompt-input
validatedev Jan 23, 2026
3afddca
Merge remote-tracking branch 'upstream/dev' into feat/expose-acp-thin…
validatedev Jan 23, 2026
fd883bf
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 23, 2026
224d1f9
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 24, 2026
e1d6234
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 24, 2026
68110bd
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 25, 2026
47f871e
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 26, 2026
2a3b792
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 27, 2026
5cddcc3
Merge branch 'dev' into feat/expose-acp-thinking-variants
rekram1-node Jan 29, 2026
2fa5d56
fix: remove duplicate @hey-api/types entry in bun.lock
validatedev Jan 29, 2026
61698eb
Merge branch 'dev' into feat/expose-acp-thinking-variants
validatedev Jan 29, 2026
caf7ec8
Merge branch 'dev' into feat/expose-acp-thinking-variants
rekram1-node Jan 29, 2026
2f2cae8
format
rekram1-node Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "[email protected].5",
"packageManager": "[email protected].6",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@agentclientprotocol/sdk": "0.13.0",
"@ai-sdk/amazon-bedrock": "3.0.73",
"@ai-sdk/anthropic": "2.0.57",
"@ai-sdk/azure": "2.0.91",
Expand Down
233 changes: 177 additions & 56 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type ToolCallContent,
type ToolKind,
} from "@agentclientprotocol/sdk"

import { Log } from "../util/log"
import { ACPSessionManager } from "./session"
import type { ACPConfig, ACPSessionState } from "./types"
Expand All @@ -32,6 +33,11 @@ import { LoadAPIKeyError } from "ai"
import type { 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" })

Expand Down Expand Up @@ -415,7 +421,7 @@ export namespace ACP {
sessionId,
models: load.models,
modes: load.modes,
_meta: {},
_meta: load._meta,
}
} catch (e) {
const error = MessageV2.fromError(e, {
Expand Down Expand Up @@ -670,27 +676,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(
{
Expand All @@ -700,6 +686,46 @@ 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)
let currentModeId = this.sessionManager.get(sessionId).modeId
if (!currentModeId && availableModes.length) {
const defaultAgentName = await AgentModule.defaultAgent()
currentModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
this.sessionManager.setMode(sessionId, currentModeId)
}

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 { availableModes, currentModeId } = await this.resolveModeState(directory, sessionId)
const modeState = currentModeId ? { availableModes, currentModeId } : undefined

const commands = await this.config.sdk.command
.list(
{
Expand All @@ -720,20 +746,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) {
Expand Down Expand Up @@ -787,40 +799,46 @@ export namespace ACP {
return {
sessionId,
models: {
currentModelId: `${model.providerID}/${model.modelID}`,
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
availableModels,
},
modes: {
availableModes,
currentModeId,
},
_meta: {},
modes: modeState,
_meta: buildVariantMeta({
model,
variant: this.sessionManager.getVariant(sessionId),
availableVariants,
}),
}
}

async setSessionModel(params: SetSessionModelRequest) {
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 { model, variant } = parseModelSelection(params.modelId, providers)
this.sessionManager.setModel(session.id, model)
this.sessionManager.setVariant(session.id, variant)

this.sessionManager.setModel(session.id, {
providerID: model.providerID,
modelID: model.modelID,
})
const entries = sortProvidersByName(providers)
const availableVariants = modelVariantsFromProviders(entries, model)

return {
_meta: {},
_meta: buildVariantMeta({
model,
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)
}

Expand Down Expand Up @@ -913,6 +931,7 @@ export namespace ACP {
providerID: model.providerID,
modelID: model.modelID,
},
variant: this.sessionManager.getVariant(sessionID),
parts,
agent,
directory,
Expand Down Expand Up @@ -1124,4 +1143,106 @@ 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 }
}

}
12 changes: 12 additions & 0 deletions packages/opencode/src/acp/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,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
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/acp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ACPSessionState {
providerID: string
modelID: string
}
variant?: string
modeId?: string
}

Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export namespace Flag {
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT")
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")

export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"]
Expand Down