Files
open-multi-agent/docs
MyPrototypeWhat cb0b1ab667 feat(llm): cross-provider reasoning text fallback with opt-in (#223 Phase 2) (#260)
* feat(llm): cross-provider reasoning text fallback with opt-in (#223 Phase 2)

Phase 2 of #223 — wires the shared `<thinking>` text fallback into every
adapter's outbound path behind two new opt-in flags.

User-facing API (`AgentConfig` / `LLMChatOptions`):
- `preserveReasoningAsText?: boolean` — default `false`. When `true`,
  outbound IR-to-native conversion downgrades each `ReasoningBlock` that
  the target adapter cannot natively echo to inline `<thinking>` text and
  prepends it to the next text part (rather than dropping silently).
- `compressReasoningText?: boolean | { minChars?: number }` — defaults
  default-on when `preserveReasoningAsText` is true (per maintainer's #223
  guidance to "tie compress default-on to the preserve flag"). Mirrors the
  shape of `compressToolResults`; head+tail truncation defaults to 1200
  chars per block.

Adapter wiring:
- openai-common.ts: refactored to drop the never-wired (since #234)
  `OpenAIReasoningReplayOptions` type in favour of reading the AgentConfig
  flags. Replaced the private `reasoningBlockToThinkingText` /
  `truncateReasoningForReplay` helpers with the shared
  `reasoningBlockToInlineText` introduced in Phase 1.
- openai.ts / azure-openai.ts / copilot.ts pass the new flags through
  `buildOpenAIMessageList`. Subclasses (deepseek, grok, qiniu, minimax)
  inherit chat() so the wiring propagates automatically.
- anthropic.ts: foreign-provenance reasoning falls back to plain text when
  opt-in is set; own-provenance signed blocks still native-echo (Phase 1
  contract unchanged); own-provenance UNSIGNED blocks now also fall back
  to text rather than dropping silently (was always wasted otherwise).
- gemini.ts: same own/foreign-provenance pattern as anthropic, with the
  same outbound capability gate.
- bedrock.ts: capability is `'never'` (until the deferred signature
  follow-up restores native echo); all reasoning falls back to text when
  opt-in is set. Emits a standalone text block since Bedrock messages
  accept multiple content blocks.
- ai-sdk.ts: capability `'never'` — when opt-in is set, swaps the
  AI SDK structured `{type: 'reasoning'}` part for a text part wrapped
  in `<thinking>`. Default-off preserves pre-Phase-2 pass-through behaviour
  to avoid silently changing what existing users have today.

Tests:
- Refreshed openai-common.test.ts to use the new flag names. Added cases
  for `compressReasoningText: false` (no truncation) and the default-on
  semantics when preserve is true and compress is undefined.
- Added per-adapter `reasoning text fallback (#223 Phase 2)` describe
  blocks to anthropic, gemini, bedrock, and ai-sdk test files. Each
  covers the default-off drop, the opt-in text-fallback, the
  own-provenance native echo path, and (for anthropic) the redacted
  placeholder.
- Updated tests/reasoning-fallback.test.ts so `maxChars: Infinity` now
  asserts pass-through (no truncation) rather than clamp-to-1. The
  positive-Infinity sentinel is how callers signal
  `compressReasoningText: false`.

Docs + example:
- docs/context-management.md gains a "Preserving Reasoning Across
  Providers" section with capability table, default behaviour rules,
  and the loop-detector caveat for local OpenAI-compatible models.
- examples/patterns/cross-provider-reasoning.ts demonstrates the
  Agent.prompt() model-swap scenario and documents the loop-detector
  failure mode.

Refs #223

* test(llm): tighten reasoning-flag propagation test scope (#223 Phase 2 self-review)

Address self-review feedback before opening the Phase 2 PR:

- tests/runner-reasoning-flags.test.ts: rewrite header to honestly describe
  what is covered. `AgentRunner.stream()` internally drives turns via
  `adapter.chat()` (`runner.ts:766`) — `adapter.stream()` is on the
  `LLMAdapter` interface but unused by the runner. The previous "stream path"
  claim was therefore testing a non-existent code path. Drop the dead
  `capturedStream` instrumentation; replace it with a `runner.stream()`
  entrypoint test that exercises the same `adapter.chat()` propagation,
  pinning the contract for the stream-side flag flow.

- tests/reasoning-fallback.test.ts: add a runtime-invariant test for the
  `NO_TRUNCATION` constant. The Phase 2 commit exported the sentinel and
  documented it in JSDoc, but had no test that would CI-fail if a future
  refactor removed the `Number.POSITIVE_INFINITY` special-case branch in
  `resolveMaxChars`. The new assertion ties the constant value, the
  resolver branch, and the documented "no truncation" contract together.

Refs #223

* docs(context-management): acknowledge tool-use-only third capability state

After merging upstream/main (which added 'tool-use-only' for DeepSeek V4
via #251), update the capability table to include the new state and its
two sub-rules:
  - tool-calling conversation → native reasoning_content echo
  - non-tool conversation     → drop (DeepSeek spec)

Also clarify that 'tool-use-only' is the only capability where
same-provider native echo happens WITHOUT preserveReasoningAsText opt-in
(the DeepSeek API forces it for protocol compliance).

Refs #223, #251

* fix(llm): add missing nativeReasoningEchoProvider to ReasoningOutboundOptions

Repair an omission from merge commit 9ca9a91: the field was added to my
working tree alongside the openai-common.ts conflict resolution, but
never staged — so the merge commit landed openai-common.ts referencing
`outboundOptions?.nativeReasoningEchoProvider` while the type declaration
on the receiving end (`ReasoningOutboundOptions` in reasoning-fallback.ts)
did not carry the field.

Locally the bug was masked because vitest runs against the working tree
where the field exists; `npm test` reported 869/874 passing. The pushed
branch would compile-fail at the openai-common.ts access site for any
reviewer pulling the PR.

This commit just adds the field declaration + its JSDoc (already used
correctly throughout openai-common.ts and openai.ts). No behaviour change
relative to what the merge intended.

Refs #223, #251
2026-05-28 15:17:36 +08:00
..