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