import { type AssistantMessage, type Model, createAssistantMessageEventStream } from "../internal/llm.js"; import { type BrainTimeoutConfig } from "./timeout.js"; /** * Shared streaming engine for the OpenAI- and Anthropic-shaped brains (design/32, consolidation). * * It owns the **bug-prone machinery that was duplicated verbatim** across `openai.ts` and * `anthropic.ts` — the connect-timeout + retry loop, the three-tier timer logic (first-token + idle * watchdog) with their flag resets, the SSE read loop + line framing, the one-shot cleanup, and the * `.catch/.finally` error wrapping. Centralizing the timer logic is the point: the same first-token / * idle reset bug was fixed twice (1.38.2 / 1.40.2) precisely because this lived in two copies. * * Each provider keeps its OWN request-shaping (`buildRequest`) and SSE parsing + finalization * (`makeParser`) — those genuinely differ (chat-completions deltas vs typed content-block events, * thinking signatures, per-provider usage/finish-reason) and stay verbatim in the adapters, so this * refactor is behavior-preserving (safety net: resilience/streaming/brain tests). */ export interface StreamEngineConfig extends BrainTimeoutConfig { /** Retries for transient failures (network / 5xx / 429) BEFORE the body streams. Default 2. */ maxRetries?: number; /** Base backoff ms (exponential * full-jitter). Default 400. */ retryDelayMs?: number; } export interface SSERequest { url: string; headers: Record; body: string; } /** Controls that the engine passes to the per-stream parser. */ export interface StreamControls { out: ReturnType; /** Shared mutable partial-snapshot message; the parser mutates `content` and emits `{ ...partial }`. */ partial: AssistantMessage; /** Call on every CONTENT delta (text/thinking/tool input) — resets the first-token + idle watchdogs. */ sawContentToken(): void; /** Cancel the underlying stream (e.g. degenerate-repetition cutoff). The read loop then ends. */ cancel(): void; } export interface StreamParser { /** Handle one raw SSE line (`data: {...}`); parse, emit events, call `ctrl.sawContentToken()` on content. */ onLine(line: string): void; /** Build + emit the final `done`/`error` message after the stream ends. */ finalize(): void; /** * design/124 §0.5-2: introspection for the mid-stream failure tiering. Read directly off the * parser's accumulated state (cheap, no side effects): * - `hasSubstantiveText` — non-blank answer text (or a streamed refusal) has been emitted → tier A * (partial finalize) territory. * - `hasCompletedToolCall` — at least one tool call CLOSED successfully (a `toolcall_end` was * emitted, so an in-stream executor may already be running it) → tier A; also the retry-safety * assertion (§0.5-4): a re-send is FORBIDDEN once this is true (double execution). * - `hasOnlyThinking` — thinking streamed but nothing substantive (no non-blank text, no completed * tool call) → tier B (seal + whole-turn retry). A dangling tool-call accumulation alongside the * thinking does not veto this (nothing was admitted; the retry's snapshot replacement drops it). */ snapshot(): { hasSubstantiveText: boolean; hasCompletedToolCall: boolean; hasOnlyThinking: boolean; }; /** * design/124 §0.5-2 tier B: seal the UI stream before a whole-turn retry — emit `thinking_end` for * any OPEN thinking block (the block state is parser-private; the engine cannot emit a correct * close). The retry attempt's parser may then legally REOPEN a thinking block at the SAME * contentIndex — consumers render by partial snapshot (replacement semantics), so index reuse is * part of the event contract (see AssistantMessageEvent thinking_start note). Idempotent. */ sealForRetry(): void; } /** * Run one streaming request end-to-end and drive the provider `parser`. Returns a fresh event stream; * never throws (errors are pushed as `error` events, mirroring the Brain contract). */ export declare function runStreamingBrain(args: { model: Model; doFetch: typeof fetch; signal?: AbortSignal; config: StreamEngineConfig; /** HTTP error message prefix (e.g. "gateway" / "anthropic"). */ httpLabel: string; /** Build the one request. May throw (→ surfaced as an error event). */ buildRequest: () => SSERequest; /** Create the per-stream parser once the partial + controls exist. */ makeParser: (ctrl: StreamControls) => StreamParser; }): ReturnType; //# sourceMappingURL=stream-engine.d.ts.map