Show upstream ACP JSON-RPC errors instead of [object Object]

* Preserve ACP JSON-RPC error details

* Unslop ACP error summarization
This commit is contained in:
Mohamed Boudra
2026-05-13 17:43:40 +08:00
committed by GitHub
parent 9eb1ddf29d
commit 65d14d4433
3 changed files with 116 additions and 4 deletions
@@ -4195,6 +4195,8 @@ test("turn_failed surfaces provider code and diagnostic in system error message"
await expect(manager.runAgent(agent.id, "hello")).rejects.toThrow("Provider execution failed");
expect(manager.getAgent(agent.id)?.lastError).toBe("Provider execution failed");
const systemError = manager
.getTimeline(agent.id)
.find(
@@ -1,7 +1,13 @@
import { type ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { afterEach, describe, expect, test, vi } from "vitest";
import type {
import {
AgentSideConnection,
ClientSideConnection,
PROTOCOL_VERSION,
RequestError,
ndJsonStream,
type Agent,
PermissionOption,
PromptResponse,
RequestPermissionRequest,
@@ -1575,4 +1581,79 @@ describe("ACPAgentSession", () => {
});
expect(asInternals<ACPSessionInternals>(session).activeForegroundTurnId).toBeNull();
});
test("startTurn preserves JSON-RPC error details from a real ACP prompt response", async () => {
const session = createSession();
const clientToAgent = new TransformStream();
const agentToClient = new TransformStream();
const upstreamMessage =
"Authentication failed: Please authenticate to continue. Run `/login` to log in.";
const upstreamData = {
cause: "auth_required",
errorMessage: "Please authenticate to continue. Run `/login` to log in.",
};
const agent: Agent = {
async initialize() {
return {
protocolVersion: PROTOCOL_VERSION,
agentCapabilities: {},
authMethods: [{ id: "windsurf-api-key", name: "API Key" }],
};
},
async newSession() {
return { sessionId: "session-1" };
},
async prompt() {
throw new RequestError(-32000, upstreamMessage, upstreamData);
},
async authenticate() {},
async cancel() {},
};
const agentConnection = new AgentSideConnection(
() => agent,
ndJsonStream(agentToClient.writable, clientToAgent.readable),
);
const connection = new ClientSideConnection(
() => ({
async requestPermission() {
return { outcome: { outcome: "cancelled" } };
},
async sessionUpdate() {},
}),
ndJsonStream(clientToAgent.writable, agentToClient.readable),
);
await connection.initialize({
protocolVersion: PROTOCOL_VERSION,
clientCapabilities: {},
clientInfo: { name: "Paseo test", version: "dev" },
});
expect(agentConnection.signal.aborted).toBe(false);
const sessionResponse = await connection.newSession({
cwd: "/tmp/paseo-acp-test",
mcpServers: [],
});
const turnFailed = new Promise<Extract<AgentStreamEvent, { type: "turn_failed" }>>(
(resolve) => {
session.subscribe((event) => {
if (event.type === "turn_failed") {
resolve(event);
}
});
},
);
asInternals<ACPSessionInternals>(session).sessionId = sessionResponse.sessionId;
asInternals<ACPSessionInternals>(session).connection = connection;
await session.startTurn("hello");
await expect(turnFailed).resolves.toMatchObject({
error: expect.stringContaining(upstreamMessage),
code: "-32000",
diagnostic: expect.stringContaining("auth_required"),
});
await expect(turnFailed).resolves.toMatchObject({
error: expect.not.stringContaining("[object Object]"),
});
});
});
@@ -11,6 +11,7 @@ import {
ClientSideConnection,
PROTOCOL_VERSION,
type AgentCapabilities as ACPAgentCapabilities,
type Error as ACPError,
type AnyMessage,
type Client as ACPClient,
type ClientCapabilities as ACPClientCapabilities,
@@ -107,6 +108,33 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === "object" && !Array.isArray(value);
}
function isACPError(value: unknown): value is ACPError {
return isRecord(value) && typeof value.message === "string" && typeof value.code === "number";
}
function summarizeACPRequestError(error: unknown): {
message: string;
code?: string;
diagnostic?: string;
} {
// Promise rejections are untyped, but the ACP SDK rejects JSON-RPC failures as response.error.
if (isACPError(error)) {
const code = String(error.code);
const data = error.data === undefined ? "" : ` | data=${JSON.stringify(error.data)}`;
return {
message: error.message,
code,
diagnostic: `${error.message} | code=${code}${data}`,
};
}
if (error instanceof Error) {
return { message: error.message };
}
return { message: String(error) };
}
function resolveTerminalCommand(
command: string,
args?: string[],
@@ -1045,12 +1073,13 @@ export class ACPAgentSession implements AgentSession, ACPClient {
return;
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error);
const summary = summarizeACPRequestError(error);
this.finishTurn({
type: "turn_failed",
provider: this.provider,
error: message,
diagnostic: this.collectDiagnostic(message),
error: summary.message,
code: summary.code,
diagnostic: this.collectDiagnostic(summary.diagnostic ?? summary.message),
turnId,
});
});