From c059a978cd38ff7157205011dc2453c40e5f7de0 Mon Sep 17 00:00:00 2001 From: Kenay Perez Date: Thu, 11 Jun 2026 11:50:02 -0700 Subject: [PATCH 1/2] fix(agent): drop rawOutput for non-MCP tool results Standard tool results (Read, Bash, ...) render from `content`, so also copying the full result into `rawOutput` duplicated large payloads on their way to the renderer. Only MCP tools consume rawOutput (App bridge replay), so emit it solely when an MCP tool result is present. --- .../claude/conversion/sdk-to-acp.test.ts | 80 +++++++++++++++++++ .../adapters/claude/conversion/sdk-to-acp.ts | 21 ++--- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.test.ts b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.test.ts index 7e9d15709..0767b9882 100644 --- a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.test.ts +++ b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.test.ts @@ -5,6 +5,7 @@ import type { import type { SDKAssistantMessage, SDKPartialAssistantMessage, + SDKUserMessage, } from "@anthropic-ai/claude-agent-sdk"; import { describe, expect, it } from "vitest"; import { Logger } from "../../../utils/logger"; @@ -260,3 +261,82 @@ describe("assembled assistant text fallback", () => { expect(chunkTexts(updates, "agent_message_chunk")).toEqual([]); }); }); + +async function registerToolUse( + context: MessageHandlerContext, + toolUseId: string, + name: string, + input: Record = {}, +): Promise { + await handleUserAssistantMessage( + assistantMessage("msg_tool", [ + { type: "tool_use", id: toolUseId, name, input }, + ]), + context, + ); +} + +function userToolResult( + toolUseId: string, + content: unknown, + toolUseResult?: Record, +): SDKUserMessage { + return { + type: "user", + parent_tool_use_id: null, + uuid: "00000000-0000-0000-0000-000000000003", + session_id: "test-session", + message: { + role: "user", + content: [{ type: "tool_result", tool_use_id: toolUseId, content }], + }, + ...(toolUseResult ? { tool_use_result: toolUseResult } : {}), + } as unknown as SDKUserMessage; +} + +function toolUpdate(updates: SessionNotification[]) { + return updates.find((u) => u.update.sessionUpdate === "tool_call_update") + ?.update as + | (Extract< + SessionNotification["update"], + { sessionUpdate: "tool_call_update" } + > & { rawOutput?: unknown }) + | undefined; +} + +describe("tool_call_update rawOutput", () => { + it("omits rawOutput for standard tool results so content is not duplicated", async () => { + const { context, updates } = createHandlerContext(); + await registerToolUse(context, "tool_read", "Read", { + file_path: "/test/a.ts", + }); + updates.length = 0; + await handleUserAssistantMessage( + userToolResult("tool_read", "line one\nline two"), + context, + ); + const update = toolUpdate(updates); + expect(update?.status).toBe("completed"); + expect(update && "rawOutput" in update).toBe(false); + }); + + it("forwards rawOutput for MCP tool results consumed by the App bridge", async () => { + const { context, updates } = createHandlerContext(); + await registerToolUse(context, "tool_mcp", "mcp__srv__do"); + updates.length = 0; + await handleUserAssistantMessage( + userToolResult("tool_mcp", "ignored", { + content: [{ type: "text", text: "mcp payload" }], + structuredContent: { ok: true }, + }), + context, + ); + const update = toolUpdate(updates); + expect(update?.status).toBe("completed"); + expect(update?.rawOutput).toEqual({ + content: [{ type: "text", text: "mcp payload" }], + structuredContent: { ok: true }, + isError: false, + }); + }); +}); diff --git a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts index 3d5dc0e28..36b5f4188 100644 --- a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts +++ b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts @@ -439,16 +439,17 @@ function handleToolResultChunk( toolCallId: chunk.tool_use_id, sessionUpdate: "tool_call_update", status: chunk.is_error ? "failed" : "completed", - rawOutput: ctx.mcpToolUseResult - ? { ...ctx.mcpToolUseResult, isError: chunk.is_error ?? false } - : { - content: Array.isArray(chunk.content) - ? chunk.content - : typeof chunk.content === "string" - ? [{ type: "text" as const, text: chunk.content }] - : [], - isError: chunk.is_error ?? false, - }, + // Only MCP tools need rawOutput: their App bridge replays it. Standard + // tools (Read, Bash, ...) render from `content`, so forwarding the full + // result here would duplicate large payloads to the renderer. + ...(ctx.mcpToolUseResult + ? { + rawOutput: { + ...ctx.mcpToolUseResult, + isError: chunk.is_error ?? false, + }, + } + : {}), ...toolUpdate, }); From c1c46016b3338378b054bfb1b2e533c8964fe295 Mon Sep 17 00:00:00 2001 From: Kenay Perez Date: Thu, 11 Jun 2026 19:00:22 -0700 Subject: [PATCH 2/2] refactor: parameterise tool_call_update rawOutput tests --- .../claude/conversion/sdk-to-acp.test.ts | 83 ++++++++++++------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.test.ts b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.test.ts index 0767b9882..c9aa36ed3 100644 --- a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.test.ts +++ b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.test.ts @@ -305,38 +305,57 @@ function toolUpdate(updates: SessionNotification[]) { } describe("tool_call_update rawOutput", () => { - it("omits rawOutput for standard tool results so content is not duplicated", async () => { - const { context, updates } = createHandlerContext(); - await registerToolUse(context, "tool_read", "Read", { - file_path: "/test/a.ts", - }); - updates.length = 0; - await handleUserAssistantMessage( - userToolResult("tool_read", "line one\nline two"), - context, - ); - const update = toolUpdate(updates); - expect(update?.status).toBe("completed"); - expect(update && "rawOutput" in update).toBe(false); - }); - - it("forwards rawOutput for MCP tool results consumed by the App bridge", async () => { - const { context, updates } = createHandlerContext(); - await registerToolUse(context, "tool_mcp", "mcp__srv__do"); - updates.length = 0; - await handleUserAssistantMessage( - userToolResult("tool_mcp", "ignored", { + it.each([ + { + name: "omits rawOutput for standard tool results so content is not duplicated", + toolName: "Read", + toolUseId: "tool_read", + content: "line one\nline two", + toolUseResult: undefined, + expectedRawOutput: false, + }, + { + name: "forwards rawOutput for MCP tool results consumed by the App bridge", + toolName: "mcp__srv__do", + toolUseId: "tool_mcp", + content: "ignored", + toolUseResult: { content: [{ type: "text", text: "mcp payload" }], structuredContent: { ok: true }, - }), - context, - ); - const update = toolUpdate(updates); - expect(update?.status).toBe("completed"); - expect(update?.rawOutput).toEqual({ - content: [{ type: "text", text: "mcp payload" }], - structuredContent: { ok: true }, - isError: false, - }); - }); + }, + expectedRawOutput: { + content: [{ type: "text", text: "mcp payload" }], + structuredContent: { ok: true }, + isError: false, + }, + }, + ])( + "$name", + async ({ + toolName, + toolUseId, + content, + toolUseResult, + expectedRawOutput, + }) => { + const { context, updates } = createHandlerContext(); + await registerToolUse(context, toolUseId, toolName); + updates.length = 0; + await handleUserAssistantMessage( + userToolResult( + toolUseId, + content, + toolUseResult as Record | undefined, + ), + context, + ); + const update = toolUpdate(updates); + expect(update?.status).toBe("completed"); + if (expectedRawOutput === false) { + expect(update && "rawOutput" in update).toBe(false); + } else { + expect(update?.rawOutput).toEqual(expectedRawOutput); + } + }, + ); });