fix(frontend): exclude hidden, reasoning, and tool payloads from chat export

`formatThreadAsMarkdown` / `formatThreadAsJSON` iterated raw messages without
running the UI-level `isHiddenFromUIMessage` filter. Exported transcripts
therefore included `hide_from_ui` system reminders, memory injections,
provider `reasoning_content`, tool calls, and tool result messages — content
that is intentionally hidden in the chat view.

Filter the export to the user-visible transcript by default and gate
reasoning / tool calls / tool messages / hidden messages behind explicit
`ExportOptions` flags so a future debug export can opt back in without
forking the formatter.

Refs: bytedance/deer-flow#3107 (BUG-006)
This commit is contained in:
fancyboi999
2026-05-21 15:08:59 +08:00
parent 308d43c9bd
commit 9b77ef160f
2 changed files with 180 additions and 5 deletions
+46 -5
View File
@@ -5,12 +5,45 @@ import {
extractReasoningContentFromMessage,
hasContent,
hasToolCalls,
isHiddenFromUIMessage,
stripUploadedFilesTag,
} from "../messages/utils";
import type { AgentThread } from "./types";
import { titleOfThread } from "./utils";
/**
* Optional debug switches for advanced exports.
*
* Bytedance/deer-flow issue #3107 BUG-006: by default, the user-facing chat
* export must include only the visible transcript. Internal payloads —
* `hide_from_ui` messages, reasoning content, tool calls, and tool result
* messages — stay out unless the caller explicitly opts in. There is no UI
* surface for this today; the flags exist so a future "debug export" can
* reuse the same formatter instead of forking it.
*/
export interface ExportOptions {
includeReasoning?: boolean;
includeToolCalls?: boolean;
includeToolMessages?: boolean;
includeHidden?: boolean;
}
function visibleMessages(
messages: Message[],
options: ExportOptions,
): Message[] {
return messages.filter((message) => {
if (!options.includeHidden && isHiddenFromUIMessage(message)) {
return false;
}
if (!options.includeToolMessages && message.type === "tool") {
return false;
}
return true;
});
}
function formatMessageContent(message: Message): string {
const text = extractContentFromMessage(message);
if (!text) return "";
@@ -26,6 +59,7 @@ function formatToolCalls(message: Message): string {
export function formatThreadAsMarkdown(
thread: AgentThread,
messages: Message[],
options: ExportOptions = {},
): string {
const title = titleOfThread(thread);
const createdAt = thread.created_at
@@ -41,16 +75,20 @@ export function formatThreadAsMarkdown(
"",
];
for (const message of messages) {
for (const message of visibleMessages(messages, options)) {
if (message.type === "human") {
const content = formatMessageContent(message);
if (content) {
lines.push(`## 🧑 User`, "", content, "", "---", "");
}
} else if (message.type === "ai") {
const reasoning = extractReasoningContentFromMessage(message);
const reasoning = options.includeReasoning
? extractReasoningContentFromMessage(message)
: undefined;
const content = formatMessageContent(message);
const toolCalls = formatToolCalls(message);
const toolCalls = options.includeToolCalls
? formatToolCalls(message)
: "";
if (!content && !toolCalls && !reasoning) continue;
@@ -86,17 +124,20 @@ export function formatThreadAsMarkdown(
export function formatThreadAsJSON(
thread: AgentThread,
messages: Message[],
options: ExportOptions = {},
): string {
const exportData = {
title: titleOfThread(thread),
thread_id: thread.thread_id,
created_at: thread.created_at,
exported_at: new Date().toISOString(),
messages: messages.map((msg) => ({
messages: visibleMessages(messages, options).map((msg) => ({
type: msg.type,
id: msg.id,
content: typeof msg.content === "string" ? msg.content : msg.content,
...(msg.type === "ai" && msg.tool_calls?.length
...(options.includeToolCalls &&
msg.type === "ai" &&
msg.tool_calls?.length
? { tool_calls: msg.tool_calls }
: {}),
})),