Choose non-fast variants for ACP models (#1952)

* fix(providers): expose ACP fast-mode selection

Provider wrappers now forward draft feature lookups to inner ACP clients, and ACP adapters can add initialize metadata for model parameterization. Fast-mode config options flow through the existing provider feature path.

* test(providers): cover wrapped ACP command lookup

* fix(providers): submit Cursor non-fast defaults

* fix(app): preserve parameterized model preferences

* fix(providers): preserve Cursor ACP feature params

* fix(providers): keep Cursor legacy model fallbacks

* fix(providers): preserve Cursor model parameter switches

* refactor(providers): simplify ACP model feature negotiation
This commit is contained in:
Mohamed Boudra
2026-07-09 23:16:17 +02:00
committed by GitHub
parent 6af1379357
commit b2c6476817
6 changed files with 212 additions and 12 deletions

View File

@@ -3,8 +3,10 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
import { createTestLogger } from "../../test-utils/test-logger.js";
import type {
AgentClient,
AgentFeature,
AgentModelDefinition,
AgentMode,
AgentSessionConfig,
ProviderCatalog,
} from "./agent-sdk-types.js";
@@ -41,6 +43,7 @@ const mockState = vi.hoisted(() => {
},
isCommandAvailable: vi.fn(async (_command: string) => false),
runtimeModels: new Map<string, AgentModelDefinition[]>(),
cursorListFeaturesConfigs: [] as AgentSessionConfig[],
reset() {
this.constructorArgs.claude = [];
this.constructorArgs.codex = [];
@@ -52,6 +55,7 @@ const mockState = vi.hoisted(() => {
this.isCommandAvailable.mockReset();
this.isCommandAvailable.mockImplementation(async (_command: string) => false);
this.runtimeModels.clear();
this.cursorListFeaturesConfigs = [];
},
};
});
@@ -378,6 +382,19 @@ vi.mock("./providers/cursor-acp-agent.js", () => ({
async isAvailable(): Promise<boolean> {
return true;
}
async listFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> {
mockState.cursorListFeaturesConfigs.push(config);
return [
{
type: "select",
id: "fast",
label: "Fast",
value: "false",
options: [{ id: "false", label: "Off" }],
},
];
}
},
}));
@@ -697,6 +714,41 @@ test("cursor provider extending acp uses CursorACPAgentClient", () => {
expect(mockState.constructorArgs.genericAcp).toEqual([]);
});
test("wrapped cursor client lists ACP features through the inner provider", async () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: {
cursor: {
extends: "acp",
label: "Cursor",
command: ["cursor-agent", "acp"],
},
},
});
const client = registry.cursor.createClient(logger);
await expect(
client.listFeatures?.({
provider: "cursor",
cwd: "/tmp/cursor",
}),
).resolves.toEqual([
{
type: "select",
id: "fast",
label: "Fast",
value: "false",
options: [{ id: "false", label: "Off" }],
},
]);
expect(mockState.cursorListFeaturesConfigs).toEqual([
{
provider: "acp",
cwd: "/tmp/cursor",
},
]);
});
test("traecli provider extending acp uses TraeACPAgentClient", () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: {

View File

@@ -398,6 +398,7 @@ function wrapClientProvider(
): AgentClient {
const listImportableSessions = inner.listImportableSessions?.bind(inner);
const importSession = inner.importSession?.bind(inner);
const listFeatures = inner.listFeatures?.bind(inner);
return {
provider,
@@ -441,6 +442,9 @@ function wrapClientProvider(
},
resolveCreateConfig: inner.resolveCreateConfig?.bind(inner),
isCreateConfigUnattended: inner.isCreateConfigUnattended?.bind(inner),
listFeatures: listFeatures
? async (config) => await listFeatures({ ...config, provider: inner.provider })
: undefined,
listImportableSessions: listImportableSessions
? async (options) => await listImportableSessions(options)
: undefined,

View File

@@ -229,7 +229,7 @@ export const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = {
supportsRewindBoth: false,
};
const ACP_CLIENT_CAPABILITIES: ACPClientCapabilities = {
const BASE_ACP_CLIENT_CAPABILITIES: ACPClientCapabilities = {
fs: {
readTextFile: true,
writeTextFile: true,
@@ -237,6 +237,18 @@ const ACP_CLIENT_CAPABILITIES: ACPClientCapabilities = {
terminal: true,
};
export type ACPClientCapabilityMeta = Record<string, unknown>;
export function buildACPClientCapabilities(meta?: ACPClientCapabilityMeta): ACPClientCapabilities {
if (!meta || Object.keys(meta).length === 0) {
return BASE_ACP_CLIENT_CAPABILITIES;
}
return {
...BASE_ACP_CLIENT_CAPABILITIES,
_meta: meta,
};
}
// Suppress interactive auth side-effects (e.g. Gemini CLI opening a Google
// sign-in URL in the browser) when probing an ACP agent for models/modes.
// NO_BROWSER is honored by Gemini CLI; other ACP agents ignore it.
@@ -359,6 +371,7 @@ interface ACPAgentClientOptions {
sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse;
configOptionsTransformer?: (configOptions: SessionConfigOption[]) => SessionConfigOption[];
configFeatureOptions?: ACPConfigFeatureOption[];
clientCapabilityMeta?: ACPClientCapabilityMeta;
modeIdTransformer?: (modeId: string) => string | null;
toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
providerModeWriter?: (
@@ -387,6 +400,7 @@ interface ACPAgentSessionOptions {
sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse;
configOptionsTransformer?: (configOptions: SessionConfigOption[]) => SessionConfigOption[];
configFeatureOptions?: ACPConfigFeatureOption[];
clientCapabilityMeta?: ACPClientCapabilityMeta;
modeIdTransformer?: (modeId: string) => string | null;
toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
providerModeWriter?: (
@@ -486,7 +500,7 @@ interface ConfigOptionSelector {
export interface ACPConfigFeatureOption {
id: string;
configId: string;
category: string;
category?: string;
label: string;
description?: string;
tooltip?: string;
@@ -694,6 +708,7 @@ export class ACPAgentClient implements AgentClient {
configOptions: SessionConfigOption[],
) => SessionConfigOption[];
private readonly configFeatureOptions: ACPConfigFeatureOption[];
private readonly clientCapabilityMeta?: ACPClientCapabilityMeta;
private readonly modeIdTransformer?: (modeId: string) => string | null;
private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
private readonly providerModeWriter?: (
@@ -727,6 +742,7 @@ export class ACPAgentClient implements AgentClient {
this.sessionResponseTransformer = options.sessionResponseTransformer;
this.configOptionsTransformer = options.configOptionsTransformer;
this.configFeatureOptions = options.configFeatureOptions ?? [];
this.clientCapabilityMeta = options.clientCapabilityMeta;
this.modeIdTransformer = options.modeIdTransformer;
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
this.providerModeWriter = options.providerModeWriter;
@@ -754,6 +770,7 @@ export class ACPAgentClient implements AgentClient {
sessionResponseTransformer: this.sessionResponseTransformer,
configOptionsTransformer: this.configOptionsTransformer,
configFeatureOptions: this.configFeatureOptions,
clientCapabilityMeta: this.clientCapabilityMeta,
modeIdTransformer: this.modeIdTransformer,
toolSnapshotTransformer: this.toolSnapshotTransformer,
providerModeWriter: this.providerModeWriter,
@@ -802,6 +819,7 @@ export class ACPAgentClient implements AgentClient {
sessionResponseTransformer: this.sessionResponseTransformer,
configOptionsTransformer: this.configOptionsTransformer,
configFeatureOptions: this.configFeatureOptions,
clientCapabilityMeta: this.clientCapabilityMeta,
modeIdTransformer: this.modeIdTransformer,
toolSnapshotTransformer: this.toolSnapshotTransformer,
providerModeWriter: this.providerModeWriter,
@@ -1039,7 +1057,7 @@ export class ACPAgentClient implements AgentClient {
Promise.race([
transport.connection.initialize({
protocolVersion: PROTOCOL_VERSION,
clientCapabilities: ACP_CLIENT_CAPABILITIES,
clientCapabilities: buildACPClientCapabilities(this.clientCapabilityMeta),
clientInfo: { name: "Paseo", version: "dev" },
}),
transport.spawnError,
@@ -1251,6 +1269,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
configOptions: SessionConfigOption[],
) => SessionConfigOption[];
private readonly configFeatureOptions: ACPConfigFeatureOption[];
private readonly clientCapabilityMeta?: ACPClientCapabilityMeta;
private readonly modeIdTransformer?: (modeId: string) => string | null;
private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
private readonly providerModeWriter?: (
@@ -1315,6 +1334,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
this.sessionResponseTransformer = options.sessionResponseTransformer;
this.configOptionsTransformer = options.configOptionsTransformer;
this.configFeatureOptions = options.configFeatureOptions ?? [];
this.clientCapabilityMeta = options.clientCapabilityMeta;
this.modeIdTransformer = options.modeIdTransformer;
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
this.providerModeWriter = options.providerModeWriter;
@@ -1890,16 +1910,19 @@ export class ACPAgentSession implements AgentSession, ACPClient {
}: {
response: { configOptions: SessionConfigOption[] };
configId: string;
category: string;
category?: string;
requestedValue: string;
label: string;
}): string {
this.configOptions = this.transformConfigOptions(response.configOptions);
const responseOption = findSelectConfigOption({
configOptions: this.configOptions,
category,
id: configId,
});
const responseOption =
category === undefined
? findSelectConfigOptionById({ configOptions: this.configOptions, id: configId })
: findSelectConfigOption({
configOptions: this.configOptions,
category,
id: configId,
});
if (responseOption?.currentValue != null) {
return responseOption.currentValue;
}
@@ -2297,7 +2320,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
const initialize = await this.runACPRequest(() =>
connection.initialize({
protocolVersion: PROTOCOL_VERSION,
clientCapabilities: ACP_CLIENT_CAPABILITIES,
clientCapabilities: buildACPClientCapabilities(this.clientCapabilityMeta),
clientInfo: { name: "Paseo", version: "dev" },
}),
);
@@ -2743,6 +2766,19 @@ function findSelectConfigOption({
return option ?? null;
}
function findSelectConfigOptionById({
configOptions,
id,
}: {
configOptions: SessionConfigOption[] | null | undefined;
id: string;
}): SelectConfigOption | null {
const option = configOptions?.find(
(entry): entry is SelectConfigOption => entry.type === "select" && entry.id === id,
);
return option ?? null;
}
function findSelectConfigFeatureOption(
configOptions: SessionConfigOption[] | null | undefined,
featureOption: ACPConfigFeatureOption,
@@ -2751,7 +2787,7 @@ function findSelectConfigFeatureOption(
(entry): entry is SelectConfigOption =>
entry.type === "select" &&
entry.id === featureOption.configId &&
entry.category === featureOption.category,
(featureOption.category === undefined || entry.category === featureOption.category),
);
return option ?? null;
}

View File

@@ -1,10 +1,22 @@
import { describe, expect, test, vi } from "vitest";
import type { SpawnedACPProcess, SessionStateResponse } from "./acp-agent.js";
import { CursorACPAgentClient } from "./cursor-acp-agent.js";
import { CURSOR_FAST_FEATURE_OPTION, CursorACPAgentClient } from "./cursor-acp-agent.js";
import { createTestLogger } from "../../../test-utils/test-logger.js";
describe("CursorACPAgentClient model discovery", () => {
function fastConfigOption(currentValue: "false" | "true") {
return {
id: "fast",
name: "Fast",
type: "select" as const,
currentValue,
options: [
{ value: "false", name: "Off" },
{ value: "true", name: "Fast" },
],
};
}
class TestCursorACPAgentClient extends CursorACPAgentClient {
constructor(response: SessionStateResponse) {
super({
@@ -77,4 +89,79 @@ describe("CursorACPAgentClient model discovery", () => {
modes: [],
});
});
test("keeps modern Cursor models as plain ACP ids", async () => {
const client = new TestCursorACPAgentClient({
sessionId: "session-1",
models: {
currentModelId: "composer-2.5",
availableModels: [
{
modelId: "composer-2.5",
name: "Composer 2.5",
description: null,
},
],
},
configOptions: [fastConfigOption("false")],
});
await expect(
client.fetchCatalog({ scope: "workspace", cwd: "/tmp/cursor", force: false }),
).resolves.toEqual({
models: [
{
provider: "acp",
id: "composer-2.5",
label: "Composer 2.5",
description: undefined,
isDefault: true,
thinkingOptions: undefined,
defaultThinkingOptionId: undefined,
},
],
modes: [],
});
});
test("exposes Cursor fast mode through provider features", async () => {
const client = new TestCursorACPAgentClient({
sessionId: "session-1",
models: null,
configOptions: [fastConfigOption("false")],
});
await expect(
client.listFeatures({
provider: "acp",
cwd: "/tmp/cursor",
}),
).resolves.toEqual([
{
type: "select",
id: CURSOR_FAST_FEATURE_OPTION.id,
label: "Fast",
description: "Cursor fast mode",
tooltip: "Select Cursor fast mode",
icon: "zap",
value: "false",
options: [
{
id: "false",
label: "Off",
isDefault: true,
description: undefined,
metadata: undefined,
},
{
id: "true",
label: "Fast",
isDefault: false,
description: undefined,
metadata: undefined,
},
],
},
]);
});
});

View File

@@ -1,5 +1,6 @@
import type { Logger } from "pino";
import type { ACPConfigFeatureOption } from "./acp-agent.js";
import { GenericACPAgentClient } from "./generic-acp-agent.js";
interface CursorACPAgentClientOptions {
@@ -12,6 +13,18 @@ interface CursorACPAgentClientOptions {
}
const CURSOR_INITIAL_COMMANDS_WAIT_TIMEOUT_MS = 10_000;
const CURSOR_CLIENT_CAPABILITY_META = {
parameterizedModelPicker: true,
};
export const CURSOR_FAST_FEATURE_OPTION: ACPConfigFeatureOption = {
id: "fast",
configId: "fast",
label: "Fast",
description: "Cursor fast mode",
tooltip: "Select Cursor fast mode",
icon: "zap",
};
export class CursorACPAgentClient extends GenericACPAgentClient {
constructor(options: CursorACPAgentClientOptions) {
@@ -25,6 +38,8 @@ export class CursorACPAgentClient extends GenericACPAgentClient {
// cursor-agent publishes slash commands asynchronously via available_commands_update.
waitForInitialCommands: true,
initialCommandsWaitTimeoutMs: CURSOR_INITIAL_COMMANDS_WAIT_TIMEOUT_MS,
clientCapabilityMeta: CURSOR_CLIENT_CAPABILITY_META,
configFeatureOptions: [CURSOR_FAST_FEATURE_OPTION],
});
}
}

View File

@@ -5,6 +5,8 @@ import type { AgentCapabilityFlags } from "../agent-sdk-types.js";
import { checkProviderLaunchAvailable, resolveProviderLaunch } from "../provider-launch-config.js";
import {
ACPAgentClient,
type ACPClientCapabilityMeta,
type ACPConfigFeatureOption,
DEFAULT_ACP_CAPABILITIES,
type ACPExtensionCommandsParser,
} from "./acp-agent.js";
@@ -33,6 +35,8 @@ interface GenericACPAgentClientOptions {
waitForInitialCommands?: boolean;
initialCommandsWaitTimeoutMs?: number;
diagnosticPhaseTimeoutMs?: number;
clientCapabilityMeta?: ACPClientCapabilityMeta;
configFeatureOptions?: ACPConfigFeatureOption[];
extensionCommandsParser?: ACPExtensionCommandsParser;
}
@@ -53,6 +57,8 @@ export class GenericACPAgentClient extends ACPAgentClient {
capabilities: buildGenericACPCapabilities(options),
waitForInitialCommands: options.waitForInitialCommands,
initialCommandsWaitTimeoutMs: options.initialCommandsWaitTimeoutMs,
clientCapabilityMeta: options.clientCapabilityMeta,
configFeatureOptions: options.configFeatureOptions,
extensionCommandsParser: options.extensionCommandsParser,
});