Wrappers
Compose Calls without changing them. cache, retry, timeout, with_model, fallback, mock, log.
A wrapper takes a Call and returns a new Call. It does not change the inner call — it surrounds it with new behavior. Wrappers nest, and the order reads outer-to-inner exactly as written:
cache(retry(timeout(step("..."), seconds=10), attempts=3))Resolving the outer wrapper drives the chain: the cache checks first; on miss, retry runs; each attempt opens timeout; timeout dispatches the step. Each layer emits its own start/end events on the trace.
This page summarizes the seven wrappers shipped in v0.1.0. Reach for the reference when you need exact parameter tables.
cache
Memoize a call. On first evaluation, the result is computed and stored. On subsequent evaluations against the same key, the stored value is returned without re-running.
from bridle import cache
plan = cache(step("draft a research plan", schema=Plan, context=topic))The default key is deterministic — it hashes the inner call's kind, schema, context, prompt, and tools. Backends ship for memory and file; see caching for the full surface.
retry
Re-evaluate a call when it raises. Each attempt clones the inner call so a failed attempt's cached result doesn't leak into the next.
from bridle import retry, BridleError
answer = retry(step("compose the brief", schema=Brief, context=ctx), attempts=3)By default, retries fire on any BridleError. Narrow the catch with on=:
from bridle import SchemaSatisfactionError
answer = retry(step("..."), attempts=2, on=SchemaSatisfactionError)Add backoff as a fixed delay or a callable:
retry(step("..."), attempts=3, backoff=2.0) # 2s between attempts
retry(step("..."), attempts=3, backoff=lambda n: 2 ** n) # 1s, 2s, 4stimeout
Abort a call if it exceeds a wall-clock deadline. Raises bridle.TimeoutError.
from bridle import timeout
answer = timeout(step("compose the brief", schema=Brief, context=ctx), seconds=30)The implementation runs the inner call in a worker thread and waits with a deadline. On timeout, the wrapper raises but the underlying request keeps running until the SDK gives up. For hard cancellation of a model call, prefer the SDK's own timeout=. This wrapper is a coarser deadline at the program level.
with_model
Override the model for one call. Highest-precedence layer of model resolution:
from bridle import with_model
quick = with_model(step("classify the topic", schema=Topic, context=text), "claude-haiku-4-5")Composes with any wrapper. The override propagates down to whichever inner step actually invokes the model.
fallback
Try the primary call; on failure, try each alternate in order. The first that resolves without raising BridleError wins. If all options fail, the last error propagates.
from bridle import fallback, with_model
answer = fallback(
with_model(step(prompt, schema=Out, context=ctx), "claude-opus-4-7"),
with_model(step(prompt, schema=Out, context=ctx), "claude-sonnet-4-6"),
with_model(step(prompt, schema=Out, context=ctx), "claude-haiku-4-5"),
)Each candidate is cloned before evaluation, so a failed attempt cannot poison the next one's cache.
mock
Replace dispatch with a constant. The wrapped call's structure is preserved on the trace, so test assertions on shape still pass — only the dispatch is short-circuited.
from bridle import mock
# In tests:
fixed = mock(step("compose the brief", schema=Brief, context=ctx), Brief(headline="...", body="..."))
result = bridle.resolve(fixed)Use mock to substitute deterministic results for steps that would otherwise hit the model. The most common pattern is mocking inside a fixture so an end-to-end test runs without an API key.
log
Stream the trace for the wrapped subtree to a Python logger. Subscribes a handler before evaluation and unsubscribes after.
import logging
from bridle import log
logging.basicConfig(level=logging.INFO)
result = log(brief_writer("Mars weather"), level="INFO")
bridle.resolve(result)Output is one line per event with kind, call_kind, label, and error. For richer integration, subscribe directly to the trace — see traces.
Composition rules
Order matters. cache(retry(...)) caches the final result and retries on inner failures. retry(cache(...)) retries the cache lookup itself, which makes sense in fewer cases.
Each layer emits events. A cache_hit shows up under the cache; the inner call only fires on a miss. A retry event fires per attempt. Read the trace to confirm the layering matches your intent.
Cloning isolates attempts. retry, fallback, and cache all clone the inner Call before evaluating. The original Call you held remains an unresolved description.
Pitfalls
Wrapping a resolved value. Wrappers expect a Call. If you read the result before wrapping, the wrapper has nothing to do. Construct the wrapper around the unevaluated call:
# WRONG
brief = step("write a brief", schema=Brief, context=ctx).body # forces resolution
cached = cache(brief) # type error — brief is now a string
# RIGHT
cached = cache(step("write a brief", schema=Brief, context=ctx))
brief = bridle.resolve(cached).bodyForgetting that mock short-circuits everything below it. A mock around a cache skips the cache. A mock around a retry short-circuits the retry. Mocks are tests-only; do not ship them.
Using timeout for hard cancellation. It does not cancel the underlying HTTP request. The SDK keeps running; only your program raises. For real cancellation, use the SDK's timeout= parameter or sit a concurrent.futures future on top yourself.