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:

Diagram

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:

  1. The light stuff — graph structure, variables, small fields — goes into an append-only log of EDN frames. Appending never rewrites history.
  2. 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):

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.