fix(agents): strip antml thinking tags in streaming

This commit is contained in:
xialonglee
2026-04-20 17:47:27 +08:00
committed by Ayaan Zaidi
parent 40e5d9adc7
commit d7da3d470e
4 changed files with 23 additions and 7 deletions

View File

@@ -13,6 +13,7 @@ export const THINKING_TAG_CASES = [
{ tag: "thinking", open: "<thinking>", close: "</thinking>" },
{ tag: "thought", open: "<thought>", close: "</thought>" },
{ tag: "antthinking", open: "<antthinking>", close: "</antthinking>" },
{ tag: "antml:thinking", open: "<antml:thinking>", close: "</antml:thinking>" },
] as const;
export function createStubSessionHarness(): {

View File

@@ -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");

View File

@@ -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: "<antml:thinking>hidden</antml:thinking>Visible" }],
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",

View File

@@ -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 "";