From 8ce899a04b9138a0c2543b94baa0a18c1a196528 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Thu, 20 Aug 2026 09:36:24 +0200 Subject: [PATCH] feat(agent): omit resume documents from the copied conversation json --- .../agent/-components/agent-chat.test.tsx | 43 ++++++++++++++++++- .../routes/agent/-components/agent-chat.tsx | 29 ++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/apps/web/src/routes/agent/-components/agent-chat.test.tsx b/apps/web/src/routes/agent/-components/agent-chat.test.tsx index 4677ab4ce..1c66c1a96 100644 --- a/apps/web/src/routes/agent/-components/agent-chat.test.tsx +++ b/apps/web/src/routes/agent/-components/agent-chat.test.tsx @@ -5,7 +5,7 @@ import { fireEvent, render, screen, within } from "@testing-library/react"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; -import { AskUserQuestion, AssistantMarkdown } from "./agent-chat"; +import { AskUserQuestion, AssistantMarkdown, withoutResumeDataForExport } from "./agent-chat"; describe("AssistantMarkdown", () => { it("renders GitHub-style pipe tables as tables", () => { @@ -27,6 +27,47 @@ describe("AssistantMarkdown", () => { }); }); +describe("withoutResumeDataForExport", () => { + it("strips the resume document from tool outputs but keeps everything else", () => { + const messages = [ + { + id: "m1", + role: "assistant", + parts: [ + { + type: "tool-read_resume", + toolCallId: "c1", + state: "output-available", + input: {}, + output: { id: "r1", updatedAt: "2026-08-20T00:00:00.000Z", data: { basics: { name: "John" } } }, + }, + { + type: "tool-apply_resume_patch", + toolCallId: "c2", + state: "output-available", + input: { title: "Edit" }, + output: { actionId: "a1", changedPaths: ["/basics/name"], resume: { basics: { name: "John" } } }, + }, + { type: "text", text: "Done." }, + ], + }, + ] as unknown as UIMessage[]; + + const exported = withoutResumeDataForExport(messages); + + const exportedParts = (exported[0]?.parts ?? []) as Array<{ output?: Record }>; + expect(exportedParts[0]?.output?.data).toBe("[resume data omitted]"); + expect(exportedParts[0]?.output?.updatedAt).toBe("2026-08-20T00:00:00.000Z"); + expect(exportedParts[1]?.output?.resume).toBe("[resume data omitted]"); + expect(exportedParts[1]?.output?.changedPaths).toEqual(["/basics/name"]); + expect(exported[0]?.parts[2]).toEqual({ type: "text", text: "Done." }); + + // The live chat state is never mutated — only the copied structure is redacted. + const originalParts = (messages[0]?.parts ?? []) as Array<{ output?: Record }>; + expect(originalParts[0]?.output?.data).toEqual({ basics: { name: "John" } }); + }); +}); + const questionPart = { type: "tool-ask_user_question", toolCallId: "call-1", diff --git a/apps/web/src/routes/agent/-components/agent-chat.tsx b/apps/web/src/routes/agent/-components/agent-chat.tsx index 294d461c6..0da734c45 100644 --- a/apps/web/src/routes/agent/-components/agent-chat.tsx +++ b/apps/web/src/routes/agent/-components/agent-chat.tsx @@ -200,6 +200,33 @@ type AgentChatComposerProps = { const ANSWER_FIELD = "answer"; +const OMITTED_RESUME_DATA = "[resume data omitted]"; + +// "Copy JSON" is for sharing/debugging a conversation; the full resume document embedded in every +// read_resume result (`output.data`) and fresh-document patch result (`output.resume`) would make +// the export enormous and repetitive. Strip those two payloads; everything else copies verbatim. +export function withoutResumeDataForExport(messages: UIMessage[]): UIMessage[] { + return messages.map((message) => ({ + ...message, + parts: message.parts.map((part) => { + if (part.type !== "tool-read_resume" && part.type !== "tool-apply_resume_patch") return part; + + const output = "output" in part && typeof part.output === "object" && part.output ? part.output : null; + if (!output) return part; + const outputRecord = output as Record; + + if (part.type === "tool-read_resume" && "data" in outputRecord) { + return { ...part, output: { ...outputRecord, data: OMITTED_RESUME_DATA } } as UIMessage["parts"][number]; + } + if (part.type === "tool-apply_resume_patch" && "resume" in outputRecord) { + return { ...part, output: { ...outputRecord, resume: OMITTED_RESUME_DATA } } as UIMessage["parts"][number]; + } + + return part; + }), + })); +} + function toRecord(value: unknown) { return typeof value === "object" && value !== null ? (value as Record) : null; } @@ -994,7 +1021,7 @@ export function AgentChat({ chatStatus: status, isReadOnly, readOnlyReason, - messages, + messages: withoutResumeDataForExport(messages), actions, }, null,