langgraph-ledger
Full, tamper-evident traceability for LangGraph agents — a port of DeepSeek Harness (dsh)'s traceability design: content-addressed labels on every tool call, a hash-chained append-only event ledger, the execution DAG, replay, crash recovery, and first-class fork/rollback.
pip install langgraph-ledger
Why
LangGraph already checkpoints state and can time-travel. What it does not give you is an audit-grade record: were these events edited after the fact? Which tool call exactly — same name, or same content? What did the run look like as a graph, and can I fork from any point of it?
This plugin ports the traceability design of DeepSeek Harness (dsh) — append-only session log as the single source of truth, format versioning, fork lineage — onto LangGraph's checkpointer contract, and adds what neither has out of the box: content hashing.
The combination is the point:
| Capability | Mechanism |
|---|---|
| Tool-call identity | content-addressed label tl_<hash> — same (name, input) ⇒ same label |
| Tamper evidence | every event hash-chained to its predecessor (merkle-style) |
| Execution structure | DAG over chain + call/result + checkpoint-parent edges |
| Rollback | time_travel_config (native fork) and fork_thread (dsh-style seeded fork with lineage event) |
| Failure analysis | error timeline, exact-repeat loop detection (falls out of the labels) |
| Verification | verify_log re-checks the chain; verify_thread re-hashes stored checkpoints against logged claims |
Differentiation (as of 2026-08)
Honest snapshot of the neighborhood — see POSITIONING for details:
| Project | Hash chain | Execution DAG | State rollback/fork | LangGraph-native |
|---|---|---|---|---|
| langgraph-ledger | ✅ | ✅ | ✅ | ✅ (checkpointer drop-in) |
| LangSmith / Langfuse / AgentOps | ✗ (hosted observability) | trace view | ✗ | SDK |
| CONTINUUM | ✅ | ✗ | crash recovery focus | ✗ (MCP server) |
| burnout / VeritasAgent / memtrail | ✅ | ✗ | ✗ | partial |
| langgraph checkpointers (redis/mysql/…) | ✗ | parent links only | time-travel only | ✅ |
Nobody else ships hash-labels + DAG + rollback as one LangGraph-native unit. That is the slot this project occupies.
Install
pip install langgraph-ledger
Quickstart
from langgraph.checkpoint.memory import InMemorySaver
from langgraph_ledger import TracingCheckpointSaver, DshTraceCallbackHandler
saver = TracingCheckpointSaver(InMemorySaver(), trace_root="./traces")
graph = builder.compile(checkpointer=saver) # your graph, unchanged
graph.invoke(input, config={
"configurable": {"thread_id": "run-42"},
"callbacks": [DshTraceCallbackHandler()], # LLM/tool/node events
})
Every run now leaves ./traces/run-42.jsonl — one hash-chained JSON event per line:
{"v":0,"seq":7,"ts":"…","kind":"tool/call","payload":{"label":"tl_9f2e…","name":"search",…},"id":"…","prev":"…"}
{"v":0,"seq":8,"ts":"…","kind":"state/snapshot","payload":{"checkpoint_id":"…","label":"cp_2c01…","parent_checkpoint_id":"…","checkpoint_sha256":"…"},"id":"…","prev":"…"}
Rollback & fork
from langgraph_ledger import time_travel_config, fork_thread
# resume/fork from any recorded checkpoint (LangGraph-native time travel)
cfg = time_travel_config("run-42", checkpoint_id="<past-id>")
graph.update_state(cfg, {"count": 100}) # forks from that point
# dsh-style: a new thread seeded with the ancestry up to a checkpoint
new_tid = fork_thread(saver, "run-42", at_checkpoint_id="<past-id>")
Audit
python -m langgraph_ledger verify traces/run-42.jsonl # hash chain intact?
python -m langgraph_ledger analyze traces/run-42.jsonl # errors, loops, timeline
python -m langgraph_ledger dag traces/run-42.jsonl --mermaid
python -m langgraph_ledger repair traces/ # close crash-orphaned runs
python -m langgraph_ledger replay traces/run-42.jsonl # rebuild the message timeline
from langgraph_ledger import verify_thread
report = verify_thread(saver, "traces/run-42.jsonl") # stored state == logged claim?
assert report["ok"]
Crash recovery & replay
If the process dies mid-run, the log ends with an unclosed run/start.
verify flags it as an open run; repair appends an honest
run/end {status: "interrupted"} through the hash chain (idempotent, never
rewrites history). replay_messages() rebuilds the conversation timeline from
the log — digests by default, full text when the handler was created with
record_full=True. For fail-closed operation (a run that cannot record must
not proceed), use TraceRecorder(..., strict=True) — enforced at the recorder
and the checkpointer. Caveat: LangChain's callback manager may swallow handler
exceptions, so for hard guarantees prefer the checkpointer path.
Design mapping from DeepSeek Harness
A design study, not a port of code (dsh is TypeScript/Node; this is Python/LangGraph):
| dsh concept | here |
|---|---|
SessionEvent append-only log, single source of truth |
one hash-chained JSONL per thread |
SESSION_FORMAT_VERSION + ignorable marker |
v field; unknown kinds counted, not rejected |
| turn / step hierarchy | run/* / node/* events |
parentSession + seedLength fork lineage |
fork event payload |
sourceEventSeqs provenance |
DAG edges: chain, call→result, snapshot→parent |
| Model-Visible ⟺ Logged invariant | payload digests (sha256 + head); strict=True enforces fail-closed recording |
crash-orphaned turns closed as interrupted on reload |
repair — appends run/end {status: interrupted} through the hash chain |
deriveMessages (log → conversation) |
replay_messages — digests or full text (record_full=True) |
| fail-closed invariants | append-time strict JSON validation; verify_* refuse on mismatch |
Deliberate deviations: prompt/response full text is not stored by default (digest only, opt-in via record_full=True); dsh's byte-level replay and compaction are out of scope.
Proven in production
Before extraction, this design was battle-tested inside a production LangGraph agent product (private, Aug 2026) — a multi-round, tool-calling, real-model workload:
- Tracing overhead measured <0.1% of a turn — microsecond-level append+flush per event vs. hundreds of ms per LLM round-trip. "ON by default" was justified by measurement, not hope.
- 10 hook points wired with zero signature changes to existing code (contextvars propagation); with tracing OFF, behavior is bit-identical — pinned by tests.
- Snapshot/rollback drill passed end-to-end: file-level preimages, fail-closed on missing preimage (refuses rather than destroys), idempotent re-application.
- 3,150-test suite green after wiring the full hook surface.
- The trace layer was how failures were audited: one reviewed failure class went from 17/17 of cases to 0 after the trace made its mechanism visible; routing dead-ends went 2 → 0. (The latency cost of that refactor came from extra model rounds, not from tracing — the ledger itself stayed under 0.1%.)
Threat model (read before you rely on "tamper-evident")
The hash chain is keyless by default — and since 0.3.0, optionally keyed:
- ✅ Proven (keyless): given a trusted chain head, ANY deletion, reordering, or edit of ANY logged line is detectable (
verifyrecomputes the full chain). - ❌ Not proven (keyless): an attacker with write access to the log file who rewrites the entire file and re-chains it from genesis. No secret is involved, so nothing stops a full rewrite.
- 🔑 Keyed mode (HMAC-SHA256): pass
hmac_key=toTraceRecorder/recorder_for, and the chain cannot be re-chained without the key — the full-rewrite attack fails even against an attacker who controls the file. Keep the key out of the log's trust domain (env var, secrets manager); losing the key means losing the ability to verify.
TraceRecorder("traces", thread_id, hmac_key=os.environ["LEDGER_HMAC_KEY"])
export LANGGRAPH_LEDGER_HMAC_KEY=... # never pass keys as CLI args
python -m langgraph_ledger verify traces/run-42.jsonl
Keyed mode is fully backward compatible: keyless logs verify exactly as before (just don't pass a key), and content labels (tl_*, cp_*, payload digests) stay keyless so cross-log dedup and loop detection keep working without any secret.
Whether keyed or not: anchor the head outside the log's trust domain. Export it after each run and store it somewhere the process cannot write:
python -m langgraph_ledger head traces/run-42.jsonl # {"seq": N, "id": "<hex>"}
Later, verify + a head comparison gives you end-to-end integrity. A daily cron appending heads to an append-only store (or an email to yourself) is enough.
Two more honest limits:
- One writer process per log. The append lock is in-process (
threading.Lock); two processes appending to the same<thread>.jsonlwill fork the chain. Use one trace root per process, or serialize externally. verify_threadneeds the live saver. Checkpoint drift detection re-reads the saver, so it works across restarts only with a persistent backend (SQLite/Postgres). The log itself (verify_log) verifies anywhere.
What it is NOT
- Not a hosted observability platform — the log is a local file you own.
- Not byte-level replay of model streams — it is for error localization, audit and state rollback.
- Rollback restores agent state; side effects your tools made in the world are yours to undo (pair tool calls with your own preimages if you need that).
Development
pip install -e ".[test]"
pytest tests/
License
MIT — see LICENSE.
Metadata
Release files for langgraph-ledger 0.3.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 | Provenance | Actions |
|---|---|---|---|---|
| langgraph_ledger-0.3.0.tar.gz | 42.1 kB | With provenance | Details for langgraph_ledger-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| langgraph_ledger-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for langgraph_ledger-0.3.0-py3-none-any.whl |
Total release size: 78.2 kB
Release files / langgraph_ledger-0.3.0.tar.gz
| Download URL | langgraph_ledger-0.3.0.tar.gz |
|---|---|
| Size | 42.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9f21518a9474ec59bf2efaa65f9432484918afc67147c543ffad8461d4e3ac2e
|
|
BLAKE2b-256 checksum How to use checksums |
d0f22f4fc0c8e0b17bc985bb96ab333b9d1ac99e1dfce1d070c5ea3c7485bb79
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.
Transparency logRelease files / langgraph_ledger-0.3.0-py3-none-any.whl
| Download URL | langgraph_ledger-0.3.0-py3-none-any.whl |
|---|---|
| Size | 36.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
96269437f0d4330a1a416c91173110d46efe9cbdcceb043b30b554b6d16147f0
|
|
BLAKE2b-256 checksum How to use checksums |
044e8eb4ec50c72ff1620280d3ea659ad52777768f4b1082f6bc036e17e03cb5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.
Transparency logSecurity for langgraph-ledger 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for langgraph_ledger-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.