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
52 changes: 51 additions & 1 deletion src/acp.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach } from "vitest";
import { describe, it, expect, beforeEach, vi } from "vitest";
import {
Agent,
ClientSideConnection,
Expand All @@ -24,6 +24,8 @@ import {
SessionNotification,
PROTOCOL_VERSION,
ndJsonStream,
RequestError,
ErrorCode,
} from "./acp.js";

describe("Connection", () => {
Expand Down Expand Up @@ -1105,4 +1107,52 @@ describe("Connection", () => {
});
expect(loadResponse).toEqual({});
});

it("logs RESOURCE_NOT_FOUND at debug level", async () => {
const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

const client: Client = {
readTextFile: () => Promise.reject(RequestError.resourceNotFound()),
writeTextFile: () => Promise.resolve({}),
requestPermission: () =>
Promise.resolve({ outcome: { outcome: "selected", optionId: "allow" } }),
sessionUpdate: () => Promise.resolve(),
};

const agent: Agent = {
initialize: () =>
Promise.resolve({
protocolVersion: PROTOCOL_VERSION,
agentCapabilities: { loadSession: false },
}),
newSession: () => Promise.resolve({ sessionId: "s1" }),
authenticate: () => Promise.resolve(),
prompt: () => Promise.resolve({ stopReason: "end_turn" }),
cancel: () => Promise.resolve(),
};

new ClientSideConnection(
() => client,
ndJsonStream(clientToAgent.writable, agentToClient.readable),
);
const conn = new AgentSideConnection(
() => agent,
ndJsonStream(agentToClient.writable, clientToAgent.readable),
);

await expect(
conn.readTextFile({ path: "/test.txt", sessionId: "s1" }),
).rejects.toThrow();

expect(debugSpy).toHaveBeenCalledWith(
"Error handling request",
expect.anything(),
expect.objectContaining({ code: ErrorCode.RESOURCE_NOT_FOUND }),
);
expect(errorSpy).not.toHaveBeenCalled();

debugSpy.mockRestore();
errorSpy.mockRestore();
});
});
27 changes: 25 additions & 2 deletions src/acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,16 @@ class Connection {
}
}

#logError(context: string, message: AnyMessage, error: ErrorResponse) {
// RESOURCE_NOT_FOUND is expected when checking file existence
// before write, since ACP has no stat API
if (error.code === ErrorCode.RESOURCE_NOT_FOUND) {
console.debug(context, message, error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this matter though? stdout is for ACP messages, and all logs need to go out on stderr (most agents end up pointing console.debug to console.error for this reason)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so gemini-cli reads before writes at the moment (to create a diff for the permission approval), so there's a bit of spam every time it tries to create a new file... I'm not really used to seeing scary things for common issues.. any ideas on this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or are you saying that console.debug will always be written to stderr anyway.. basically I'm open to options I may not have the best solution in mind.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes for gemini cli + claude code all console logs are routed to stderr, otherwise they would be sent over stdout to the ACP client

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I meant to ask if there is a better way to deprioritize this type of log message.. assuming any log has to go to stderr or otherwise not stdout. Or if we should close this PR and just accept that a resource not found due to "check before write" will have this effect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean... it is a good question... these messages are kind of already in the stdin feed.

I could see this as something we filter out. But I guess I can also see cases where this error is helpful for other reasons..

It is also on my todo list somewhere to add a stat api to ACP because it would be useful in more than just this case :D

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me close this out, then. let's focus on stat and I'll try to get gemini to use it. Is that an RFD? (we can follow-up on zulip)

} else {
console.error(context, message, error);
}
}

async #processMessage(message: AnyMessage) {
if ("method" in message && "id" in message) {
// It's a request
Expand All @@ -890,7 +900,7 @@ class Connection {
message.params,
);
if ("error" in response) {
console.error("Error handling request", message, response.error);
this.#logError("Error handling request", message, response.error);
}

await this.#sendMessage({
Expand All @@ -905,7 +915,7 @@ class Connection {
message.params,
);
if ("error" in response) {
console.error("Error handling notification", message, response.error);
this.#logError("Error handling notification", message, response.error);
}
} else if ("id" in message) {
// It's a response
Expand Down Expand Up @@ -1038,6 +1048,19 @@ class Connection {
}
}

/**
* JSON-RPC error codes used by the ACP protocol.
*/
export const ErrorCode = {
PARSE_ERROR: -32700,
INVALID_REQUEST: -32600,
METHOD_NOT_FOUND: -32601,
INVALID_PARAMS: -32602,
INTERNAL_ERROR: -32603,
AUTH_REQUIRED: -32000,
RESOURCE_NOT_FOUND: -32002,
} as const;

/**
* JSON-RPC error object.
*
Expand Down