diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 64a7c78ba..0619aa130 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,11 +4,14 @@ ### Changed +- Enhanced cross-model reasoning recovery to support additional thinking dialects and leakage patterns + - Demote cross-vendor reasoning to plain text when the target does not natively support it - Refine cross-model reasoning preservation to prevent leaking inert context into structured fields - Rendered demoted cross-model reasoning blocks in the target model's canonical thinking dialect - Improved reliability of AI model responses by implementing automatic retry logic for detected thinking-loop stalls - Changed cross-provider/cross-model thinking demotion to render the prior turn's reasoning in the target model's canonical inline thinking dialect (a ```` ```thinking ```` fence for Gemini, ``/`` tags for others) instead of bare prose, with a neutral `` fallback for control-token dialects (Harmony, Gemma) so chat-template tokens never leak into history. Replaying it as a native `thought` block was ruled out: end-to-end testing against Gemini 3 confirmed an unsigned `thought` part is schema-accepted but silently discarded — neither recalled nor influencing generation. +- Broadened the leaked-thinking stream healer (`StreamMarkupHealing`'s `thinking` pattern) to recover reasoning emitted in any dialect's canonical idiom — Gemini's ` ```thinking ` fence, Gemma's `<|channel>thought` channel, Harmony's `analysis` message, and `` — not just ``/`` tags, so leaked chain-of-thought is routed to thinking events for every dialect instead of rendered as raw markup ### Removed diff --git a/packages/ai/src/dialect/index.ts b/packages/ai/src/dialect/index.ts index 5fd103854..5734e0d64 100644 --- a/packages/ai/src/dialect/index.ts +++ b/packages/ai/src/dialect/index.ts @@ -11,4 +11,5 @@ export * from "./owned-stream"; // consumer needs (the legacy markdown `/dump` reuses its `` envelope // unwrap), so re-export only that symbol rather than `export *`-ing the rest. export { renderDelimitedThinking } from "./rendering"; +export * from "./thinking"; export * from "./types"; diff --git a/packages/ai/src/dialect/thinking.ts b/packages/ai/src/dialect/thinking.ts index f12c60652..af3c0c7a0 100644 --- a/packages/ai/src/dialect/thinking.ts +++ b/packages/ai/src/dialect/thinking.ts @@ -1,18 +1,30 @@ import { partialSuffixOverlapAny } from "./coercion"; import type { InbandScanEvent, InbandScanner } from "./types"; -const THINK_OPEN = ""; -const THINK_CLOSE = ""; -const THINKING_OPEN = ""; -const THINKING_CLOSE = ""; -const TAGS = [ - { open: THINK_OPEN, close: THINK_CLOSE }, - { open: THINKING_OPEN, close: THINKING_CLOSE }, -] as const; -const OPENS = [THINK_OPEN, THINKING_OPEN] as const; - type Tag = { readonly open: string; readonly close: string }; +/** + * Every dialect's in-band thinking section in its canonical `renderThinking` + * form (see the sibling `./*.ts` scanners). {@link ThinkingInbandScanner} heals + * reasoning a model leaked into its visible text channel back into thinking + * events, whichever dialect idiom the leak used. + * + * Plain (attribute-free) delimiters only — matching what `renderThinking` + * emits and what models leak in practice. Attributed or namespaced XML thinking + * tags (``, `antml:thinking`) are recovered by the owned + * anthropic-dialect parser, not this text-channel healing fallback. + */ +const TAGS: readonly Tag[] = [ + { open: "", close: "" }, // deepseek, glm, hermes, kimi, qwen3 (and anthropic/minimax/xml) + { open: "", close: "" }, // anthropic, minimax, xml + { open: "", close: "" }, // anthropic + { open: "```thinking\n", close: "```" }, // gemini fenced thinking + { open: "<|channel>thought\n", close: "" }, // gemma reasoning channel + { open: "<|start|>assistant<|channel|>analysis<|message|>", close: "<|end|>" }, // harmony analysis (rendered) + { open: "<|channel|>analysis<|message|>", close: "<|end|>" }, // harmony analysis (bare leak) +]; +const OPENS = TAGS.map(tag => tag.open); + export class ThinkingInbandScanner implements InbandScanner { #buffer = ""; #closeTag = ""; diff --git a/packages/ai/test/dialect-thinking.test.ts b/packages/ai/test/dialect-thinking.test.ts index 8e3a57365..f0d07afba 100644 --- a/packages/ai/test/dialect-thinking.test.ts +++ b/packages/ai/test/dialect-thinking.test.ts @@ -164,7 +164,6 @@ describe("kimi think tags (…)", () => { }); }); - describe("every dialect round-trips thinking (no missing thinking element)", () => { const dialects: Dialect[] = [ "anthropic", diff --git a/packages/ai/test/tool-examples.test.ts b/packages/ai/test/tool-examples.test.ts index b1c612567..84303ab6d 100644 --- a/packages/ai/test/tool-examples.test.ts +++ b/packages/ai/test/tool-examples.test.ts @@ -31,7 +31,6 @@ describe("renderToolExamples", () => { expect(rendered).toContain(""); }); - it("renders call example in hermes format", () => { const tool: InbandTool = { name: "find",