From 65d14d44336e3b4df2ddd70b5df80433e616a5ae Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 13 May 2026 17:43:40 +0800 Subject: [PATCH] Show upstream ACP JSON-RPC errors instead of [object Object] * Preserve ACP JSON-RPC error details * Unslop ACP error summarization --- .../src/server/agent/agent-manager.test.ts | 2 + .../server/agent/providers/acp-agent.test.ts | 83 ++++++++++++++++++- .../src/server/agent/providers/acp-agent.ts | 35 +++++++- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 705ddbabe..b2425f584 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -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( diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index e2ef5f338..0cbc2a4df 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -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(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>( + (resolve) => { + session.subscribe((event) => { + if (event.type === "turn_failed") { + resolve(event); + } + }); + }, + ); + + asInternals(session).sessionId = sessionResponse.sessionId; + asInternals(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]"), + }); + }); }); diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 8ca3307a6..a9f7924c5 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -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 { 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, }); });