/** * Cross-provider reasoning preservation via `preserveReasoningAsText` * * When an `Agent.prompt()` conversation runs against a "reasoning" model that * emits `reasoning_content` (OpenAI o-series, DeepSeek reasoner, Anthropic * extended-thinking-via-Bedrock, etc.), the model's intermediate thought * stream is normally either: * * (a) native-echoed back when the next turn targets the same provider with * a valid signature (Anthropic, Gemini own-issued path), OR * (b) silently dropped on the next outbound conversion (everyone else). * * Opt into {@link AgentConfig.preserveReasoningAsText} to replace (b) with a * downgrade — the prior reasoning is wrapped in `...` * text and prepended to the next assistant message, so the receiving model * has full context of what was previously considered. See #223 for design. * * Run: * npx tsx examples/patterns/cross-provider-reasoning.ts * * Prerequisites: * ANTHROPIC_API_KEY * OPENAI_API_KEY (uses an o-series model for the reasoning emit) */ import { Agent } from '../../src/index.js' // --- Caveat: loop-detector interaction -------------------------------------- // // Some OpenAI-compatible local models (and a few hosted ones at low quality // tiers) re-emit `...` text back into their assistant // response as if the tag were an instruction template. When this happens // across multiple turns, the framework's loop-detector may flag the agent as // stuck because the assistant text matches turn-over-turn. // // Mitigation if you hit this: // - Set `loopDetection: { enabled: false }` on the AgentConfig (loses the // safety net for other genuine loop cases — use as a debug measure only). // - Switch to a model that ignores `` text on input (most o-series // and Claude models do; problem is mostly with smaller local models). // - Disable `preserveReasoningAsText` entirely; accept reasoning loss. // // The example below does NOT trigger the issue against gpt-5 / Claude, but is // included so contributors testing local models know what to watch for. // ---------------------------------------------------------------------------- async function main(): Promise { if (!process.env['ANTHROPIC_API_KEY'] || !process.env['OPENAI_API_KEY']) { console.log('Skip: ANTHROPIC_API_KEY and OPENAI_API_KEY both required.') process.exit(0) } // Step 1: a reasoning-capable OpenAI model produces a turn that includes // a `reasoning_content` chunk. The adapter extracts it into a ReasoningBlock // with `provenance: 'openai'`, stored in the agent's persistent history. const agent = new Agent({ name: 'cross-provider-demo', model: 'gpt-5', // o-series-style reasoning model provider: 'openai', systemPrompt: 'You reason carefully then give a short final answer.', preserveReasoningAsText: true, // compressReasoningText defaults to `true` whenever preserve is true, // so long reasoning chains are head+tail truncated to 1200 chars by // default. Override with `{ minChars: N }` to tune, or `false` to disable // (footgun — long CoT will eat your prompt budget). compressReasoningText: { minChars: 1500 }, }) const firstAnswer = await agent.prompt( 'A train leaves Boston at 60 mph heading west. Another leaves NYC at 80 mph heading north. ' + 'After 2 hours, what is the straight-line distance between them? Show your reasoning briefly.', ) console.log('--- Turn 1 (OpenAI o-series) ---') console.log(firstAnswer.output.slice(0, 500)) // Step 2: swap the model to Anthropic mid-conversation. Without // `preserveReasoningAsText`, the prior reasoning would be silently dropped // because Anthropic's outbound only round-trips its own-signed thinking // blocks. With opt-in on, the prior OpenAI reasoning is replayed as // `` text so Claude sees the chain. // // (Note: in real code you'd construct a new Agent or use a per-call model // override. The Agent instance owns its adapter; this example keeps things // short and just illustrates the persistence model.) console.log('\n(Hypothetical: if you constructed a second Agent against ' + 'Anthropic and replayed the same messageHistory, the prior OpenAI ' + 'reasoning would arrive as `` text on its first request.)') } void main().catch((err: unknown) => { console.error(err) process.exit(1) })