Caching
Memoize calls, agents, or wrapped subtrees. Pluggable backends, deterministic keys.
Bridle's cache is opt-in per call via the cache wrapper. Wrap any Call and it becomes memoized; the inner work runs once per distinct key and the result is reused after.
from bridle import cache, step
plan = cache(step("draft a research plan", schema=Plan, context=topic))How the key is built
By default, the cache key is a deterministic SHA-256 hash of the inner call's:
kind—step,branch,loop, oragent. Two semantically different calls with otherwise identical parameters get distinct keys.- Schema fingerprint — the JSON Schema of the schema, hashed. Changing a Pydantic model invalidates the cache automatically.
- Prompt — the prompt string.
- Context — JSON-encoded with sorted keys.
- Tools — name and parameter-schema fingerprint per tool.
Two calls with the same five parts produce the same key, across processes, across runs. Change any of them and you get a new key.
Note: kind is part of the key
A step and a branch with otherwise identical fields would produce different keys. This is intentional — step and branch differ in trace semantics and tool availability, so caching one against the other would be unsafe.
Set the active backend
bridle.set_cache(backend) registers the active backend. Every cache(...) wrapper uses it unless it passes its own backend=.
import bridle
from bridle.cache.file import FileCache
bridle.set_cache(FileCache("./.bridle-cache"))Without an active backend, cache(...) falls back to a process-local in-memory cache (MemoryCache). Useful for tests; not useful across runs.
Backends shipped
MemoryCache — process-local, threading-safe, optional TTL. The implicit default.
from bridle.cache.memory import MemoryCache
bridle.set_cache(MemoryCache())FileCache — pickle-per-key on disk. Atomic writes, optional TTL, survives restart. Good for local development:
from bridle.cache.file import FileCache
bridle.set_cache(FileCache("./.bridle-cache"))RedisCache — reserved for v0.2.0. The class exists in bridle.cache.redis but raises on construction; ship a Redis-backed cache with v0.2.
Per-call options
cache(
call,
*,
key=None, # str, callable, or None for the default
backend=None, # override the active backend
ttl=None, # seconds; None means no expiry
label=None, # custom label for the trace
)Override the key for two cases.
A fixed key. Useful when the call's context is large or non-deterministic but the result should be shared:
cache(step("daily summary", schema=Summary, context=now_date), key="daily-summary-v1")A custom derivation. A callable that takes the inner Call and returns a string:
def by_topic(call):
return f"brief:{call.context}"
cache(step("write the brief", schema=Brief, context=topic), key=by_topic)TTL
ttl= sets an expiry in seconds. After the TTL elapses, the cached entry is treated as a miss and the inner work runs again:
cache(step("..."), ttl=60 * 60) # 1 hourMemoryCache and FileCache both honor TTL. The check happens on get — expired entries are deleted as a side effect of the lookup.
What gets cached
The whole resolved value. For a step, that's the typed result. For a wrapped retry, that's the value the successful attempt produced. For an @agent call wrapped in cache, that's the agent's return value.
@agent(input=str, output=Brief)
def brief_writer(topic: str) -> Brief:
...
cached = cache(brief_writer("Mars weather")) # cache the whole agent run
brief = bridle.resolve(cached)Trace events
Cache wrappers emit two custom events:
cache_hit— key found, value returned, inner call skipped.cache_miss— key absent, inner call ran, value written.
If a write fails (disk full, backend offline), an additional cache_miss event with write_error is emitted. The result is still returned; only the write fails.
Pitfalls
Caching non-deterministic context. If context includes a timestamp or random ID, every call has a unique key and the cache is useless. Either pin the context to a stable representation, or pass an explicit key=.
Caching tools that have side effects. A step with a send_email tool, cached, can replay the same email on a hit. The cache stores the return value, not a re-execution. If the tool's effect was the point, do not cache it.
Sharing a FileCache across processes without coordination. Atomic writes prevent corruption, but cache thrashing is possible if many processes write the same key concurrently. For shared use across machines, wait for Redis in v0.2.
Forgetting that schema changes invalidate the cache. A field rename in a Pydantic model produces a new schema fingerprint, which produces a new key. Old entries linger but are unused. Periodically prune.
A complete example
import bridle
from bridle import agent, cache, step
from bridle.cache.file import FileCache
from bridle.models.anthropic import install
from pydantic import BaseModel
class Plan(BaseModel):
topics: list[str]
@agent(input=str, output=Plan, model="claude-sonnet-4-6")
def planner(topic: str) -> Plan:
return cache(
step("draft a plan with three topics", schema=Plan, context=topic),
ttl=60 * 60,
)
install()
bridle.set_cache(FileCache("./.bridle-cache"))
# First run hits the model. Second run reads from disk.
print(bridle.resolve(planner("Mars weather")))
print(bridle.resolve(planner("Mars weather")))