From d7da3d470e6906c16d1be1197d9c9d1d02c95a57 Mon Sep 17 00:00:00 2001
From: xialonglee
Date: Mon, 20 Apr 2026 17:47:27 +0800
Subject: [PATCH] fix(agents): strip antml thinking tags in streaming
---
src/agents/pi-embedded-subscribe.e2e-harness.ts | 1 +
src/agents/pi-embedded-subscribe.ts | 3 ++-
src/agents/pi-embedded-utils.test.ts | 14 ++++++++++++++
src/agents/pi-embedded-utils.ts | 12 ++++++------
4 files changed, 23 insertions(+), 7 deletions(-)
diff --git a/src/agents/pi-embedded-subscribe.e2e-harness.ts b/src/agents/pi-embedded-subscribe.e2e-harness.ts
index 53fc38233f4..12138fefbcd 100644
--- a/src/agents/pi-embedded-subscribe.e2e-harness.ts
+++ b/src/agents/pi-embedded-subscribe.e2e-harness.ts
@@ -13,6 +13,7 @@ export const THINKING_TAG_CASES = [
{ tag: "thinking", open: "", close: "" },
{ tag: "thought", open: "", close: "" },
{ tag: "antthinking", open: "", close: "" },
+ { tag: "antml:thinking", open: "", close: "" },
] as const;
export function createStubSessionHarness(): {
diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts
index e257abf58b5..24b4bdb4718 100644
--- a/src/agents/pi-embedded-subscribe.ts
+++ b/src/agents/pi-embedded-subscribe.ts
@@ -36,7 +36,8 @@ import type { SubscribeEmbeddedPiSessionParams } from "./pi-embedded-subscribe.t
import { formatReasoningMessage, stripDowngradedToolCallText } from "./pi-embedded-utils.js";
import { hasNonzeroUsage, normalizeUsage, type UsageLike } from "./usage.js";
-const THINKING_TAG_SCAN_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
+const THINKING_TAG_SCAN_RE =
+ /<\s*(\/?)\s*(?:(?:antml:)?(?:think(?:ing)?|thought)|antthinking)\s*>/gi;
const FINAL_TAG_SCAN_RE = /<\s*(\/?)\s*final\s*>/gi;
const log = createSubsystemLogger("agent/embedded");
diff --git a/src/agents/pi-embedded-utils.test.ts b/src/agents/pi-embedded-utils.test.ts
index 2c69c9614bd..01cf81e386f 100644
--- a/src/agents/pi-embedded-utils.test.ts
+++ b/src/agents/pi-embedded-utils.test.ts
@@ -806,6 +806,20 @@ describe("promoteThinkingTagsToBlocks", () => {
expect(types).toContain("text");
});
+ it("splits antml namespaced thinking tags into thinking blocks", () => {
+ const msg = makeAssistantMessage({
+ role: "assistant",
+ content: [{ type: "text", text: "hiddenVisible" }],
+ timestamp: Date.now(),
+ });
+
+ promoteThinkingTagsToBlocks(msg);
+ expect(msg.content).toEqual([
+ { type: "thinking", thinking: "hidden" },
+ { type: "text", text: "Visible" },
+ ]);
+ });
+
it("does not crash on undefined content entries", () => {
const msg = makeAssistantMessage({
role: "assistant",
diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/pi-embedded-utils.ts
index 666716ed20b..67b4b6adb47 100644
--- a/src/agents/pi-embedded-utils.ts
+++ b/src/agents/pi-embedded-utils.ts
@@ -189,8 +189,8 @@ export function splitThinkingTaggedText(text: string): ThinkTaggedSplitBlock[] |
if (!trimmedStart.startsWith("<")) {
return null;
}
- const openRe = /<\s*(?:think(?:ing)?|thought|antthinking)\s*>/i;
- const closeRe = /<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/i;
+ const openRe = /<\s*(?:(?:antml:)?(?:think(?:ing)?|thought)|antthinking)\s*>/i;
+ const closeRe = /<\s*\/\s*(?:(?:antml:)?(?:think(?:ing)?|thought)|antthinking)\s*>/i;
if (!openRe.test(trimmedStart)) {
return null;
}
@@ -198,7 +198,7 @@ export function splitThinkingTaggedText(text: string): ThinkTaggedSplitBlock[] |
return null;
}
- const scanRe = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
+ const scanRe = /<\s*(\/?)\s*(?:(?:antml:)?(?:think(?:ing)?|thought)|antthinking)\s*>/gi;
let inThinking = false;
let cursor = 0;
let thinkingStart = 0;
@@ -299,7 +299,7 @@ export function extractThinkingFromTaggedText(text: string): string {
if (!text) {
return "";
}
- const scanRe = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
+ const scanRe = /<\s*(\/?)\s*(?:(?:antml:)?(?:think(?:ing)?|thought)|antthinking)\s*>/gi;
let result = "";
let lastIndex = 0;
let inThinking = false;
@@ -324,8 +324,8 @@ export function extractThinkingFromTaggedStream(text: string): string {
return closed;
}
- const openRe = /<\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
- const closeRe = /<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
+ const openRe = /<\s*(?:(?:antml:)?(?:think(?:ing)?|thought)|antthinking)\s*>/gi;
+ const closeRe = /<\s*\/\s*(?:(?:antml:)?(?:think(?:ing)?|thought)|antthinking)\s*>/gi;
const openMatches = [...text.matchAll(openRe)];
if (openMatches.length === 0) {
return "";