@sema-agent/core
Version:
Stateless, task-oriented AI agent core
69 lines (68 loc) • 4.66 kB
TypeScript
/**
* 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;