import type { InvokeTransport } from "./types.js"; /** * Read `Retry-After` as WHOLE SECONDS, or `undefined`. * * HTTP also allows an absolute HTTP-date, which is deliberately NOT parsed: * the pod only ever emits a delta-seconds integer, and silently mis-reading a * date as `NaN` seconds is worse than falling back to the fixed delay. * * Exported because every transport that builds an {@link AgentResponseError} * has to fill `retryAfterSeconds` the same way for {@link withSaturationRetry} * to honour the same hint — a second hand-rolled regex in a host adapter is * exactly the drift this module exists to prevent. */ export declare function parseRetryAfterSeconds(header: string | null): number | undefined; /** Options for {@link withSaturationRetry}. */ export interface SaturationRetryOptions { /** * The saturation-retry wait. Injectable so tests drive the retry without a * real 15s timer; production uses an abort-aware `setTimeout`. */ sleep?: (ms: number, signal: AbortSignal) => Promise; /** * Total send attempts on a saturated pod (guuey#406). Default 1 retry * (2 attempts) — the historical behavior; capped at 5. A capacity-1 pod * (demo fixtures, xs plans) refuses the SECOND simultaneous visitor, so * end-user surfaces budget higher and pair it with `onSaturationWait` so * the wait is a visible busy state, never a silent hang or a generic * error boundary (the 2026-08-24 standalone incident). */ attempts?: number; /** * Fired before each saturation wait — the surface's hook for an honest * "the agent is helping someone else" state. Never fired for other error * classes; the turn stays `connecting` throughout (the hook's state * machine deliberately has no `retrying` status). */ onSaturationWait?: (info: { attempt: number; totalAttempts: number; waitMs: number; }) => void; } /** * Wrap an invoke transport with ONE automatic retry on a saturated pod. * * ## What retries, and what deliberately does not * * `POD_SATURATED` (503) means the pod is at its concurrent-turn cap right now * — a transient queue state that clears as in-flight turns finish, so a single * delayed re-send usually just works. The wait is the pod's own `Retry-After` * hint (via {@link AgentResponseError.retryAfterSeconds}), defaulting to 15s * when it sent none and capped at 30s. * * `DRAINING` (also 503 + `Retry-After`) is NOT retried in v1. The refusing pod * is shutting down: its readiness probe is already failing and the endpoint * pull is in flight, so the useful retry is the one that reaches a DIFFERENT * pod — and a wrapped transport re-sends to the same URL. Retrying here would * spend the user's 15s to arrive back at the same draining pod (or at a fresh * one by luck), which is not a guarantee worth building on. When the retry can * be made routing-aware, this is the code to revisit. * * Exactly ONE retry: a second saturation propagates as * {@link AgentResponseError}, so a genuinely overloaded agent surfaces instead * of looping. Nothing is retried once a chunk has been yielded — replaying * mid-stream would duplicate a partial assistant turn (the same `yielded` * guard the widget's `withIdentifiedToken` 401-retry uses). An abort during * the wait skips the retry and surfaces the original refusal. * * The retry is INVISIBLE to `useAgentInvoke`: no frames were yielded, so the * turn simply stays in `connecting` for the duration of the wait. There is no * `retrying` status by design — the hook's state machine describes the pod's * turn lifecycle, not the transport's plumbing. * * The wrapped transport is re-invoked from scratch for the retry, so a host * that resolves identity inside its own generator (Portal's RN transport reads * the bearer bridge per attempt) re-reads it on the second try rather than * replaying a token that may have expired during the wait. */ export declare function withSaturationRetry(transport: InvokeTransport, options?: SaturationRetryOptions): InvokeTransport; /** Options for {@link withColdStartRetry}. */ export interface ColdStartRetryOptions { /** * Retries after the initial attempt (`0` disables the wrapper's behaviour * entirely). Default 3 — a small, bounded budget: the point is parity with * guuey's first-party embeds during the ordinary post-redeploy window, not * riding out an outage. Raise it for an unattended harness that would * rather wait than fail. */ attempts?: number; /** * First wait in ms; each subsequent wait doubles, capped at * {@link maxDelayMs}. Default 2000 → 2s / 4s / 8s for the default budget. */ baseDelayMs?: number; /** Ceiling on any single wait (hinted or computed), in ms. Default 10000. */ maxDelayMs?: number; /** The wait itself — injectable so tests drive the retry without timers. */ sleep?: (ms: number, signal: AbortSignal) => Promise; } /** * Wrap an invoke transport with a bounded retry on cold-start 503s * (guuey#186 Gap 3 — parity with first-party embeds, which already carry * this behaviour; SDK consumers were eating the raw 503 window instead). * * Matches ONLY {@link isColdStartRefusal} — an envelope-less 503 — and * retries up to `attempts` times with doubling, capped backoff (honouring a * `Retry-After` hint when the response carried one). Exhaustion propagates * the final refusal untouched. * * Nothing is retried once a chunk has been yielded: a stream that dies * MID-turn is never silently re-POSTed — the turn may have had side effects * and the consumer already saw partial output. Same `yielded` guard as * {@link withSaturationRetry}, same reasoning. An abort during a wait * surfaces the refusal that caused the wait. * * Like the saturation wrapper, the retry is invisible to `useAgentInvoke` * (the turn stays in `connecting`), and the wrapped transport is re-invoked * from scratch so per-attempt identity resolution re-runs. */ export declare function withColdStartRetry(transport: InvokeTransport, options?: ColdStartRetryOptions): InvokeTransport; //# sourceMappingURL=saturation-retry.d.ts.map