Bridle
Guide

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, 4s

timeout

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).body

Forgetting 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.

Next

On this page