authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-03 10:06:52-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-03 17:06:52+00:00
logd78e02cd0b01faa48db4009de2a5497e95c14636
treeaba7ce0e73e66d57fc7a0eb397efe4e82107cd5e
parentf0116e44b6806a949fbd449b4548f3cfc6638510
signature Signed by PGP key B5690EEEBB952194

Probe Cursor models via list_available_models (#2428)

Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <julius@mac.lan>

6 files changed, 121 insertions(+), 360 deletions(-)

apps/server/scripts/acp-mock-agent.ts+35
......@@ -183,6 +183,35 @@ function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
183183 ];
184184}
185185
186function modelConfigOptionsFor(modelId: string): ReadonlyArray<AcpSchema.SessionConfigOption> {
187 const previousModelId = currentModelId;
188 try {
189 currentModelId = modelId;
190 return configOptions().filter(
191 (option) => option.category !== "mode" && option.category !== "model",
192 );
193 } finally {
194 currentModelId = previousModelId;
195 }
196}
197
198function availableModels(): ReadonlyArray<{
199 readonly value: string;
200 readonly name: string;
201 readonly configOptions: ReadonlyArray<AcpSchema.SessionConfigOption>;
202}> {
203 return [
204 { value: "default", name: "Auto" },
205 { value: "composer-2", name: "Composer 2" },
206 { value: "gpt-5.4", name: "GPT-5.4" },
207 { value: "claude-opus-4-6", name: "Opus 4.6" },
208 ].map((model) => ({
209 value: model.value,
210 name: model.name,
211 configOptions: modelConfigOptionsFor(model.value),
212 }));
213}
214
186215const availableModes: ReadonlyArray<AcpSchema.SessionMode> = [
187216 {
188217 id: "ask",
......@@ -517,6 +546,12 @@ const program = Effect.gen(function* () {
517546 );
518547
519548 yield* agent.handleUnknownExtRequest((method, params) => {
549 if (method === "cursor/list_available_models") {
550 return Effect.succeed({
551 models: availableModels(),
552 });
553 }
554
520555 if (method !== "session/mode/set") {
521556 return Effect.fail(AcpError.AcpRequestError.methodNotFound(method));
522557 }
apps/server/src/provider/Drivers/CursorDriver.ts+6-10
......@@ -1,10 +1,9 @@
11/**
22 * CursorDriver — `ProviderDriver` for the Cursor Agent (`agent`) runtime.
33 *
4 * Cursor exposes an ACP-based CLI. The driver is still a plain value, but
5 * its snapshot uses `makeManagedServerProvider`'s optional `enrichSnapshot`
6 * hook to run the slow ACP model-capability probe in the background without
7 * blocking the initial `ready`-state publish.
4 * Cursor exposes an ACP-based CLI. Model catalog and capability refreshes
5 * happen during the managed provider status check via Cursor's
6 * `list_available_models` extension method.
87 *
98 * Text generation is supported via the ACP runtime — `makeCursorTextGeneration`
109 * drives `runtime.prompt` with a structured-output schema and collects the
......@@ -139,20 +138,17 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
139138 initialSnapshot: (settings) =>
140139 buildInitialCursorProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
141140 checkProvider,
142 // Preserve the background ACP model-capability probe that used to
143 // live on `CursorProviderLive`. Only fires when the snapshot reports
144 // an authenticated, enabled provider with at least one non-custom
145 // model whose capabilities haven't been captured yet.
141 // Model catalog and capabilities come exclusively from Cursor's
142 // list_available_models extension method during provider checks.
146143 enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
147144 enrichCursorSnapshot({
148145 settings,
149 environment: processEnv,
150146 snapshot: currentSnapshot,
151147 maintenanceCapabilities,
152148 publishSnapshot,
153149 stampIdentity,
154150 httpClient,
155 }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner)),
151 }),
156152 refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
157153 }).pipe(
158154 Effect.mapError(
apps/server/src/provider/Layers/CursorProvider.test.ts+1-133
......@@ -6,15 +6,13 @@ import * as FileSystem from "effect/FileSystem";
66import * as Path from "effect/Path";
77import { describe, expect, it } from "vitest";
88import type * as EffectAcpSchema from "effect-acp/schema";
9import type { CursorSettings, ServerProviderModel } from "@t3tools/contracts";
9import type { CursorSettings } from "@t3tools/contracts";
1010import { createModelCapabilities } from "@t3tools/shared/model";
1111
1212import {
1313 buildCursorProviderSnapshot,
1414 buildCursorCapabilitiesFromConfigOptions,
15 buildCursorDiscoveredModelsFromConfigOptions,
1615 checkCursorProviderStatus,
17 discoverCursorModelCapabilitiesViaAcp,
1816 discoverCursorModelsViaAcp,
1917 getCursorFallbackModels,
2018 getCursorParameterizedModelPickerUnsupportedMessage,
......@@ -295,48 +293,6 @@ const parameterizedClaudeModelOptionConfigOptions = [
295293 },
296294] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>;
297295
298const sessionNewCursorConfigOptions = [
299 {
300 type: "select",
301 currentValue: "agent",
302 options: [
303 { name: "Agent", value: "agent", description: "Full agent capabilities with tool access" },
304 ],
305 category: "mode",
306 id: "mode",
307 name: "Mode",
308 description: "Controls how the agent executes tasks",
309 },
310 {
311 type: "select",
312 currentValue: "composer-2",
313 options: [
314 { name: "Auto", value: "default" },
315 { name: "Composer 2", value: "composer-2" },
316 { name: "GPT-5.4", value: "gpt-5.4" },
317 { name: "Sonnet 4.6", value: "claude-sonnet-4-6" },
318 { name: "Opus 4.6", value: "claude-opus-4-6" },
319 { name: "Codex 5.3 Spark", value: "gpt-5.3-codex-spark" },
320 ],
321 category: "model",
322 id: "model",
323 name: "Model",
324 description: "Controls which model is used for responses",
325 },
326 {
327 type: "select",
328 currentValue: "true",
329 options: [
330 { name: "Off", value: "false" },
331 { name: "Fast", value: "true" },
332 ],
333 category: "model_config",
334 id: "fast",
335 name: "Fast",
336 description: "Faster speeds.",
337 },
338] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>;
339
340296const baseCursorSettings: CursorSettings = {
341297 enabled: true,
342298 binaryPath: "agent",
......@@ -344,8 +300,6 @@ const baseCursorSettings: CursorSettings = {
344300 customModels: [],
345301};
346302
347const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] });
348
349303describe("getCursorFallbackModels", () => {
350304 it("does not publish any built-in cursor models before ACP discovery", () => {
351305 expect(
......@@ -462,51 +416,6 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => {
462416 });
463417});
464418
465describe("buildCursorDiscoveredModelsFromConfigOptions", () => {
466 it("publishes ACP model choices immediately from session/new config options", () => {
467 expect(buildCursorDiscoveredModelsFromConfigOptions(sessionNewCursorConfigOptions)).toEqual([
468 {
469 slug: "default",
470 name: "Auto",
471 isCustom: false,
472 capabilities: emptyCapabilities,
473 },
474 {
475 slug: "composer-2",
476 name: "Composer 2",
477 isCustom: false,
478 capabilities: createModelCapabilities({
479 optionDescriptors: [booleanDescriptor("fastMode", "Fast", true)],
480 }),
481 },
482 {
483 slug: "gpt-5.4",
484 name: "GPT-5.4",
485 isCustom: false,
486 capabilities: emptyCapabilities,
487 },
488 {
489 slug: "claude-sonnet-4-6",
490 name: "Sonnet 4.6",
491 isCustom: false,
492 capabilities: emptyCapabilities,
493 },
494 {
495 slug: "claude-opus-4-6",
496 name: "Opus 4.6",
497 isCustom: false,
498 capabilities: emptyCapabilities,
499 },
500 {
501 slug: "gpt-5.3-codex-spark",
502 name: "Codex 5.3 Spark",
503 isCustom: false,
504 capabilities: emptyCapabilities,
505 },
506 ]);
507 });
508});
509
510419describe("checkCursorProviderStatus", () => {
511420 it("passes the injected environment to ACP model discovery", async () => {
512421 const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture());
......@@ -576,47 +485,6 @@ describe("discoverCursorModelsViaAcp", () => {
576485 });
577486});
578487
579describe("discoverCursorModelCapabilitiesViaAcp", () => {
580 it("closes all ACP probe runtimes after capability enrichment completes", async () => {
581 const { exitLogPath, wrapperPath } = await runNode(
582 makeExitLogFixture("cursor-capabilities-exit-log-"),
583 );
584 const existingModels: ReadonlyArray<ServerProviderModel> = [
585 { slug: "default", name: "Auto", isCustom: false, capabilities: emptyCapabilities },
586 { slug: "composer-2", name: "Composer 2", isCustom: false, capabilities: emptyCapabilities },
587 { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, capabilities: emptyCapabilities },
588 {
589 slug: "claude-opus-4-6",
590 name: "Opus 4.6",
591 isCustom: false,
592 capabilities: emptyCapabilities,
593 },
594 ];
595
596 const models = await Effect.runPromise(
597 discoverCursorModelCapabilitiesViaAcp(
598 {
599 enabled: true,
600 binaryPath: wrapperPath,
601 apiEndpoint: "",
602 customModels: [],
603 },
604 existingModels,
605 ).pipe(Effect.provide(NodeServices.layer)),
606 );
607
608 expect(models.map((model) => model.slug)).toEqual([
609 "default",
610 "composer-2",
611 "gpt-5.4",
612 "claude-opus-4-6",
613 ]);
614
615 const exitLog = await runNode(waitForFileContent(exitLogPath));
616 expect(exitLog.match(/SIGTERM/g)?.length ?? 0).toBe(4);
617 });
618});
619
620488describe("parseCursorAboutOutput", () => {
621489 it("parses json about output and forwards subscription metadata", () => {
622490 expect(
apps/server/src/provider/Layers/CursorProvider.ts+41-217
......@@ -19,6 +19,7 @@ import * as Layer from "effect/Layer";
1919import * as Option from "effect/Option";
2020import * as Path from "effect/Path";
2121import * as Result from "effect/Result";
22import * as Schema from "effect/Schema";
2223import { HttpClient } from "effect/unstable/http";
2324import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2425import {
......@@ -42,6 +43,7 @@ import {
4243 type ProviderMaintenanceCapabilities,
4344} from "../providerMaintenance.ts";
4445import { AcpSessionRuntime } from "../acp/AcpSessionRuntime.ts";
46import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts";
4547
4648const PROVIDER = ProviderDriverKind.make("cursor");
4749const CURSOR_PRESENTATION = {
......@@ -54,8 +56,6 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
5456});
5557
5658const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
57const CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT = "4 seconds";
58const CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY = 4;
5959const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08;
6060export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {
6161 _meta: {
......@@ -154,12 +154,6 @@ function normalizeCursorReasoningValue(value: string | null | undefined): string
154154 }
155155}
156156
157function findCursorModelConfigOption(
158 configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>,
159): EffectAcpSchema.SessionConfigOption | undefined {
160 return configOptions.find((option) => option.category === "model");
161}
162
163157function getCursorConfigOptionCategory(option: EffectAcpSchema.SessionConfigOption): string {
164158 return option.category?.trim().toLowerCase() ?? "";
165159}
......@@ -373,36 +367,25 @@ function buildCursorDiscoveredModels(
373367 });
374368}
375369
376function hasCursorModelCapabilities(model: Pick<ServerProviderModel, "capabilities">): boolean {
377 return (model.capabilities?.optionDescriptors?.length ?? 0) > 0;
378}
379
380export function buildCursorDiscoveredModelsFromConfigOptions(
381 configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption> | null | undefined,
370function buildCursorDiscoveredModelsFromAvailableModelsResponse(
371 response: typeof CursorListAvailableModelsResponse.Type,
382372): ReadonlyArray<ServerProviderModel> {
383 if (!configOptions || configOptions.length === 0) {
384 return [];
385 }
386
387 const modelOption = findCursorModelConfigOption(configOptions);
388 const modelChoices = flattenSessionConfigSelectOptions(modelOption);
389 if (!modelOption || modelChoices.length === 0) {
390 return [];
391 }
392
393 const currentModelValue =
394 modelOption.type === "select" ? modelOption.currentValue?.trim() || undefined : undefined;
395 const currentModelCapabilities = buildCursorCapabilitiesFromConfigOptions(configOptions);
396
397373 return buildCursorDiscoveredModels(
398 modelChoices.map((modelChoice) => ({
399 slug: modelChoice.value.trim(),
400 name: modelChoice.name.trim(),
401 capabilities:
402 currentModelValue === modelChoice.value.trim()
403 ? currentModelCapabilities
404 : EMPTY_CAPABILITIES,
405 })),
374 response.models.flatMap((model) => {
375 const slug = model.value.trim();
376 const name = model.name.trim();
377 if (!slug || !name) {
378 return [];
379 }
380
381 return [
382 {
383 slug,
384 name,
385 capabilities: buildCursorCapabilitiesFromConfigOptions(model.configOptions),
386 },
387 ];
388 }),
406389 );
407390}
408391
......@@ -554,140 +537,28 @@ export function resolveCursorAcpConfigUpdates(
554537 return updates;
555538}
556539
557export const discoverCursorModelsViaAcp = (
540const discoverCursorModelsViaListAvailableModels = (
558541 cursorSettings: CursorSettings,
559542 environment: NodeJS.ProcessEnv = process.env,
560543) =>
561544 withCursorAcpProbeRuntime(
562545 cursorSettings,
563546 (acp) =>
564 Effect.map(acp.start(), (started) =>
565 buildCursorDiscoveredModelsFromConfigOptions(
566 started.sessionSetupResult.configOptions ?? [],
567 ),
568 ),
547 Effect.gen(function* () {
548 yield* acp.start();
549 const response = yield* acp.request("cursor/list_available_models", {});
550 const decoded = yield* Schema.decodeUnknownEffect(CursorListAvailableModelsResponse)(
551 response,
552 );
553 return buildCursorDiscoveredModelsFromAvailableModelsResponse(decoded);
554 }),
569555 environment,
570556 );
571557
572export const discoverCursorModelCapabilitiesViaAcp = (
558export const discoverCursorModelsViaAcp = (
573559 cursorSettings: CursorSettings,
574 existingModels: ReadonlyArray<ServerProviderModel>,
575560 environment: NodeJS.ProcessEnv = process.env,
576) =>
577 withCursorAcpProbeRuntime(
578 cursorSettings,
579 (acp) =>
580 Effect.gen(function* () {
581 const started = yield* acp.start();
582 const initialConfigOptions = started.sessionSetupResult.configOptions ?? [];
583 const modelOption = findCursorModelConfigOption(initialConfigOptions);
584 const modelChoices = flattenSessionConfigSelectOptions(modelOption);
585 if (!modelOption || modelChoices.length === 0) {
586 return [];
587 }
588
589 const currentModelValue =
590 modelOption.type === "select" ? modelOption.currentValue?.trim() || undefined : undefined;
591 const capabilitiesBySlug = new Map<string, ModelCapabilities>();
592 if (currentModelValue) {
593 capabilitiesBySlug.set(
594 currentModelValue,
595 buildCursorCapabilitiesFromConfigOptions(initialConfigOptions),
596 );
597 }
598
599 const targetModelSlugs = new Set<string>();
600 for (const model of existingModels) {
601 if (!model.isCustom && !hasCursorModelCapabilities(model)) {
602 targetModelSlugs.add(model.slug);
603 }
604 }
605 if (targetModelSlugs.size === 0) {
606 return buildCursorDiscoveredModels(
607 modelChoices.map((modelChoice) => ({
608 slug: modelChoice.value.trim(),
609 name: modelChoice.name.trim(),
610 capabilities: capabilitiesBySlug.get(modelChoice.value.trim()) ?? EMPTY_CAPABILITIES,
611 })),
612 );
613 }
614
615 const probedCapabilities = yield* Effect.forEach(
616 modelChoices,
617 (modelChoice) => {
618 const modelSlug = modelChoice.value.trim();
619 if (
620 !modelSlug ||
621 !targetModelSlugs.has(modelSlug) ||
622 capabilitiesBySlug.has(modelSlug)
623 ) {
624 return Effect.void.pipe(
625 Effect.as<readonly [string, ModelCapabilities] | undefined>(undefined),
626 );
627 }
628
629 return withCursorAcpProbeRuntime(
630 cursorSettings,
631 (probeAcp) =>
632 Effect.gen(function* () {
633 const probeStarted = yield* probeAcp.start();
634 const probeConfigOptions = probeStarted.sessionSetupResult.configOptions ?? [];
635 const probeModelOption = findCursorModelConfigOption(probeConfigOptions);
636 const probeCurrentModelValue =
637 probeModelOption?.type === "select"
638 ? probeModelOption.currentValue?.trim() || undefined
639 : undefined;
640 yield* Effect.annotateCurrentSpan({
641 "cursor.acp.model.value": modelSlug,
642 "cursor.acp.model.currentValue": probeCurrentModelValue,
643 "cursor.acp.config_option_id": probeModelOption?.id ?? modelOption.id,
644 });
645 const nextConfigOptions =
646 probeCurrentModelValue === modelSlug
647 ? probeConfigOptions
648 : yield* probeAcp
649 .setConfigOption(probeModelOption?.id ?? modelOption.id, modelSlug)
650 .pipe(
651 Effect.map((response) => response.configOptions ?? probeConfigOptions),
652 );
653 return [
654 modelSlug,
655 buildCursorCapabilitiesFromConfigOptions(nextConfigOptions),
656 ] as const;
657 }),
658 environment,
659 ).pipe(
660 Effect.timeout(CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT),
661 Effect.retry({ times: 3 }),
662 Effect.withSpan("cursor-acp-model-capability-probe"),
663 Effect.catchCause((cause) =>
664 Effect.logWarning("Cursor ACP capability probe failed", {
665 modelSlug,
666 cause: Cause.pretty(cause),
667 }),
668 ),
669 );
670 },
671 { concurrency: CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY },
672 );
673
674 for (const entry of probedCapabilities) {
675 if (!entry) {
676 continue;
677 }
678 capabilitiesBySlug.set(entry[0], entry[1]);
679 }
680
681 return buildCursorDiscoveredModels(
682 modelChoices.map((modelChoice) => ({
683 slug: modelChoice.value.trim(),
684 name: modelChoice.name.trim(),
685 capabilities: capabilitiesBySlug.get(modelChoice.value.trim()) ?? EMPTY_CAPABILITIES,
686 })),
687 );
688 }).pipe(Effect.withSpan("cursor-acp-model-capability-discovery", {})),
689 environment,
690 );
561) => discoverCursorModelsViaListAvailableModels(cursorSettings, environment);
691562
692563export function getCursorFallbackModels(
693564 cursorSettings: Pick<CursorSettings, "customModels">,
......@@ -1219,36 +1090,30 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
12191090 });
12201091});
12211092
1222export function hasUncapturedCursorModels(snapshot: Pick<ServerProvider, "models">): boolean {
1223 return snapshot.models.some((model) => !model.isCustom && !hasCursorModelCapabilities(model));
1224}
1225
12261093/**
1227 * Background capability enrichment for a Cursor snapshot.
1094 * Background maintenance enrichment for a Cursor snapshot.
12281095 *
12291096 * Used by `CursorDriver` as the `makeManagedServerProvider.enrichSnapshot`
1230 * hook: runs the slow ACP per-model capability probe, and republishes the
1231 * snapshot through `publishSnapshot` when new capabilities arrive. Skips
1232 * the probe when the provider is disabled, unauthenticated, or has no
1233 * uncaptured models. Keeps `EMPTY_CAPABILITIES` and the `PROVIDER` literal
1234 * private to this module.
1097 * hook: republishes update/version advisory metadata without performing any
1098 * model or capability discovery. Cursor model data comes exclusively from
1099 * `cursor/list_available_models` during provider status checks.
12351100 */
12361101export const enrichCursorSnapshot = (input: {
12371102 readonly settings: CursorSettings;
1238 readonly environment?: NodeJS.ProcessEnv;
12391103 readonly snapshot: ServerProvider;
12401104 readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
12411105 readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>;
12421106 readonly stampIdentity?: (snapshot: ServerProvider) => ServerProvider;
12431107 readonly httpClient: HttpClient.HttpClient;
1244}): Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner> => {
1108}): Effect.Effect<void> => {
12451109 const { settings, snapshot, publishSnapshot } = input;
12461110 const stampIdentity = input.stampIdentity ?? ((value) => value);
12471111
1248 const enrichVersionAdvisory = enrichProviderSnapshotWithVersionAdvisory(
1249 snapshot,
1250 input.maintenanceCapabilities,
1251 ).pipe(
1112 if (!settings.enabled || snapshot.auth.status === "unauthenticated") {
1113 return Effect.void;
1114 }
1115
1116 return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities).pipe(
12521117 Effect.provideService(HttpClient.HttpClient, input.httpClient),
12531118 Effect.flatMap((enrichedSnapshot) =>
12541119 publishSnapshot(stampIdentity(enrichedSnapshot)).pipe(Effect.as(enrichedSnapshot)),
......@@ -1256,48 +1121,7 @@ export const enrichCursorSnapshot = (input: {
12561121 Effect.catchCause((cause) =>
12571122 Effect.logWarning("Cursor version advisory enrichment failed", {
12581123 cause: Cause.pretty(cause),
1259 }).pipe(Effect.as(snapshot)),
1124 }).pipe(Effect.asVoid),
12601125 ),
12611126 );
1262
1263 return enrichVersionAdvisory.pipe(
1264 Effect.flatMap((baseSnapshot) => {
1265 if (
1266 !settings.enabled ||
1267 baseSnapshot.auth.status === "unauthenticated" ||
1268 !hasUncapturedCursorModels(baseSnapshot)
1269 ) {
1270 return Effect.void;
1271 }
1272
1273 return discoverCursorModelCapabilitiesViaAcp(
1274 settings,
1275 baseSnapshot.models,
1276 input.environment,
1277 ).pipe(
1278 Effect.flatMap((discoveredModels) => {
1279 if (discoveredModels.length === 0) {
1280 return Effect.void;
1281 }
1282 return publishSnapshot(
1283 stampIdentity({
1284 ...baseSnapshot,
1285 models: providerModelsFromSettings(
1286 discoveredModels,
1287 PROVIDER,
1288 settings.customModels,
1289 EMPTY_CAPABILITIES,
1290 ),
1291 }),
1292 );
1293 }),
1294 Effect.catchCause((cause) =>
1295 Effect.logWarning("Cursor ACP background capability enrichment failed", {
1296 models: baseSnapshot.models.map((model) => model.slug),
1297 cause: Cause.pretty(cause),
1298 }).pipe(Effect.asVoid),
1299 ),
1300 );
1301 }),
1302 );
13031127};
apps/server/src/provider/acp/CursorAcpExtension.test.ts+27
......@@ -1,6 +1,7 @@
11import { describe, expect, it } from "vitest";
22
33import {
4 CursorListAvailableModelsResponse,
45 extractAskQuestions,
56 extractPlanMarkdown,
67 extractTodosAsPlan,
......@@ -105,4 +106,30 @@ describe("CursorAcpExtension", () => {
105106 ],
106107 });
107108 });
109
110 it("decodes Cursor list_available_models responses with per-model config options", () => {
111 const decoded = CursorListAvailableModelsResponse.make({
112 models: [
113 {
114 value: "gpt-5.4",
115 name: "GPT-5.4",
116 configOptions: [
117 {
118 id: "reasoning",
119 name: "Reasoning",
120 category: "thought_level",
121 type: "select",
122 currentValue: "medium",
123 options: [
124 { value: "low", name: "Low" },
125 { value: "medium", name: "Medium" },
126 ],
127 },
128 ],
129 },
130 ],
131 });
132
133 expect(decoded.models[0]?.configOptions?.[0]?.id).toBe("reasoning");
134 });
108135});
apps/server/src/provider/acp/CursorAcpExtension.ts+11
......@@ -3,6 +3,7 @@
33 * Additional reference provided by the Cursor team: https://anysphere.enterprise.slack.com/files/U068SSJE141/F0APT1HSZRP/cursor-acp-extension-method-schemas.md
44 */
55import type { UserInputQuestion } from "@t3tools/contracts";
6import * as AcpSchema from "effect-acp/schema";
67import * as Schema from "effect/Schema";
78
89const CursorAskQuestionOption = Schema.Struct({
......@@ -53,6 +54,16 @@ export const CursorUpdateTodosRequest = Schema.Struct({
5354 merge: Schema.Boolean,
5455});
5556
57const CursorAvailableModel = Schema.Struct({
58 value: Schema.String,
59 name: Schema.String,
60 configOptions: Schema.optional(Schema.Array(AcpSchema.SessionConfigOption)),
61});
62
63export const CursorListAvailableModelsResponse = Schema.Struct({
64 models: Schema.Array(CursorAvailableModel),
65});
66
5667export function extractAskQuestions(
5768 params: typeof CursorAskQuestionRequest.Type,
5869): ReadonlyArray<UserInputQuestion> {