🧊 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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dagcache-0.1.0.tar.gz.
File metadata
- Download URL: dagcache-0.1.0.tar.gz
- Upload date:
- Size: 32.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5365de5e68fd3d45bdd1076fa75a83aca08a65606c8f130eda905e06347dc990
|
|
| MD5 |
c689894770e344c7af3ac1605d589768
|
|
| BLAKE2b-256 |
7e62f42d3ebe620325202d0528b7aff4e2397235dee6abb980635449da3230e4
|
File details
Details for the file dagcache-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dagcache-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8972bc64e5be2fcb0a96485d3f25e5a7495b491e6baa30dba8e86ab3e4e64c8
|
|
| MD5 |
b5542b2422b90bf98c920aac3fdf9565
|
|
| BLAKE2b-256 |
cbd0af4e2253b0448b30fc75e1f0218f869bb603a45386203dde516ba41bbcd1
|