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:Runnablewrappers — apply to any Runnable: aClient, 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: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 aPipelinedClient 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:
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: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_tokenanddeadline.
See also
Middleware
The full middleware catalog.
Caching
Don’t pay for repeated calls.
Going to production
Putting it all together.