diff --git a/CHANGELOG.md b/CHANGELOG.md index f83a95475..03350f799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.clawd.bot - Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky. - Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla. - Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer. +- Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x. - Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok. - Agents: surface concrete API error details instead of generic AI service errors. - Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484) diff --git a/src/agents/tools/sessions-helpers.test.ts b/src/agents/tools/sessions-helpers.test.ts new file mode 100644 index 000000000..cc6d1fbc7 --- /dev/null +++ b/src/agents/tools/sessions-helpers.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { extractAssistantText, sanitizeTextContent } from "./sessions-helpers.js"; + +describe("sanitizeTextContent", () => { + it("strips minimax tool call XML and downgraded markers", () => { + const input = + 'Hello payload ' + + "[Tool Call: foo (ID: 1)] world"; + const result = sanitizeTextContent(input).trim(); + expect(result).toBe("Hello world"); + expect(result).not.toContain("invoke"); + expect(result).not.toContain("Tool Call"); + }); + + it("strips thinking tags", () => { + const input = "Before secret after"; + const result = sanitizeTextContent(input).trim(); + expect(result).toBe("Before after"); + }); +}); + +describe("extractAssistantText", () => { + it("sanitizes blocks without injecting newlines", () => { + const message = { + role: "assistant", + content: [ + { type: "text", text: "Hi " }, + { type: "text", text: "secretthere" }, + ], + }; + expect(extractAssistantText(message)).toBe("Hi there"); + }); +}); diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index 7af647080..c4ec120ed 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -111,9 +111,8 @@ export function stripToolMessages(messages: unknown[]): unknown[] { * This ensures user-facing text doesn't leak internal tool representations. */ export function sanitizeTextContent(text: string): string { - return stripThinkingTagsFromText( - stripDowngradedToolCallText(stripMinimaxToolCallXml(text)), - ).trim(); + if (!text) return text; + return stripThinkingTagsFromText(stripDowngradedToolCallText(stripMinimaxToolCallXml(text))); } export function extractAssistantText(message: unknown): string | undefined { @@ -128,11 +127,11 @@ export function extractAssistantText(message: unknown): string | undefined { const text = (block as { text?: unknown }).text; if (typeof text === "string") { const sanitized = sanitizeTextContent(text); - if (sanitized) { + if (sanitized.trim()) { chunks.push(sanitized); } } } - const joined = chunks.join("\n").trim(); + const joined = chunks.join("").trim(); return joined ? sanitizeUserFacingText(joined) : undefined; } diff --git a/src/auto-reply/reply/commands-subagents.test.ts b/src/auto-reply/reply/commands-subagents.test.ts new file mode 100644 index 000000000..eaf7c3026 --- /dev/null +++ b/src/auto-reply/reply/commands-subagents.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { extractMessageText } from "./commands-subagents.js"; + +describe("extractMessageText", () => { + it("preserves user text that looks like tool call markers", () => { + const message = { + role: "user", + content: "Here [Tool Call: foo (ID: 1)] ok", + }; + const result = extractMessageText(message); + expect(result?.text).toContain("[Tool Call: foo (ID: 1)]"); + }); + + it("sanitizes assistant tool call markers", () => { + const message = { + role: "assistant", + content: "Here [Tool Call: foo (ID: 1)] ok", + }; + const result = extractMessageText(message); + expect(result?.text).toBe("Here ok"); + }); +}); diff --git a/src/auto-reply/reply/commands-subagents.ts b/src/auto-reply/reply/commands-subagents.ts index 7122dc01b..5009a30fa 100644 --- a/src/auto-reply/reply/commands-subagents.ts +++ b/src/auto-reply/reply/commands-subagents.ts @@ -107,12 +107,14 @@ function normalizeMessageText(text: string) { return text.replace(/\s+/g, " ").trim(); } -function extractMessageText(message: ChatMessage): { role: string; text: string } | null { +export function extractMessageText(message: ChatMessage): { role: string; text: string } | null { const role = typeof message.role === "string" ? message.role : ""; + const shouldSanitize = role === "assistant"; const content = message.content; if (typeof content === "string") { - const sanitized = sanitizeTextContent(content); - const normalized = normalizeMessageText(sanitized); + const normalized = normalizeMessageText( + shouldSanitize ? sanitizeTextContent(content) : content, + ); return normalized ? { role, text: normalized } : null; } if (!Array.isArray(content)) return null; @@ -122,9 +124,9 @@ function extractMessageText(message: ChatMessage): { role: string; text: string if ((block as { type?: unknown }).type !== "text") continue; const text = (block as { text?: unknown }).text; if (typeof text === "string") { - const sanitized = sanitizeTextContent(text); - if (sanitized) { - chunks.push(sanitized); + const value = shouldSanitize ? sanitizeTextContent(text) : text; + if (value.trim()) { + chunks.push(value); } } }