Your Agent Framework Dumps JSON Blobs. Here Are the Three Storage Layers It Should Have.
Last night I built a small tool called daglog: a flight recorder for AI agent runs. Tonight my job-hunting bot writes every scan, score, and browser autofill into it.
While building it, I went back through my research notes on execution engines, and one comparison kept nagging me: how traditional frameworks store state versus how immutable engines do.
The difference is not philosophical. It is three concrete storage layers, each replacing something heavy with something almost free. This post is my reference notes on all three — with the receipts from shipping layer two today.
The Blob-Dumping Problem
In most agent frameworks, state is one big mutable dictionary: messages, tool results, scratch variables, everything. At every super-step the framework serializes the entire dictionary into a massive JSON string and writes it to an external database.
PostgreSQL, MongoDB, S3 — the destination varies. The pattern does not:
| Step | What gets written | Actual new information |
|---|---|---|
| 1 | Full 50 KB state snapshot | 50 KB |
| 2 | Full 55 KB state snapshot | ~5 KB |
| 10 | Full 80 KB state snapshot | ~5 KB |
Ninety percent of every checkpoint is a re-upload of data you already stored. Replay means pulling blobs back and hoping they match reality.
An immutable EDN/DAG engine replaces that with three layers. Here is the map:
Layer 1: In-Memory — Pointer Graphs, Not Blobs
During execution, state never exists as duplicated copies. It lives in a Hash Array Mapped Trie (HAMT) — the structure behind every Clojure persistent collection.
The mental model: a tree where every piece of data is reachable by path. When the agent takes a step, the engine allocates a tiny new node for whatever changed, and the new root points at the old root’s untouched branches.
This is not a simulation. Clojure gives you it for free, and you can watch sharing happen:
(def root-1 {:messages ["hi"] :tool-results []})
(def root-2 (assoc-in root-1 [:tool-results :search "10MB payload"]))
(identical? (:messages root-1) (:messages root-2))
;; => true — same memory address, zero copy
Step 2 did not copy step 1. It allocated one new branch and pointed at everything else.
The payoff compounds: retaining 100 historical steps costs nearly the same RAM as keeping one, because 99% of the addresses are structurally shared across roots. Mutable frameworks get none of this — copying or in-place mutation both lose the ability to hold history cheaply.
Layer 2: On-Disk — Content-Addressable Storage
For persistence, the engine works like Git. Two kinds of data, two strategies:
- The light stuff — graph structure, variables, small fields — goes into an append-only log of EDN frames. Appending never rewrites history.
- The heavy stuff — a 10 MB web page dump, a PDF extraction — gets hashed with SHA-256, written once under its hash, and referenced by that hash forever.
I shipped exactly this last night. A run on disk looks like:
data/runs/<run-id>/
├── log.edn the DAG: nodes + chained head hash
└── blobs/
└── 9c1fa3... any payload ever referenced, stored once
Each node in log.edn carries hashes instead of
payloads:
{:id :n3
:fn "apply.autofill"
:deps []
:ts "2026-08-26T07:14:03Z"
:in-hash "a1b2c3..." ; the packet JSON, one blob
:out-hash "d4e5f6..."} ; autofill stdout, another blob
The deduplication math is the point: if twenty steps or twenty separate runs reference the same 10 MB page, disk grows by exactly zero additional bytes after the first write. My job bot applies to the same career pages repeatedly — every repeated fetch is now free.
And because every append re-computes a chained head hash over the whole log, editing one byte of history breaks verification:
$ bb daglog verify 2026-08-26-score-3f2a
{:ok true, :nodes 4, :head "9c1f..."} ; intact
# tamper with log.edn ...
$ bb daglog verify 2026-08-26-score-3f2a
daglog: verification FAILED: chain broken
A checkpoint you cannot trust is worse than no checkpoint. Git solved this decades ago; agent state is finally catching up.
Layer 3: The Traffic Proxy Store
Layers one and two record your agent’s thinking. Layer three records the world’s answers — and it is what makes zero-token replay possible.
A lightweight sidecar proxy sits between the agent and the network. Every outbound call passes through, and the proxy keeps its own key-value database (a flat binary file or SQLite):
- Key: SHA-256 of HTTP method + normalized URL + request body
- Value: the raw response — body, headers, status code
During replay, the engine intercepts outbound requests before they leave the machine, computes the hash, and serves the stored response back in microseconds. No API bill. No rate limit. No flaky third party changing the answer mid-debug.
| Live re-run | Zero-token replay | |
|---|---|---|
| Cost | tokens + API quota per attempt | ~0 |
| Latency | seconds per call | microseconds per lookup |
| Determinism | provider can drift | byte-identical responses |
| Works offline | no | yes |
Honest status: layers one and two exist in my repo today, and layer
three just landed too — as a zero-dependency file-CAS trace
store: trace-put records any request/response pair
keyed by SHA256(METHOD | normalized URL | body),
trace-get serves it back byte-identical after param
reordering or a host-case change, and a with-replay wrapper
makes any fetch function replay-aware in one line. The intercepting
sidecar proxy is deliberately deferred; the store is the interface.
Side-by-Side
| Metric | Mutable JSON frameworks | Immutable DAG engine |
|---|---|---|
| In-RAM state | full dictionaries copied or mutated in place | shared pointer tree (HAMT), memory scales near O(1) |
| Checkpoints | duplicated JSON blobs into SQL/NoSQL every super-step | append-only delta log + SHA-256 deduplicated blob store (CAS) |
| Heavy payloads | re-serialized into every snapshot | hashed once, referenced everywhere |
| Network traces | logged out-of-band to cloud SaaS (LangSmith, Langfuse) | captured locally in a proxy KV/SQLite store |
| Replay | pull blobs, hope they match, pay tokens | deterministic rewalk, zero tokens |
| Tamper-evidence | none — rows are mutable | chained head hash — any edit fails verify |
The pattern across all six rows: traditional designs pay repeatedly for information that was already known, and trust whatever comes back.
Why I Care More Than the Benchmarks
Debugging an agent is archaeology. You find out something went wrong hours later, and you want to ask exactly three questions: what did the agent see, what did it do, and would it do it again?
Blob databases make those questions expensive. The three-layer design makes them cheap — and the chained hash adds something dashboards never offer: proof that the evidence was not edited after the fact.
My research converged on a verdict I keep re-verifying: agents need flight recorders, not more dashboards.
Takeaway: stop storing what changed everywhere else — store what changed, hash what stayed, and replay without asking anyone for permission.
Built today: daglog — a single Babashka
script
(init / step / verify / replay / diff / export / trace-put / trace-get),
zero dependencies, first instrumented agent being my own job bot. All
three layers are real now.