From 373d940eb884bb08c74d5c4e04582d6db569fbdc Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 17 Oct 2025 11:58:36 +0200 Subject: [PATCH] Update to claude-agent-sdk 0.1.21 and support streaming partial messages (#105) --- CHANGELOG.md | 5 ++ package-lock.json | 44 +++++++-------- package.json | 12 ++-- src/acp-agent.ts | 110 ++++++++++++++++++++++++++++++------ src/tests/acp-agent.test.ts | 4 +- src/tools.ts | 32 +++++++++-- src/utils.ts | 8 ++- 7 files changed, 165 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e537b3..b9557062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.6.4 + +- Support streaming partial messages! +- Update to @anthropic-ai/claude-agent-sdk@v0.1.21 + ## 0.6.3 - Fix issue where slash commands were loaded before initialization was complete. diff --git a/package-lock.json b/package-lock.json index 019adc89..6386a551 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "@zed-industries/claude-code-acp", - "version": "0.6.3", + "version": "0.6.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zed-industries/claude-code-acp", - "version": "0.6.3", + "version": "0.6.4", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "0.4.8", - "@anthropic-ai/claude-agent-sdk": "0.1.19", - "@anthropic-ai/claude-code": "2.0.19", - "@modelcontextprotocol/sdk": "1.20.0", + "@anthropic-ai/claude-agent-sdk": "0.1.21", + "@anthropic-ai/claude-code": "2.0.21", + "@modelcontextprotocol/sdk": "1.20.1", "diff": "8.0.2", "express": "5.1.0", "minimist": "1.2.8", @@ -22,10 +22,10 @@ "claude-code-acp": "dist/index.js" }, "devDependencies": { - "@anthropic-ai/sdk": "^0.66.0", + "@anthropic-ai/sdk": "^0.67.0", "@types/express": "5.0.3", "@types/minimist": "1.2.5", - "@types/node": "24.8.0", + "@types/node": "24.8.1", "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "8.46.1", "@typescript-eslint/parser": "8.46.1", @@ -49,9 +49,9 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.19.tgz", - "integrity": "sha512-k4GtWygAkp3d0Npo/cS16MAfnSEcjttT5ijZzEWIzpWClL5wOYCpVyuMjnZGsYPDDZCb26pVb7Wm0ZDFj149Cg==", + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.21.tgz", + "integrity": "sha512-8qUD//GfS/SXHx2nnVOg4uwS2aqrULp/KKi6HvPqQpkTAOH0o393uWHNIi69WCshy9RLGO7NZWodlxTT3v+RdQ==", "license": "SEE LICENSE IN README.md", "engines": { "node": ">=18.0.0" @@ -69,9 +69,9 @@ } }, "node_modules/@anthropic-ai/claude-code": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.0.19.tgz", - "integrity": "sha512-9InIRKliS3TaPbWfuzJA0SoC3hLQ0Lk2lmBVFBChgd8f6aTVV5dQwHNgDnLjtl1L+HhRfMqjSgBfNaeUmzet/A==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.0.21.tgz", + "integrity": "sha512-02NcxPVjioLjo4/djzIt4aAYkYqnZsHgdzgMexLURYLR9q1+B+OLkKddKwOHL8fas3WnUGV1oyybu73BfHqQ4A==", "license": "SEE LICENSE IN README.md", "bin": { "claude": "cli.js" @@ -89,9 +89,9 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.66.0.tgz", - "integrity": "sha512-GBSSby0P4BW/sOdvsTXaHJDPnGEL5tuB4TtsU4SXG7+dVULQ9MkKgNznCALDCgSV5yhrtQlctvEdMqePVIXTiw==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.67.0.tgz", + "integrity": "sha512-Buxbf6jYJ+pPtfCgXe1pcFtZmdXPrbdqhBjiscFt9irS1G0hCsmR/fPA+DwKTk4GPjqeNnnCYNecXH6uVZ4G/A==", "dev": true, "license": "MIT", "dependencies": { @@ -1066,9 +1066,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.0.tgz", - "integrity": "sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", + "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -1568,9 +1568,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.0.tgz", - "integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==", + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d7f485e8..5d2b4999 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "bin": { "claude-code-acp": "./dist/index.js" }, - "version": "0.6.3", + "version": "0.6.4", "description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)", "main": "dist/index.js", "type": "module", @@ -52,19 +52,19 @@ "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "0.4.8", - "@anthropic-ai/claude-agent-sdk": "0.1.19", - "@anthropic-ai/claude-code": "2.0.19", - "@modelcontextprotocol/sdk": "1.20.0", + "@anthropic-ai/claude-agent-sdk": "0.1.21", + "@anthropic-ai/claude-code": "2.0.21", + "@modelcontextprotocol/sdk": "1.20.1", "diff": "8.0.2", "express": "5.1.0", "minimist": "1.2.8", "uuid": "13.0.0" }, "devDependencies": { - "@anthropic-ai/sdk": "^0.66.0", + "@anthropic-ai/sdk": "^0.67.0", "@types/express": "5.0.3", "@types/minimist": "1.2.5", - "@types/node": "24.8.0", + "@types/node": "24.8.1", "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "8.46.1", "@typescript-eslint/parser": "8.46.1", diff --git a/src/acp-agent.ts b/src/acp-agent.ts index c041c037..d3814a48 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -31,9 +31,8 @@ import { PermissionMode, Query, query, - SDKAssistantMessage, + SDKPartialAssistantMessage, SDKUserMessage, - SDKUserMessageReplay, } from "@anthropic-ai/claude-agent-sdk"; import * as fs from "node:fs"; import * as path from "node:path"; @@ -54,6 +53,8 @@ import { toolUpdateFromToolResult, ClaudePlanEntry, } from "./tools.js"; +import { ContentBlockParam } from "@anthropic-ai/sdk/resources"; +import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs"; type Session = { query: Query; @@ -74,7 +75,12 @@ type BackgroundTerminal = }; type ToolUseCache = { - [key: string]: { type: "tool_use"; id: string; name: string; input: any }; + [key: string]: { + type: "tool_use" | "server_tool_use" | "mcp_tool_use"; + id: string; + name: string; + input: any; + }; }; const DEFAULT_MODEL_ID = "default"; @@ -190,6 +196,7 @@ export class ClaudeAcpAgent implements Agent { const options: Options = { cwd: params.cwd, + includePartialMessages: true, mcpServers, systemPrompt, settingSources: ["user", "project", "local"], @@ -339,6 +346,17 @@ export class ClaudeAcpAgent implements Agent { return { stopReason: "refusal" }; } } + case "stream_event": { + for (const notification of streamEventToAcpNotifications( + message, + params.sessionId, + this.toolUseCache, + this.fileContentCache, + )) { + await this.client.sessionUpdate(notification); + } + break; + } case "user": case "assistant": { if (this.sessions[params.sessionId].cancelled) { @@ -379,7 +397,8 @@ export class ClaudeAcpAgent implements Agent { } for (const notification of toAcpNotifications( - message, + message.message.content, + message.message.role, params.sessionId, this.toolUseCache, this.fileContentCache, @@ -615,20 +634,18 @@ function promptToClaude(prompt: PromptRequest): SDKUserMessage { * Only handles text, image, and thinking chunks for now. */ export function toAcpNotifications( - message: SDKAssistantMessage | SDKUserMessage | SDKUserMessageReplay, + content: string | ContentBlockParam[] | BetaContentBlock[] | BetaRawContentBlockDelta[], + role: "assistant" | "user", sessionId: string, toolUseCache: ToolUseCache, fileContentCache: { [key: string]: string }, ): SessionNotification[] { - const content = message.message.content; - if (typeof content === "string") { return [ { sessionId, update: { - sessionUpdate: - message.type === "assistant" ? "agent_message_chunk" : "user_message_chunk", + sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk", content: { type: "text", text: content, @@ -644,9 +661,9 @@ export function toAcpNotifications( let update: SessionNotification["update"] | null = null; switch (chunk.type) { case "text": + case "text_delta": update = { - sessionUpdate: - message.type === "assistant" ? "agent_message_chunk" : "user_message_chunk", + sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk", content: { type: "text", text: chunk.text, @@ -655,8 +672,7 @@ export function toAcpNotifications( break; case "image": update = { - sessionUpdate: - message.type === "assistant" ? "agent_message_chunk" : "user_message_chunk", + sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk", content: { type: "image", data: chunk.source.type === "base64" ? chunk.source.data : "", @@ -666,6 +682,7 @@ export function toAcpNotifications( }; break; case "thinking": + case "thinking_delta": update = { sessionUpdate: "agent_thought_chunk", content: { @@ -675,6 +692,8 @@ export function toAcpNotifications( }; break; case "tool_use": + case "server_tool_use": + case "mcp_tool_use": { toolUseCache[chunk.id] = chunk; if (chunk.name === "TodoWrite") { update = { @@ -697,8 +716,15 @@ export function toAcpNotifications( }; } break; + } - case "tool_result": { + case "tool_result": + case "web_fetch_tool_result": + case "web_search_tool_result": + case "code_execution_tool_result": + case "bash_code_execution_tool_result": + case "text_editor_code_execution_tool_result": + case "mcp_tool_result": { const toolUse = toolUseCache[chunk.tool_use_id]; if (!toolUse) { console.error( @@ -711,15 +737,24 @@ export function toAcpNotifications( update = { toolCallId: chunk.tool_use_id, sessionUpdate: "tool_call_update", - status: chunk.is_error ? "failed" : "completed", + status: "is_error" in chunk && chunk.is_error ? "failed" : "completed", ...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]), }; } break; } + case "document": + case "search_result": + case "redacted_thinking": + case "input_json_delta": + case "citations_delta": + case "signature_delta": + case "container_upload": + break; + default: - throw new Error("unhandled chunk type: " + chunk.type); + unreachable(chunk); } if (update) { output.push({ sessionId, update }); @@ -729,6 +764,49 @@ export function toAcpNotifications( return output; } +export function streamEventToAcpNotifications( + message: SDKPartialAssistantMessage, + sessionId: string, + toolUseCache: ToolUseCache, + fileContentCache: { [key: string]: string }, +): SessionNotification[] { + const event = message.event; + switch (event.type) { + case "message_start": + return toAcpNotifications( + event.message.content, + event.message.role, + sessionId, + toolUseCache, + fileContentCache, + ); + case "content_block_start": + return toAcpNotifications( + [event.content_block], + "assistant", + sessionId, + toolUseCache, + fileContentCache, + ); + case "content_block_delta": + return toAcpNotifications( + [event.delta], + "assistant", + sessionId, + toolUseCache, + fileContentCache, + ); + case "message_delta": + case "message_stop": + case "content_block_stop": + // do we need stop reason? + return []; + + default: + unreachable(event); + } +} + export function runAcp() { const input = nodeToWebWritable(process.stdout); const output = nodeToWebReadable(process.stdin); diff --git a/src/tests/acp-agent.test.ts b/src/tests/acp-agent.test.ts index d0be9802..6ac995d9 100644 --- a/src/tests/acp-agent.test.ts +++ b/src/tests/acp-agent.test.ts @@ -649,7 +649,9 @@ describe("tool conversions", () => { session_id: "d056596f-e328-41e9-badd-b07122ae5227", uuid: "b7c3330c-de8f-4bba-ac53-68c7f76ffeb5", }; - expect(toAcpNotifications(received, "test", {}, {})).toStrictEqual([ + expect( + toAcpNotifications(received.message.content, received.message.role, "test", {}, {}), + ).toStrictEqual([ { sessionId: "test", update: { diff --git a/src/tools.ts b/src/tools.ts index 9f7b44d1..4d9d7445 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,6 +1,14 @@ import { PlanEntry, ToolCallContent, ToolCallLocation, ToolKind } from "@agentclientprotocol/sdk"; import { replaceAndCalculateLocation, SYSTEM_REMINDER, toolNames } from "./mcp-server.js"; -import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources"; +import { ToolResultBlockParam, WebSearchToolResultBlockParam } from "@anthropic-ai/sdk/resources"; +import { + BetaBashCodeExecutionToolResultBlockParam, + BetaCodeExecutionToolResultBlockParam, + BetaRequestMCPToolResultBlockParam, + BetaTextEditorCodeExecutionToolResultBlockParam, + BetaWebFetchToolResultBlockParam, + BetaWebSearchToolResultBlockParam, +} from "@anthropic-ai/sdk/resources/beta.mjs"; interface ToolInfo { title: string; @@ -385,7 +393,15 @@ export function toolInfoFromToolUse( } export function toolUpdateFromToolResult( - toolResult: ToolResultBlockParam, + toolResult: + | ToolResultBlockParam + | BetaWebSearchToolResultBlockParam + | BetaWebFetchToolResultBlockParam + | WebSearchToolResultBlockParam + | BetaCodeExecutionToolResultBlockParam + | BetaBashCodeExecutionToolResultBlockParam + | BetaTextEditorCodeExecutionToolResultBlockParam + | BetaRequestMCPToolResultBlockParam, toolUse: any | undefined, ): ToolUpdate { switch (toolUse?.name) { @@ -425,7 +441,12 @@ export function toolUpdateFromToolResult( case toolNames.edit: case toolNames.write: case "Write": { - if (toolResult.is_error && toolResult.content && toolResult.content.length > 0) { + if ( + "is_error" in toolResult && + toolResult.is_error && + toolResult.content && + toolResult.content.length > 0 + ) { // Only return errors return toAcpContentUpdate(toolResult.content, true); } @@ -451,7 +472,10 @@ export function toolUpdateFromToolResult( case "WebSearch": case "Other": default: { - return toAcpContentUpdate(toolResult.content, toolResult.is_error); + return toAcpContentUpdate( + toolResult.content, + "is_error" in toolResult ? toolResult.is_error : false, + ); } } } diff --git a/src/utils.ts b/src/utils.ts index 40ba5c82..cce45888 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -76,7 +76,13 @@ export function nodeToWebReadable(nodeStream: Readable): ReadableStream {