History

Add ACP support with Cursor provider (#1355)

Julius Marminge committed 7 months ago

Commit 9c64f121 parent 0f184c298 files · +26731 −847Browse files

Showing the changes to apps/server/scripts (2 of 98 files). Show all

Unified · Split

2 files
  1. apps/server/scripts/acp-mock-agent.ts +590 −0
  2. apps/server/scripts/cursor-acp-model-mismatch-probe.ts +435 −0
apps/server/scripts/acp-mock-agent.ts+590−0
@@ -0,0 +1,590 @@
1#!/usr/bin/env bun
2import { appendFileSync } from "node:fs";
3
4import * as Effect from "effect/Effect";
5
6import * as NodeServices from "@effect/platform-node/NodeServices";
7import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
8
9import * as EffectAcpAgent from "effect-acp/agent";
10import * as AcpError from "effect-acp/errors";
11import type * as AcpSchema from "effect-acp/schema";
12
13const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH;
14const exitLogPath = process.env.T3_ACP_EXIT_LOG_PATH;
15const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1";
16const emitInterleavedAssistantToolCalls =
17 process.env.T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS === "1";
18const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1";
19const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1";
20const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1";
21const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1";
22const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
23const sessionId = "mock-session-1";
24
25let currentModeId = "ask";
26let currentModelId = "default";
27let parameterizedModelPicker = false;
28let currentReasoning = "medium";
29let currentContext = "272k";
30let currentFast = false;
31const cancelledSessions = new Set<string>();
32
33function logExit(reason: string): void {
34 if (!exitLogPath) {
35 return;
36 }
37 appendFileSync(exitLogPath, `${reason}\n`, "utf8");
38}
39
40process.once("SIGTERM", () => {
41 logExit("SIGTERM");
42 process.exit(0);
43});
44
45process.once("SIGINT", () => {
46 logExit("SIGINT");
47 process.exit(0);
48});
49
50process.once("exit", (code) => {
51 logExit(`exit:${code}`);
52});
53
54function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
55 if (parameterizedModelPicker) {
56 const baseOptions: Array<AcpSchema.SessionConfigOption> = [
57 {
58 id: "mode",
59 name: "Mode",
60 category: "mode",
61 type: "select",
62 currentValue: currentModeId,
63 options: availableModes.map((mode) => ({
64 value: mode.id,
65 name: mode.name,
66 ...(mode.description ? { description: mode.description } : {}),
67 })),
68 },
69 {
70 id: "model",
71 name: "Model",
72 category: "model",
73 type: "select",
74 currentValue: currentModelId,
75 options: [
76 { value: "default", name: "Auto" },
77 { value: "composer-2", name: "Composer 2" },
78 { value: "gpt-5.4", name: "GPT-5.4" },
79 { value: "claude-opus-4-6", name: "Opus 4.6" },
80 ],
81 },
82 ];
83
84 switch (currentModelId) {
85 case "gpt-5.4":
86 return [
87 ...baseOptions,
88 {
89 id: "reasoning",
90 name: "Reasoning",
91 category: "thought_level",
92 type: "select",
93 currentValue: currentReasoning,
94 options: [
95 { value: "none", name: "None" },
96 { value: "low", name: "Low" },
97 { value: "medium", name: "Medium" },
98 { value: "high", name: "High" },
99 { value: "extra-high", name: "Extra High" },
100 ],
101 },
102 {
103 id: "context",
104 name: "Context",
105 category: "model_config",
106 type: "select",
107 currentValue: currentContext,
108 options: [
109 { value: "272k", name: "272K" },
110 { value: "1m", name: "1M" },
111 ],
112 },
113 {
114 id: "fast",
115 name: "Fast",
116 category: "model_config",
117 type: "select",
118 currentValue: String(currentFast),
119 options: [
120 { value: "false", name: "Off" },
121 { value: "true", name: "Fast" },
122 ],
123 },
124 ];
125 case "composer-2":
126 return [
127 ...baseOptions,
128 {
129 id: "fast",
130 name: "Fast",
131 category: "model_config",
132 type: "select",
133 currentValue: String(currentFast),
134 options: [
135 { value: "false", name: "Off" },
136 { value: "true", name: "Fast" },
137 ],
138 },
139 ];
140 case "claude-opus-4-6":
141 return [
142 ...baseOptions,
143 {
144 id: "reasoning",
145 name: "Reasoning",
146 category: "thought_level",
147 type: "select",
148 currentValue: currentReasoning,
149 options: [
150 { value: "low", name: "Low" },
151 { value: "medium", name: "Medium" },
152 { value: "high", name: "High" },
153 ],
154 },
155 {
156 id: "thinking",
157 name: "Thinking",
158 category: "model_config",
159 type: "boolean",
160 currentValue: true,
161 },
162 ];
163 default:
164 return baseOptions;
165 }
166 }
167
168 return [
169 {
170 id: "model",
171 name: "Model",
172 category: "model",
173 type: "select" as const,
174 currentValue: currentModelId,
175 options: [
176 { value: "default", name: "Auto" },
177 { value: "composer-2", name: "Composer 2" },
178 { value: "composer-2[fast=true]", name: "Composer 2 Fast" },
179 { value: "gpt-5.3-codex[reasoning=medium,fast=false]", name: "Codex 5.3" },
180 ],
181 },
182 ];
183}
184
185const availableModes: ReadonlyArray<AcpSchema.SessionMode> = [
186 {
187 id: "ask",
188 name: "Ask",
189 description: "Request permission before making any changes",
190 },
191 {
192 id: "architect",
193 name: "Architect",
194 description: "Design and plan software systems without implementation",
195 },
196 {
197 id: "code",
198 name: "Code",
199 description: "Write and modify code with full tool access",
200 },
201];
202
203function modeState(): AcpSchema.SessionModeState {
204 return {
205 currentModeId,
206 availableModes,
207 };
208}
209
210const program = Effect.gen(function* () {
211 const agent = yield* EffectAcpAgent.AcpAgent;
212
213 yield* agent.handleInitialize((request) =>
214 Effect.sync(() => {
215 parameterizedModelPicker =
216 request.clientCapabilities?._meta?.parameterizedModelPicker === true;
217 return {
218 protocolVersion: 1,
219 agentCapabilities: { loadSession: true },
220 };
221 }),
222 );
223
224 yield* agent.handleAuthenticate(() => Effect.succeed({}));
225
226 yield* agent.handleCreateSession(() =>
227 Effect.succeed({
228 sessionId,
229 modes: modeState(),
230 configOptions: configOptions(),
231 }),
232 );
233
234 yield* agent.handleLoadSession((request) =>
235 agent.client
236 .sessionUpdate({
237 sessionId: String(request.sessionId ?? sessionId),
238 update: {
239 sessionUpdate: "user_message_chunk",
240 content: { type: "text", text: "replay" },
241 },
242 })
243 .pipe(
244 Effect.as({
245 modes: modeState(),
246 configOptions: configOptions(),
247 }),
248 ),
249 );
250
251 yield* agent.handleSetSessionConfigOption((request) =>
252 Effect.gen(function* () {
253 if (exitOnSetConfigOption) {
254 return yield* Effect.sync(() => {
255 process.exit(7);
256 });
257 }
258 if (failSetConfigOption) {
259 return yield* AcpError.AcpRequestError.invalidParams(
260 "Mock invalid params for session/set_config_option",
261 {
262 method: "session/set_config_option",
263 params: request,
264 },
265 );
266 }
267 if (request.configId === "mode" && typeof request.value === "string") {
268 currentModeId = request.value;
269 }
270 if (request.configId === "model" && typeof request.value === "string") {
271 currentModelId = request.value;
272 }
273 if (request.configId === "reasoning" && typeof request.value === "string") {
274 currentReasoning = request.value;
275 }
276 if (request.configId === "context" && typeof request.value === "string") {
277 currentContext = request.value;
278 }
279 if (request.configId === "fast") {
280 currentFast = request.value === true || request.value === "true";
281 }
282 return {
283 configOptions: configOptions(),
284 };
285 }),
286 );
287
288 yield* agent.handleCancel(({ sessionId }) =>
289 Effect.sync(() => {
290 cancelledSessions.add(String(sessionId ?? "mock-session-1"));
291 }),
292 );
293
294 yield* agent.handlePrompt((request) =>
295 Effect.gen(function* () {
296 const requestedSessionId = String(request.sessionId ?? sessionId);
297
298 if (emitInterleavedAssistantToolCalls) {
299 const toolCallId = "tool-call-1";
300
301 yield* agent.client.sessionUpdate({
302 sessionId: requestedSessionId,
303 update: {
304 sessionUpdate: "agent_message_chunk",
305 content: { type: "text", text: "before tool" },
306 },
307 });
308
309 yield* agent.client.sessionUpdate({
310 sessionId: requestedSessionId,
311 update: {
312 sessionUpdate: "tool_call",
313 toolCallId,
314 title: "Terminal",
315 kind: "execute",
316 status: "pending",
317 rawInput: {
318 command: ["echo", "hello"],
319 },
320 },
321 });
322
323 yield* agent.client.sessionUpdate({
324 sessionId: requestedSessionId,
325 update: {
326 sessionUpdate: "tool_call_update",
327 toolCallId,
328 status: "completed",
329 rawOutput: {
330 exitCode: 0,
331 stdout: "hello",
332 stderr: "",
333 },
334 },
335 });
336
337 yield* agent.client.sessionUpdate({
338 sessionId: requestedSessionId,
339 update: {
340 sessionUpdate: "agent_message_chunk",
341 content: { type: "text", text: "after tool" },
342 },
343 });
344
345 return { stopReason: "end_turn" };
346 }
347
348 if (emitToolCalls) {
349 const toolCallId = "tool-call-1";
350
351 yield* agent.client.sessionUpdate({
352 sessionId: requestedSessionId,
353 update: {
354 sessionUpdate: "tool_call",
355 toolCallId,
356 title: "Terminal",
357 kind: "execute",
358 status: "pending",
359 rawInput: {
360 command: ["cat", "server/package.json"],
361 },
362 },
363 });
364
365 yield* agent.client.sessionUpdate({
366 sessionId: requestedSessionId,
367 update: {
368 sessionUpdate: "tool_call_update",
369 toolCallId,
370 status: "in_progress",
371 },
372 });
373
374 const permission = yield* agent.client.requestPermission({
375 sessionId: requestedSessionId,
376 toolCall: {
377 toolCallId,
378 title: "`cat server/package.json`",
379 kind: "execute",
380 status: "pending",
381 content: [
382 {
383 type: "content",
384 content: {
385 type: "text",
386 text: "Not in allowlist: cat server/package.json",
387 },
388 },
389 ],
390 },
391 options: [
392 { optionId: "allow-once", name: "Allow once", kind: "allow_once" },
393 { optionId: "allow-always", name: "Allow always", kind: "allow_always" },
394 { optionId: "reject-once", name: "Reject", kind: "reject_once" },
395 ],
396 });
397
398 const cancelled =
399 cancelledSessions.delete(requestedSessionId) ||
400 permission.outcome.outcome === "cancelled";
401
402 yield* agent.client.sessionUpdate({
403 sessionId: requestedSessionId,
404 update: {
405 sessionUpdate: "tool_call_update",
406 toolCallId,
407 title: "Terminal",
408 kind: "execute",
409 status: "completed",
410 rawOutput: {
411 exitCode: 0,
412 stdout: '{ "name": "t3" }',
413 stderr: "",
414 },
415 },
416 });
417
418 yield* agent.client.sessionUpdate({
419 sessionId: requestedSessionId,
420 update: {
421 sessionUpdate: "agent_message_chunk",
422 content: { type: "text", text: "hello from mock" },
423 },
424 });
425
426 return { stopReason: cancelled ? "cancelled" : "end_turn" };
427 }
428
429 if (emitGenericToolPlaceholders) {
430 const toolCallId = "tool-call-generic-1";
431
432 yield* agent.client.sessionUpdate({
433 sessionId: requestedSessionId,
434 update: {
435 sessionUpdate: "tool_call",
436 toolCallId,
437 title: "Read File",
438 kind: "read",
439 status: "pending",
440 rawInput: {},
441 },
442 });
443
444 yield* agent.client.sessionUpdate({
445 sessionId: requestedSessionId,
446 update: {
447 sessionUpdate: "tool_call_update",
448 toolCallId,
449 status: "in_progress",
450 },
451 });
452
453 yield* agent.client.sessionUpdate({
454 sessionId: requestedSessionId,
455 update: {
456 sessionUpdate: "tool_call_update",
457 toolCallId,
458 status: "completed",
459 rawOutput: {
460 content: "package.json\n",
461 },
462 },
463 });
464
465 return { stopReason: "end_turn" };
466 }
467
468 if (emitAskQuestion) {
469 yield* agent.client.extRequest("cursor/ask_question", {
470 toolCallId: "ask-question-tool-call-1",
471 title: "Question",
472 questions: [
473 {
474 id: "scope",
475 prompt: "Which scope?",
476 options: [
477 { id: "workspace", label: "Workspace" },
478 { id: "session", label: "Session" },
479 ],
480 },
481 ],
482 });
483
484 return { stopReason: "end_turn" };
485 }
486
487 yield* agent.client.sessionUpdate({
488 sessionId: requestedSessionId,
489 update: {
490 sessionUpdate: "plan",
491 entries: [
492 {
493 content: "Inspect mock ACP state",
494 priority: "high",
495 status: "completed",
496 },
497 {
498 content: "Implement the requested change",
499 priority: "high",
500 status: "in_progress",
501 },
502 ],
503 },
504 });
505
506 yield* agent.client.sessionUpdate({
507 sessionId: requestedSessionId,
508 update: {
509 sessionUpdate: "agent_message_chunk",
510 content: { type: "text", text: promptResponseText ?? "hello from mock" },
511 },
512 });
513
514 return { stopReason: "end_turn" };
515 }),
516 );
517
518 yield* agent.handleUnknownExtRequest((method, params) => {
519 if (method !== "session/mode/set") {
520 return Effect.fail(AcpError.AcpRequestError.methodNotFound(method));
521 }
522
523 const nextModeId =
524 typeof params === "object" &&
525 params !== null &&
526 "modeId" in params &&
527 typeof params.modeId === "string"
528 ? params.modeId
529 : typeof params === "object" &&
530 params !== null &&
531 "mode" in params &&
532 typeof params.mode === "string"
533 ? params.mode
534 : undefined;
535 const requestedSessionId =
536 typeof params === "object" &&
537 params !== null &&
538 "sessionId" in params &&
539 typeof params.sessionId === "string"
540 ? params.sessionId
541 : sessionId;
542
543 if (typeof nextModeId === "string" && nextModeId.trim()) {
544 currentModeId = nextModeId.trim();
545 return agent.client
546 .sessionUpdate({
547 sessionId: requestedSessionId,
548 update: {
549 sessionUpdate: "current_mode_update",
550 currentModeId,
551 },
552 })
553 .pipe(Effect.as({}));
554 }
555
556 return Effect.succeed({});
557 });
558
559 return yield* Effect.never;
560}).pipe(
561 Effect.provide(
562 EffectAcpAgent.layerStdio(
563 requestLogPath
564 ? {
565 logIncoming: true,
566 logger: (event) => {
567 if (event.direction !== "incoming" || event.stage !== "raw") {
568 return Effect.void;
569 }
570 if (typeof event.payload !== "string") {
571 return Effect.void;
572 }
573 const payload = event.payload;
574 return Effect.sync(() => {
575 appendFileSync(
576 requestLogPath,
577 payload.endsWith("\n") ? payload : `${payload}\n`,
578 "utf8",
579 );
580 });
581 },
582 }
583 : {},
584 ),
585 ),
586 Effect.scoped,
587 Effect.provide(NodeServices.layer),
588);
589
590NodeRuntime.runMain(program);
apps/server/scripts/cursor-acp-model-mismatch-probe.ts+435−0
@@ -0,0 +1,435 @@
1import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2import process from "node:process";
3import readline from "node:readline";
4
5type JsonPrimitive = null | boolean | number | string;
6type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
7
8type JsonRpcId = number | string;
9
10type JsonRpcMessage = {
11 jsonrpc?: string;
12 id?: JsonRpcId;
13 method?: string;
14 params?: JsonValue;
15 result?: JsonValue;
16 error?: JsonValue;
17 headers?: JsonValue;
18};
19
20type SelectLeafOption = {
21 value: string;
22 label?: string;
23 name?: string;
24};
25
26type SelectGroupOption = {
27 label?: string;
28 name?: string;
29 options: SelectLeafOption[];
30};
31
32type SessionConfigOption = {
33 id: string;
34 name?: string;
35 category?: string;
36 type?: string;
37 options?: Array<SelectLeafOption | SelectGroupOption>;
38};
39
40type SessionNewResult = {
41 sessionId: string;
42 configOptions?: SessionConfigOption[];
43};
44
45type SetConfigResult = {
46 configOptions?: SessionConfigOption[];
47};
48
49type PendingRequest = {
50 method: string;
51 resolve: (value: JsonValue | undefined) => void;
52 reject: (error: Error) => void;
53};
54
55const targetCwd = process.argv[2] ?? process.cwd();
56const targetModel = process.argv[3] ?? "gpt-5.4";
57const promptText = process.argv[4] ?? "helo";
58const targetReasoning = process.env.CURSOR_REASONING ?? "";
59const targetContext = process.env.CURSOR_CONTEXT ?? "";
60const targetFast = process.env.CURSOR_FAST ?? "";
61const agentBin = process.env.CURSOR_AGENT_BIN ?? "agent";
62const promptWaitMs = Number(process.env.CURSOR_PROMPT_WAIT_MS ?? "4000");
63const requestTimeoutMs = Number(process.env.CURSOR_REQUEST_TIMEOUT_MS ?? "20000");
64
65function logSection(title: string, value: unknown) {
66 process.stdout.write(`\n=== ${title} ===\n`);
67 process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
68}
69
70function fail(message: string): never {
71 throw new Error(message);
72}
73
74function asString(value: JsonValue | undefined): string | null {
75 return typeof value === "string" ? value : null;
76}
77
78function flattenSelectValues(option: SessionConfigOption | undefined): string[] {
79 if (!option || option.type !== "select" || !Array.isArray(option.options)) {
80 return [];
81 }
82
83 const values: string[] = [];
84 for (const entry of option.options) {
85 if (!entry || typeof entry !== "object") {
86 continue;
87 }
88 if ("value" in entry && typeof entry.value === "string") {
89 values.push(entry.value);
90 continue;
91 }
92 if ("options" in entry && Array.isArray(entry.options)) {
93 for (const nested of entry.options) {
94 if (nested && typeof nested === "object" && typeof nested.value === "string") {
95 values.push(nested.value);
96 }
97 }
98 }
99 }
100 return values;
101}
102
103function findConfigOption(
104 configOptions: SessionConfigOption[],
105 predicate: (option: SessionConfigOption) => boolean,
106): SessionConfigOption | undefined {
107 return configOptions.find(predicate);
108}
109
110function matchesKeyword(option: SessionConfigOption, keyword: string): boolean {
111 const haystack = `${option.id} ${option.name ?? ""}`.toLowerCase();
112 return haystack.includes(keyword.toLowerCase());
113}
114
115function sleep(ms: number) {
116 return new Promise<void>((resolve) => {
117 setTimeout(resolve, ms);
118 });
119}
120
121class JsonRpcChild {
122 readonly child: ChildProcessWithoutNullStreams;
123 readonly pending = new Map<JsonRpcId, PendingRequest>();
124 nextId = 1;
125 closed = false;
126
127 constructor(bin: string, args: string[], cwd: string) {
128 this.child = spawn(bin, args, {
129 cwd,
130 shell: process.platform === "win32",
131 stdio: ["pipe", "pipe", "pipe"],
132 env: process.env,
133 });
134
135 this.child.on("exit", (code, signal) => {
136 this.closed = true;
137 const detail = `ACP process exited (code=${String(code)}, signal=${String(signal)})`;
138 for (const pending of this.pending.values()) {
139 pending.reject(new Error(`${detail} while waiting for ${pending.method}`));
140 }
141 this.pending.clear();
142 });
143
144 this.child.on("error", (error) => {
145 this.closed = true;
146 for (const pending of this.pending.values()) {
147 pending.reject(error);
148 }
149 this.pending.clear();
150 });
151
152 const stdout = readline.createInterface({ input: this.child.stdout });
153 stdout.on("line", (line) => {
154 void this.handleStdoutLine(line);
155 });
156
157 const stderr = readline.createInterface({ input: this.child.stderr });
158 stderr.on("line", (line) => {
159 process.stdout.write(`[stderr] ${line}\n`);
160 });
161 }
162
163 write(message: JsonRpcMessage) {
164 if (this.closed) {
165 fail("ACP process is already closed.");
166 }
167 const payload = JSON.stringify({
168 jsonrpc: "2.0",
169 headers: [],
170 ...message,
171 });
172 process.stdout.write(`>>> ${payload}\n`);
173 this.child.stdin.write(`${payload}\n`);
174 }
175
176 async request(method: string, params: JsonValue, timeoutMs = requestTimeoutMs) {
177 const id = this.nextId++;
178
179 const responsePromise = new Promise<JsonValue | undefined>((resolve, reject) => {
180 const timeout = setTimeout(() => {
181 this.pending.delete(id);
182 reject(new Error(`Timed out waiting for ${method} response after ${timeoutMs}ms.`));
183 }, timeoutMs);
184
185 this.pending.set(id, {
186 method,
187 resolve: (value) => {
188 clearTimeout(timeout);
189 resolve(value);
190 },
191 reject: (error) => {
192 clearTimeout(timeout);
193 reject(error);
194 },
195 });
196 });
197
198 this.write({
199 id,
200 method,
201 params,
202 });
203
204 return responsePromise;
205 }
206
207 notify(method: string, params: JsonValue) {
208 this.write({
209 method,
210 params,
211 });
212 }
213
214 respond(id: JsonRpcId, result: JsonValue) {
215 this.write({
216 id,
217 result,
218 });
219 }
220
221 respondError(id: JsonRpcId, code: number, message: string) {
222 this.write({
223 id,
224 error: {
225 code,
226 message,
227 },
228 });
229 }
230
231 async handleStdoutLine(line: string) {
232 if (line.trim().length === 0) {
233 return;
234 }
235
236 process.stdout.write(`<<< ${line}\n`);
237
238 let message: JsonRpcMessage;
239 try {
240 message = JSON.parse(line) as JsonRpcMessage;
241 } catch (error) {
242 process.stdout.write(`[parse-error] ${(error as Error).message}\n`);
243 return;
244 }
245
246 if (typeof message.id !== "undefined" && !message.method) {
247 const pending = this.pending.get(message.id);
248 if (!pending) {
249 return;
250 }
251 this.pending.delete(message.id);
252 if (typeof message.error !== "undefined") {
253 pending.reject(
254 new Error(`RPC ${pending.method} failed: ${JSON.stringify(message.error, null, 2)}`),
255 );
256 return;
257 }
258 pending.resolve(message.result);
259 return;
260 }
261
262 if (message.method === "session/request_permission" && typeof message.id !== "undefined") {
263 this.respond(message.id, {
264 outcome: {
265 outcome: "selected",
266 optionId: "allow",
267 },
268 });
269 return;
270 }
271
272 if (typeof message.id !== "undefined" && message.id !== "") {
273 this.respondError(
274 message.id,
275 -32601,
276 `Unhandled server request: ${message.method ?? "unknown"}`,
277 );
278 }
279 }
280
281 async close() {
282 if (this.closed) {
283 return;
284 }
285 this.child.kill("SIGTERM");
286 await sleep(250);
287 if (!this.closed) {
288 this.child.kill("SIGKILL");
289 }
290 }
291}
292
293async function setSelectOptionIfAdvertised(
294 rpc: JsonRpcChild,
295 sessionId: string,
296 configOptions: SessionConfigOption[],
297 predicate: (option: SessionConfigOption) => boolean,
298 value: string,
299 label: string,
300) {
301 if (value.length === 0) {
302 return configOptions;
303 }
304
305 const option = findConfigOption(configOptions, predicate);
306 const values = flattenSelectValues(option);
307 if (!option || !values.includes(value)) {
308 logSection(`SKIP_${label}`, {
309 requestedValue: value,
310 availableValues: values,
311 });
312 return configOptions;
313 }
314
315 const response = (await rpc.request("session/set_config_option", {
316 sessionId,
317 configId: option.id,
318 value,
319 })) as SetConfigResult | null | undefined;
320
321 logSection(`SET_${label}_RESPONSE`, response);
322 return response?.configOptions ?? configOptions;
323}
324
325async function main() {
326 const rpc = new JsonRpcChild(agentBin, ["acp"], targetCwd);
327
328 try {
329 const initializeResponse = await rpc.request("initialize", {
330 protocolVersion: 1,
331 clientCapabilities: {
332 fs: { readTextFile: false, writeTextFile: false },
333 terminal: false,
334 _meta: {
335 parameterizedModelPicker: true,
336 },
337 },
338 clientInfo: {
339 name: "cursor-acp-model-mismatch-probe",
340 version: "0.0.0",
341 },
342 });
343 logSection("INITIALIZE_RESPONSE", initializeResponse);
344
345 const authenticateResponse = await rpc.request("authenticate", {
346 methodId: "cursor_login",
347 });
348 logSection("AUTHENTICATE_RESPONSE", authenticateResponse);
349
350 const sessionResponse = (await rpc.request("session/new", {
351 cwd: targetCwd,
352 mcpServers: [],
353 })) as SessionNewResult;
354 logSection("SESSION_NEW_RESPONSE", sessionResponse);
355
356 const sessionId = asString(sessionResponse.sessionId);
357 if (!sessionId) {
358 fail("session/new did not return a sessionId.");
359 }
360
361 let configOptions = sessionResponse.configOptions ?? [];
362 const modelConfig = findConfigOption(configOptions, (option) => option.category === "model");
363 const advertisedModels = flattenSelectValues(modelConfig);
364 logSection("ADVERTISED_MODEL_VALUES", advertisedModels);
365
366 if (!modelConfig || modelConfig.type !== "select") {
367 fail("Cursor ACP did not expose a select-type model config option.");
368 }
369
370 if (!advertisedModels.includes(targetModel)) {
371 fail(
372 `Cursor ACP did not advertise model ${JSON.stringify(targetModel)}. Advertised values: ${advertisedModels.join(", ")}`,
373 );
374 }
375
376 const setModelResponse = (await rpc.request("session/set_config_option", {
377 sessionId,
378 configId: modelConfig.id,
379 value: targetModel,
380 })) as SetConfigResult | null | undefined;
381 logSection("SET_MODEL_RESPONSE", setModelResponse);
382
383 configOptions = setModelResponse?.configOptions ?? configOptions;
384
385 configOptions = await setSelectOptionIfAdvertised(
386 rpc,
387 sessionId,
388 configOptions,
389 (option) => option.category === "thought_level",
390 targetReasoning,
391 "REASONING",
392 );
393
394 configOptions = await setSelectOptionIfAdvertised(
395 rpc,
396 sessionId,
397 configOptions,
398 (option) => option.category === "model_config" && matchesKeyword(option, "context"),
399 targetContext,
400 "CONTEXT",
401 );
402
403 configOptions = await setSelectOptionIfAdvertised(
404 rpc,
405 sessionId,
406 configOptions,
407 (option) => option.category === "model_config" && matchesKeyword(option, "fast"),
408 targetFast,
409 "FAST",
410 );
411
412 const promptResponse = await rpc.request("session/prompt", {
413 sessionId,
414 prompt: [
415 {
416 type: "text",
417 text: promptText,
418 },
419 ],
420 });
421 logSection("PROMPT_RESPONSE", promptResponse);
422
423 await sleep(promptWaitMs);
424 rpc.notify("session/cancel", { sessionId });
425 } finally {
426 await rpc.close();
427 }
428}
429
430void main().catch((error: unknown) => {
431 process.stderr.write(
432 `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
433 );
434 process.exitCode = 1;
435});

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t