/** * thinking-label-strip — remove stray tags from * assistant thinking blocks. * * Some models wrap their own reasoning text in literal * tags inside the `thinking` field of a ThinkingContent block. The tag * itself is noise — pi already marks the block as type "thinking", so the * tag is redundant. When it leaks into the context it confuses downstream * consumers (compaction, other models that echo it back) and pollutes the * session transcript. * * This hook intercepts `message_end` for assistant messages and strips * every pair from each ThinkingContent block's text, * preserving the reasoning between the tags. The edit is persistent: the * stored message is replaced, so the context sent to subsequent LLM calls * no longer contains the literal tag. */ import type { Module } from "./skeleton.js"; const THINKING_OPEN = ""; const THINKING_CLOSE = ""; /** Strip a leading and/or trailing from `text`. * Models often emit these tags as delimiters inside the `thinking` field; * pi already marks the block type, so the tags are noise that leaks into * the context and the TUI. We only trim at the head/tail — tags buried in * the middle are part of the reasoning text and left alone, which avoids * corrupting the model's argument if it literally discusses the tag. */ export function stripThinkingLabels(text: string): string { if (!text.includes(THINKING_OPEN) && !text.includes(THINKING_CLOSE)) { return text; } let out = text; // trimStart would loop-strip repeating opens; we want a single leading // open tag (optionally with whitespace before it) removed. const leadingOpen = out.match(/^\s*/); if (leadingOpen) { out = out.slice(leadingOpen[0].length); } const trailingClose = out.match(/<\/thinking>\s*$/); if (trailingClose) { out = out.slice(0, out.length - trailingClose[0].length); } return out; } /** Return a new content array with thinking blocks cleaned, or undefined * if nothing changed. */ function cleanContent(content: any[]): any[] | undefined { let changed = false; const next = content.map((block) => { if (block && block.type === "thinking" && typeof block.thinking === "string") { const cleaned = stripThinkingLabels(block.thinking); if (cleaned !== block.thinking) { changed = true; return { ...block, thinking: cleaned }; } } return block; }); return changed ? next : undefined; } export const thinkingLabelStripModule: Module = { name: "thinking-label-strip", hooks: { message_end: [ (event: any) => { const message = event?.message; if (!message || message.role !== "assistant") return undefined; if (!Array.isArray(message.content)) return undefined; const cleaned = cleanContent(message.content); if (!cleaned) return undefined; return { message: { ...message, content: cleaned } }; }, ], }, };