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"); 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 const systemError = manager
.getTimeline(agent.id) .getTimeline(agent.id)
.find( .find(
@@ -1,7 +1,13 @@
import { type ChildProcess } from "node:child_process"; import { type ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import { afterEach, describe, expect, test, vi } from "vitest"; import { afterEach, describe, expect, test, vi } from "vitest";
import type { import {
AgentSideConnection,
ClientSideConnection,
PROTOCOL_VERSION,
RequestError,
ndJsonStream,
type Agent,
PermissionOption, PermissionOption,
PromptResponse, PromptResponse,
RequestPermissionRequest, RequestPermissionRequest,
@@ -1575,4 +1581,79 @@ describe("ACPAgentSession", () => {
}); });
expect(asInternals<ACPSessionInternals>(session).activeForegroundTurnId).toBeNull(); 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, ClientSideConnection,
PROTOCOL_VERSION, PROTOCOL_VERSION,
type AgentCapabilities as ACPAgentCapabilities, type AgentCapabilities as ACPAgentCapabilities,
type Error as ACPError,
type AnyMessage, type AnyMessage,
type Client as ACPClient, type Client as ACPClient,
type ClientCapabilities as ACPClientCapabilities, 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); 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( function resolveTerminalCommand(
command: string, command: string,
args?: string[], args?: string[],
@@ -1045,12 +1073,13 @@ export class ACPAgentSession implements AgentSession, ACPClient {
return; return;
}) })
.catch((error) => { .catch((error) => {
const message = error instanceof Error ? error.message : String(error); const summary = summarizeACPRequestError(error);
this.finishTurn({ this.finishTurn({
type: "turn_failed", type: "turn_failed",
provider: this.provider, provider: this.provider,
error: message, error: summary.message,
diagnostic: this.collectDiagnostic(message), code: summary.code,
diagnostic: this.collectDiagnostic(summary.diagnostic ?? summary.message),
turnId, turnId,
}); });
}); });