diff --git a/src/agents/pi-embedded-subscribe.handlers.messages.ts b/src/agents/pi-embedded-subscribe.handlers.messages.ts index 1f515e113..fd7b3bdb0 100644 --- a/src/agents/pi-embedded-subscribe.handlers.messages.ts +++ b/src/agents/pi-embedded-subscribe.handlers.messages.ts @@ -107,10 +107,12 @@ export function handleMessageUpdate( inlineCode: createInlineCodeState(), }) .trim(); + if (next && next !== ctx.state.lastStreamedAssistant) { const previousText = ctx.state.lastStreamedAssistant ?? ""; const { text: cleanedText, mediaUrls } = parseReplyDirectives(next); const { text: previousCleanedText } = parseReplyDirectives(previousText); + if (cleanedText.startsWith(previousCleanedText)) { const deltaText = cleanedText.slice(previousCleanedText.length); ctx.state.lastStreamedAssistant = next; diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index a4a4b906a..7f91ae0e1 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -265,6 +265,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar // 1. Handle blocks (stateful, strip content inside) let processed = ""; + let everInThinking = state.thinking; THINKING_TAG_SCAN_RE.lastIndex = 0; let lastIndex = 0; let inThinking = state.thinking; @@ -276,6 +277,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar } const isClose = match[1] === "/"; inThinking = !isClose; + if (inThinking) everInThinking = true; lastIndex = idx + match[0].length; } if (!inThinking) { @@ -300,10 +302,12 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar let lastFinalIndex = 0; let inFinal = state.final; let everInFinal = state.final; + let everSawAnyFinalTag = state.final; for (const match of processed.matchAll(FINAL_TAG_SCAN_RE)) { const idx = match.index ?? 0; if (finalCodeSpans.isInside(idx)) continue; + everSawAnyFinalTag = true; const isClose = match[1] === "/"; if (!inFinal && !isClose) { @@ -327,7 +331,17 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar // Strict Mode: If enforcing final tags, we MUST NOT return content unless // we have seen a tag. Otherwise, we leak "thinking out loud" text // (e.g. "**Locating Manulife**...") that the model emitted without tags. + // + // Fallback: If the model never used ANY reasoning tags (/, + // open or close), it likely doesn't support the tag format at all (e.g. + // small Ollama models). In that case, return the processed text as-is + // rather than silently dropping the entire response. if (!everInFinal) { + if (!everInThinking && !everSawAnyFinalTag) { + // Model doesn't use reasoning tags at all — pass through. + state.inlineCode = finalCodeSpans.inlineState; + return stripTagsOutsideCodeSpans(processed, FINAL_TAG_SCAN_RE, finalCodeSpans.isInside); + } return ""; }