Transient-failure classification and backoff for the pre-stream provider call.
When Agentix.Provider.stream/3 returns {:error, reason} before any token has
streamed, the agent retries it according to the conversation's retry policy (see
Agentix.Conversation.Config). This module is the policy's two pure pieces:
retryable?/1— is this error transient (worth retrying) or terminal?delay/3— how long to back off before the next attempt.
A failure that happens after streaming has begun is never retried (re-issuing the request would duplicate already-emitted output); that path crashes the streaming task and fails the turn, and never reaches this module.
What counts as retryable
Errors surface as ReqLLM.Error.API.Request (ReqLLM normalizes every HTTP and
transport failure to this struct). The classification mirrors which failures are
genuinely transient:
- HTTP 429 (rate limited) and 5xx (server/overload) → retryable.
- A transport error (
status: nil— connection drop/timeout before any HTTP status) → retryable. - HTTP 4xx other than 429 (auth, bad request, not found) → terminal.
- Anything else → terminal (fail fast rather than hammer on an unknown error).
Classification matches ReqLLM.Error.API.Request (ReqLLM is the canonical provider
model the library is built on). An error of any other shape — e.g. from a custom
Agentix.Provider returning a bespoke reason — is treated as terminal (not
retried). A future Agentix.Provider.Error normalization could make retry
provider-agnostic; until then, fail-fast on the unknown is the safe default.
Summary
Functions
Backoff before the next attempt, in milliseconds, for a 1-based attempt number.
Extracts a server-requested backoff from a retry-after response header, in
milliseconds, or nil when absent/unparseable. Accepts the delta-seconds form
(a count of seconds); the HTTP-date form is ignored (returns nil).
Classifies a provider error reason as transient (true) or terminal (false).
Functions
@spec delay( pos_integer(), %{base_ms: pos_integer(), max_ms: pos_integer()}, non_neg_integer() | nil ) :: non_neg_integer()
Backoff before the next attempt, in milliseconds, for a 1-based attempt number.
Exponential base_ms * 2^(attempt-1) capped at max_ms, then "equal jitter" (half
fixed, half random) so retrying clients de-synchronize. When the server asked for a
longer wait via retry-after, that wins — but the whole result is capped at
60000 ms so a hostile or buggy server's retry-after can't pin the
streaming task in Process.sleep for an unbounded time.
The result is always ≤ 60000 and (below that ceiling) ≥ retry_after_ms.
@spec retry_after_ms(term()) :: non_neg_integer() | nil
Extracts a server-requested backoff from a retry-after response header, in
milliseconds, or nil when absent/unparseable. Accepts the delta-seconds form
(a count of seconds); the HTTP-date form is ignored (returns nil).
Header shapes vary by provider path: ReqLLM carries Req.Response headers as a
case-insensitive map of name => [values], but other paths use a list of
{name, value} tuples. Both are handled, the lookup is case-insensitive, and a
multi-value header takes its first value.
Classifies a provider error reason as transient (true) or terminal (false).
iex> Agentix.Retry.retryable?(ReqLLM.Error.API.Request.exception(status: 503))
true
iex> Agentix.Retry.retryable?(ReqLLM.Error.API.Request.exception(status: 401))
false