diff --git a/src/agents/pi-embedded-utils.test.ts b/src/agents/pi-embedded-utils.test.ts index 5fdd74623..3e3d56a5e 100644 --- a/src/agents/pi-embedded-utils.test.ts +++ b/src/agents/pi-embedded-utils.test.ts @@ -358,4 +358,84 @@ File contents here`, const result = extractAssistantText(msg); expect(result).toBe("Here's what I found:\nDone checking."); }); + + it("strips thinking tags from text content", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: "El usuario quiere retomar una tarea...Aquí está tu respuesta.", + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("Aquí está tu respuesta."); + }); + + it("strips thinking tags without closing tag", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: "Pensando sobre el problema...", + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe(""); + }); + + it("strips thinking tags with various formats", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: "Beforeinternal reasoningAfter", + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("BeforeAfter"); + }); + + it("strips antthinking tags", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: "Some reasoningThe actual answer.", + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("The actual answer."); + }); + + it("handles nested or multiple thinking blocks", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: "Startfirst thoughtMiddlesecond thoughtEnd", + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("StartMiddleEnd"); + }); }); diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/pi-embedded-utils.ts index f973b0ab1..d3ccd4485 100644 --- a/src/agents/pi-embedded-utils.ts +++ b/src/agents/pi-embedded-utils.ts @@ -47,6 +47,44 @@ function stripDowngradedToolCallText(text: string): string { return cleaned.trim(); } +/** + * Strip thinking tags and their content from text. + * This is a safety net for cases where the model outputs tags + * that slip through other filtering mechanisms. + */ +function stripThinkingTagsFromText(text: string): string { + if (!text) return text; + // Quick check to avoid regex overhead when no tags present. + if (!/(?:think(?:ing)?|thought|antthinking)/i.test(text)) return text; + + const tagRe = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi; + let result = ""; + let lastIndex = 0; + let inThinking = false; + + for (const match of text.matchAll(tagRe)) { + const idx = match.index ?? 0; + const isClose = match[1] === "/"; + + if (!inThinking && !isClose) { + // Opening tag - save text before it. + result += text.slice(lastIndex, idx); + inThinking = true; + } else if (inThinking && isClose) { + // Closing tag - skip content inside. + inThinking = false; + } + lastIndex = idx + match[0].length; + } + + // Append remaining text if we're not inside thinking. + if (!inThinking) { + result += text.slice(lastIndex); + } + + return result.trim(); +} + export function extractAssistantText(msg: AssistantMessage): string { const isTextBlock = (block: unknown): block is { type: "text"; text: string } => { if (!block || typeof block !== "object") return false; @@ -58,7 +96,9 @@ export function extractAssistantText(msg: AssistantMessage): string { ? msg.content .filter(isTextBlock) .map((c) => - stripDowngradedToolCallText(stripMinimaxToolCallXml(c.text)).trim(), + stripThinkingTagsFromText( + stripDowngradedToolCallText(stripMinimaxToolCallXml(c.text)), + ).trim(), ) .filter(Boolean) : [];