Skip to main content
LLM providers fail. Networks blip. Rate limits get hit. Cognis ships idiomatic patterns for all of these as Runnable wrappers and middleware so adding resilience is one line, not a refactor.

The mental model

Three layers, picked by which kind of failure you’re absorbing:
  • Runnable wrappers — apply to any Runnable: a Client, a tool, a chain. Best for individual call resilience.
  • Agent middleware — applies to every model call inside the agent loop. Best for cross-cutting policy.
  • Strategy — domain-specific recovery (LLM-as-judge, escalation chains, retry-with-different-model). Best when generic retry isn’t enough.

Quick example

A production-grade Client:
The chain reads top-to-bottom: try the primary three times with a 30s timeout; if all retries fail, fall back to a cheaper backup.

Wrappers reference

For more, see Runnables → Wrappers.

Middleware reference

For policies that apply on every model call regardless of caller, use the middleware pipeline. Build a PipelinedClient and either use it directly or feed it through a custom provider when you need it inside an AgentBuilder agent — see Middleware → Wiring middleware into an agent.
The pipeline runs outside-in: the most-recently-pushed layer is the outermost wrapper. So RateLimit pushed last means the limiter sees every retry attempt. See Middleware for the full catalog.

Retry policies

RetryPolicy::new(attempts) is the default exponential policy. For finer control:
Exponential backoff with jitter is the right default for rate-limited APIs — you don’t want all retries marching in lockstep.

Rate limiting strategies

RateLimit accepts any RateLimiter impl. Built-ins: For provider-specific quotas (e.g., OpenAI’s per-org TPM), match the bucket size to your tier.

When retries don’t fit

Some failures aren’t transient. The model emitted bad JSON. The tool returned a 4xx your code can fix. Use recovery middleware for these:
Recovery sees the error and the call context. It can synthesize a response, re-prompt with a fix, or escalate. To use it inside an AgentBuilder agent, see the bridging pattern in Middleware → Wiring middleware into an agent.

How it works

  • Wrappers compose by re-wrapping. client.with_max_retries(3).with_timeout(d) builds nested Runnables — types are explicit at every layer.
  • Middleware runs outside-in: most-recently-pushed is outermost. pipeline.push(ModelFallback).push(ModelRetry).push(RateLimit) means the rate limiter sees the original call, then retry runs (each retry hits the limiter again), then fallback fires only when retries are exhausted.
  • Errors carry structure. CognisError::RateLimited { retry_after_ms } lets retry policy honor the provider’s hint. CognisError::ProviderError { provider, message, .. } distinguishes between provider classes.
  • Cancellation is cooperative. All wrappers honor RunnableConfig::cancel_token and deadline.

See also

Middleware

The full middleware catalog.

Caching

Don’t pay for repeated calls.

Going to production

Putting it all together.