fix: unify reasoning tags + agent ids (#1613) (thanks @kyleok) (#1629)

This commit is contained in:
Peter Steinberger
2026-01-24 19:56:02 +00:00
committed by GitHub
parent 71457fa100
commit 390b730b37
6 changed files with 85 additions and 64 deletions

View File

@@ -460,6 +460,22 @@ File contents here`,
expect(result).toBe("The actual answer.");
});
it("strips final tags while keeping content", () => {
const msg: AssistantMessage = {
role: "assistant",
content: [
{
type: "text",
text: "<final>\nAnswer\n</final>",
},
],
timestamp: Date.now(),
};
const result = extractAssistantText(msg);
expect(result).toBe("Answer");
});
it("strips thought tags", () => {
const msg: AssistantMessage = {
role: "assistant",

View File

@@ -1,4 +1,5 @@
import type { AssistantMessage } from "@mariozechner/pi-ai";
import { stripReasoningTagsFromText } from "../shared/text/reasoning-tags.js";
import { sanitizeUserFacingText } from "./pi-embedded-helpers.js";
import { formatToolDetail, resolveToolDisplay } from "./tool-display.js";
@@ -166,36 +167,7 @@ export function stripDowngradedToolCallText(text: string): string {
* that slip through other filtering mechanisms.
*/
export 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)\b[^>]*>/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();
return stripReasoningTagsFromText(text, { mode: "strict", trim: "both" });
}
export function extractAssistantText(msg: AssistantMessage): string {

View File

@@ -67,9 +67,10 @@ export function normalizeAgentId(value: string | undefined | null): string {
export function sanitizeAgentId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) return DEFAULT_AGENT_ID;
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed;
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed.toLowerCase();
return (
trimmed
.toLowerCase()
.replace(/[^a-z0-9_-]+/gi, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")

View File

@@ -0,0 +1,61 @@
export type ReasoningTagMode = "strict" | "preserve";
export type ReasoningTagTrim = "none" | "start" | "both";
const QUICK_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking|final)\b/i;
const FINAL_TAG_RE = /<\s*\/?\s*final\b[^>]*>/gi;
const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;
function applyTrim(value: string, mode: ReasoningTagTrim): string {
if (mode === "none") return value;
if (mode === "start") return value.trimStart();
return value.trim();
}
export function stripReasoningTagsFromText(
text: string,
options?: {
mode?: ReasoningTagMode;
trim?: ReasoningTagTrim;
},
): string {
if (!text) return text;
if (!QUICK_TAG_RE.test(text)) return text;
const mode = options?.mode ?? "strict";
const trimMode = options?.trim ?? "both";
let cleaned = text;
if (FINAL_TAG_RE.test(cleaned)) {
FINAL_TAG_RE.lastIndex = 0;
cleaned = cleaned.replace(FINAL_TAG_RE, "");
} else {
FINAL_TAG_RE.lastIndex = 0;
}
THINKING_TAG_RE.lastIndex = 0;
let result = "";
let lastIndex = 0;
let inThinking = false;
for (const match of cleaned.matchAll(THINKING_TAG_RE)) {
const idx = match.index ?? 0;
const isClose = match[1] === "/";
if (!inThinking) {
result += cleaned.slice(lastIndex, idx);
if (!isClose) {
inThinking = true;
}
} else if (isClose) {
inThinking = false;
}
lastIndex = idx + match[0].length;
}
if (!inThinking || mode === "preserve") {
result += cleaned.slice(lastIndex);
}
return applyTrim(result, trimMode);
}