mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aea8fd0e1 | ||
|
|
81a541c644 | ||
|
|
b49154bef8 | ||
|
|
5fa453a33a | ||
|
|
c2ab2da5ca | ||
|
|
373d940eb8 | ||
|
|
2c16a41fa4 | ||
|
|
656cbe5921 | ||
|
|
2f811f8a85 | ||
|
|
6aa6537f67 | ||
|
|
1b72ac7c8a | ||
|
|
6ae2c05ddb | ||
|
|
8f0fc59a48 | ||
|
|
c25150523a | ||
|
|
ea796f3cbd | ||
|
|
7f5ebca3fb | ||
|
|
75b44e945e | ||
|
|
ad8611c399 | ||
|
|
c5abff0e43 | ||
|
|
5c98bd0404 | ||
|
|
71b660ae3b | ||
|
|
4ee25af3f9 | ||
|
|
1a5a9c16c6 |
57
CHANGELOG.md
57
CHANGELOG.md
@@ -1,5 +1,62 @@
|
||||
# Changelog
|
||||
|
||||
## 0.6.7
|
||||
|
||||
- Fix for invalid plan input from the model introduced in latest agent-sdk
|
||||
|
||||
## 0.6.6
|
||||
|
||||
- Do not enable bypassPermissions mode if in root/sudo mode, because Claude Code will not start
|
||||
|
||||
## 0.6.5
|
||||
|
||||
- Fix for duplicated text content after streaming
|
||||
|
||||
## 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.
|
||||
|
||||
## 0.6.2
|
||||
|
||||
- Fix bug where mode selection would sometimes fire before initialization was complete.
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.19
|
||||
|
||||
## 0.6.1
|
||||
|
||||
- Fix to allow bypassPermissions mode to be selected (it wasn't permitted previously)
|
||||
|
||||
## 0.6.0
|
||||
|
||||
- Provide a model selector. We use the "default" model by default, and the user can change it via the client.
|
||||
- Make sure writes require permissions when necessary: https://github.com/zed-industries/claude-code-acp/pull/92
|
||||
- Add support for appending or overriding the system prompt: https://github.com/zed-industries/claude-code-acp/pull/91
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.15
|
||||
- Update to @agentclientprotocol/sdk@0.4.8
|
||||
|
||||
## 0.5.5
|
||||
|
||||
- Migrate to @agentclientprotocol/sdk@0.4.5
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.13
|
||||
|
||||
## 0.5.4
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.11
|
||||
- Enable setting CLAUDE_CODE_EXECUTABLE to override the executable used by the SDK https://github.com/zed-industries/claude-code-acp/pull/86
|
||||
|
||||
## 0.5.3
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.8
|
||||
- Update to @zed-industries/agent-client-protocol@v0.4.5
|
||||
|
||||
## 0.5.2
|
||||
|
||||
- Add back @anthropic-ai/claude-code@2.0.1 as runtime dependency
|
||||
|
||||
## 0.5.1
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.1
|
||||
|
||||
743
package-lock.json
generated
743
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -6,7 +6,7 @@
|
||||
"bin": {
|
||||
"claude-code-acp": "./dist/index.js"
|
||||
},
|
||||
"version": "0.5.1",
|
||||
"version": "0.6.7",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
@@ -51,29 +51,30 @@
|
||||
"author": "Zed Industries",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.1",
|
||||
"@modelcontextprotocol/sdk": "1.18.2",
|
||||
"@zed-industries/agent-client-protocol": "0.4.3",
|
||||
"@agentclientprotocol/sdk": "0.4.8",
|
||||
"@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.65.0",
|
||||
"@anthropic-ai/sdk": "^0.67.0",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "24.6.0",
|
||||
"@types/node": "24.8.1",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||
"@typescript-eslint/parser": "8.45.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.46.1",
|
||||
"@typescript-eslint/parser": "8.46.1",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"eslint": "9.36.0",
|
||||
"eslint": "9.37.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "16.4.0",
|
||||
"prettier": "3.6.2",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
269
src/acp-agent.ts
269
src/acp-agent.ts
@@ -15,29 +15,31 @@ import {
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestError,
|
||||
SessionModelState,
|
||||
SetSessionModelRequest,
|
||||
SetSessionModelResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
TerminalHandle,
|
||||
TerminalOutputResponse,
|
||||
WriteTextFileRequest,
|
||||
WriteTextFileResponse,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import {
|
||||
McpServerConfig,
|
||||
Options,
|
||||
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";
|
||||
import * as os from "node:os";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
||||
import { SessionNotification } from "@zed-industries/agent-client-protocol";
|
||||
import { SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import {
|
||||
createMcpServer,
|
||||
createPermissionMcpServer,
|
||||
@@ -51,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;
|
||||
@@ -71,9 +75,19 @@ 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";
|
||||
|
||||
// Bypass Permissions doesn't work if we are a root/sudo user
|
||||
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
|
||||
|
||||
// Implement the ACP Agent interface
|
||||
export class ClaudeAcpAgent implements Agent {
|
||||
sessions: {
|
||||
@@ -169,16 +183,39 @@ export class ClaudeAcpAgent implements Agent {
|
||||
},
|
||||
};
|
||||
|
||||
let systemPrompt: Options["systemPrompt"] = { type: "preset", preset: "claude_code" };
|
||||
if (params._meta?.systemPrompt) {
|
||||
const customPrompt = params._meta.systemPrompt;
|
||||
if (typeof customPrompt === "string") {
|
||||
systemPrompt = customPrompt;
|
||||
} else if (
|
||||
typeof customPrompt === "object" &&
|
||||
"append" in customPrompt &&
|
||||
typeof customPrompt.append === "string"
|
||||
) {
|
||||
systemPrompt.append = customPrompt.append;
|
||||
}
|
||||
}
|
||||
|
||||
const permissionMode = "default";
|
||||
|
||||
const options: Options = {
|
||||
cwd: params.cwd,
|
||||
includePartialMessages: true,
|
||||
mcpServers,
|
||||
systemPrompt: { type: "preset", preset: "claude_code" },
|
||||
systemPrompt,
|
||||
settingSources: ["user", "project", "local"],
|
||||
// If we want bypassPermissions to be an option, we have to start the session with it on.
|
||||
// We change it immediately back to default below before returning the session.
|
||||
permissionMode: IS_ROOT ? permissionMode : "bypassPermissions",
|
||||
permissionPromptToolName: PERMISSION_TOOL_NAME,
|
||||
stderr: (err) => console.error(err),
|
||||
// note: although not documented by the types, passing an absolute path
|
||||
// here works to find zed's managed node version.
|
||||
executable: process.execPath as any,
|
||||
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
||||
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
||||
}),
|
||||
};
|
||||
|
||||
const allowedTools = [];
|
||||
@@ -188,7 +225,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
disallowedTools.push("Read");
|
||||
}
|
||||
if (this.clientCapabilities?.fs?.writeTextFile) {
|
||||
allowedTools.push(toolNames.write);
|
||||
disallowedTools.push("Write", "Edit");
|
||||
}
|
||||
if (this.clientCapabilities?.terminal) {
|
||||
@@ -207,14 +243,23 @@ export class ClaudeAcpAgent implements Agent {
|
||||
prompt: input,
|
||||
options,
|
||||
});
|
||||
|
||||
this.sessions[sessionId] = {
|
||||
query: q,
|
||||
input: input,
|
||||
cancelled: false,
|
||||
permissionMode: "default",
|
||||
permissionMode,
|
||||
};
|
||||
|
||||
getAvailableSlashCommands(q).then((availableCommands) => {
|
||||
const availableCommands = await getAvailableSlashCommands(q);
|
||||
const models = await getAvailableModels(q);
|
||||
// Change it back to default
|
||||
if (!IS_ROOT) {
|
||||
await q.setPermissionMode(permissionMode);
|
||||
}
|
||||
|
||||
// Needs to happen after we return the session
|
||||
setTimeout(() => {
|
||||
this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
@@ -222,34 +267,40 @@ export class ClaudeAcpAgent implements Agent {
|
||||
availableCommands,
|
||||
},
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
|
||||
const availableModes = [
|
||||
{
|
||||
id: "default",
|
||||
name: "Always Ask",
|
||||
description: "Prompts for permission on first use of each tool",
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Automatically accepts file edit permissions for the session",
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan Mode",
|
||||
description: "Claude can analyze but not modify files or execute commands",
|
||||
},
|
||||
];
|
||||
// Only works in non-root mode
|
||||
if (!IS_ROOT) {
|
||||
availableModes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass Permissions",
|
||||
description: "Skips all permission prompts",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
models,
|
||||
modes: {
|
||||
currentModeId: "default",
|
||||
availableModes: [
|
||||
{
|
||||
id: "default",
|
||||
name: "Always Ask",
|
||||
description: "Prompts for permission on first use of each tool",
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Automatically accepts file edit permissions for the session",
|
||||
},
|
||||
{
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass Permissions",
|
||||
description: "Skips all permission prompts",
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan Mode",
|
||||
description: "Claude can analyze but not modify files or execute commands",
|
||||
},
|
||||
],
|
||||
currentModeId: permissionMode,
|
||||
availableModes,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -308,6 +359,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) {
|
||||
@@ -347,8 +409,15 @@ export class ClaudeAcpAgent implements Agent {
|
||||
throw RequestError.authRequired();
|
||||
}
|
||||
|
||||
const content =
|
||||
message.type === "assistant"
|
||||
? // Handled by stream events above
|
||||
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
||||
: message.message.content;
|
||||
|
||||
for (const notification of toAcpNotifications(
|
||||
message,
|
||||
content,
|
||||
message.message.role,
|
||||
params.sessionId,
|
||||
this.toolUseCache,
|
||||
this.fileContentCache,
|
||||
@@ -372,6 +441,13 @@ export class ClaudeAcpAgent implements Agent {
|
||||
await this.sessions[params.sessionId].query.interrupt();
|
||||
}
|
||||
|
||||
async setSessionModel(params: SetSessionModelRequest): Promise<SetSessionModelResponse | void> {
|
||||
if (!this.sessions[params.sessionId]) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
await this.sessions[params.sessionId].query.setModel(params.modelId);
|
||||
}
|
||||
|
||||
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse> {
|
||||
if (!this.sessions[params.sessionId]) {
|
||||
throw new Error("Session not found");
|
||||
@@ -383,10 +459,17 @@ export class ClaudeAcpAgent implements Agent {
|
||||
case "bypassPermissions":
|
||||
case "plan":
|
||||
this.sessions[params.sessionId].permissionMode = params.modeId;
|
||||
await this.sessions[params.sessionId].query.setPermissionMode(params.modeId);
|
||||
try {
|
||||
await this.sessions[params.sessionId].query.setPermissionMode(params.modeId);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error && error.message ? error.message : "Invalid Mode";
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return {};
|
||||
default:
|
||||
throw new Error("Invalid mode");
|
||||
throw new Error("Invalid Mode");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +488,26 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
}
|
||||
|
||||
async function getAvailableModels(query: Query): Promise<SessionModelState> {
|
||||
const models = await query.supportedModels();
|
||||
|
||||
// Query doesn't give us access to the currently selected model, so we default to "default" if it is there,
|
||||
// otherwise we just choose the first model in the list.
|
||||
const currentModel = models.find((model) => model.value === DEFAULT_MODEL_ID) || models[0];
|
||||
await query.setModel(currentModel.value);
|
||||
|
||||
const availableModels = models.map((model) => ({
|
||||
modelId: model.value,
|
||||
name: model.displayName,
|
||||
description: model.description,
|
||||
}));
|
||||
|
||||
return {
|
||||
availableModels,
|
||||
currentModelId: currentModel.value,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand[]> {
|
||||
const UNSUPPORTED_COMMANDS = [
|
||||
"add-dir",
|
||||
@@ -550,20 +653,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,
|
||||
@@ -579,9 +680,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,
|
||||
@@ -590,8 +691,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 : "",
|
||||
@@ -601,6 +701,7 @@ export function toAcpNotifications(
|
||||
};
|
||||
break;
|
||||
case "thinking":
|
||||
case "thinking_delta":
|
||||
update = {
|
||||
sessionUpdate: "agent_thought_chunk",
|
||||
content: {
|
||||
@@ -610,12 +711,17 @@ export function toAcpNotifications(
|
||||
};
|
||||
break;
|
||||
case "tool_use":
|
||||
case "server_tool_use":
|
||||
case "mcp_tool_use": {
|
||||
toolUseCache[chunk.id] = chunk;
|
||||
if (chunk.name === "TodoWrite") {
|
||||
update = {
|
||||
sessionUpdate: "plan",
|
||||
entries: planEntries(chunk.input as { todos: ClaudePlanEntry[] }),
|
||||
};
|
||||
// @ts-expect-error - sometimes input is empty object
|
||||
if (Array.isArray(chunk.input.todos)) {
|
||||
update = {
|
||||
sessionUpdate: "plan",
|
||||
entries: planEntries(chunk.input as { todos: ClaudePlanEntry[] }),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
let rawInput;
|
||||
try {
|
||||
@@ -632,8 +738,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(
|
||||
@@ -646,15 +759,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 });
|
||||
@@ -664,6 +786,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);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
||||
import { z } from "zod";
|
||||
import { Server } from "node:http";
|
||||
import { ClaudeAcpAgent } from "./acp-agent.js";
|
||||
import { ClientCapabilities, TerminalOutputResponse } from "@zed-industries/agent-client-protocol";
|
||||
import { ClientCapabilities, TerminalOutputResponse } from "@agentclientprotocol/sdk";
|
||||
import * as diff from "diff";
|
||||
|
||||
import { sleep, unreachable, extractLinesWithByteLimit } from "./utils.js";
|
||||
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
SessionNotification,
|
||||
WriteTextFileRequest,
|
||||
WriteTextFileResponse,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
|
||||
import { markdownEscape, toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js";
|
||||
import { toAcpNotifications } from "../acp-agent.js";
|
||||
import { SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration", () => {
|
||||
let child: ReturnType<typeof spawn>;
|
||||
@@ -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: {
|
||||
@@ -780,3 +782,16 @@ describe("escape markdown", () => {
|
||||
expect(escaped).toEqual("````\nfor example:\n```markdown\nHello *world*!\n```\n````");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SDK behavior", () => {
|
||||
it.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
"query has a 'default' model",
|
||||
async () => {
|
||||
const q = query({ prompt: "hi" });
|
||||
const models = await q.supportedModels();
|
||||
const defaultModel = models.find((m) => m.value === "default");
|
||||
expect(defaultModel).toBeDefined();
|
||||
},
|
||||
10000,
|
||||
);
|
||||
});
|
||||
|
||||
41
src/tools.ts
41
src/tools.ts
@@ -1,11 +1,14 @@
|
||||
import {
|
||||
PlanEntry,
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
ToolKind,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
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;
|
||||
@@ -341,7 +344,7 @@ export function toolInfoFromToolUse(
|
||||
|
||||
case "TodoWrite":
|
||||
return {
|
||||
title: input?.todos
|
||||
title: Array.isArray(input?.todos)
|
||||
? `Update TODOs: ${input.todos.map((todo: any) => todo.content).join(", ")}`
|
||||
: "Update TODOs",
|
||||
kind: "think",
|
||||
@@ -390,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) {
|
||||
@@ -430,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);
|
||||
}
|
||||
@@ -456,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,13 @@ export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Arr
|
||||
}
|
||||
|
||||
export function unreachable(value: never): never {
|
||||
throw new Error(`Unexpected case: ${value}`);
|
||||
let valueAsString;
|
||||
try {
|
||||
valueAsString = JSON.stringify(value);
|
||||
} catch {
|
||||
valueAsString = value;
|
||||
}
|
||||
throw new Error(`Unexpected case: ${valueAsString}`);
|
||||
}
|
||||
|
||||
export function sleep(time: number): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user