From aadeefed1402b5e773d93a65f730c615496b7b7e Mon Sep 17 00:00:00 2001 From: Eduard Voiculescu Date: Wed, 19 Nov 2025 11:33:16 -0500 Subject: [PATCH 1/2] adding load session --- src/acp-agent.ts | 123 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 17c40ca..db153e0 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -7,6 +7,8 @@ import { ClientCapabilities, InitializeRequest, InitializeResponse, + LoadSessionRequest, + LoadSessionResponse, ndJsonStream, NewSessionRequest, NewSessionResponse, @@ -16,6 +18,7 @@ import { ReadTextFileResponse, RequestError, SessionModelState, + SessionNotification, SetSessionModelRequest, SetSessionModelResponse, SetSessionModeRequest, @@ -23,7 +26,7 @@ import { TerminalHandle, TerminalOutputResponse, WriteTextFileRequest, - WriteTextFileResponse, + WriteTextFileResponse } from "@agentclientprotocol/sdk"; import { CanUseTool, @@ -35,24 +38,23 @@ import { SDKPartialAssistantMessage, SDKUserMessage, } from "@anthropic-ai/claude-agent-sdk"; +import { ContentBlockParam } from "@anthropic-ai/sdk/resources"; +import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs"; import * as fs from "node:fs"; -import * as path from "node:path"; import * as os from "node:os"; +import * as path from "node:path"; import { v7 as uuidv7 } from "uuid"; -import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js"; -import { SessionNotification } from "@agentclientprotocol/sdk"; +import packageJson from "../package.json" with { type: "json" }; import { createMcpServer, EDIT_TOOL_NAMES, toolNames } from "./mcp-server.js"; import { - toolInfoFromToolUse, - planEntries, - toolUpdateFromToolResult, ClaudePlanEntry, - registerHookCallback, + planEntries, postToolUseHook, + registerHookCallback, + toolInfoFromToolUse, + toolUpdateFromToolResult, } from "./tools.js"; -import { ContentBlockParam } from "@anthropic-ai/sdk/resources"; -import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs"; -import packageJson from "../package.json" with { type: "json" }; +import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js"; type Session = { query: Query; @@ -146,6 +148,7 @@ export class ClaudeAcpAgent implements Agent { http: true, sse: true, }, + loadSession: true, }, agentInfo: { name: packageJson.name, @@ -573,6 +576,104 @@ export class ClaudeAcpAgent implements Agent { return response; } + async loadSession(params: LoadSessionRequest): Promise { + const { sessionId } = params; + + if (!this.sessions[sessionId]) { + const input = new Pushable(); + + const mcpServers: Record = {}; + if (Array.isArray(params.mcpServers)) { + for (const server of params.mcpServers) { + if ("type" in server) { + mcpServers[server.name] = { + type: server.type, + url: server.url, + headers: server.headers + ? Object.fromEntries(server.headers.map((e) => [e.name, e.value])) + : undefined, + }; + } else { + mcpServers[server.name] = { + type: "stdio", + command: server.command, + args: server.args, + env: server.env + ? Object.fromEntries(server.env.map((e) => [e.name, e.value])) + : undefined, + }; + } + } + } + + const server = createMcpServer(this, sessionId, this.clientCapabilities); + mcpServers["acp"] = { + type: "sdk", + name: "acp", + instance: server, + }; + + const permissionMode = "default"; + + // As stated here: https://agentclientprotocol.com/protocol/session-setup#loading-a-session + // Clients MUST call the session/load method with: 'The Session ID to resume', 'MCP servers to connect to' and 'The working directory' + const options: Options = { + cwd: params.cwd, + includePartialMessages: true, + mcpServers, + systemPrompt: { type: "preset", preset: "claude_code" }, + settingSources: ["user", "project", "local"], + allowDangerouslySkipPermissions: !IS_ROOT, + permissionMode, + canUseTool: this.canUseTool(sessionId), + stderr: (err) => console.error(err), + executable: process.execPath as any, + ...(process.env.CLAUDE_CODE_EXECUTABLE && { + pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE, + }), + hooks: { + PostToolUse: [ + { + hooks: [postToolUseHook], + }, + ], + }, + }; + + const q = query({ + prompt: input, + options, + }); + + this.sessions[sessionId] = { + query: q, + input: input, + cancelled: false, + permissionMode, + }; + + const availableCommands = await getAvailableSlashCommands(q); + // const models = await getAvailableModels(q); // Not needed for loadSession response? + + setTimeout(() => { + this.client.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "available_commands_update", + availableCommands, + }, + }); + }, 0); + } + + // TODO: Replay conversation history + // The Agent MUST replay the entire conversation to the Client in the form of session/update notifications. + // Since we don't have persistence implemented yet, we can't replay history for a fresh session. + // If the session was already in memory, we might be able to replay if we stored history. + + return {}; + } + canUseTool(sessionId: string): CanUseTool { return async (toolName, toolInput, { suggestions, toolUseID }) => { const session = this.sessions[sessionId]; From 4c1c955abef7f94e7e7098f06ef9bc002da493f9 Mon Sep 17 00:00:00 2001 From: Eduard Voiculescu Date: Wed, 19 Nov 2025 13:10:34 -0500 Subject: [PATCH 2/2] adding in-memory implementation --- README.md | 2 +- src/acp-agent.ts | 34 +++++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e624e96..4bd28c2 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ npm install @zed-industries/claude-code-acp You can then use `claude-code-acp` as a regular ACP agent: -``` +```bash ANTHROPIC_API_KEY=sk-... claude-code-acp ``` diff --git a/src/acp-agent.ts b/src/acp-agent.ts index db153e0..8693723 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -61,6 +61,7 @@ type Session = { input: Pushable; cancelled: boolean; permissionMode: PermissionMode; + conversationHistory: SessionNotification[]; }; type BackgroundTerminal = @@ -310,6 +311,7 @@ export class ClaudeAcpAgent implements Agent { input: input, cancelled: false, permissionMode, + conversationHistory: [], }; const availableCommands = await getAvailableSlashCommands(q); @@ -445,6 +447,10 @@ export class ClaudeAcpAgent implements Agent { this.client, )) { await this.client.sessionUpdate(notification); + // Store in conversation history for potential session replay + if (this.sessions[params.sessionId]?.conversationHistory) { + this.sessions[params.sessionId].conversationHistory.push(notification); + } } break; } @@ -508,6 +514,10 @@ export class ClaudeAcpAgent implements Agent { this.client, )) { await this.client.sessionUpdate(notification); + // Store in conversation history for potential session replay + if (this.sessions[params.sessionId]?.conversationHistory) { + this.sessions[params.sessionId].conversationHistory.push(notification); + } } break; } @@ -578,6 +588,7 @@ export class ClaudeAcpAgent implements Agent { async loadSession(params: LoadSessionRequest): Promise { const { sessionId } = params; + console.log("doudou loadSession", sessionId, this.sessions); if (!this.sessions[sessionId]) { const input = new Pushable(); @@ -645,16 +656,18 @@ export class ClaudeAcpAgent implements Agent { options, }); + const availableCommands = await getAvailableSlashCommands(q); + // const models = await getAvailableModels(q); // Not needed for loadSession response? + + // Store the session in memory this.sessions[sessionId] = { query: q, input: input, cancelled: false, permissionMode, + conversationHistory: [], // Empty for now, would load from disk in full implementation }; - const availableCommands = await getAvailableSlashCommands(q); - // const models = await getAvailableModels(q); // Not needed for loadSession response? - setTimeout(() => { this.client.sessionUpdate({ sessionId, @@ -666,10 +679,17 @@ export class ClaudeAcpAgent implements Agent { }, 0); } - // TODO: Replay conversation history - // The Agent MUST replay the entire conversation to the Client in the form of session/update notifications. - // Since we don't have persistence implemented yet, we can't replay history for a fresh session. - // If the session was already in memory, we might be able to replay if we stored history. + // Replay conversation history if session already exists in memory + // Per ACP spec: "The Agent MUST replay the entire conversation to the Client + // in the form of session/update notifications." + const session = this.sessions[sessionId]; + if (session && session.conversationHistory.length > 0) { + for (const notification of session.conversationHistory) { + await this.client.sessionUpdate(notification); + } + } + // Note: For sessions not in memory, you would need to implement persistence + // (e.g., database, file system) to load and replay the conversation history. return {}; }