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)
: [];