fix(agent): buffer streaming output until <final> tag appears

- Enforces strict buffering when enforceFinalTag is enabled.
- Prevents 'thinking out loud' planning steps (e.g. '*Locating Manulife*') from leaking to WhatsApp.
- Hardens <final> tag stripping to remove nested/hallucinated tags.
This commit is contained in:
Keith the Silly Goose
2026-01-12 16:11:26 +13:00
committed by Peter Steinberger
parent efdf874407
commit a7cb270999
2 changed files with 19 additions and 8 deletions

View File

@@ -369,8 +369,12 @@ export function subscribeEmbeddedPiSession(params: {
state.thinking = inThinking; state.thinking = inThinking;
// 2. Handle <final> blocks (stateful, strip content OUTSIDE) // 2. Handle <final> blocks (stateful, strip content OUTSIDE)
// If enforcement is disabled, just return processed text as-is. // If enforcement is disabled, we still strip the tags themselves to prevent
if (!params.enforceFinalTag) return processed; // hallucinations (e.g. Minimax copying the style) from leaking, but we
// do not enforce buffering/extraction logic.
if (!params.enforceFinalTag) {
return processed.replace(FINAL_TAG_SCAN_RE, "");
}
// If enforcement is enabled, only return text that appeared inside a <final> block. // If enforcement is enabled, only return text that appeared inside a <final> block.
let result = ""; let result = "";
@@ -414,14 +418,16 @@ export function subscribeEmbeddedPiSession(params: {
})); }));
} }
// Fallback: if we are at the end of the process and never saw a final tag, // Strict Mode: If enforcing final tags, we MUST NOT return content unless
// but we have processed text, use the processed text. // we have seen a <final> tag. Otherwise, we leak "thinking out loud" text
// NOTE: This fallback only triggers if we explicitly pass a state that we can check. // (e.g. "**Locating Manulife**...") that the model emitted without <think> tags.
if (!everInFinal && processed.trim().length > 0) { if (!everInFinal) {
return processed; return "";
} }
return result; // Hardened Cleanup: Remove any remaining <final> tags that might have been
// missed (e.g. nested tags or hallucinations) to prevent leakage.
return result.replace(FINAL_TAG_SCAN_RE, "");
}; };
const emitBlockChunk = (text: string) => { const emitBlockChunk = (text: string) => {

View File

@@ -25,5 +25,10 @@ export function isReasoningTagProvider(provider: string | undefined | null): boo
return true; return true;
} }
// Handle Minimax (M2.1 is chatty/reasoning-like)
if (normalized.includes("minimax")) {
return true;
}
return false; return false;
} }