/**
 * throttleRetry — when an MCP server refuses a request with a response the
 * retry policy recognises (HTTP 429), wait and ask again.
 *
 * ── Why this exists ──────────────────────────────────────────────────────────
 * Managed MCP gateways enforce per-principal rate limits. A throttled
 * `tools/call` is not a fault, it is the endpoint working as designed — and
 * before 8.11.0 it arrived at the model as a thrown tool error, which the model
 * reads as **"this tool is broken"**. It then apologises, picks a different
 * tool, or invents an answer. A designed, transient, self-clearing condition
 * was being turned into a wrong answer.
 *
 * ── Who may ask again: the app, by declaration (`replay.ts`) ─────────────────
 * HTTP 429 says only that the request was rate-limited (RFC 6585 §4). It does
 * not prove the server ran nothing, and `Retry-After` says when to ask again,
 * not that asking is safe. So permission to send a tool call again is the
 * APP's, declared per (server, tool) when the client is built (`mcpClient({
 * replay })`), and kept apart from what is known of an attempt's effect. Before
 * every repeat the series asks `permit`; the record keeps its answer.
 *
 *   - `'compatibility'` (the default, kept in 12.0) keeps today's behaviour by
 *     name: a 429 is repeated for every tool, ASSUMING the server refused it
 *     before running it — and every such repeat records that assumption.
 *   - `'app-authorized'` (opt-in, by name) repeats a 429 only for a tool the
 *     app declared repeatable on it.
 *   - A request that failed in transport without an answer (footprintjs's
 *     `network`: a lost connection, a timeout below the series) is repeated
 *     only when the app declared that for the tool. A 5xx is an answer, never
 *     repeated. A cut or a deadline ends the series.
 *   - Under both, a tool the app declares as making changes is never repeated
 *     automatically unless the server is declared to deduplicate by a key the
 *     call carries — footprintjs's side-effect guard, now wired here.
 *
 * The loop takes its question as a {@link RetryDecision}, so the one other
 * refusal a service documents as one to retry — AgentCore Runtime's "session
 * operation in progress" — reuses this loop rather than growing a second one.
 * That decision is applied only by that vendor's own transport, and its rule
 * carries the adapter's retry assumption (`RetryRule.assumes`) — a policy,
 * never evidence: its attempts record effect `unknown`, as a 429's do.
 *
 * ── Why it lives at the fetch seam and not around `Tool.execute` ─────────────
 * Because `Retry-After` is only reachable here. The MCP SDK reads the response,
 * throws `StreamableHTTPError(status, text)` and drops the `Response` — so by
 * the time a throttle reaches `Tool.execute` the header is gone and the status
 * survives only as `err.code`. A retry wrapper around the tool could guess a
 * backoff; only a wrapper around `fetch` can honour the number the server
 * actually asked for. Honouring it is also what keeps a fleet of agents from
 * synchronising into a thundering herd.
 *
 * ── Ordering: retry OUTSIDE, signing and vending INSIDE ──────────────────────
 * The wrapper composes over the caller's `fetch` (a SigV4/DPoP signer) or over
 * `createVendingFetch` (a per-request credential). Sitting outside means every
 * attempt is signed afresh and vended afresh — signatures expire, and a token
 * that would have died during the wait is simply never the one reused. The
 * gateway secrecy invariant is untouched: each attempt vends, applies, drops.
 *
 * ── The loop is footprintjs's attempt runner, one series per call ────────────
 * Each call runs ONE `attempt()` series (footprintjs, used as it ships). Its
 * runner makes an attempt record per try. `onRetry`, before each wait, is what
 * reaches a caller; the RUN gets the records: for a call the agent dispatched,
 * the series files every attempt and how the request ended into that call's
 * run (`seriesEvidence.ts`, through the connection's `requestLedger.ts`). An
 * attempt sends the request once, and an attempt that got a RESPONSE is not an
 * operation that succeeded: a 503 is delivered as the answer it was, never
 * retried — and written down as `answered` with its status, never success.
 * A response a rule's decision recognises is thrown as a refusal that names
 * its kind, and the policy's `retryOn` accepts those refusals — and a network
 * failure the app declared for the tool — when the replay rule permits them,
 * and nothing else: a 5xx ends the series on the attempt it happened in, as it
 * always did. When the series ends on a refusal (attempts spent, a ceiling
 * reached, no time left, no permission), the refusal's response is delivered
 * as the server sent it: the caller sees the 429, never an error of ours. The
 * call decides what that means for it (`callLifetime.ts`): under
 * 'app-authorized', a call that genuinely lacked an answer somewhere and does
 * not complete fails as "may have run". This series reports the HTTP half of
 * that — an attempt that got no response at all, and the status it delivered;
 * a received refusal is an answer, its effect still unknown.
 *
 * ── A fetch that already retries is JOINED, never wrapped ────────────────────
 * A transport whose own fetch runs a series (AgentCore Runtime's session-busy
 * retry) says how to rebuild itself with one more rule (`joinableRetry`).
 * `retryingFetch` over such a fetch — what `mcpClient` applies to every http
 * transport — adds its 429 rule to THAT series instead of starting a second
 * one around it. One series means one attempt count, one waiting total and one
 * deadline per call, by construction: the layers can never multiply requests.
 * The series' FIRST rule holds the call's budget — its `maxAttempts`,
 * `maxWaitMs` and `maxElapsedMs` bound every attempt and wait, whatever the
 * reason. A rule that joins keeps its own values as limits on its own kind:
 * they can stop its retries sooner, never make the call longer.
 *
 * ── The wait: a stated one wins, whole ───────────────────────────────────────
 * Each retry's wait is `statedWaitFirst(retry-after, exponentialBackoff({ …,
 * maxDelayMs }))`: the server's `Retry-After` when it sent one, else our
 * jittered backoff, whose CEILING is capped at `maxDelayMs` before the draw
 * (so capped waits keep their jitter). The cap is on the SCHEDULE only —
 * never on a stated wait — so the footprintjs policy's `maxBackoffMs`,
 * which would cut every wait and send the request again before the server
 * said it may, is left unset. A stated wait too long for `maxWaitMs` or the
 * deadline is never shortened either: the series stops, and the 429 is
 * delivered as the server sent it.
 *
 * Pattern: Decorator over `fetch`. Role: Layer-3 tool transport.
 */
import { type ReplayDeclaration } from './replay.js';
import type { RequestLedger } from './requestLedger.js';
/**
 * The fetch shape the MCP SDK's `fetch` hook uses — deliberately NARROWER
 * than `gatewayTransport`'s `FetchLike` (no `Request` input), because that is
 * exactly what `StreamableHTTPClientTransport` declares and what
 * `McpHttpTransport.fetch` promises a consumer. A wider `FetchLike` still
 * passes in: a function that accepts more inputs satisfies a contract that
 * supplies fewer.
 */
export type ThrottleFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
/**
 * Which refusal a retry answers: HTTP 429 (`'throttle'`), or AgentCore
 * Runtime's "session operation in progress" (`'session-busy'`).
 */
export type RetryKind = 'throttle' | 'session-busy';
/** Reported to {@link ThrottleRetryOptions.onRetry} before each wait. */
export interface ThrottleRetryInfo {
    /**
     * Which refusal this retry answers — or `'network'`, a tool call the app
     * declared repeatable after a request that failed without an answer
     * (`McpClientOptions.replay`). It is reported to the series' first rule.
     */
    readonly kind: RetryKind | 'network';
    /** The attempt about to START. `2` is the first retry. */
    readonly attempt: number;
    /** Ceiling in force for this refusal, including the first attempt. */
    readonly maxAttempts: number;
    /** How long we are about to wait, in milliseconds. */
    readonly waitMs: number;
    /** What the server's `Retry-After` asked for, when it sent one. Absent
     *  means the wait below is our own jittered backoff. */
    readonly retryAfterMs?: number;
    /**
     * The ORIGIN of the endpoint being retried — scheme, host and port, nothing
     * else. Never its path, user info (`user:pass@`), query or fragment: a
     * token, a credential or a customer id can ride in any of them. The empty
     * string when the URL has no origin (a relative one).
     */
    readonly url: string;
    /**
     * The {@link ThrottleRetryOptions.endpointLabel} you declared for this
     * endpoint — absent when you declared none. Never derived from the URL.
     */
    readonly endpoint?: string;
}
/**
 * Fine-tuning for {@link McpClientOptions.retryOnThrottle}, and — with its own
 * defaults — for `agentCoreRuntimeMcpTransport`'s `retry`. The defaults below
 * are `retryOnThrottle`'s.
 *
 * Every limit here is the CALL's — unless this retry joins a series a
 * transport already runs (an AgentCore Runtime transport under `mcpClient`).
 * Then the transport's own values are the call's limits, and these limit only
 * the 429 retries inside them: each can stop a 429 retry sooner (a 429 is
 * retried only while the call has made fewer attempts than `maxAttempts`, has
 * waited less than `maxWaitMs`, …), never make the call longer. The deadline
 * that cuts a request in flight is the transport's `maxElapsedMs`.
 */
export interface ThrottleRetryOptions {
    /** Total attempts including the first. Default 3. Minimum 1. */
    readonly maxAttempts?: number;
    /**
     * Ceiling on the TOTAL time spent waiting across one call, in
     * milliseconds. Default 10000.
     *
     * This is the safety rail on `Retry-After`: a server (or a misconfigured
     * proxy) that asks for a 24-hour wait does not get one — when the next wait
     * would push past this ceiling we stop retrying and let the throttle
     * surface, so the worst case is a slower failure and never a hung agent.
     */
    readonly maxWaitMs?: number;
    /**
     * A deadline for the whole call, in milliseconds from its start — requests
     * included, not just waits. A wait that would end past it is never begun
     * (the refusal is delivered at once), and a request still waiting for its
     * answer when it passes is cut: the call fails with the deadline's error.
     * Default: none — `maxWaitMs` is the default rail.
     */
    readonly maxElapsedMs?: number;
    /**
     * The ceiling of the first wait our own backoff draws, in milliseconds, when
     * the server stated none. Default 200.
     */
    readonly baseDelayMs?: number;
    /** How much each backoff ceiling grows over the last. Default 2. At least 1. */
    readonly multiplier?: number;
    /**
     * How a backoff wait is drawn below its ceiling: `'equal'` (the default)
     * waits between half the ceiling and all of it, `'full'` anywhere from zero
     * up to it, `'none'` exactly it. Jitter is what keeps clients refused at the
     * same moment from asking again at the same moment.
     */
    readonly jitter?: 'none' | 'equal' | 'full';
    /**
     * The most one of OUR backoff waits may last, in milliseconds. Default: none.
     *
     * It caps the scheduled wait's ceiling before the jittered draw, so capped
     * waits still spread between half the cap and the cap. It never caps a
     * server's `Retry-After` — unlike footprintjs's `RetryPolicy.maxBackoffMs`,
     * which caps every wait. A stated wait is waited whole; asking sooner than
     * the server said would only spend an attempt on a refusal it announced.
     */
    readonly maxDelayMs?: number;
    /**
     * A name for this endpoint, declared by you, that `onRetry` reports as
     * `endpoint` — so a log line can say which server was retried without
     * carrying any part of its URL past the origin. A dispatched call's saved
     * attempts name the endpoint by it too, in place of the origin. 1–128
     * characters, none of them control characters. Absent, `onRetry` has no
     * `endpoint` field; the library never makes one up from the URL.
     */
    readonly endpointLabel?: string;
    /**
     * A notification before each wait. It is told the refusal's kind, the
     * attempt about to start, the wait, the endpoint's origin and the label you
     * declared for it (`endpointLabel`) — never the URL's path, the request's
     * arguments or the server's answer. It is not told how the call ended. The
     * contract matches what `withRetry` and `withCredentialRetry` already use: a
     * decorator at a transport boundary reports to its consumer.
     *
     * What the RUN keeps is separate and needs no hook: for a call the agent
     * dispatched, the series files every attempt — its outcome, its reason, its
     * wait — and how the request ended, as `agentfootprint.tools.request_attempts`
     * (the label is reported there as `endpoint`).
     *
     * A retried call is not invisible without this: the wait lands inside the
     * tool's `stream.tool_end` `durationMs`, and an exhausted retry still
     * reaches the model with `error: true`. This hook is what turns "that call
     * was slow" into "that call was throttled twice".
     *
     * A throw from it is isolated: the call goes on as if it had returned.
     */
    readonly onRetry?: (info: ThrottleRetryInfo) => void;
}
/**
 * How a throttled request is handled. `true` (the default) means bounded
 * retry with default settings; `false` disables it entirely.
 */
export type RetryOnThrottle = boolean | ThrottleRetryOptions;
/**
 * What one response says about asking again. `undefined` delivers the response
 * as it is; a verdict waits, then sends the SAME request once more.
 */
export interface RetryVerdict {
    /**
     * How long the server asked for, in milliseconds, when it said. Absent, the
     * loop waits its own jittered backoff.
     */
    readonly retryAfterMs?: number;
}
/**
 * The one question the loop asks of each response.
 *
 * ── The license every decision must hold ─────────────────────────────────────
 * A decision RECOGNISES a refusal; whether the request is sent again is the
 * replay rule's (`replay.ts`), never the decision's. It may recognise only a
 * response that is a refusal — never one that could be the record of
 * half-done work. The library holds exactly two: {@link throttled} below, a
 * 429, and AgentCore Runtime's "session operation in progress" refusal, which
 * lives with that vendor's adapter (`adapters/mcp/agentcoreRuntime/`) and whose
 * rule carries the adapter's retry assumption (`RetryRule.assumes`). Either
 * one's effect stays unknown: a 429 says the request was rate-limited, and the
 * busy body is matched by its code and words, which a server behind the
 * runtime can send after it ran the call. Each stays ONE small function, so a
 * refusal has exactly one place it is recognised — and each attempt is written
 * down as one footprintjs attempt record.
 */
export type RetryDecision = (response: Response, 
/**
 * The request's signal. It fires when the caller cancels or the call's
 * deadline passes; a decision that reads the body must stop reading then,
 * and release what it read from.
 */
signal?: AbortSignal) => RetryVerdict | undefined | Promise<RetryVerdict | undefined>;
/** HTTP 429, with the server's own `Retry-After` when it sent one. */
export declare function throttled(response: Response): RetryVerdict | undefined;
/**
 * @internal What a rule's options fall back to. `Infinity` means none: no
 * deadline (`maxElapsedMs`), no cap on the scheduled wait (`maxDelayMs`).
 */
export interface RetryDefaults {
    readonly maxAttempts: number;
    readonly maxWaitMs: number;
    readonly maxElapsedMs: number;
    readonly baseDelayMs: number;
    readonly multiplier: number;
    readonly jitter: 'none' | 'equal' | 'full';
    readonly maxDelayMs: number;
}
/**
 * `retryOnThrottle`'s defaults — unchanged since 8.11.0: 3 attempts, a 200 ms
 * backoff doubling with equal jitter (100–200 ms, then 200–400 ms), never more
 * than 10 s of waiting in total. No deadline and no cap on one wait.
 */
export declare const THROTTLE_RETRY_DEFAULTS: RetryDefaults;
/**
 * @internal One refusal a series may retry: the decision that recognises it,
 * the kind it reports as, and the limits and backoff it retries by.
 */
export interface RetryRule {
    readonly kind: RetryKind;
    readonly decide: RetryDecision;
    /** Total attempts while this refusal is the reason to go on. ≥ 1; `Infinity` is no count. */
    readonly maxAttempts: number;
    readonly maxWaitMs: number;
    readonly maxElapsedMs: number;
    readonly maxDelayMs: number;
    readonly baseDelayMs: number;
    readonly multiplier: number;
    readonly jitter: 'none' | 'equal' | 'full';
    /** The caller's own name for the endpoint, reported as `endpoint`. */
    readonly endpointLabel?: string;
    readonly onRetry?: (info: ThrottleRetryInfo) => void;
    /**
     * Set only by an adapter that retries this refusal as its own policy
     * (`sessionBusy.ts`): what it assumes in doing so. The assumption is recorded
     * beside each repeat (`permission: { by: 'adapter', assumes }`) — never as
     * evidence: the attempt's effect stays `unknown`.
     */
    readonly assumes?: 'refused-before-execution';
}
/**
 * @internal A rule from a caller's options — `undefined` when retry is off
 * (`false`). An option field left out, or set to `undefined`, takes its
 * default. `maxAttempts` and `maxWaitMs` keep the clamps they always had (below
 * 1 reads as 1, below 0 as 0).
 *
 * @throws TypeError, when the rule is BUILT, for a value the schedule cannot
 *   mean — named by option, never echoing a value that is not a number.
 */
export declare function retryRule(kind: RetryKind, decide: RetryDecision, config: RetryOnThrottle | undefined, defaults: RetryDefaults, where: string, assumes?: 'refused-before-execution'): RetryRule | undefined;
/**
 * Wrap a `fetch` so HTTP 429 responses are retried, honouring `Retry-After`.
 *
 * Returns `inner` untouched when retry is disabled — including `undefined`,
 * so a transport that passed no custom fetch keeps passing none and its
 * behaviour is byte-identical to before this existed.
 *
 * When `inner` already runs a retry series of its own (an AgentCore Runtime
 * transport's fetch), the 429 rule JOINS that series rather than wrapping it,
 * so the two can never multiply requests — see the module note.
 *
 * ── Why this is public ───────────────────────────────────────────────────────
 * `mcpClient({ transport })` applies it for you, ON by default. `mcpClient({
 * connection })` cannot: the library builds no transport on that arm, so there
 * is no `fetch` of its own to wrap — and a browser consumer, who is the main
 * reason that arm exists, would silently lose the 429 handling Node gets for
 * free. That asymmetry is invisible from the outside, which is why the answer
 * is to hand over the same implementation rather than to document the gap:
 *
 * ```ts
 * import { retryingFetch } from 'agentfootprint/providers';
 * import { StreamableHTTPClientTransport }
 *   from '@modelcontextprotocol/sdk/client/streamableHttp.js';
 *
 * const transport = new StreamableHTTPClientTransport(new URL('/mcp', location.href), {
 *   fetch: retryingFetch(undefined, { maxAttempts: 5 }),
 * });
 * ```
 *
 * @param inner the fetch to wrap. `undefined` means the global `fetch`,
 *   resolved at CALL time so a later polyfill still wins.
 * @param config `true` / `undefined` for the defaults, `false` to get `inner`
 *   back unchanged, or an object to tune the ceilings and the backoff.
 * @throws TypeError when an option is outside its range — here, when the fetch
 *   is built, never at the first call.
 */
export declare function retryingFetch(inner: ThrottleFetch | undefined, config: RetryOnThrottle | undefined): ThrottleFetch | undefined;
/**
 * @internal The series for a transport whose 429 rule is off (`retryOnThrottle:
 * false`) while the app's replay declaration still needs one (`replay.ts` ·
 * `needsSeries`): a tool declared repeatable on `'network'`, or the
 * `'app-authorized'` policy, whose record and "may have run" rest on the series.
 * `retryOnThrottle: false` turns off the 429 rule and nothing else, so its one
 * rule is that rule switched off: it recognises no response and holds the
 * call's budget (`retryOnThrottle`'s defaults). A transport fetch that already
 * runs a series is joined, as `retryingFetch` joins it.
 */
export declare function seriesWithoutThrottle(inner: ThrottleFetch | undefined): ThrottleFetch;
/**
 * @internal What one call's series is built from: the rules it asks, in order;
 * the connection's ledger — which tool call each request belongs to, so the
 * series can file its attempts into that call's run (`requestLedger.ts`); and
 * the app's replay declaration — who may send a tool call again (`replay.ts`).
 * No declaration is the compatibility policy, naming no tool.
 */
export interface SeriesSpec {
    readonly rules: readonly RetryRule[];
    readonly ledger?: RequestLedger;
    readonly replay?: ReplayDeclaration;
}
/**
 * @internal What a joinable fetch's series can be rebuilt with: one more rule,
 * or a ledger and the replay declaration (they arrive together, from `mcpClient`).
 */
export type SeriesJoin = {
    readonly rule: RetryRule;
} | {
    readonly ledger: RequestLedger;
    readonly replay?: ReplayDeclaration;
};
/** @internal `spec` with `join` in it. */
export declare function joinedSpec(spec: SeriesSpec, join: SeriesJoin): SeriesSpec;
/**
 * @internal Declare that `fetch` runs its retries in ONE series, and how to
 * rebuild it with one more rule — or with a ledger — in that same series.
 * `retryingFetch` joins such a fetch instead of wrapping it. A transport whose
 * public fetch wraps its series (a session applied around it, its answers
 * rewritten after it) uses this so the rule lands INSIDE that wrapping.
 */
export declare function joinableRetry(fetch: ThrottleFetch, rebuild: (join: SeriesJoin) => ThrottleFetch): ThrottleFetch;
/**
 * @internal `fetch` rebuilt so its series files each request's attempts through
 * `ledger`, and asks `replay` before sending a tool call again — or `undefined`
 * when `fetch` runs no series (retry off), so there is nothing to file and the
 * caller keeps `fetch` as it is.
 */
export declare function ledgeredFetch(fetch: ThrottleFetch, ledger: RequestLedger, replay?: ReplayDeclaration): ThrottleFetch | undefined;
/**
 * @internal The one series per call over `inner`, asking `spec.rules` in order —
 * `inner` itself when there are none. The first rule holds the call's budget;
 * see the module note. The result is joinable.
 */
export declare function retryingFetchFor(inner: ThrottleFetch, spec: SeriesSpec): ThrottleFetch;
/**
 * `Retry-After` in either legal form: delta-seconds (`"2"`) or an HTTP-date
 * (`"Wed, 21 Oct 2026 07:28:00 GMT"`). Anything else is treated as absent so a
 * malformed header degrades to our own backoff rather than to a wrong wait.
 */
export declare function parseRetryAfter(value: string | null | undefined): number | undefined;
