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:
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user