UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

69 lines (68 loc) • 4.66 kB
/** * Transient-failure retry backoff (RB-420-b, CC 2.1.220 parity). * * The shape is **increasing exponential + light additive jitter**, not full jitter: * `min(base * 2^attempt, 32s)` plus up to 25% of that. Full jitter (`random(0, exp)`) made the Nth * wait routinely SHORTER than the first, which destroys the one property a rate-limited provider * needs from a client — that each successive attempt backs further off. The 25% additive spread is * still enough de-synchronization to keep many clients from re-converging into a thundering herd. * * Provider wait hints ride at ONE layer above the computed value, both as a FLOOR * (`max(hint, computed)`) — retrying before the server said just gets rejected again, but a hint must * never SHORTEN a backoff we already escalated past it: * - `Retry-After` (delta-seconds or HTTP-date), on any retryable response. * - `anthropic-ratelimit-unified-reset` (an absolute epoch-seconds stamp of when the rate-limit window * reopens), read only off a 429 — it is a rate-limit signal. * When both are present the larger wins. Both are clamped so a untrusted/buggy value * (`Retry-After: 2147483647`, a reset stamp years out) cannot pin the caller for hours. */ /** Parse a `Retry-After` header (delta-seconds or HTTP-date) to ms; undefined if absent/unparseable. */ export declare function parseRetryAfter(res: Response | undefined): number | undefined; /** * Parse `anthropic-ratelimit-unified-reset` — an ABSOLUTE epoch-seconds stamp of when the unified * rate-limit window reopens — into ms from now. Undefined when absent, unparseable, or already past * (a stale stamp carries no wait, so the caller falls back to its computed backoff). */ export declare function parseRateLimitReset(res: Response | undefined): number | undefined; /** * A single provider wait hint after clamping, carrying enough of its own history to disclose a * truncation: `rawMs` is only set when the response actually asked for more than {@link * MAX_HEADER_WAIT_MS} and the hint was cut down to it. */ export interface ProviderWaitHint { /** The value honored (post-clamp), ms. */ readonly ms: number; /** The value the response asked for, ms — present only when it exceeded the clamp. */ readonly rawMs?: number; } /** The larger of the response's two wait hints, each clamped; undefined when the response carries * neither (or only stale/unparseable ones). Exported so a caller can disclose a truncation instead * of silently honoring less than the response asked for. * * Ranks by the CLAMPED `ms` first (that is the value actually honored — unchanged from before this * hint carried `rawMs`), but a tie there is broken by the UNCLAMPED value: two hints can clamp to the * same 60s ceiling while asking for very different waits (`Retry-After: 60` vs a reset a day out), and * picking whichever came first would silently return the one with no `rawMs` — reporting the smaller * ask as if nothing had been truncated at all, even though the pair's honored wait is identical either * way. */ export declare function providerWaitHint(res: Response | undefined): ProviderWaitHint | undefined; /** * Ceiling on the exponential term for the SHORT lane that serves deterministic connect failures * (`ECONNREFUSED` / a name with no address — see `errors.ts` `classifyConnectFailure`). * * That lane exists because waiting does not change the answer: the only thing that can is the peer * itself coming up. So its ladder is sized to cover a service that is already mid-restart, not to * out-wait an overloaded provider — which is what the 32s ceiling above is for. The two are separate * constants rather than one parameter because they answer different questions, and collapsing them * would make either lane's number look arbitrary. */ export declare const FAST_MAX_BACKOFF_MS = 1000; /** * Backoff (ms) for retry `attempt` (0-based) of a transient failure: an increasing capped exponential * plus ≤25% additive jitter, floored by the response's provider wait hints (see the module note). * `rand` is injectable for deterministic tests. `maxBackoffMs` overrides the exponential ceiling for a * caller running a deliberately shorter lane; it bounds only the COMPUTED term — a provider wait hint * still floors the result, because a lane choosing to be short is not a licence to ignore a server that * said when to come back (and such a lane's failures carry no response to read a hint off anyway). */ export declare function retryBackoffMs(baseDelayMs: number, attempt: number, res?: Response, rand?: () => number, maxBackoffMs?: number): number;