🧊 dagcache
Record your AI agent once. Replay it on every repeat task.
Agents waste time and money re-planning tasks they've already solved. dagcache watches an agent run, remembers the steps it took, and replays those steps the next time a similar task comes in — the LLM only gets called for genuinely new situations.
💡 The idea
Workflow tools like Luigi make you write out the whole workflow up front. dagcache flips that around: it watches your agent work and learns the workflow on its own. Once a way of solving a task proves it works, dagcache saves it and reuses it — like VCR cassettes, but for agent behavior instead of HTTP.
run agent once ──▶ save the steps it took ──▶ approve the winning route
│
similar task comes in ──▶ replay the steps (no planning LLM call)
│
something changed in the world? ──▶ fall back to the live agent
🚀 Install
pip install dagcache # zero runtime dependencies
⚡ Quickstart
import dagcache
@dagcache.tool(pure=True) # read-only: safe to skip in frozen replay
def search_kb(query: str) -> str: ...
@dagcache.tool(pure=False) # has side effects: always runs for real
def send_reply(ticket_id: str, body: str) -> str: ...
@dagcache.llm(planning=True) # decides what to do: skipped on replay
def plan(prompt: str) -> str: ...
@dagcache.llm # writes user-facing text: runs fresh
def draft(prompt: str) -> str: ...
@dagcache.agent
def resolve_ticket(ticket: dict) -> str:
plan(f"handle {ticket['title']}")
article = search_kb(ticket["title"])
order = fetch_order(ticket["order_id"])
send_reply(ticket["id"], f"see {article}")
return draft(f"article={article}; status={order['status']}")
| Run | What happens |
|---|---|
| Run 1 🎬 | The agent runs normally. dagcache records the steps it took (search_kb > fetch_order > send_reply, plus the LLM calls) into .dagcache/store.db. |
| Run 2 🔁 | A new ticket of the same shape comes in. dagcache replays the saved steps: tools run again, but with values taken from the new ticket (e.g. its order_id), the planning LLM is never called, and the final reply is written fresh using the new data. |
| Run 3 🌍 | The world changed (a tool returns something unexpected, a value is missing, a tool crashes). Replay stops and the live agent takes over automatically. Worst case, dagcache behaves exactly like your agent does today. |
▶️ Try it: python examples/ticket_agent.py
🧠 What gets saved, exactly
dagcache doesn't cache answers — it caches the plan. Two things decide whether a saved plan applies:
- which task it is (the
@agentfunction), and - the shape of the inputs (their types and field names — never the actual values).
The saved plan is a graph of steps. Each step's arguments are stored as references, not fixed values:
| Reference type | What it means at replay |
|---|---|
input |
take it from the new task's input (e.g. ticket.title) |
node |
take it from an earlier step's fresh result (e.g. n1.order_id) |
literal |
a value the LLM made up; reused as-is (with old→new values swapped in) |
If the agent finds two different ways to solve the same kind of task, both are
kept and ranked by how often they work. dagcache approve locks in the
winner. A saved plan that keeps failing is dropped after 3 failures.
🔁 Replay modes
- verified (default) — tools run for real with fresh values (real side effects, fresh data), planning LLM calls are skipped, text-writing LLM calls run again. Think of it as Luigi's worker, where the learned plan is the workflow.
- frozen — nothing runs; recorded results are returned as-is. Like
Luigi's
output().exists()across the whole graph — VCR mode, great for tests and CI.
dagcache.configure(db_path=".dagcache/store.db", replay_mode="frozen")
# or env: DAGCACHE_MODE=record|off, DAGCACHE_REPLAY=verified|frozen, DAGCACHE_DB=...
Two tasks can have the same input shape but mean different things (a refund ticket vs. a complaint). Give the agent a discriminator — its value becomes part of what makes a task unique:
@dagcache.agent(key=lambda ticket: ticket["category"])
🛠 CLI
dagcache ls # id, status, recordings, hits, failures, steps
dagcache show 3 # full plan JSON
dagcache approve 3 # lock in the winning route
dagcache demote 3 # back to probation
dagcache diff 3 7 # compare two candidate routes
dagcache export 3 -o c.json # save a cassette for code review
dagcache import c.json
dagcache prune --status dead --older-than 30
🔌 Framework integrations
OpenAI (duck-typed — works with
any .chat.completions.create client):
from dagcache.adapters.openai import wrap_client
client = wrap_client(OpenAI()) # role="auto"|"planning"|"output"
Responses containing tool_calls are treated as planning (skipped on
replay); text responses as output writing (run again on replay, with the
prompt updated to fresh data).
LangChain (duck-typed — no langchain import required):
from dagcache.adapters.langchain import wrap_tools, wrap_llm
agent = build_agent(llm=wrap_llm(ChatOpenAI(...)), tools=wrap_tools([...]))
⚠️ Honest limitations
- Matching is exact, not "similar". A saved plan only applies when the task type, input shape, and key all match exactly. Fuzzy/embedding matching is deliberately not in v0.1 — a "close enough" match replaying a path with real side effects is how the cache refunds the wrong customer.
- Side effects really happen. Verified replay runs effectful tools for
real. That's automation, not caching. Mark them
pure=Falseand keep fuzzy matching off. - Made-up values can go stale. If the LLM invented a value that came from neither the inputs nor tool results (e.g. a date it guessed), it's replayed verbatim. Change detection only checks the shape of results.
- Prompt updating is a heuristic: old→new value substitution with word-boundary matching. A value that changed but still looks the same (e.g. a price) is not detected.
- The agent function must return its final call's result. Custom post-processing after the last LLM/tool call won't run on replay.
- Cache poisoning is a thing: if agent inputs are attacker-controlled,
keep a separate store per principal and
approveplans before production.
💎 Ruby
A RubyLLM plugin on the same mental
model lives in the companion repo
ruby_llm-dagcache —
YAML cassettes, DagCache.watch(agent), automatic RubyLLM::Tool
instrumentation.
📚 Prior art
- Agentic Plan Caching (arXiv 2506.14852) — research prototype; still spends an LLM call adapting templates.
- Agent Workflow Memory (arXiv 2409.07429) — injects past workflows into prompts; doesn't skip the agent.
- GPTCache / LangChain caches — cache single LLM calls by semantic similarity.
- LangGraph checkpointing / Temporal — resume interrupted runs, not reuse across tasks.
- vcrpy / VCR — the right mental model, at the wrong layer.
📄 License
MIT © dagcache contributors
Metadata
Release files for dagcache 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dagcache-0.1.0.tar.gz | 32.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dagcache-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.2 kB
Release files / dagcache-0.1.0.tar.gz
| Download URL | dagcache-0.1.0.tar.gz |
|---|---|
| Size | 32.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5365de5e68fd3d45bdd1076fa75a83aca08a65606c8f130eda905e06347dc990
|
|
BLAKE2b-256 checksum How to use checksums |
7e62f42d3ebe620325202d0528b7aff4e2397235dee6abb980635449da3230e4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / dagcache-0.1.0-py3-none-any.whl
| Download URL | dagcache-0.1.0-py3-none-any.whl |
|---|---|
| Size | 26.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a8972bc64e5be2fcb0a96485d3f25e5a7495b491e6baa30dba8e86ab3e4e64c8
|
|
BLAKE2b-256 checksum How to use checksums |
cbd0af4e2253b0448b30fc75e1f0218f869bb603a45386203dde516ba41bbcd1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|