From 03f0cbbf8469fbc1652ea0b11eae64e4698169a2 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Wed, 5 Nov 2025 18:51:24 +0100 Subject: [PATCH 1/8] Proof of concept: session/resume and session/fork This is a proof of concept for two new methods: "session/resume" and "session/fork". The idea is to be able to continue an existing chat using Claude's resume and forkSession functionalities, without the need to stream all the existing conversation history (which prevents us from having session/load with the Claude Agent SDK). For now, I implemented this as extension methods, but I could see it fit into the core protocol as optional agent capabilities. The methods accept the same parameters as session/new and also return the same response format. The only extra parameter that is required is the "claudeSessionId", but it could also be called "resumeSessionToken". It is required because Claude internally has its own session ID that is independent from the ACP ID (it is only created when sending the first message). Currently, there's a special extension notification called "claude/sessionId", that send a mapping `{ sessionId, claudeSessionId }`. Again, for a proper implementation, this could be called `resumeSessionToken` instead. --- src/acp-agent.ts | 502 +++++++++++++++++++++++++++-------------------- 1 file changed, 288 insertions(+), 214 deletions(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index ef326936..83dd4306 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -137,6 +137,7 @@ export class ClaudeAcpAgent implements Agent { backgroundTerminals: { [key: string]: BackgroundTerminal } = {}; clientCapabilities?: ClientCapabilities; logger: Logger; + claudeSessionIds: { [acpSessionId: string]: string } = {}; constructor(client: AgentSideConnection, logger?: Logger) { this.sessions = {}; @@ -180,6 +181,10 @@ export class ClaudeAcpAgent implements Agent { http: true, sse: true, }, + _meta: { + "claude/session/resume": true, + "claude/session/fork": true, + }, }, agentInfo: { name: packageJson.name, @@ -197,220 +202,7 @@ export class ClaudeAcpAgent implements Agent { throw RequestError.authRequired(); } - const sessionId = randomUUID(); - 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, - }; - } - } - } - - // Only add the acp MCP server if built-in tools are not disabled - if (!params._meta?.disableBuiltInTools) { - const server = createMcpServer(this, sessionId, this.clientCapabilities); - mcpServers["acp"] = { - type: "sdk", - name: "acp", - instance: server, - }; - } - - 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"; - - // Extract options from _meta if provided - const userProvidedOptions = (params._meta as NewSessionMeta | undefined)?.claudeCode?.options; - - const options: Options = { - systemPrompt, - settingSources: ["user", "project", "local"], - stderr: (err) => this.logger.error(err), - ...userProvidedOptions, - // Override certain fields that must be controlled by ACP - cwd: params.cwd, - includePartialMessages: true, - mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers }, - // Set our own session id - extraArgs: { ...userProvidedOptions?.extraArgs, "session-id": sessionId }, - // If we want bypassPermissions to be an option, we have to allow it here. - // But it doesn't work in root mode, so we only activate it if it will work. - allowDangerouslySkipPermissions: !IS_ROOT, - permissionMode, - canUseTool: this.canUseTool(sessionId), - // 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, - }), - hooks: { - ...userProvidedOptions?.hooks, - PostToolUse: [ - ...(userProvidedOptions?.hooks?.PostToolUse || []), - { - hooks: [createPostToolUseHook(this.logger)], - }, - ], - }, - }; - - const allowedTools = []; - const disallowedTools = []; - - // Check if built-in tools should be disabled - const disableBuiltInTools = params._meta?.disableBuiltInTools === true; - - if (!disableBuiltInTools) { - if (this.clientCapabilities?.fs?.readTextFile) { - allowedTools.push(toolNames.read); - disallowedTools.push("Read"); - } - if (this.clientCapabilities?.fs?.writeTextFile) { - disallowedTools.push("Write", "Edit"); - } - if (this.clientCapabilities?.terminal) { - allowedTools.push(toolNames.bashOutput, toolNames.killShell); - disallowedTools.push("Bash", "BashOutput", "KillShell"); - } - } else { - // When built-in tools are disabled, explicitly disallow all of them - disallowedTools.push( - toolNames.read, - toolNames.write, - toolNames.edit, - toolNames.bash, - toolNames.bashOutput, - toolNames.killShell, - "Read", - "Write", - "Edit", - "Bash", - "BashOutput", - "KillShell", - "Glob", - "Grep", - "Task", - "TodoWrite", - "ExitPlanMode", - "WebSearch", - "WebFetch", - "AskUserQuestion", - "SlashCommand", - "Skill", - "NotebookEdit", - ); - } - - if (allowedTools.length > 0) { - options.allowedTools = allowedTools; - } - if (disallowedTools.length > 0) { - options.disallowedTools = disallowedTools; - } - - // Handle abort controller from meta options - const abortController = userProvidedOptions?.abortController; - if (abortController?.signal.aborted) { - throw new Error("Cancelled"); - } - - 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); - - // Needs to happen after we return the session - setTimeout(() => { - this.client.sessionUpdate({ - sessionId, - update: { - sessionUpdate: "available_commands_update", - availableCommands, - }, - }); - }, 0); - - const availableModes = [ - { - id: "default", - name: "Default", - description: "Standard behavior, prompts for dangerous operations", - }, - { - id: "acceptEdits", - name: "Accept Edits", - description: "Auto-accept file edit operations", - }, - { - id: "plan", - name: "Plan Mode", - description: "Planning mode, no actual tool execution", - }, - { - id: "dontAsk", - name: "Don't Ask", - description: "Don't prompt for permissions, deny if not pre-approved", - }, - ]; - // Only works in non-root mode - if (!IS_ROOT) { - availableModes.push({ - id: "bypassPermissions", - name: "Bypass Permissions", - description: "Bypass all permission checks", - }); - } - - return { - sessionId, - models, - modes: { - currentModeId: permissionMode, - availableModes, - }, - }; + return await this.createSession(params, {}); } async authenticate(_params: AuthenticateRequest): Promise { @@ -435,6 +227,12 @@ export class ClaudeAcpAgent implements Agent { } break; } + + // Track Claude session ID from all messages + if (message.session_id) { + await this.trackAndNotifyClaudeSessionId(params.sessionId, message.session_id); + } + switch (message.type) { case "system": switch (message.subtype) { @@ -762,6 +560,282 @@ export class ClaudeAcpAgent implements Agent { } }; } + + async extMethod(method: string, params: Record): Promise> { + switch (method) { + case "claude/session/resume": { + const claudeSessionId = params.claudeSessionId as string; + if (!claudeSessionId) { + throw RequestError.invalidParams( + { claudeSessionId: params.claudeSessionId }, + "Missing or invalid claudeSessionId parameter", + ); + } + + return (await this.createSession(params as unknown as NewSessionRequest, { + resume: claudeSessionId, + })) as unknown as Record; + } + + case "claude/session/fork": { + const acpSessionId = params.sessionId as string; + if (!acpSessionId) { + throw RequestError.invalidParams( + { sessionId: params.sessionId }, + "Missing or invalid sessionId parameter", + ); + } + + // Look up the Claude session ID from the ACP session ID + const claudeSessionId = this.claudeSessionIds[acpSessionId]; + if (!claudeSessionId) { + throw RequestError.resourceNotFound( + `Session ${acpSessionId} not found or has no Claude session ID yet`, + ); + } + + return (await this.createSession({} as NewSessionRequest, { + resume: claudeSessionId, + forkSession: true, + })) as unknown as Record; + } + + default: + throw RequestError.methodNotFound(method); + } + } + + private async trackAndNotifyClaudeSessionId(acpSessionId: string, claudeSessionId: string): Promise { + // Only update and notify if the session ID has changed + if (this.claudeSessionIds[acpSessionId] !== claudeSessionId) { + this.claudeSessionIds[acpSessionId] = claudeSessionId; + await this.client.extNotification("claude/sessionId", { + sessionId: acpSessionId, + claudeSessionId: claudeSessionId, + }); + } + } + + private async createSession( + params: NewSessionRequest, + creationOpts: { resume?: string; forkSession?: boolean } = {}, + ): Promise { + const sessionId = randomUUID(); + 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, + }; + } + } + } + + // Only add the acp MCP server if built-in tools are not disabled + if (!params._meta?.disableBuiltInTools) { + const server = createMcpServer(this, sessionId, this.clientCapabilities); + mcpServers["acp"] = { + type: "sdk", + name: "acp", + instance: server, + }; + } + + 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"; + + // Extract options from _meta if provided + const userProvidedOptions = (params._meta as NewSessionMeta | undefined)?.claudeCode?.options; + + const options: Options = { + systemPrompt, + settingSources: ["user", "project", "local"], + stderr: (err) => this.logger.error(err), + ...userProvidedOptions, + // Override certain fields that must be controlled by ACP + cwd: params.cwd, + includePartialMessages: true, + mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers }, + // Set our own session id + extraArgs: { ...userProvidedOptions?.extraArgs, "session-id": sessionId }, + // If we want bypassPermissions to be an option, we have to allow it here. + // But it doesn't work in root mode, so we only activate it if it will work. + allowDangerouslySkipPermissions: !IS_ROOT, + permissionMode, + canUseTool: this.canUseTool(sessionId), + // 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, + }), + hooks: { + ...userProvidedOptions?.hooks, + PostToolUse: [ + ...(userProvidedOptions?.hooks?.PostToolUse || []), + { + hooks: [createPostToolUseHook(this.logger)], + }, + ], + }, + ...creationOpts, + }; + + const allowedTools = []; + const disallowedTools = []; + + // Check if built-in tools should be disabled + const disableBuiltInTools = params._meta?.disableBuiltInTools === true; + + if (!disableBuiltInTools) { + if (this.clientCapabilities?.fs?.readTextFile) { + allowedTools.push(toolNames.read); + disallowedTools.push("Read"); + } + if (this.clientCapabilities?.fs?.writeTextFile) { + disallowedTools.push("Write", "Edit"); + } + if (this.clientCapabilities?.terminal) { + allowedTools.push(toolNames.bashOutput, toolNames.killShell); + disallowedTools.push("Bash", "BashOutput", "KillShell"); + } + } else { + // When built-in tools are disabled, explicitly disallow all of them + disallowedTools.push( + toolNames.read, + toolNames.write, + toolNames.edit, + toolNames.bash, + toolNames.bashOutput, + toolNames.killShell, + "Read", + "Write", + "Edit", + "Bash", + "BashOutput", + "KillShell", + "Glob", + "Grep", + "Task", + "TodoWrite", + "ExitPlanMode", + "WebSearch", + "WebFetch", + "AskUserQuestion", + "SlashCommand", + "Skill", + "NotebookEdit", + ); + } + + if (allowedTools.length > 0) { + options.allowedTools = allowedTools; + } + if (disallowedTools.length > 0) { + options.disallowedTools = disallowedTools; + } + + // Handle abort controller from meta options + const abortController = userProvidedOptions?.abortController; + if (abortController?.signal.aborted) { + throw new Error("Cancelled"); + } + + 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); + + // Needs to happen after we return the session + setTimeout(() => { + this.client.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "available_commands_update", + availableCommands, + }, + }); + }, 0); + + const availableModes = [ + { + id: "default", + name: "Default", + description: "Standard behavior, prompts for dangerous operations", + }, + { + id: "acceptEdits", + name: "Accept Edits", + description: "Auto-accept file edit operations", + }, + { + id: "plan", + name: "Plan Mode", + description: "Planning mode, no actual tool execution", + }, + { + id: "dontAsk", + name: "Don't Ask", + description: "Don't prompt for permissions, deny if not pre-approved", + }, + ]; + // Only works in non-root mode + if (!IS_ROOT) { + availableModes.push({ + id: "bypassPermissions", + name: "Bypass Permissions", + description: "Bypass all permission checks", + }); + } + + return { + sessionId, + models, + modes: { + currentModeId: permissionMode, + availableModes, + }, + }; + } } async function getAvailableModels(query: Query): Promise { From da549f0d3fa17d85433f249478f46ee4cad79f20 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Fri, 28 Nov 2025 12:57:19 +0100 Subject: [PATCH 2/8] fork using session/fork --- package-lock.json | 5 ++- package.json | 2 +- src/acp-agent.ts | 87 +++++++++++++++-------------------------------- 3 files changed, 30 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b6d1875..018ba0c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.12.1", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "0.8.0", + "@agentclientprotocol/sdk": "github:SteffenDE/acp-typescript-sdk#sd-session-fork-dist", "@anthropic-ai/claude-agent-sdk": "0.1.61", "@modelcontextprotocol/sdk": "1.24.3", "diff": "8.0.2" @@ -33,8 +33,7 @@ }, "node_modules/@agentclientprotocol/sdk": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.8.0.tgz", - "integrity": "sha512-RnYsrYqrGFNpPc/cgHgMPHjgn52F1tw5PWyr7UucQ2V1O72JWy3fCCkUgbbRgn7wbNVFsrEZMuBS186Mxgkhfg==", + "resolved": "git+ssh://git@github.com/SteffenDE/acp-typescript-sdk.git#cdc48d85bc4b359dbd2c3c69af40dfab5a603877", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" diff --git a/package.json b/package.json index ecea29a0..3b27f074 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "author": "Zed Industries", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "0.8.0", + "@agentclientprotocol/sdk": "github:SteffenDE/acp-typescript-sdk#sd-session-fork-dist", "@anthropic-ai/claude-agent-sdk": "0.1.61", "@modelcontextprotocol/sdk": "1.24.3", "diff": "8.0.2" diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 83dd4306..168ccaef 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -5,6 +5,8 @@ import { AvailableCommand, CancelNotification, ClientCapabilities, + ForkSessionRequest, + ForkSessionResponse, InitializeRequest, InitializeResponse, ndJsonStream, @@ -67,6 +69,7 @@ type Session = { input: Pushable; cancelled: boolean; permissionMode: PermissionMode; + params: NewSessionRequest; }; type BackgroundTerminal = @@ -181,9 +184,8 @@ export class ClaudeAcpAgent implements Agent { http: true, sse: true, }, - _meta: { - "claude/session/resume": true, - "claude/session/fork": true, + sessionCapabilities: { + fork: {}, }, }, agentInfo: { @@ -228,9 +230,9 @@ export class ClaudeAcpAgent implements Agent { break; } - // Track Claude session ID from all messages + // Track Claude session ID from all messages (needed for fork) if (message.session_id) { - await this.trackAndNotifyClaudeSessionId(params.sessionId, message.session_id); + this.claudeSessionIds[params.sessionId] = message.session_id; } switch (message.type) { @@ -431,6 +433,25 @@ export class ClaudeAcpAgent implements Agent { return response; } + async forkSession(params: ForkSessionRequest): Promise { + const session = this.sessions[params.sessionId]; + if (!session) { + throw RequestError.resourceNotFound(`Session ${params.sessionId} not found`); + } + + const claudeSessionId = this.claudeSessionIds[params.sessionId]; + if (!claudeSessionId) { + throw RequestError.resourceNotFound( + `Session ${params.sessionId} has no Claude session ID yet (no prompts sent?)`, + ); + } + + return await this.createSession(session.params, { + resume: claudeSessionId, + forkSession: true, + }); + } + canUseTool(sessionId: string): CanUseTool { return async (toolName, toolInput, { suggestions, toolUseID }) => { const session = this.sessions[sessionId]; @@ -561,61 +582,6 @@ export class ClaudeAcpAgent implements Agent { }; } - async extMethod(method: string, params: Record): Promise> { - switch (method) { - case "claude/session/resume": { - const claudeSessionId = params.claudeSessionId as string; - if (!claudeSessionId) { - throw RequestError.invalidParams( - { claudeSessionId: params.claudeSessionId }, - "Missing or invalid claudeSessionId parameter", - ); - } - - return (await this.createSession(params as unknown as NewSessionRequest, { - resume: claudeSessionId, - })) as unknown as Record; - } - - case "claude/session/fork": { - const acpSessionId = params.sessionId as string; - if (!acpSessionId) { - throw RequestError.invalidParams( - { sessionId: params.sessionId }, - "Missing or invalid sessionId parameter", - ); - } - - // Look up the Claude session ID from the ACP session ID - const claudeSessionId = this.claudeSessionIds[acpSessionId]; - if (!claudeSessionId) { - throw RequestError.resourceNotFound( - `Session ${acpSessionId} not found or has no Claude session ID yet`, - ); - } - - return (await this.createSession({} as NewSessionRequest, { - resume: claudeSessionId, - forkSession: true, - })) as unknown as Record; - } - - default: - throw RequestError.methodNotFound(method); - } - } - - private async trackAndNotifyClaudeSessionId(acpSessionId: string, claudeSessionId: string): Promise { - // Only update and notify if the session ID has changed - if (this.claudeSessionIds[acpSessionId] !== claudeSessionId) { - this.claudeSessionIds[acpSessionId] = claudeSessionId; - await this.client.extNotification("claude/sessionId", { - sessionId: acpSessionId, - claudeSessionId: claudeSessionId, - }); - } - } - private async createSession( params: NewSessionRequest, creationOpts: { resume?: string; forkSession?: boolean } = {}, @@ -780,6 +746,7 @@ export class ClaudeAcpAgent implements Agent { input: input, cancelled: false, permissionMode, + params, }; const availableCommands = await getAvailableSlashCommands(q); From 2c56c13118254e9d9308f7ab0f05d1823bda0323 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Tue, 9 Dec 2025 17:02:19 +0100 Subject: [PATCH 3/8] don't include custom sessionId for now --- src/acp-agent.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 168ccaef..dc02cdaf 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -140,7 +140,6 @@ export class ClaudeAcpAgent implements Agent { backgroundTerminals: { [key: string]: BackgroundTerminal } = {}; clientCapabilities?: ClientCapabilities; logger: Logger; - claudeSessionIds: { [acpSessionId: string]: string } = {}; constructor(client: AgentSideConnection, logger?: Logger) { this.sessions = {}; @@ -230,11 +229,6 @@ export class ClaudeAcpAgent implements Agent { break; } - // Track Claude session ID from all messages (needed for fork) - if (message.session_id) { - this.claudeSessionIds[params.sessionId] = message.session_id; - } - switch (message.type) { case "system": switch (message.subtype) { @@ -439,15 +433,8 @@ export class ClaudeAcpAgent implements Agent { throw RequestError.resourceNotFound(`Session ${params.sessionId} not found`); } - const claudeSessionId = this.claudeSessionIds[params.sessionId]; - if (!claudeSessionId) { - throw RequestError.resourceNotFound( - `Session ${params.sessionId} has no Claude session ID yet (no prompts sent?)`, - ); - } - return await this.createSession(session.params, { - resume: claudeSessionId, + resume: params.sessionId, forkSession: true, }); } @@ -652,7 +639,8 @@ export class ClaudeAcpAgent implements Agent { includePartialMessages: true, mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers }, // Set our own session id - extraArgs: { ...userProvidedOptions?.extraArgs, "session-id": sessionId }, + // TODO: find a way to make this work for fork + extraArgs: { ...userProvidedOptions?.extraArgs, ...(creationOpts.resume ? {} : {"session-id": sessionId }) }, // If we want bypassPermissions to be an option, we have to allow it here. // But it doesn't work in root mode, so we only activate it if it will work. allowDangerouslySkipPermissions: !IS_ROOT, From e1a3194ea69c496b5c58656a539b537f917cc25c Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 11 Dec 2025 12:54:16 +0100 Subject: [PATCH 4/8] Go back to main sdk --- package-lock.json | 7 ++++--- package.json | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f1705b8..aa894a7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.12.2", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "github:SteffenDE/acp-typescript-sdk#sd-session-fork-dist", + "@agentclientprotocol/sdk": "0.9.0", "@anthropic-ai/claude-agent-sdk": "0.1.61", "@modelcontextprotocol/sdk": "1.24.3", "diff": "8.0.2" @@ -32,8 +32,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "0.8.0", - "resolved": "git+ssh://git@github.com/SteffenDE/acp-typescript-sdk.git#cdc48d85bc4b359dbd2c3c69af40dfab5a603877", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.9.0.tgz", + "integrity": "sha512-JoLjIYS+lfxKXnSBRDCQDzY19gGp6DYcYojMXHNC430b3R32CUOXCMZjndI+yz8QDFY87Dx+S76oLAZ99X+W5w==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" diff --git a/package.json b/package.json index aae73715..61d7514c 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "author": "Zed Industries", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "github:SteffenDE/acp-typescript-sdk#sd-session-fork-dist", + "@agentclientprotocol/sdk": "0.9.0", "@anthropic-ai/claude-agent-sdk": "0.1.61", "@modelcontextprotocol/sdk": "1.24.3", "diff": "8.0.2" From 4e2fa360aa5ecfbf2071d02f099bd6700c5579d2 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 11 Dec 2025 23:19:20 +0100 Subject: [PATCH 5/8] bump sdk version --- package-lock.json | 17 +++++++++++++---- package.json | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55164745..2a37b426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.12.3", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "0.9.0", + "@agentclientprotocol/sdk": "0.10.0", "@anthropic-ai/claude-agent-sdk": "0.1.65", "@modelcontextprotocol/sdk": "1.24.3", "diff": "8.0.2", @@ -33,9 +33,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.9.0.tgz", - "integrity": "sha512-JoLjIYS+lfxKXnSBRDCQDzY19gGp6DYcYojMXHNC430b3R32CUOXCMZjndI+yz8QDFY87Dx+S76oLAZ99X+W5w==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.10.0.tgz", + "integrity": "sha512-+Bwmf8Uli480h4ONjANxVP6tnACqHOaCiLJslgwOJHJvjDFzhAHZX4aL/bPKpOPc3LU1RgkPnJ98rCdPBnakaQ==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" @@ -1579,6 +1579,7 @@ "integrity": "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1618,6 +1619,7 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -1962,6 +1964,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2457,6 +2460,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2760,6 +2764,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3618,6 +3623,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4209,6 +4215,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4265,6 +4272,7 @@ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4488,6 +4496,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 66bce3f9..361fced8 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "author": "Zed Industries", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "0.9.0", + "@agentclientprotocol/sdk": "0.10.0", "@anthropic-ai/claude-agent-sdk": "0.1.65", "@modelcontextprotocol/sdk": "1.24.3", "diff": "8.0.2", From 7007f59fca0099ba07316c79470eee7e782a38d9 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Fri, 12 Dec 2025 13:45:20 +0100 Subject: [PATCH 6/8] use cwd and mcpServers from params --- src/acp-agent.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 798c5d73..32d6206c 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -74,7 +74,6 @@ type Session = { input: Pushable; cancelled: boolean; permissionMode: PermissionMode; - params: NewSessionRequest; settingsManager: SettingsManager; }; @@ -201,6 +200,7 @@ export class ClaudeAcpAgent implements Agent { authMethods: [authMethod], }; } + async newSession(params: NewSessionRequest): Promise { if ( fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) && @@ -215,6 +215,17 @@ export class ClaudeAcpAgent implements Agent { }); } + async forkSession(params: ForkSessionRequest): Promise { + return await this.createSession({ + cwd: params.cwd, + mcpServers: params.mcpServers ?? [], + _meta: params._meta, + }, { + resume: params.sessionId, + forkSession: true, + }); + } + async authenticate(_params: AuthenticateRequest): Promise { throw new Error("Method not implemented."); } @@ -436,18 +447,6 @@ export class ClaudeAcpAgent implements Agent { return response; } - async forkSession(params: ForkSessionRequest): Promise { - const session = this.sessions[params.sessionId]; - if (!session) { - throw RequestError.resourceNotFound(`Session ${params.sessionId} not found`); - } - - return await this.createSession(session.params, { - resume: params.sessionId, - forkSession: true, - }); - } - canUseTool(sessionId: string): CanUseTool { return async (toolName, toolInput, { signal, suggestions, toolUseID }) => { const session = this.sessions[sessionId]; @@ -764,7 +763,6 @@ export class ClaudeAcpAgent implements Agent { input: input, cancelled: false, permissionMode, - params, settingsManager, }; From 0065cad1ad153cc7897d11dad32c4efa21a59f07 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Fri, 12 Dec 2025 13:52:56 +0100 Subject: [PATCH 7/8] format --- src/acp-agent.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 32d6206c..2ed2b0fb 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -216,14 +216,17 @@ export class ClaudeAcpAgent implements Agent { } async forkSession(params: ForkSessionRequest): Promise { - return await this.createSession({ - cwd: params.cwd, - mcpServers: params.mcpServers ?? [], - _meta: params._meta, - }, { - resume: params.sessionId, - forkSession: true, - }); + return await this.createSession( + { + cwd: params.cwd, + mcpServers: params.mcpServers ?? [], + _meta: params._meta, + }, + { + resume: params.sessionId, + forkSession: true, + }, + ); } async authenticate(_params: AuthenticateRequest): Promise { From 209070c2099006f66335b489595019a8fc8b06c7 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Fri, 12 Dec 2025 14:37:31 +0100 Subject: [PATCH 8/8] don't announce fork capability for now --- src/acp-agent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 2ed2b0fb..2843da59 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -189,7 +189,8 @@ export class ClaudeAcpAgent implements Agent { sse: true, }, sessionCapabilities: { - fork: {}, + // TODO: announce fork capability when sessionId handling is fixed + // fork: {}, }, }, agentInfo: {