From 001614b459a8f791eed8a16c4eff292735cd22d6 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Thu, 26 Feb 2026 22:45:12 +0100 Subject: [PATCH] expose parent_tool_use_id for subagents (#341) I'm not sure yet how a good protocol for subagents could look like, so instead of opening an RFD, I opted to expose Claude's `parent_tool_use_id` in `_meta.claudeCode.parentToolUseId`. This will allow Tidewave to render tools from subagents in a better way. --- src/acp-agent.ts | 62 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 6a3c8e05..b5507de9 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -640,7 +640,10 @@ export class ClaudeAcpAgent implements Agent { this.toolUseCache, this.client, this.logger, - { clientCapabilities: this.clientCapabilities }, + { + clientCapabilities: this.clientCapabilities, + parentToolUseId: message.parent_tool_use_id, + }, )) { await this.client.sessionUpdate(notification); } @@ -1393,23 +1396,34 @@ export function toAcpNotifications( toolUseCache: ToolUseCache, client: AgentSideConnection, logger: Logger, - options?: { registerHooks?: boolean; clientCapabilities?: ClientCapabilities }, + options?: { + registerHooks?: boolean; + clientCapabilities?: ClientCapabilities; + parentToolUseId?: string | null; + }, ): SessionNotification[] { const registerHooks = options?.registerHooks !== false; const supportsTerminalOutput = options?.clientCapabilities?._meta?.["terminal_output"] === true; if (typeof content === "string") { - return [ - { - sessionId, - update: { - sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk", - content: { - type: "text", - text: content, - }, - }, + const update: SessionNotification["update"] = { + sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk", + content: { + type: "text", + text: content, }, - ]; + }; + + if (options?.parentToolUseId) { + update._meta = { + ...update._meta, + claudeCode: { + ...(update._meta?.claudeCode || {}), + parentToolUseId: options.parentToolUseId, + }, + }; + } + + return [{ sessionId, update }]; } const output = []; @@ -1573,6 +1587,9 @@ export function toAcpNotifications( update: { _meta: { terminal_output: toolMeta.terminal_output, + ...(options?.parentToolUseId + ? { claudeCode: { parentToolUseId: options.parentToolUseId } } + : {}), }, toolCallId: chunk.tool_use_id, sessionUpdate: "tool_call_update" as const, @@ -1613,6 +1630,15 @@ export function toAcpNotifications( break; } if (update) { + if (options?.parentToolUseId) { + update._meta = { + ...update._meta, + claudeCode: { + ...(update._meta?.claudeCode || {}), + parentToolUseId: options.parentToolUseId, + }, + }; + } output.push({ sessionId, update }); } } @@ -1638,7 +1664,10 @@ export function streamEventToAcpNotifications( toolUseCache, client, logger, - { clientCapabilities: options?.clientCapabilities }, + { + clientCapabilities: options?.clientCapabilities, + parentToolUseId: message.parent_tool_use_id, + }, ); case "content_block_delta": return toAcpNotifications( @@ -1648,7 +1677,10 @@ export function streamEventToAcpNotifications( toolUseCache, client, logger, - { clientCapabilities: options?.clientCapabilities }, + { + clientCapabilities: options?.clientCapabilities, + parentToolUseId: message.parent_tool_use_id, + }, ); // No content case "message_start":