mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user