import { type AssistantMessage, type Model, type Usage, createAssistantMessageEventStream } from "../internal/llm.js";
import type { BrainRetryErrClass } from "../core/types.js";
import type { AssertAllKeysHandled } from "../core/ask-origin.js";
import { type RequestRoute } from "./request-route.js";
import { type ProviderDialect } from "./provider-dialect.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 first-byte window + 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, and for the
     * mid-stream tier-C re-send that shares this budget. Absent ⇒ {@link resolveMaxRetries} (the
     * `SEMA_MAX_RETRIES` env, else 10). An explicit value here is authoritative and NOT clamped —
     * deployment sovereignty, same posture as the timeout fields. Sovereignty covers a chosen NUMBER: a
     * non-finite value (CLS-A-8) falls back to the env/default instead of disabling the budget.
     */
    maxRetries?: number;
    /** First-retry backoff ms; each further attempt doubles it (capped, +≤25% jitter). Default 500. */
    retryDelayMs?: number;
    /**
     * Retries allowed for a DETERMINISTIC connect failure — one whose error codes say the target itself
     * is not there (`ECONNREFUSED`, or a name with no address) or that the proxy on the way refused the tunnel
     * (`PROXY_TUNNEL_REFUSED`); see `errors.ts` `readConnectFailure`.
     * Absent ⇒ {@link DEFAULT_DETERMINISTIC_CONNECT_RETRIES}.
     *
     * These retries exist for one case only: a peer that is mid-restart and about to accept. Waiting
     * longer cannot change any other outcome — re-sending at an address where nothing listens gets the
     * same refusal in milliseconds — so spending the full transient budget on it buys nothing and costs
     * an unattended caller minutes of silence.
     *
     * The value is capped by {@link maxRetries} (this lane can only ever be the SHORTER of the two).
     * Setting it AT or ABOVE `maxRetries` turns the lane off entirely: budget and backoff both revert to
     * the shared transient ladder, which is the opt-out for a deployment that wants the old behavior.
     */
    deterministicConnectRetries?: number;
}
/**
 * Resolve the transient-failure retry budget — explicit config > `SEMA_MAX_RETRIES` env > default 10
 * (the same three-level shape as the shell's timeout caps). A non-numeric, negative, or empty env
 * value is ignored rather than silently meaning "zero retries"; an oversized one is clamped.
 */
export declare function resolveMaxRetries(configured: number | undefined): number;
/**
 * Resolve the deterministic-connect retry budget. Same posture as {@link resolveMaxRetries}: a chosen
 * NUMBER is honored, a non-finite or negative one falls back to the default rather than silently
 * meaning something (a NaN here would compare false against every budget gate and disable the lane by
 * accident, which is the sentinel-inversion shape CLS-A-8 pinned for the shared budget).
 */
export declare function resolveDeterministicConnectRetries(configured: number | undefined): number;
/**
 * Is `rebuilt` still THE SAME REQUEST as `original`, differing only in the output cap and the values
 * that follow from it (ruled 2026-08-05)?
 *
 * `buildRequest` re-reads the adapter's model, config, options and context, so a re-shaped re-send is
 * only trustworthy to the extent that those did not move underneath it. Rather than deep-copying all
 * of them, the two built requests are compared directly: same destination, same headers, and a body
 * identical outside {@link SSERequest.capDerivedPaths}. That covers a changed model id, prompt, tool
 * set, temperature, reasoning posture, or passthrough parameter with one rule instead of a list of
 * things to remember.
 *
 * Conservative on anything it cannot read: a body that is not a JSON object compares by exact string,
 * so an unparseable pair declines rather than being waved through.
 */
export declare function sameRequestModuloCap(original: SSERequest, rebuilt: SSERequest, imposedCap: number): boolean;
/**
 * CC parity (`Q1b`, ruled 2026-08-05) — read the provider's EXPLICIT retry verdict off the response.
 *
 * The status code is an inference about whether a re-send can help; this header is the provider
 * SAYING so, and it outranks the inference in BOTH directions:
 *   - `x-should-retry: true`  — retry even a status the predicate calls terminal (the upstream knows
 *     the condition is transient, e.g. a fenced-off shard behind a 4xx).
 *   - `x-should-retry: false` — do NOT retry even a 5xx/429/408/409. Without this, a provider that
 *     has already decided the request is unservable is re-sent the same request up to the whole
 *     budget (10 by default), which costs the caller and the upstream alike for a known-refused call.
 *
 * Returns `undefined` for an absent header AND for any value other than the two exact tokens — "no
 * opinion", so the status-code predicate decides and the pre-existing behavior is byte-identical.
 * Exact-match (no case folding / trimming) is deliberate: it mirrors CC's comparison, and `Headers`
 * already normalizes surrounding whitespace, so anything else reaching here is a value we did not
 * define and must not guess at.
 *
 * Tolerates a `doFetch` (BYOM injection point) whose Response-shaped return has no usable `headers`.
 */
export declare function shouldRetryHeaderVerdict(res: Response | undefined): boolean | undefined;
/**
 * #530 — the first-token WAIT disclosure. While a first-token watchdog is armed
 * (`firstTokenTimeoutMs > 0`) and no content token has arrived, the engine says so on the status
 * channel: the FIRST `waiting_first_token` frame goes out once the wait has lasted
 * {@link WAITING_FIRST_TOKEN_AFTER_MS}, and one more every {@link WAITING_FIRST_TOKEN_EVERY_MS} after
 * that, until the first token lands, the watchdog fires, or the attempt ends. A short wait (under the
 * threshold) announces nothing — the frame exists for the unattended "no answer for a minute" case,
 * not for every request. A wait whose watchdog fires AT the threshold is disclosed once, at that
 * instant, before the cut is announced. The same 30s slice the retry countdown re-announces on.
 */
export declare const WAITING_FIRST_TOKEN_AFTER_MS = 30000;
export declare const WAITING_FIRST_TOKEN_EVERY_MS = 30000;
export interface SSERequest {
    url: string;
    headers: Record<string, string>;
    body: string;
    /**
     * The per-request output cap this request ACTUALLY carries on the wire, when the adapter sets one
     * (the adapters disagree on the body key, so the engine cannot read it off `body`). Absent means
     * "no cap sent" or "this adapter does not report it".
     *
     * The context-overflow recovery needs it because the number the PROVIDER echoes back in its error
     * is not evidence of what was sent: a stale or malformed 400 reporting a larger cap than the
     * request carried would otherwise let the recovery "lower" the cap to a value ABOVE the caller's
     * own explicit bound. It doubles as the check that an adapter honored an imposed cap at all.
     */
    outputCapTokens?: number;
    /**
     * The body PATHS this adapter may legitimately re-derive when the engine imposes a different output
     * cap — the cap's own key(s), plus anything computed from it. Dotted for nested leaves
     * (`"thinking.budget_tokens"`), deliberately: naming the whole `thinking` object would excuse a
     * change of reasoning MODE as if it followed from the cap, when only the budget does.
     *
     * The engine uses this to check that a re-shaped re-send is still THE SAME REQUEST. `buildRequest`
     * re-reads the adapter's model/config/options/context, so a deployment that changes any of them
     * while a call is in flight could otherwise turn "lower the cap and try again" into a request with
     * a different model id, prompt, tool set, or reasoning posture. Everything outside these paths must
     * come back identical; an adapter that declares nothing therefore permits no body change at all.
     */
    capDerivedPaths?: readonly string[];
    /**
     * The keys `Model.extraBody` carried that this adapter owns and therefore stripped from the body
     * ({@link import("./request-params.js").applyExtraBody}'s `dropped`), names only; empty or absent when
     * nothing was stripped. The engine reports them to the run's sink ({@link reportExtraBodyDropped}) when this
     * request is committed to be sent, which is where the Runner announces them.
     */
    extraBodyDropped?: readonly string[];
    /**
     * The route this request takes, as the adapter that built it states it ({@link RequestRoute}, minted by
     * `requestRouteOf` from the same credential and root resolutions the build used). The engine lifts the committed
     * request's route onto the terminal shell of a provider failure (`AssistantMessage.apiErrorRoute`, beside
     * `isApiErrorMessage`) — every failure of the request states the same route, so it is declared once here rather
     * than on each failure. Absent = the adapter states none.
     */
    route?: RequestRoute;
    /**
     * Called by the engine once, if and only if THIS request becomes the one that is sent.
     *
     * `buildRequest` is also called SPECULATIVELY — the context-overflow recovery builds a candidate
     * and may then decline it. Any bookkeeping the adapter keeps for its own later diagnostics (which
     * output cap actually went out, which knob supplied it, whether thinking was requested) therefore
     * belongs here rather than in the build: assigned during a build, a rejected candidate's numbers
     * would outlive it and be reported against the request that really was sent.
     */
    onCommitted?: () => void;
}
/**
 * Per-attempt request overrides the ENGINE imposes on a re-send (ruled 2026-08-05). Absent — which is
 * every ordinary build — the adapter shapes its request exactly as before, byte for byte.
 *
 * An adapter MUST honor every field it can express. Ignoring one is not silent: the only producer is
 * the context-overflow recovery, whose no-progress guard sees the unchanged cap come back in the next
 * provider error and fails loud with the original failure instead of looping.
 */
export interface BuildRequestOverrides {
    /**
     * The per-request output cap to send, outranking every caller/model knob for THIS attempt. Set when
     * the provider reported that `input + max_tokens` exceeds the context limit; whichever body key the
     * adapter uses for the cap (`max_tokens` / `max_completion_tokens` / `max_output_tokens`) must carry
     * this value, and any field derived from the cap must be re-derived from it — the point of routing
     * this through the adapter rather than patching the serialized body.
     */
    maxOutputTokens?: number;
}
/** Controls that the engine passes to the per-stream parser. */
export interface StreamControls {
    out: ReturnType<typeof createAssistantMessageEventStream>;
    /** 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;
    /** The endpoint this call goes to, as the provider dialect table names it (`undefined` = none declares it):
     *  what an in-band error frame is judged by. */
    dialect: ProviderDialect | undefined;
    /** Hand the engine what this attempt produced — its final content and the provider-reported usage — before
     *  raising the provider's own in-band failure from `finalize`: the terminal message the engine mints for that
     *  failure keeps it (the text the provider streamed, the usage it reported), rather than an empty shell. */
    keepOutput(output: KeptOutput): void;
}
/** The output a parser hands over with {@link StreamControls.keepOutput}. */
export interface KeptOutput {
    content: AssistantMessage["content"];
    usage: Usage;
    /** The provider reported no usage frame (the zeros are not a measurement). */
    usageMissing?: true;
}
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.
     *
     * `truncated: true` = this finalize is a design/124 tier-A PROMOTION of a mid-stream failure
     * (stall / connection cut / in-band overload downgrade): the provider never terminated the
     * stream, so any still-open prose segment was CUT, not closed. A parser that mints segment-end
     * frames (`text_end`) must withhold them on this arm — the TaskEvent contract promises the frame
     * means "the model closed the block" (HONEST ABSENCE otherwise) — while still building the same
     * final message (the facade stamps `partialFinalized` + the warning on the terminal event).
     */
    finalize(opts?: {
        truncated?: boolean;
    }): 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).
     * - `terminalFrameSeen` — the provider TERMINATED this stream (openai `finish_reason`, anthropic
     *   `message_delta.stop_reason`, responses `response.completed`/`response.incomplete`). A FACT, not a tier:
     *   the parser reports it off the same state its own finalize reads, and the tiering owns what it means.
     */
    snapshot(): {
        hasSubstantiveText: boolean;
        hasCompletedToolCall: boolean;
        hasOnlyThinking: boolean;
        terminalFrameSeen: 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;
}
/**
 * The phase a retry-wait frame of each {@link BrainRetryErrClass} carries — ONE table, so a frame's two
 * classifications are never mapped separately: the phase is what the brain is doing about the wait (waiting
 * out a rate limit, re-connecting, re-sending), read off why it waits. Every retry-wait frame this engine
 * emits — the connect ladder, the first-byte lane, the output-cap re-send, the mid-stream tiers — reads it.
 */
declare const RETRY_PHASE_OF_ERR_CLASS: {
    readonly connect_refused: "reconnecting";
    readonly transport: "reconnecting";
    readonly stall: "reconnecting";
    readonly rate_limit: "rate_limited";
    readonly server: "retrying";
    readonly http: "retrying";
    readonly output_cap: "retrying";
};
/** Compile-time fence: every retry class has a phase row. */
export type RetryPhaseTableCoversEveryErrClass = AssertAllKeysHandled<Exclude<BrainRetryErrClass, keyof typeof RETRY_PHASE_OF_ERR_CLASS>>;
/**
 * 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). Called again with
     *  {@link BuildRequestOverrides} when the engine re-shapes a re-send (context-overflow recovery). */
    buildRequest: (overrides?: BuildRequestOverrides) => SSERequest;
    /** Create the per-stream parser once the partial + controls exist. */
    makeParser: (ctrl: StreamControls) => StreamParser;
    /**
     * 1.296 件A — per-call ADVISORY stall watchdogs (`StreamOptions.stallTimeouts`). Priority chain:
     * brain construction-time config (`BrainTimeoutConfig.firstByteTimeoutMs`/`firstTokenTimeoutMs`/
     * `idleTimeoutMs`) wins whenever PRESENT — including an explicit 0, which means "deliberately
     * disabled" — and only an ABSENT (undefined) field adopts the advisory value; both absent ⇒ the
     * engine default constant for that segment (`timeout.ts`). `firstByteMs` bounds the
     * fetch→response-headers wait: the engine's post-headers timers can't see a gateway that never
     * returns headers. The connect clock is not per-call: it is the process dispatcher's (`transport.ts`).
     */
    stallTimeouts?: {
        firstByteMs?: number;
        firstTokenMs?: number;
        idleMs?: number;
    };
    /** Per-call CAP on the transient retry budget (`StreamOptions.maxRetries`): the call retries at most this many times
     *  and never more than the brain's own budget ({@link resolveMaxRetries}); a non-finite or negative value caps nothing. */
    maxRetries?: number;
}): ReturnType<typeof createAssistantMessageEventStream>;
export {};
