mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
Assign content correctly in read tool
This commit is contained in:
@@ -35,7 +35,11 @@ import { ContentBlock } from "@zed-industries/agent-client-protocol";
|
||||
import { SessionNotification } from "@zed-industries/agent-client-protocol";
|
||||
import { createMcpServer } from "./mcp-server.js";
|
||||
import { AddressInfo } from "node:net";
|
||||
import { extractToolInfo, planEntries } from "./tools.js";
|
||||
import {
|
||||
toolInfoFromToolUse,
|
||||
planEntries,
|
||||
toolUpdateFromToolResult,
|
||||
} from "./tools.js";
|
||||
|
||||
type Session = {
|
||||
query: Query;
|
||||
@@ -49,10 +53,12 @@ export class ClaudeAcpAgent implements Agent {
|
||||
[key: string]: Session;
|
||||
};
|
||||
client: Client;
|
||||
toolUseCache: { [key: string]: any };
|
||||
|
||||
constructor(client: Client) {
|
||||
this.sessions = {};
|
||||
this.client = client;
|
||||
this.toolUseCache = {};
|
||||
}
|
||||
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
|
||||
return {
|
||||
@@ -103,7 +109,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
"x-acp-proxy-session-id": sessionId,
|
||||
},
|
||||
};
|
||||
console.error(mcpServers);
|
||||
|
||||
let q = query({
|
||||
prompt: input,
|
||||
@@ -150,7 +155,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
break;
|
||||
}
|
||||
console.error(JSON.stringify(message, null, 4));
|
||||
switch (message.type) {
|
||||
case "system":
|
||||
break;
|
||||
@@ -162,7 +166,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
// todo!() how is rate-limiting handled?
|
||||
switch (message.subtype) {
|
||||
case "success": {
|
||||
console.error(message.result);
|
||||
if (message.result.includes("Please run /login")) {
|
||||
throw RequestError.authRequired();
|
||||
}
|
||||
@@ -192,8 +195,9 @@ export class ClaudeAcpAgent implements Agent {
|
||||
for (const notification of toAcpNotifications(
|
||||
message,
|
||||
params.sessionId,
|
||||
this.toolUseCache,
|
||||
)) {
|
||||
this.client.sessionUpdate(notification);
|
||||
await this.client.sessionUpdate(notification);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -201,7 +205,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
unreachable(message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Session did not end in result");
|
||||
}
|
||||
|
||||
@@ -309,6 +312,7 @@ function promptToClaude(prompt: PromptRequest): SDKUserMessage {
|
||||
export function toAcpNotifications(
|
||||
message: SDKAssistantMessage | SDKUserMessage,
|
||||
sessionId: string,
|
||||
toolUseCache: { [key: string]: any },
|
||||
): SessionNotification[] {
|
||||
let chunks = message.message.content as ContentChunk[];
|
||||
let output = [];
|
||||
@@ -347,31 +351,29 @@ export function toAcpNotifications(
|
||||
};
|
||||
break;
|
||||
case "tool_use":
|
||||
toolUseCache[chunk.id] = chunk;
|
||||
if (chunk.name == "TodoWrite") {
|
||||
update = {
|
||||
sessionUpdate: "plan",
|
||||
entries: planEntries(chunk.input),
|
||||
};
|
||||
break;
|
||||
} else {
|
||||
update = {
|
||||
toolCallId: chunk.id,
|
||||
sessionUpdate: "tool_call",
|
||||
rawInput: chunk.input,
|
||||
status: "pending",
|
||||
...extractToolInfo(chunk),
|
||||
...toolInfoFromToolUse(chunk),
|
||||
};
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "tool_result":
|
||||
update = {
|
||||
toolCallId: chunk.tool_use_id,
|
||||
sessionUpdate: "tool_call_update",
|
||||
content: [
|
||||
{ type: "content", content: { type: "text", text: chunk.content } },
|
||||
],
|
||||
status: chunk.is_error ? "failed" : "completed",
|
||||
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
|
||||
};
|
||||
break;
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ In sessions with mcp__acp__read always use it instead of Read as it contains the
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
console.error("READ TOOL", input);
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
@@ -66,7 +65,6 @@ In sessions with mcp__acp__read always use it instead of Read as it contains the
|
||||
limit: input.limit,
|
||||
line: input.offset,
|
||||
});
|
||||
console.error("READ FILE READ", content);
|
||||
|
||||
return {
|
||||
content: [
|
||||
@@ -117,7 +115,6 @@ allow the user to conveniently review changes.`,
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
console.error("WRITE TOOL", input);
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
@@ -186,7 +183,6 @@ File editing instructions:
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
console.error("EDIT TOOL", input);
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
@@ -386,7 +382,6 @@ File editing instructions:
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
@@ -406,13 +401,6 @@ File editing instructions:
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const address = listener.address();
|
||||
console.error(address);
|
||||
if (typeof address === "object" && address && "port" in address) {
|
||||
console.error(
|
||||
`MCP HTTP server listening on http://127.0.0.1:${address.port}/mcp`,
|
||||
);
|
||||
}
|
||||
resolve(listener);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { spawn } from "child_process";
|
||||
import { spawn, spawnSync } from "child_process";
|
||||
import {
|
||||
Agent,
|
||||
Client,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
WriteTextFileResponse,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
|
||||
import { extractToolInfo } from "../tools.js";
|
||||
import { toolInfoFromToolUse } from "../tools.js";
|
||||
import { toAcpNotifications } from "../acp-agent.js";
|
||||
import { SDKAssistantMessage } from "@anthropic-ai/claude-code";
|
||||
|
||||
@@ -23,11 +23,21 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
let child: ReturnType<typeof spawn>;
|
||||
|
||||
beforeAll(async () => {
|
||||
let valid = spawnSync("tsc", { stdio: "inherit" });
|
||||
if (valid.status) {
|
||||
throw new Error("failed to compile");
|
||||
}
|
||||
// Start the subprocess
|
||||
child = spawn("npm", ["run", "--silent", "dev"], {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
env: process.env,
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
console.error("Error starting subprocess:", error);
|
||||
});
|
||||
child.on("exit", (exit) => {
|
||||
console.error("Exited with", exit);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -46,15 +56,20 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async sessionUpdate(params: SessionNotification): Promise<void> {
|
||||
console.error(params);
|
||||
console.error("RECEVIED", params);
|
||||
}
|
||||
writeTextFile(
|
||||
params: WriteTextFileRequest,
|
||||
): Promise<WriteTextFileResponse> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
throw new Error("Method not implemented.");
|
||||
async readTextFile(
|
||||
params: ReadTextFileRequest,
|
||||
): Promise<ReadTextFileResponse> {
|
||||
return {
|
||||
content:
|
||||
"This project attempts to take over the world by solving the Goldbach conjecture.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +84,16 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
|
||||
let session = await connection.newSession({ cwd: "./", mcpServers: [] });
|
||||
await connection.prompt({
|
||||
prompt: [{ type: "text", text: "Hello Claude!" }],
|
||||
prompt: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What does this project do? Read the README.md file.",
|
||||
},
|
||||
],
|
||||
sessionId: session.sessionId,
|
||||
});
|
||||
});
|
||||
console.log("DONE AWAITING");
|
||||
}, 20000);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -88,7 +109,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: "`rm README.md.rm`",
|
||||
content: [
|
||||
@@ -113,7 +134,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "search",
|
||||
title: "Find */**.ts",
|
||||
content: [],
|
||||
@@ -133,7 +154,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "think",
|
||||
title: "Handle user's work request",
|
||||
content: [
|
||||
@@ -158,7 +179,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "search",
|
||||
title: "/Users/benbrandt/github/claude-code-acp",
|
||||
content: [],
|
||||
@@ -175,7 +196,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "search",
|
||||
title: 'grep ".*"',
|
||||
content: [],
|
||||
@@ -193,7 +214,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "edit",
|
||||
title: "Write /Users/test/project/example.txt",
|
||||
content: [
|
||||
@@ -218,7 +239,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "edit",
|
||||
title: "Write /Users/test/project/config.json",
|
||||
content: [
|
||||
@@ -242,7 +263,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read File",
|
||||
content: [
|
||||
@@ -264,7 +285,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/data.json",
|
||||
content: [],
|
||||
@@ -282,7 +303,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (1 - 100)",
|
||||
content: [],
|
||||
@@ -301,7 +322,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (51 - 150)",
|
||||
content: [],
|
||||
@@ -319,7 +340,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (from line 201)",
|
||||
content: [],
|
||||
@@ -338,7 +359,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "fetch",
|
||||
title: "Fetch https://agentclientprotocol.com",
|
||||
content: [
|
||||
@@ -363,7 +384,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "fetch",
|
||||
title: '"agentclientprotocol.com"',
|
||||
content: [],
|
||||
@@ -380,7 +401,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: `Kill Process`,
|
||||
content: [],
|
||||
@@ -397,7 +418,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractToolInfo(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: `Tail Logs`,
|
||||
content: [],
|
||||
@@ -482,7 +503,7 @@ describe("tool conversions", () => {
|
||||
session_id: "d056596f-e328-41e9-badd-b07122ae5227",
|
||||
uuid: "b7c3330c-de8f-4bba-ac53-68c7f76ffeb5",
|
||||
};
|
||||
expect(toAcpNotifications(received, "test")).toStrictEqual([
|
||||
expect(toAcpNotifications(received, "test", {})).toStrictEqual([
|
||||
{
|
||||
sessionId: "test",
|
||||
update: {
|
||||
|
||||
45
src/tools.ts
45
src/tools.ts
@@ -2,6 +2,7 @@ import { ContentBlock } from "@modelcontextprotocol/sdk/types.js";
|
||||
import {
|
||||
PlanEntry,
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
ToolKind,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
|
||||
@@ -9,9 +10,15 @@ interface ToolInfo {
|
||||
title: string;
|
||||
kind: ToolKind;
|
||||
content: ToolCallContent[];
|
||||
locations?: ToolCallLocation[];
|
||||
}
|
||||
|
||||
export function extractToolInfo(toolUse: any): ToolInfo {
|
||||
interface ToolUpdate {
|
||||
title?: string;
|
||||
content?: ToolCallContent[];
|
||||
}
|
||||
|
||||
export function toolInfoFromToolUse(toolUse: any): ToolInfo {
|
||||
const name = toolUse.name;
|
||||
const input = toolUse.input;
|
||||
|
||||
@@ -114,6 +121,12 @@ export function extractToolInfo(toolUse: any): ToolInfo {
|
||||
return {
|
||||
title: "Read " + (input.abs_path ?? "File") + limit,
|
||||
kind: "read",
|
||||
locations: [
|
||||
{
|
||||
path: input.abs_path,
|
||||
line: input.offset,
|
||||
},
|
||||
],
|
||||
content: [],
|
||||
};
|
||||
}
|
||||
@@ -145,6 +158,11 @@ export function extractToolInfo(toolUse: any): ToolInfo {
|
||||
return {
|
||||
title: input?.abs_path ? `Edit ${input.abs_path}` : "Edit",
|
||||
kind: "edit",
|
||||
locations: [
|
||||
{
|
||||
path: input.abs_path,
|
||||
},
|
||||
],
|
||||
content:
|
||||
input && input.abs_path
|
||||
? [
|
||||
@@ -371,6 +389,31 @@ export function extractToolInfo(toolUse: any): ToolInfo {
|
||||
}
|
||||
}
|
||||
|
||||
export function toolUpdateFromToolResult(
|
||||
toolResult: any,
|
||||
toolUse: any | undefined,
|
||||
): ToolUpdate {
|
||||
// This happens for the mcp__acp__read tool,
|
||||
// but may also for others...
|
||||
if (Array.isArray(toolResult.content)) {
|
||||
return {
|
||||
content: toolResult.content.map((content: any) => ({
|
||||
type: "content",
|
||||
content,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: toolResult.content },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
type ClaudePlanEntry = {
|
||||
content: string;
|
||||
status: "pending" | "in_progress" | "completed";
|
||||
|
||||
Reference in New Issue
Block a user