Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/claude-tty-acp/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ClaudeTtyAgent } from "./agent.ts";
import { useAnswersDirectory } from "./card-answers.ts";
import { readIdleTimeout } from "./idle-timeout.ts";
import { enableLogFile, writeLog } from "./log.ts";
import { withRequestErrors } from "./request-errors.ts";
import { cleanupAbandonedRuntimeDirectories } from "./runtime-directories.ts";
import { useSettingsFile } from "./settings-document.ts";

Expand Down Expand Up @@ -34,7 +35,7 @@ export async function runAcpServer(settingsFile: string | null, answersDirectory
void shutdown("workspace_removed").finally(() => process.exit(0));
},
});
return agent;
return withRequestErrors(agent);
}, ndJsonStream(output, input));

const handleSignal = (signal: NodeJS.Signals): void => {
Expand Down
50 changes: 50 additions & 0 deletions apps/claude-tty-acp/src/request-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
AgentSideConnection,
ClientSideConnection,
ndJsonStream,
PROTOCOL_VERSION,
RequestError,
type Client,
} from "@agentclientprotocol/sdk";
import { ClaudeTtyAgent } from "./agent.ts";
import { withRequestErrors } from "./request-errors.ts";

/** Connects a client to the agent over the SDK's own JSON-RPC, so the test sees what Paseo sees. */
function connect(): { client: ClientSideConnection; agent: () => ClaudeTtyAgent | null } {
const toAgent = new TransformStream<Uint8Array, Uint8Array>();
const toClient = new TransformStream<Uint8Array, Uint8Array>();
let agent: ClaudeTtyAgent | null = null;
new AgentSideConnection((connection) => {
agent = new ClaudeTtyAgent(connection);
return withRequestErrors(agent);
}, ndJsonStream(toClient.writable, toAgent.readable));
const client = new ClientSideConnection(
() => ({ requestPermission: async () => ({ outcome: { outcome: "cancelled" } }), sessionUpdate: async () => undefined }) as Client,
ndJsonStream(toAgent.writable, toClient.readable),
);
return { client, agent: () => agent };
}

test("answers a failed request with the adapter's own sentence", async () => {
const { client, agent } = connect();
await client.initialize({ protocolVersion: PROTOCOL_VERSION });

await assert.rejects(client.prompt({ sessionId: "missing", prompt: [{ type: "text", text: "hi" }] }), {
code: -32603,
message: "Session missing not found",
});
await agent()?.close();
});

test("passes a RequestError through as it was thrown", async () => {
const thrown = RequestError.invalidParams(undefined, "no such mode");
const wrapped = withRequestErrors({
initialize: async () => {
throw thrown;
},
} as unknown as ClaudeTtyAgent);

await assert.rejects(wrapped.initialize({ protocolVersion: PROTOCOL_VERSION }), (error) => error === thrown);
});
28 changes: 28 additions & 0 deletions apps/claude-tty-acp/src/request-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type Agent, RequestError } from "@agentclientprotocol/sdk";

/**
* Hands the SDK an agent whose failures carry their own text as the JSON-RPC message.
* The SDK answers any error that is not a `RequestError` with the message "Internal error" and
* the real text in `data.details`, which a client is free to drop, and Paseo has. The code stays
* -32603: Paseo branches on -32602 alone, so another code would change nothing but the meaning.
*/
export function withRequestErrors<T extends Agent>(agent: T): T {
return new Proxy(agent, {
get(target, property, receiver) {
const value: unknown = Reflect.get(target, property, receiver);
if (typeof value !== "function") return value;
return async (...args: unknown[]) => {
try {
return await value.apply(target, args);
} catch (error) {
throw toRequestError(error);
}
};
},
});
}

export function toRequestError(error: unknown): RequestError {
if (error instanceof RequestError) return error;
return new RequestError(-32603, error instanceof Error ? error.message : String(error));
}
Loading