Skip to content

Errors

Typed errors, retry with backoff, and run-level budgets.

agent.run() wraps provider failures in typed errors, so you match on instanceof instead of parsing provider strings.

Typed errors#

Each provider's classifyError(err) returns a ClassifiedError or null:

kindThrown as
'context_exceeded'AgentContextExceededError
'provider_error', or nullAgentProviderError
'aborted'AgentAbortedError, also for agent.abort() or an aborted AbortSignal, whatever the provider reports

A rate limit, a spent quota or an overload also sets limit. It has reason ('rate_limit', 'quota' or 'overloaded'), provider, and retryAfterMs when the server says when it resets. The message is then one sentence from describeProviderLimit(limit), such as Claude rate limit reached. Resets in 1 h 45 min. The thrown AgentProviderError keeps the limit on err.limit.

TypeScript
import { AgentAbortedError, AgentBudgetExceededError, AgentContextExceededError, AgentProviderError } from 'zidane'

try {
  await agent.run({ prompt })
}
catch (err) {
  if (err instanceof AgentContextExceededError) { /* prune history, retry */ }
  else if (err instanceof AgentAbortedError) { /* user cancelled */ }
  else if (err instanceof AgentBudgetExceededError) {
    console.error(`Hit ${err.limit} cap: ${err.actualValue} > ${err.limitValue}`)
  }
  else if (err instanceof AgentProviderError) {
    console.error(`${err.provider}: ${err.message} (${err.providerCode})`)
  }
}

The loop throws three more:

ErrorThrown when
AgentToolNotAllowedErrorThe model calls a tool outside the active skill's allowed-tools (skills)
AgentLengthResumeExhaustedErrorresumeOnMaxTokens keeps resuming replies that hit the output cap with no text and no tool calls. err.consecutiveEmptyTurns holds the count.
AgentToolPairingErrorbehavior.strictToolPairing is on and a tool call has no result. err.repairs lists the repairs that zidane skipped.

toTypedError(err) converts a native error by hand. Compaction throws CompactInvalidInputError and CompactPromptTooLongError (compaction). Decisions return failures unless you ask for DecisionError (decisions).

Retry with backoff#

zidane retries transient provider failures (429, 5xx, Anthropic overloaded_error mid-stream, truncated OpenAI-compatible streams) with exponential backoff and full jitter.

TypeScript
await agent.run({
  prompt: '...',
  behavior: {
    retry: { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 30_000 }, // defaults
  },
})

Retry n waits a random time up to initialDelayMs * 2^(n-1), capped at maxDelayMs: with the defaults, up to 1 s, then up to 2 s. A retry-after header replaces the computed delay, with the same cap. maxAttempts: 1 fails fast.

zidane does not retry after the turn streamed text, tool-call content or provider-side tool activity, because a replay corrupts the transcript. Reasoning alone does not block a retry: the failed attempt's reasoning is dropped, the new attempt generates its own, and stream:retry carries discardedThinking: true so a host can drop what it showed. stream:retry fires per attempt (attempt, nextAttempt, maxAttempts, delayMs, err, statusCode?, requestId?, discardedThinking?). stream:error fires only on the final failure, which goes to the model fallback chain under the same rule.

A stream that goes silent after its first event is caught only by providerStreamIdleTimeoutMs, off by default because some providers send nothing while a server-side tool runs. The chat apps set it to 5 minutes; set it on your agent to get the same retry and fallback on a stalled stream.

AgentContextExceededError does not go to the fallback chain. The loop stubs older media and tool output up to a cutoff and re-issues the stream (watch context:overflow). Each new attempt moves the cutoff further forward. The loop remembers the cutoff, so later turns and runs send the stubs up front. A request whose text alone is too long still fails.

Run-level budgets#

The loop reads these soft caps on behavior after each turn, so a run can go over by up to one turn. Unset or 0 means no cap. maxWallMs also caps the provider stream timeouts.

Optionerr.limitCounts
maxCostUsd'cost'Sum of per-turn cost in USD
maxTotalTokens'tokens'Input plus output tokens, without cache reads and writes
maxWallMs'wallClock'Elapsed run time in ms

A breach throws AgentBudgetExceededError and closes the session run as 'aborted'. When several trip on one turn, cost wins, then tokens.

behavior.maxTurns caps the turn count. maxTurnsWarning: n tells the model to wrap up when n turns remain. maxTurnsWarningMessage replaces that text.

The apps show these caps as Run limits, with run time in minutes (limits in the apps). Per-tool caps are on hooks.

Reference#