fix(cursor): stop leaking agent tool errors as text

Emit SSE error frame for unsupported Cursor AgentService IDE tools
instead of assistant content, and drop frames after the turn finishes
to avoid double-closing the stream controller.
This commit is contained in:
Kyle Welsworth
2026-07-29 19:29:27 +07:00
committed by decolua
parent 16cb40fda1
commit 5e59790824
2 changed files with 128 additions and 3 deletions
@@ -0,0 +1,114 @@
import { describe, it, expect } from "vitest";
import { CursorExecutor } from "../../open-sse/executors/cursor.js";
import { encodeField, wrapConnectRPCFrame } from "../../open-sse/utils/cursorProtobuf.js";
const LEN = 2;
// agent.v1.AgentServerMessage.exec_request (field 2) carrying one ExecServerMessage variant.
function execRequestFrame(execField) {
const execServerMessage = Buffer.from(encodeField(execField, LEN, new Uint8Array()));
return Buffer.from(wrapConnectRPCFrame(encodeField(2, LEN, execServerMessage)));
}
// agent.v1.AgentServerMessage.interaction_update (field 1) → text delta.
function textFrame(text) {
const textPart = Buffer.from(encodeField(1, LEN, text));
const update = Buffer.from(encodeField(1, LEN, textPart));
return Buffer.from(wrapConnectRPCFrame(encodeField(1, LEN, update)));
}
function stubAgentSession(executor, frames) {
const written = [];
const queue = [...frames];
executor.openAgentHttp2Stream = () => ({
responseHeaders: Promise.resolve({ ":status": 200 }),
write: (frame) => written.push(Buffer.from(frame)),
end() {},
close() {},
async read() {
if (!queue.length) return { value: undefined, done: true };
return { value: queue.shift(), done: false };
},
});
return written;
}
const credentials = {
accessToken: "test-token",
providerSpecificData: { machineId: "a".repeat(64) },
};
function parseSSE(text) {
return text
.split("\n\n")
.filter((chunk) => chunk.startsWith("data: "))
.map((chunk) => chunk.slice("data: ".length))
.filter((data) => data !== "[DONE]")
.map((data) => JSON.parse(data));
}
async function runAgent({ frames, stream }) {
const executor = new CursorExecutor();
const written = stubAgentSession(executor, frames);
const result = await executor.executeAgent({
model: "gpt-5.2",
body: { messages: [{ role: "user", content: "hi" }] },
stream,
credentials,
});
return { result, written };
}
describe("CursorExecutor AgentService exec_request handling", () => {
it("acknowledges a request-context exec request without ending the turn", async () => {
const { result, written } = await runAgent({
frames: [execRequestFrame(10), textFrame("hello")],
stream: true,
});
expect(written.length).toBe(2); // run frame + request-context reply
const events = parseSSE(await result.response.text());
const content = events.map((e) => e.choices?.[0]?.delta?.content || "").join("");
expect(content).toBe("hello");
});
it("does not render an unsupported exec request as assistant content", async () => {
const { result } = await runAgent({
frames: [textFrame("partial answer"), execRequestFrame(2)],
stream: true,
});
const body = await result.response.text();
expect(body).not.toContain("unsupported IDE tool\\n");
const events = parseSSE(body);
const content = events.map((e) => e.choices?.[0]?.delta?.content || "").join("");
expect(content).toBe("partial answer");
const errorEvent = events.find((e) => e.error);
expect(errorEvent?.error?.message).toContain("unsupported IDE tool");
expect(events.some((e) => e.choices?.[0]?.finish_reason === "stop")).toBe(false);
});
it("drops frames batched behind an unsupported exec request in the same read", async () => {
const { result } = await runAgent({
frames: [Buffer.concat([execRequestFrame(2), textFrame("late")])],
stream: true,
});
const body = await result.response.text();
expect(body).toContain("unsupported IDE tool");
expect(body).not.toContain("late");
});
it("returns a non-200 error body for an unsupported exec request when not streaming", async () => {
const { result } = await runAgent({
frames: [execRequestFrame(11)],
stream: false,
});
expect(result.response.status).not.toBe(200);
const payload = await result.response.json();
expect(payload.error.message).toContain("unsupported IDE tool");
});
});