Created
June 4, 2026 18:07
-
-
Save juliusmarminge/65f5a313afcedaffe015716c7b7db13b to your computer and use it in GitHub Desktop.
Cursor ACP In-session model switching reproduction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { spawn } from "node:child_process"; | |
| import readline from "node:readline"; | |
| const bin = process.env.CURSOR_AGENT_BIN ?? "cursor-agent"; | |
| const cwd = process.argv[2] ?? process.cwd(); | |
| const targetModel = process.argv[3] ?? "gpt-5.5"; | |
| const timeoutMs = Number(process.env.CURSOR_ACP_TIMEOUT_MS ?? "20000"); | |
| function sleep(ms) { | |
| return new Promise((resolve) => setTimeout(resolve, ms)); | |
| } | |
| function summarizeSession(result) { | |
| const configOptions = result?.configOptions ?? []; | |
| return { | |
| sessionId: result?.sessionId, | |
| modelsCurrentModelId: result?.models?.currentModelId, | |
| configCurrentValues: Object.fromEntries( | |
| configOptions.map((option) => [option.id, option.currentValue]), | |
| ), | |
| }; | |
| } | |
| class AcpProcess { | |
| constructor(args) { | |
| this.child = spawn(bin, args, { | |
| cwd, | |
| env: process.env, | |
| stdio: ["pipe", "pipe", "pipe"], | |
| }); | |
| this.nextId = 1; | |
| this.pending = new Map(); | |
| this.assistantText = ""; | |
| readline.createInterface({ input: this.child.stdout }).on("line", (line) => { | |
| const message = JSON.parse(line); | |
| if (message.method === "session/update") { | |
| const update = message.params?.update; | |
| if (update?.sessionUpdate === "agent_message_chunk" && update.content?.type === "text") { | |
| this.assistantText += update.content.text; | |
| } | |
| return; | |
| } | |
| if (message.id === undefined || message.method) return; | |
| const pending = this.pending.get(message.id); | |
| if (!pending) return; | |
| this.pending.delete(message.id); | |
| clearTimeout(pending.timeout); | |
| if (message.error) { | |
| pending.reject(new Error(JSON.stringify(message.error))); | |
| } else { | |
| pending.resolve(message.result); | |
| } | |
| }); | |
| this.child.stderr.on("data", (chunk) => { | |
| process.stderr.write(chunk); | |
| }); | |
| } | |
| request(method, params) { | |
| const id = this.nextId++; | |
| this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`); | |
| return new Promise((resolve, reject) => { | |
| const timeout = setTimeout(() => { | |
| this.pending.delete(id); | |
| reject(new Error(`Timed out waiting for ${method}`)); | |
| }, timeoutMs); | |
| this.pending.set(id, { resolve, reject, timeout }); | |
| }); | |
| } | |
| async start() { | |
| await this.request("initialize", { | |
| protocolVersion: 1, | |
| clientCapabilities: { | |
| fs: { readTextFile: false, writeTextFile: false }, | |
| terminal: false, | |
| _meta: { parameterizedModelPicker: true }, | |
| }, | |
| clientInfo: { name: "cursor-acp-model-switch-repro", version: "0.0.0" }, | |
| }); | |
| await this.request("authenticate", { methodId: "cursor_login" }); | |
| return this.request("session/new", { cwd, mcpServers: [] }); | |
| } | |
| async promptForModel(sessionId) { | |
| this.assistantText = ""; | |
| await this.request("session/prompt", { | |
| sessionId, | |
| prompt: [{ type: "text", text: "What model are you? Reply with only the exact model family and version." }], | |
| }); | |
| return this.assistantText.trim(); | |
| } | |
| async close() { | |
| this.child.kill("SIGTERM"); | |
| await sleep(100); | |
| if (!this.child.killed) this.child.kill("SIGKILL"); | |
| } | |
| } | |
| async function withAcp(args, run) { | |
| const acp = new AcpProcess(args); | |
| try { | |
| return await run(acp); | |
| } finally { | |
| await acp.close(); | |
| } | |
| } | |
| console.log({ bin, cwd, targetModel }); | |
| await withAcp(["acp"], async (acp) => { | |
| const created = await acp.start(); | |
| console.log("\n1. session/new without --model"); | |
| console.log(summarizeSession(created)); | |
| const setConfigResult = await acp.request("session/set_config_option", { | |
| sessionId: created.sessionId, | |
| configId: "model", | |
| value: targetModel, | |
| }); | |
| console.log("\n2. session/set_config_option model switch"); | |
| console.log(summarizeSession(setConfigResult)); | |
| console.log("assistant:", await acp.promptForModel(created.sessionId)); | |
| }); | |
| await withAcp(["acp"], async (acp) => { | |
| const created = await acp.start(); | |
| console.log("\n3. fresh session/new before session/set_model"); | |
| console.log(summarizeSession(created)); | |
| const setModelResult = await acp.request("session/set_model", { | |
| sessionId: created.sessionId, | |
| modelId: targetModel, | |
| }); | |
| console.log("\n4. session/set_model model switch"); | |
| console.log(setModelResult); | |
| console.log("assistant:", await acp.promptForModel(created.sessionId)); | |
| }); | |
| await withAcp(["--model", targetModel, "acp"], async (acp) => { | |
| const created = await acp.start(); | |
| console.log("\n5. session/new with startup --model"); | |
| console.log(summarizeSession(created)); | |
| console.log("assistant:", await acp.promptForModel(created.sessionId)); | |
| }); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample run whose output proves: