langgraph-ledger 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. langgraph_ledger-0.2.0/LICENSE +21 -0
  2. langgraph_ledger-0.2.0/PKG-INFO +162 -0
  3. langgraph_ledger-0.2.0/README.md +138 -0
  4. langgraph_ledger-0.2.0/pyproject.toml +40 -0
  5. langgraph_ledger-0.2.0/setup.cfg +4 -0
  6. langgraph_ledger-0.2.0/src/langgraph_ledger/__init__.py +39 -0
  7. langgraph_ledger-0.2.0/src/langgraph_ledger/__main__.py +6 -0
  8. langgraph_ledger-0.2.0/src/langgraph_ledger/analysis.py +113 -0
  9. langgraph_ledger-0.2.0/src/langgraph_ledger/callbacks.py +221 -0
  10. langgraph_ledger-0.2.0/src/langgraph_ledger/checkpointer.py +213 -0
  11. langgraph_ledger-0.2.0/src/langgraph_ledger/cli.py +71 -0
  12. langgraph_ledger-0.2.0/src/langgraph_ledger/dag.py +155 -0
  13. langgraph_ledger-0.2.0/src/langgraph_ledger/events.py +161 -0
  14. langgraph_ledger-0.2.0/src/langgraph_ledger/hashing.py +76 -0
  15. langgraph_ledger-0.2.0/src/langgraph_ledger/py.typed +0 -0
  16. langgraph_ledger-0.2.0/src/langgraph_ledger/recorder.py +240 -0
  17. langgraph_ledger-0.2.0/src/langgraph_ledger/repair.py +70 -0
  18. langgraph_ledger-0.2.0/src/langgraph_ledger/replay.py +83 -0
  19. langgraph_ledger-0.2.0/src/langgraph_ledger/rollback.py +124 -0
  20. langgraph_ledger-0.2.0/src/langgraph_ledger/verify.py +148 -0
  21. langgraph_ledger-0.2.0/src/langgraph_ledger.egg-info/PKG-INFO +162 -0
  22. langgraph_ledger-0.2.0/src/langgraph_ledger.egg-info/SOURCES.txt +29 -0
  23. langgraph_ledger-0.2.0/src/langgraph_ledger.egg-info/dependency_links.txt +1 -0
  24. langgraph_ledger-0.2.0/src/langgraph_ledger.egg-info/entry_points.txt +2 -0
  25. langgraph_ledger-0.2.0/src/langgraph_ledger.egg-info/requires.txt +6 -0
  26. langgraph_ledger-0.2.0/src/langgraph_ledger.egg-info/top_level.txt +1 -0
  27. langgraph_ledger-0.2.0/tests/test_callbacks.py +62 -0
  28. langgraph_ledger-0.2.0/tests/test_checkpointer.py +139 -0
  29. langgraph_ledger-0.2.0/tests/test_hashing.py +41 -0
  30. langgraph_ledger-0.2.0/tests/test_recorder.py +87 -0
  31. langgraph_ledger-0.2.0/tests/test_recovery.py +212 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hty8870
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: langgraph-ledger
3
+ Version: 0.2.0
4
+ Summary: Full, tamper-evident traceability for LangGraph agents — a port of DeepSeek Harness (dsh)'s traceability design: hash-chained, content-addressed, replayable, rewindable.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/hty8870/langgraph-ledger
7
+ Project-URL: Repository, https://github.com/hty8870/langgraph-ledger
8
+ Keywords: langgraph,agent,traceability,audit,hash-chain,checkpoint,rollback,replay,dsh,deepseek-harness,ledger,agent-observability
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Debuggers
14
+ Classifier: Topic :: System :: Logging
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: langgraph-checkpoint>=2.0
19
+ Requires-Dist: langchain-core>=0.3
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest>=8; extra == "test"
22
+ Requires-Dist: langgraph>=0.2; extra == "test"
23
+ Dynamic: license-file
24
+
25
+ # langgraph-ledger
26
+
27
+ **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.
28
+
29
+ [English](README.md) · [中文](README.zh.md)
30
+
31
+ ## Why
32
+
33
+ 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?
34
+
35
+ This plugin ports the traceability design of [DeepSeek Harness](https://github.com/deepseek-ai/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**.
36
+
37
+ The combination is the point:
38
+
39
+ | Capability | Mechanism |
40
+ |---|---|
41
+ | Tool-call identity | content-addressed label `tl_<hash>` — same (name, input) ⇒ same label |
42
+ | Tamper evidence | every event hash-chained to its predecessor (merkle-style) |
43
+ | Execution structure | DAG over chain + call/result + checkpoint-parent edges |
44
+ | Rollback | `time_travel_config` (native fork) and `fork_thread` (dsh-style seeded fork with lineage event) |
45
+ | Failure analysis | error timeline, exact-repeat loop detection (falls out of the labels) |
46
+ | Verification | `verify_log` re-checks the chain; `verify_thread` re-hashes stored checkpoints against logged claims |
47
+
48
+ ## Differentiation (as of 2026-08)
49
+
50
+ Honest snapshot of the neighborhood — see [POSITIONING](docs/positioning.md) for details:
51
+
52
+ | Project | Hash chain | Execution DAG | State rollback/fork | LangGraph-native |
53
+ |---|---|---|---|---|
54
+ | **langgraph-ledger** | ✅ | ✅ | ✅ | ✅ (checkpointer drop-in) |
55
+ | LangSmith / Langfuse / AgentOps | ✗ (hosted observability) | trace view | ✗ | SDK |
56
+ | CONTINUUM | ✅ | ✗ | crash recovery focus | ✗ (MCP server) |
57
+ | burnout / VeritasAgent / memtrail | ✅ | ✗ | ✗ | partial |
58
+ | langgraph checkpointers (redis/mysql/…) | ✗ | parent links only | time-travel only | ✅ |
59
+
60
+ Nobody else ships hash-labels + DAG + rollback as one LangGraph-native unit. That is the slot this project occupies.
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install langgraph-ledger
66
+ ```
67
+
68
+ ## Quickstart
69
+
70
+ ```python
71
+ from langgraph.checkpoint.memory import InMemorySaver
72
+ from langgraph_ledger import TracingCheckpointSaver, DshTraceCallbackHandler
73
+
74
+ saver = TracingCheckpointSaver(InMemorySaver(), trace_root="./traces")
75
+ graph = builder.compile(checkpointer=saver) # your graph, unchanged
76
+
77
+ graph.invoke(input, config={
78
+ "configurable": {"thread_id": "run-42"},
79
+ "callbacks": [DshTraceCallbackHandler()], # LLM/tool/node events
80
+ })
81
+ ```
82
+
83
+ Every run now leaves `./traces/run-42.jsonl` — one hash-chained JSON event per line:
84
+
85
+ ```json
86
+ {"v":0,"seq":7,"ts":"…","kind":"tool/call","payload":{"label":"tl_9f2e…","name":"search",…},"id":"…","prev":"…"}
87
+ {"v":0,"seq":8,"ts":"…","kind":"state/snapshot","payload":{"checkpoint_id":"…","label":"cp_2c01…","parent_checkpoint_id":"…","checkpoint_sha256":"…"},"id":"…","prev":"…"}
88
+ ```
89
+
90
+ ### Rollback & fork
91
+
92
+ ```python
93
+ from langgraph_ledger import time_travel_config, fork_thread
94
+
95
+ # resume/fork from any recorded checkpoint (LangGraph-native time travel)
96
+ cfg = time_travel_config("run-42", checkpoint_id="<past-id>")
97
+ graph.update_state(cfg, {"count": 100}) # forks from that point
98
+
99
+ # dsh-style: a new thread seeded with the ancestry up to a checkpoint
100
+ new_tid = fork_thread(saver, "run-42", at_checkpoint_id="<past-id>")
101
+ ```
102
+
103
+ ### Audit
104
+
105
+ ```bash
106
+ python -m langgraph_ledger verify traces/run-42.jsonl # hash chain intact?
107
+ python -m langgraph_ledger analyze traces/run-42.jsonl # errors, loops, timeline
108
+ python -m langgraph_ledger dag traces/run-42.jsonl --mermaid
109
+ python -m langgraph_ledger repair traces/ # close crash-orphaned runs
110
+ python -m langgraph_ledger replay traces/run-42.jsonl # rebuild the message timeline
111
+ ```
112
+
113
+ ```python
114
+ from langgraph_ledger import verify_thread
115
+ report = verify_thread(saver, "traces/run-42.jsonl") # stored state == logged claim?
116
+ assert report["ok"]
117
+ ```
118
+
119
+ ### Crash recovery & replay
120
+
121
+ If the process dies mid-run, the log ends with an unclosed `run/start`.
122
+ `verify` flags it as an *open run*; `repair` appends an honest
123
+ `run/end {status: "interrupted"}` through the hash chain (idempotent, never
124
+ rewrites history). `replay_messages()` rebuilds the conversation timeline from
125
+ the log — digests by default, full text when the handler was created with
126
+ `record_full=True`. For fail-closed operation (a run that cannot record must
127
+ not proceed), use `TraceRecorder(..., strict=True)`.
128
+
129
+ ## Design mapping from DeepSeek Harness
130
+
131
+ A design study, not a port of code (dsh is TypeScript/Node; this is Python/LangGraph):
132
+
133
+ | dsh concept | here |
134
+ |---|---|
135
+ | `SessionEvent` append-only log, single source of truth | one hash-chained JSONL per thread |
136
+ | `SESSION_FORMAT_VERSION` + `ignorable` marker | `v` field; unknown kinds counted, not rejected |
137
+ | turn / step hierarchy | `run/*` / `node/*` events |
138
+ | `parentSession` + `seedLength` fork lineage | `fork` event payload |
139
+ | `sourceEventSeqs` provenance | DAG edges: chain, call→result, snapshot→parent |
140
+ | Model-Visible ⟺ Logged invariant | payload digests (sha256 + head); `strict=True` enforces fail-closed recording |
141
+ | crash-orphaned turns closed as `interrupted` on reload | `repair` — appends `run/end {status: interrupted}` through the hash chain |
142
+ | `deriveMessages` (log → conversation) | `replay_messages` — digests or full text (`record_full=True`) |
143
+ | fail-closed invariants | append-time strict JSON validation; `verify_*` refuse on mismatch |
144
+
145
+ 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.
146
+
147
+ ## What it is NOT
148
+
149
+ - Not a hosted observability platform — the log is a local file you own.
150
+ - Not byte-level replay of model streams — it is for error localization, audit and state rollback.
151
+ - 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).
152
+
153
+ ## Development
154
+
155
+ ```bash
156
+ pip install -e ".[test]"
157
+ pytest tests/
158
+ ```
159
+
160
+ ## License
161
+
162
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,138 @@
1
+ # langgraph-ledger
2
+
3
+ **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.
4
+
5
+ [English](README.md) · [中文](README.zh.md)
6
+
7
+ ## Why
8
+
9
+ 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?
10
+
11
+ This plugin ports the traceability design of [DeepSeek Harness](https://github.com/deepseek-ai/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**.
12
+
13
+ The combination is the point:
14
+
15
+ | Capability | Mechanism |
16
+ |---|---|
17
+ | Tool-call identity | content-addressed label `tl_<hash>` — same (name, input) ⇒ same label |
18
+ | Tamper evidence | every event hash-chained to its predecessor (merkle-style) |
19
+ | Execution structure | DAG over chain + call/result + checkpoint-parent edges |
20
+ | Rollback | `time_travel_config` (native fork) and `fork_thread` (dsh-style seeded fork with lineage event) |
21
+ | Failure analysis | error timeline, exact-repeat loop detection (falls out of the labels) |
22
+ | Verification | `verify_log` re-checks the chain; `verify_thread` re-hashes stored checkpoints against logged claims |
23
+
24
+ ## Differentiation (as of 2026-08)
25
+
26
+ Honest snapshot of the neighborhood — see [POSITIONING](docs/positioning.md) for details:
27
+
28
+ | Project | Hash chain | Execution DAG | State rollback/fork | LangGraph-native |
29
+ |---|---|---|---|---|
30
+ | **langgraph-ledger** | ✅ | ✅ | ✅ | ✅ (checkpointer drop-in) |
31
+ | LangSmith / Langfuse / AgentOps | ✗ (hosted observability) | trace view | ✗ | SDK |
32
+ | CONTINUUM | ✅ | ✗ | crash recovery focus | ✗ (MCP server) |
33
+ | burnout / VeritasAgent / memtrail | ✅ | ✗ | ✗ | partial |
34
+ | langgraph checkpointers (redis/mysql/…) | ✗ | parent links only | time-travel only | ✅ |
35
+
36
+ Nobody else ships hash-labels + DAG + rollback as one LangGraph-native unit. That is the slot this project occupies.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install langgraph-ledger
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ ```python
47
+ from langgraph.checkpoint.memory import InMemorySaver
48
+ from langgraph_ledger import TracingCheckpointSaver, DshTraceCallbackHandler
49
+
50
+ saver = TracingCheckpointSaver(InMemorySaver(), trace_root="./traces")
51
+ graph = builder.compile(checkpointer=saver) # your graph, unchanged
52
+
53
+ graph.invoke(input, config={
54
+ "configurable": {"thread_id": "run-42"},
55
+ "callbacks": [DshTraceCallbackHandler()], # LLM/tool/node events
56
+ })
57
+ ```
58
+
59
+ Every run now leaves `./traces/run-42.jsonl` — one hash-chained JSON event per line:
60
+
61
+ ```json
62
+ {"v":0,"seq":7,"ts":"…","kind":"tool/call","payload":{"label":"tl_9f2e…","name":"search",…},"id":"…","prev":"…"}
63
+ {"v":0,"seq":8,"ts":"…","kind":"state/snapshot","payload":{"checkpoint_id":"…","label":"cp_2c01…","parent_checkpoint_id":"…","checkpoint_sha256":"…"},"id":"…","prev":"…"}
64
+ ```
65
+
66
+ ### Rollback & fork
67
+
68
+ ```python
69
+ from langgraph_ledger import time_travel_config, fork_thread
70
+
71
+ # resume/fork from any recorded checkpoint (LangGraph-native time travel)
72
+ cfg = time_travel_config("run-42", checkpoint_id="<past-id>")
73
+ graph.update_state(cfg, {"count": 100}) # forks from that point
74
+
75
+ # dsh-style: a new thread seeded with the ancestry up to a checkpoint
76
+ new_tid = fork_thread(saver, "run-42", at_checkpoint_id="<past-id>")
77
+ ```
78
+
79
+ ### Audit
80
+
81
+ ```bash
82
+ python -m langgraph_ledger verify traces/run-42.jsonl # hash chain intact?
83
+ python -m langgraph_ledger analyze traces/run-42.jsonl # errors, loops, timeline
84
+ python -m langgraph_ledger dag traces/run-42.jsonl --mermaid
85
+ python -m langgraph_ledger repair traces/ # close crash-orphaned runs
86
+ python -m langgraph_ledger replay traces/run-42.jsonl # rebuild the message timeline
87
+ ```
88
+
89
+ ```python
90
+ from langgraph_ledger import verify_thread
91
+ report = verify_thread(saver, "traces/run-42.jsonl") # stored state == logged claim?
92
+ assert report["ok"]
93
+ ```
94
+
95
+ ### Crash recovery & replay
96
+
97
+ If the process dies mid-run, the log ends with an unclosed `run/start`.
98
+ `verify` flags it as an *open run*; `repair` appends an honest
99
+ `run/end {status: "interrupted"}` through the hash chain (idempotent, never
100
+ rewrites history). `replay_messages()` rebuilds the conversation timeline from
101
+ the log — digests by default, full text when the handler was created with
102
+ `record_full=True`. For fail-closed operation (a run that cannot record must
103
+ not proceed), use `TraceRecorder(..., strict=True)`.
104
+
105
+ ## Design mapping from DeepSeek Harness
106
+
107
+ A design study, not a port of code (dsh is TypeScript/Node; this is Python/LangGraph):
108
+
109
+ | dsh concept | here |
110
+ |---|---|
111
+ | `SessionEvent` append-only log, single source of truth | one hash-chained JSONL per thread |
112
+ | `SESSION_FORMAT_VERSION` + `ignorable` marker | `v` field; unknown kinds counted, not rejected |
113
+ | turn / step hierarchy | `run/*` / `node/*` events |
114
+ | `parentSession` + `seedLength` fork lineage | `fork` event payload |
115
+ | `sourceEventSeqs` provenance | DAG edges: chain, call→result, snapshot→parent |
116
+ | Model-Visible ⟺ Logged invariant | payload digests (sha256 + head); `strict=True` enforces fail-closed recording |
117
+ | crash-orphaned turns closed as `interrupted` on reload | `repair` — appends `run/end {status: interrupted}` through the hash chain |
118
+ | `deriveMessages` (log → conversation) | `replay_messages` — digests or full text (`record_full=True`) |
119
+ | fail-closed invariants | append-time strict JSON validation; `verify_*` refuse on mismatch |
120
+
121
+ 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.
122
+
123
+ ## What it is NOT
124
+
125
+ - Not a hosted observability platform — the log is a local file you own.
126
+ - Not byte-level replay of model streams — it is for error localization, audit and state rollback.
127
+ - 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).
128
+
129
+ ## Development
130
+
131
+ ```bash
132
+ pip install -e ".[test]"
133
+ pytest tests/
134
+ ```
135
+
136
+ ## License
137
+
138
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "langgraph-ledger"
7
+ version = "0.2.0"
8
+ description = "Full, tamper-evident traceability for LangGraph agents — a port of DeepSeek Harness (dsh)'s traceability design: hash-chained, content-addressed, replayable, rewindable."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ keywords = ["langgraph", "agent", "traceability", "audit", "hash-chain", "checkpoint", "rollback", "replay", "dsh", "deepseek-harness", "ledger", "agent-observability"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Software Development :: Debuggers",
19
+ "Topic :: System :: Logging",
20
+ ]
21
+ dependencies = [
22
+ "langgraph-checkpoint>=2.0",
23
+ "langchain-core>=0.3",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ test = ["pytest>=8", "langgraph>=0.2"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/hty8870/langgraph-ledger"
31
+ Repository = "https://github.com/hty8870/langgraph-ledger"
32
+
33
+ [project.scripts]
34
+ langgraph-ledger = "langgraph_ledger.cli:main"
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.setuptools.package-data]
40
+ langgraph_ledger = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Public API of langgraph-ledger."""
3
+ from __future__ import annotations
4
+
5
+ from .analysis import analyze_log
6
+ from .callbacks import DshTraceCallbackHandler
7
+ from .checkpointer import TracingCheckpointSaver
8
+ from .dag import RunDAG, build_dag, build_dag_from_file
9
+ from .events import EVENT_KINDS, FORMAT_VERSION
10
+ from .hashing import (canonical_json, checkpoint_label, event_id, sha256_hex,
11
+ tool_call_label)
12
+ from .recorder import (TracePayloadError, TraceRecorder, active_recorder,
13
+ bind_recorder, current_recorder, emit_event, read_log,
14
+ recorder_for)
15
+ from .repair import close_orphaned_run, find_orphaned_runs, repair_all
16
+ from .replay import ReplayError, replay_messages
17
+ from .rollback import find_checkpoint_by_label, fork_thread, time_travel_config
18
+ from .verify import VerifyReport, verify_log, verify_thread
19
+
20
+ __version__ = "0.2.0"
21
+
22
+ __all__ = [
23
+ "__version__",
24
+ # recording
25
+ "TraceRecorder", "TracePayloadError", "bind_recorder", "current_recorder",
26
+ "active_recorder", "emit_event", "read_log", "recorder_for",
27
+ # integration
28
+ "TracingCheckpointSaver", "DshTraceCallbackHandler",
29
+ # labels / hashing
30
+ "canonical_json", "sha256_hex", "event_id", "tool_call_label",
31
+ "checkpoint_label", "EVENT_KINDS", "FORMAT_VERSION",
32
+ # dag / rollback / audit
33
+ "RunDAG", "build_dag", "build_dag_from_file",
34
+ "fork_thread", "time_travel_config", "find_checkpoint_by_label",
35
+ "verify_log", "verify_thread", "VerifyReport", "analyze_log",
36
+ # crash recovery / replay
37
+ "find_orphaned_runs", "close_orphaned_run", "repair_all",
38
+ "replay_messages", "ReplayError",
39
+ ]
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Enable `python -m langgraph_ledger <command>`."""
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,113 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Failure analysis over a recorded log.
3
+
4
+ Because every tool call carries a content-addressed label, exact-repeat loops
5
+ (the agent calling the same tool with the same input over and over) fall out
6
+ for free. Combined with error events and node timings this gives a compact
7
+ first-stop failure report:
8
+
9
+ python -m langgraph_ledger analyze <log.jsonl>
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from . import events as ev
17
+ from .recorder import read_log
18
+
19
+ __all__ = ["analyze_log"]
20
+
21
+
22
+ def analyze_log(path: str | Path) -> dict[str, Any]:
23
+ """Summarize one thread log: counts, errors, loops, timeline, checkpoints."""
24
+ by_kind: dict[str, int] = {}
25
+ errors: list[dict[str, Any]] = []
26
+ tool_labels: dict[str, dict[str, Any]] = {}
27
+ checkpoints = 0
28
+ interrupted_runs = 0
29
+ models: dict[str, int] = {}
30
+ node_ms: dict[str, int] = {}
31
+ first_ts: str | None = None
32
+ last_ts: str | None = None
33
+ unknown: dict[str, int] = {}
34
+
35
+ prev_call_label: str | None = None
36
+ streaks: list[dict[str, Any]] = []
37
+ streak: dict[str, Any] | None = None
38
+
39
+ for e in read_log(path):
40
+ kind = str(e.get("kind") or "")
41
+ payload = e.get("payload") or {}
42
+ seq = e.get("seq")
43
+ first_ts = first_ts or e.get("ts")
44
+ last_ts = e.get("ts") or last_ts
45
+ if kind in ev.EVENT_KINDS:
46
+ by_kind[kind] = by_kind.get(kind, 0) + 1
47
+ else:
48
+ unknown[kind] = unknown.get(kind, 0) + 1
49
+
50
+ if kind == ev.KIND_TOOL_CALL:
51
+ label = str(payload.get("label") or "")
52
+ name = str(payload.get("name") or "")
53
+ slot = tool_labels.setdefault(label, {"name": name, "count": 0, "seqs": []})
54
+ slot["count"] += 1
55
+ slot["seqs"].append(seq)
56
+ if label == prev_call_label:
57
+ if streak is None:
58
+ streak = {"label": label, "name": name, "start_seq": seq, "length": 2}
59
+ else:
60
+ streak["length"] += 1
61
+ else:
62
+ if streak is not None:
63
+ streaks.append(streak)
64
+ streak = None
65
+ prev_call_label = label
66
+ elif kind == ev.KIND_TOOL_RESULT:
67
+ if not payload.get("ok"):
68
+ errors.append({"seq": seq, "kind": kind,
69
+ "name": payload.get("name"),
70
+ "error": payload.get("error")})
71
+ elif kind == ev.KIND_LLM_CALL and payload.get("error"):
72
+ errors.append({"seq": seq, "kind": kind, "node": payload.get("node"),
73
+ "error": payload.get("error")})
74
+ if kind == ev.KIND_LLM_CALL and payload.get("model"):
75
+ m = str(payload["model"])
76
+ models[m] = models.get(m, 0) + 1
77
+ elif kind == ev.KIND_NODE_END and not payload.get("ok", True):
78
+ errors.append({"seq": seq, "kind": kind, "run_id": payload.get("run_id"),
79
+ "error": payload.get("error")})
80
+ if kind == ev.KIND_NODE_END and payload.get("node"):
81
+ node = str(payload["node"])
82
+ node_ms[node] = node_ms.get(node, 0) + int(payload.get("ms") or 0)
83
+ elif kind == ev.KIND_RUN_END and payload.get("status") == "interrupted":
84
+ interrupted_runs += 1
85
+ elif kind == ev.KIND_ERROR:
86
+ errors.append({"seq": seq, "kind": kind, "where": payload.get("where"),
87
+ "error": payload.get("error")})
88
+ elif kind == ev.KIND_STATE_SNAPSHOT:
89
+ checkpoints += 1
90
+ prev_call_label = None
91
+
92
+ if streak is not None:
93
+ streaks.append(streak)
94
+
95
+ loops = [{"label": k, "name": v["name"], "count": v["count"], "seqs": v["seqs"]}
96
+ for k, v in tool_labels.items() if v["count"] > 1]
97
+
98
+ return {
99
+ "log": str(path),
100
+ "events": sum(by_kind.values()) + sum(unknown.values()),
101
+ "by_kind": by_kind,
102
+ "unknown_kinds": unknown,
103
+ "checkpoints": checkpoints,
104
+ "interrupted_runs": interrupted_runs,
105
+ "models": models,
106
+ "node_time_ms": node_ms,
107
+ "errors": errors,
108
+ "error_count": len(errors),
109
+ "repeated_tool_calls": loops,
110
+ "consecutive_repeat_streaks": streaks,
111
+ "first_ts": first_ts,
112
+ "last_ts": last_ts,
113
+ }