auditable 0.1.1__tar.gz → 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 (80) hide show
  1. auditable-0.1.1/README.md → auditable-0.2.0/PKG-INFO +75 -10
  2. auditable-0.1.1/PKG-INFO → auditable-0.2.0/README.md +28 -53
  3. {auditable-0.1.1 → auditable-0.2.0}/examples/README.md +57 -2
  4. auditable-0.2.0/examples/example_langchain_capture.py +100 -0
  5. auditable-0.2.0/examples/example_langgraph_live_replay.py +154 -0
  6. auditable-0.2.0/examples/example_live_monitor.py +78 -0
  7. auditable-0.2.0/examples/example_mcp_capture.py +116 -0
  8. {auditable-0.1.1 → auditable-0.2.0}/examples/example_payment_audit.py +113 -113
  9. {auditable-0.1.1 → auditable-0.2.0}/examples/example_post_rank_run.py +99 -99
  10. auditable-0.2.0/examples/example_state_anomaly.py +123 -0
  11. {auditable-0.1.1 → auditable-0.2.0}/pyproject.toml +11 -4
  12. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/__init__.py +79 -77
  13. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/analysis.py +615 -369
  14. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/data/__init__.py +35 -6
  15. auditable-0.2.0/src/auditable/data/inject.py +225 -0
  16. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/adapters/__init__.py +44 -44
  17. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/adapters/own_record.py +142 -142
  18. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/adapters/protocol.py +64 -64
  19. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/adapters/tau_bench.py +138 -138
  20. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/grounding.py +332 -332
  21. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/loader.py +250 -250
  22. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/risk.py +128 -128
  23. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/session.py +292 -292
  24. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/touch.py +122 -3
  25. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/integrations/__init__.py +4 -1
  26. auditable-0.2.0/src/auditable/integrations/langchain.py +805 -0
  27. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/integrations/langgraph.py +129 -9
  28. auditable-0.2.0/src/auditable/integrations/mcp.py +328 -0
  29. {auditable-0.1.1 → auditable-0.2.0}/tests/test_adapters.py +352 -352
  30. {auditable-0.1.1 → auditable-0.2.0}/tests/test_analysis.py +188 -188
  31. auditable-0.2.0/tests/test_data_inject.py +194 -0
  32. {auditable-0.1.1 → auditable-0.2.0}/tests/test_data_module.py +61 -1
  33. {auditable-0.1.1 → auditable-0.2.0}/tests/test_graph_loader.py +293 -293
  34. {auditable-0.1.1 → auditable-0.2.0}/tests/test_graph_risk.py +134 -134
  35. {auditable-0.1.1 → auditable-0.2.0}/tests/test_grounding.py +249 -249
  36. auditable-0.2.0/tests/test_langchain_integration.py +292 -0
  37. {auditable-0.1.1 → auditable-0.2.0}/tests/test_langgraph_integration.py +1 -1
  38. auditable-0.2.0/tests/test_langgraph_replay_bridge.py +132 -0
  39. auditable-0.2.0/tests/test_live_session.py +372 -0
  40. auditable-0.2.0/tests/test_mcp_integration.py +264 -0
  41. {auditable-0.1.1 → auditable-0.2.0}/tests/test_public_api.py +99 -92
  42. {auditable-0.1.1 → auditable-0.2.0}/tests/test_session_graph.py +157 -157
  43. {auditable-0.1.1 → auditable-0.2.0}/tests/test_session_graph_projection.py +141 -141
  44. auditable-0.2.0/tests/test_touch_replay_bridge.py +79 -0
  45. {auditable-0.1.1 → auditable-0.2.0}/.gitignore +0 -0
  46. {auditable-0.1.1 → auditable-0.2.0}/LICENSE +0 -0
  47. {auditable-0.1.1 → auditable-0.2.0}/examples/example_audit_report.py +0 -0
  48. {auditable-0.1.1 → auditable-0.2.0}/examples/example_end_to_end.py +0 -0
  49. {auditable-0.1.1 → auditable-0.2.0}/examples/example_langgraph_capture.py +0 -0
  50. {auditable-0.1.1 → auditable-0.2.0}/examples/example_langgraph_llm_agent.py +0 -0
  51. {auditable-0.1.1 → auditable-0.2.0}/examples/example_live_replay.py +0 -0
  52. {auditable-0.1.1 → auditable-0.2.0}/examples/example_pre_lint_plan.py +0 -0
  53. {auditable-0.1.1 → auditable-0.2.0}/examples/example_standalone_report.py +0 -0
  54. {auditable-0.1.1 → auditable-0.2.0}/examples/example_touch_capture.py +0 -0
  55. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/_report_figures.py +0 -0
  56. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/audit_report.py +0 -0
  57. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/chain.py +0 -0
  58. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/compound.py +0 -0
  59. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/__init__.py +0 -0
  60. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/adapters/declared_plan.py +0 -0
  61. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/features.py +0 -0
  62. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/graph/pre.py +0 -0
  63. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/harness/__init__.py +0 -0
  64. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/model/__init__.py +0 -0
  65. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/record.py +0 -0
  66. {auditable-0.1.1 → auditable-0.2.0}/src/auditable/report.py +0 -0
  67. {auditable-0.1.1 → auditable-0.2.0}/tests/test_audit_report.py +0 -0
  68. {auditable-0.1.1 → auditable-0.2.0}/tests/test_auditors.py +0 -0
  69. {auditable-0.1.1 → auditable-0.2.0}/tests/test_basic.py +0 -0
  70. {auditable-0.1.1 → auditable-0.2.0}/tests/test_compound.py +0 -0
  71. {auditable-0.1.1 → auditable-0.2.0}/tests/test_gate.py +0 -0
  72. {auditable-0.1.1 → auditable-0.2.0}/tests/test_graph.py +0 -0
  73. {auditable-0.1.1 → auditable-0.2.0}/tests/test_graph_execution_reach.py +0 -0
  74. {auditable-0.1.1 → auditable-0.2.0}/tests/test_graph_features.py +0 -0
  75. {auditable-0.1.1 → auditable-0.2.0}/tests/test_pre.py +0 -0
  76. {auditable-0.1.1 → auditable-0.2.0}/tests/test_pre_coverage.py +0 -0
  77. {auditable-0.1.1 → auditable-0.2.0}/tests/test_replay.py +0 -0
  78. {auditable-0.1.1 → auditable-0.2.0}/tests/test_report.py +0 -0
  79. {auditable-0.1.1 → auditable-0.2.0}/tests/test_sink.py +0 -0
  80. {auditable-0.1.1 → auditable-0.2.0}/tests/test_touch.py +0 -0
@@ -1,8 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: auditable
3
+ Version: 0.2.0
4
+ Summary: An open-source system of record for AI-agent decisions: it captures what each decision relied on, replays it against live state, and rolls back the committed action when it no longer holds.
5
+ Project-URL: Homepage, https://github.com/yzhao062/auditable
6
+ Project-URL: Repository, https://github.com/yzhao062/auditable
7
+ Project-URL: Issues, https://github.com/yzhao062/auditable/issues
8
+ Author-email: Yue Zhao <yzhao010@usc.edu>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: agent,agent-reliability,ai-agents,ai-risk,ai-safety,anomaly-detection,audit,auditing,compliance,langchain,langgraph,llm,mcp,model-context-protocol,observability,replay,trustworthy-ai
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: anomaly
25
+ Requires-Dist: pyod>=1.1.0; extra == 'anomaly'
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.0; extra == 'dev'
28
+ Requires-Dist: pyod>=1.1.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: twine>=5.0; extra == 'dev'
31
+ Provides-Extra: graph
32
+ Requires-Dist: networkx>=3.0; extra == 'graph'
33
+ Provides-Extra: langchain
34
+ Requires-Dist: langchain-core<2,>=0.3.0; extra == 'langchain'
35
+ Provides-Extra: langgraph
36
+ Requires-Dist: langgraph>=1.0; (python_version >= '3.10') and extra == 'langgraph'
37
+ Requires-Dist: networkx>=3.0; extra == 'langgraph'
38
+ Provides-Extra: llm
39
+ Requires-Dist: openai>=1.0; extra == 'llm'
40
+ Provides-Extra: mcp
41
+ Requires-Dist: mcp<2,>=1.28.1; (python_version >= '3.10') and extra == 'mcp'
42
+ Provides-Extra: report
43
+ Requires-Dist: fpdf2>=2.7; extra == 'report'
44
+ Requires-Dist: matplotlib>=3.5; extra == 'report'
45
+ Requires-Dist: networkx>=3.0; extra == 'report'
46
+ Description-Content-Type: text/markdown
47
+
1
48
  <div align="center">
2
49
 
3
50
  # auditable
4
51
 
5
- **Audit any agent decision across its past, present, and future, on one typed graph.**
52
+ **`auditable` is an open-source system of record for AI-agent decisions: it captures what each decision relied on, replays it against live state, and rolls back the committed action when it no longer holds.**
6
53
 
7
54
  *Your logs show what the agent did. `auditable` shows what it relied on, replays it under live state, and rolls it back when it no longer holds.*
8
55
 
@@ -82,9 +129,11 @@ pip install "auditable[langgraph]"
82
129
  python examples/example_langgraph_capture.py # a real LangGraph run -> observed=100%, keystone named
83
130
  ```
84
131
 
132
+ The same wrapped builder also yields **replayable** records, so the LIVE pillar runs on the captured run, not a hand-built dict. `builder.to_records(decisions={"approve": "vendor_payment"})` lowers a marked decision node into a `DecisionRecord` carrying the state it relied on, and `replay(record, live_state=...)` re-decides it under state that is live now and routes a rollback. See [`example_langgraph_live_replay.py`](examples/example_langgraph_live_replay.py): one real LangGraph run, captured, ranked (POST), then replayed and reversed under a drifted budget (LIVE).
133
+
85
134
  Want a real model in the loop? [`example_langgraph_llm_agent.py`](examples/example_langgraph_llm_agent.py) runs the same capture with live LLM nodes against any OpenAI-compatible endpoint (a plain OpenAI key, a gateway, or a local vLLM or Ollama server). The captured edges are identical, because the capture sees the state channels a node read and wrote, not the model call inside it.
86
135
 
87
- Not on LangGraph? The framework-agnostic `TouchRecorder` captures the same observed edges from any loop (a raw OpenAI or Anthropic agent, your own scheduler) by declaring each step's `reads()` and `writes()`. See [`examples/example_touch_capture.py`](examples/example_touch_capture.py). **Roadmap:** LangChain, CrewAI, MCP, and OpenTelemetry.
136
+ Not on LangGraph? A one-line callback captures any LangChain runnable (`auditable.integrations.langchain`), an in-place `instrument` records every tool call at an MCP server boundary (`auditable.integrations.mcp`), and the framework-agnostic `TouchRecorder` captures the same observed edges from any loop (a raw OpenAI or Anthropic agent, your own scheduler) by declaring each step's `reads()` and `writes()`. See [`examples/example_touch_capture.py`](examples/example_touch_capture.py). **Roadmap:** CrewAI and OpenTelemetry.
88
137
 
89
138
  ## Examples and Integrations
90
139
 
@@ -94,13 +143,17 @@ Point `auditable` at a scenario and it builds the same typed graph. Each row lin
94
143
  |---|---|---|---|
95
144
  | Capture a real LangGraph agent | LIVE → POST | a live `StateGraph` run (`instrument`) | [`example_langgraph_capture.py`](examples/example_langgraph_capture.py) |
96
145
  | Capture a real LLM agent (live model) | LIVE → POST | a model-driven `StateGraph` run (`instrument` + OpenAI-compatible) | [`example_langgraph_llm_agent.py`](examples/example_langgraph_llm_agent.py) |
146
+ | Capture a real LangChain runnable | LIVE → POST | a callback on any runnable (`LangChainCallbackHandler`) | [`example_langchain_capture.py`](examples/example_langchain_capture.py) |
97
147
  | Capture any tool loop by hand | LIVE → POST | declared resource touches (`TouchRecorder`) | [`example_touch_capture.py`](examples/example_touch_capture.py) |
148
+ | Capture MCP tool traffic at the server boundary | LIVE → POST | an instrumented FastMCP server (`instrument`) | [`example_mcp_capture.py`](examples/example_mcp_capture.py) |
149
+ | Capture a LangGraph agent, then replay and reverse it | LIVE → POST | a live `StateGraph` run lowered to replayable records (`instrument` + `to_records`) | [`example_langgraph_live_replay.py`](examples/example_langgraph_live_replay.py) |
98
150
  | Lint a declared plan before deploy | PRE | a framework-agnostic plan dict (`declared_plan_v1`) | [`example_pre_lint_plan.py`](examples/example_pre_lint_plan.py) |
99
151
  | Recover a payment as the budget drifts | LIVE | a decision captured live (`audit` + `replay`) | [`example_live_replay.py`](examples/example_live_replay.py) |
152
+ | Monitor a run as it streams, name the keystone live | LIVE | a run scored prefix by prefix (`LiveSession`) | [`example_live_monitor.py`](examples/example_live_monitor.py) |
100
153
  | Rank a tau-bench run, name the keystone | POST | a tau-bench trajectory (`tau_bench_prior_db_reads_v1`) | [`example_post_rank_run.py`](examples/example_post_rank_run.py) |
101
154
  | Walk one payment through every pillar | PRE, LIVE, POST | the full lifecycle (`own_record_v1` for POST) | [`example_end_to_end.py`](examples/example_end_to_end.py) |
102
155
 
103
- The first three rows capture a real run; see [Plug In Your Agent](#plug-in-your-agent) for the two-line setup. **Roadmap:** LangChain, CrewAI, MCP, and OpenTelemetry.
156
+ The first six rows capture a real run; see [Plug In Your Agent](#plug-in-your-agent) for the two-line setup. **Roadmap:** CrewAI and OpenTelemetry.
104
157
 
105
158
  ## The Lifecycle
106
159
 
@@ -122,9 +175,11 @@ The core is dependency-free and torch-free. Structural-graph analysis (`analyze_
122
175
 
123
176
  ```bash
124
177
  pip install "auditable[graph]"
178
+ pip install "auditable[anomaly]"
125
179
  ```
126
180
 
127
- The LIVE example runs on the core install alone.
181
+ The graph extra adds NetworkX. The anomaly extra adds the fitted `DataAuditor` path
182
+ through PyOD; freshness fallback and the LIVE example run on the core install alone.
128
183
 
129
184
  Docs: https://auditable-ai.readthedocs.io/
130
185
 
@@ -250,7 +305,12 @@ These standalone auditors are inputs to the record, not the headline. The compos
250
305
 
251
306
  **What ships today.** The full signed chain, replay under live state, executed recovery through a rail-neutral gate, two sinks (in-memory and append-only JSONL), the POST `analyze_run` ranking, the PRE `analyze_plan` lints plus preflight coverage report, and real-run capture into observed dependency edges (LangGraph via `instrument`, any loop via `TouchRecorder`).
252
307
 
253
- The release does **not** yet claim a learned data-anomaly method, a calibrated model-trust score, calibrated cross-layer risk, or live incremental scoring. The POST structural score is an uncalibrated ranking; the compound report is a transparent, explicitly uncalibrated debug bundle. Everything beyond this list is on the [roadmap](#roadmap).
308
+ The fitted data-anomaly path ships as an unpromoted module API, and `LiveSession`
309
+ prefix scoring ships. The release does **not** yet claim benchmark-grade detector
310
+ evidence, a calibrated model-trust score, or calibrated cross-layer risk. The POST
311
+ structural score is an uncalibrated ranking; the compound report is a transparent,
312
+ explicitly uncalibrated debug bundle. Calibrated compound scoring remains on the
313
+ [roadmap](#roadmap).
254
314
 
255
315
  </details>
256
316
 
@@ -261,17 +321,22 @@ The release does **not** yet claim a learned data-anomaly method, a calibrated m
261
321
 
262
322
  <br>
263
323
 
264
- Shipping today: the full capture, replay, and recover chain; PRE plan lints; POST run ranking; signed, hash-chained records; two sinks. Next:
324
+ Shipping today: the full capture, replay, and recover chain; PRE plan lints; POST run ranking; `LiveSession` prefix scoring; signed, hash-chained records; two sinks. The fitted PyOD detector over dependency snapshots also ships as an unpromoted module API, with freshness as the fallback; promotion waits for benchmark-grade evidence. Next:
265
325
 
266
- - **Smarter drift detection.** Learn what healthy dependency state looks like and flag anomalies, not just staleness (PyOD backend, with freshness as the fallback).
267
- - **One calibrated risk score.** Combine the data, model, and action signals into a single calibrated score, with the model treated as a first-class node in the graph.
268
- - **Live scoring.** Score decisions on the prefix graph as the agent runs, not only after the run completes. (Capturing what each step read and wrote already ships for LangGraph and the `TouchRecorder`.)
326
+ - **One calibrated compound score.** Combine the data, model, and action signals into a single calibrated score, with the model treated as a first-class node in the graph.
269
327
  - **Automatic fixes.** Refresh stale data, quarantine bad inputs, fall back to a safer model, or require human sign-off.
270
328
  - **Built-in CI checks.** A table-stakes OWASP-Agentic and CWE rule floor for continuous integration.
271
- - **Integrations (1.0).** LangGraph capture ships now (`instrument`); next are one-line hooks for LangChain and CrewAI, an MCP server, export to OpenTelemetry and LangSmith, downloadable evidence bundles, and a stable public API.
329
+ - **Integrations (1.0).** LangGraph capture (`instrument`), the LangChain callback, and MCP tool-boundary capture ship now; next are a CrewAI hook, export to OpenTelemetry and LangSmith, downloadable evidence bundles, and a stable public API.
272
330
 
273
331
  </details>
274
332
 
333
+ ## Related Projects
334
+
335
+ `auditable` is part of a small family of open agent-reliability projects:
336
+
337
+ - **[awesome-auditable-ai](https://github.com/yzhao062/awesome-auditable-ai)** — a curated reading list on AI agent reliability and auditing.
338
+ - **[GRADE](https://arxiv.org/abs/2606.22741)** — the research behind the typed two-layer graph this library is built on (also in [References](#references)).
339
+
275
340
  ## References
276
341
 
277
342
  <details>
@@ -1,51 +1,8 @@
1
- Metadata-Version: 2.4
2
- Name: auditable
3
- Version: 0.1.1
4
- Summary: Audit any agent decision across its past, present, and future, on one typed graph.
5
- Project-URL: Homepage, https://github.com/yzhao062/auditable
6
- Project-URL: Repository, https://github.com/yzhao062/auditable
7
- Project-URL: Issues, https://github.com/yzhao062/auditable/issues
8
- Author-email: Yue Zhao <yzhao010@usc.edu>
9
- License-Expression: Apache-2.0
10
- License-File: LICENSE
11
- Keywords: agent,agent-reliability,ai-agents,ai-risk,ai-safety,anomaly-detection,audit,auditing,compliance,llm,observability,replay,trustworthy-ai
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: Apache Software License
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
- Classifier: Topic :: Software Development :: Quality Assurance
23
- Requires-Python: >=3.9
24
- Provides-Extra: dev
25
- Requires-Dist: build>=1.0; extra == 'dev'
26
- Requires-Dist: pyod>=1.1.0; extra == 'dev'
27
- Requires-Dist: pytest>=7.0; extra == 'dev'
28
- Requires-Dist: twine>=5.0; extra == 'dev'
29
- Provides-Extra: graph
30
- Requires-Dist: networkx>=3.0; extra == 'graph'
31
- Provides-Extra: langchain
32
- Requires-Dist: langchain-core>=0.3.0; extra == 'langchain'
33
- Provides-Extra: langgraph
34
- Requires-Dist: langgraph>=1.0; (python_version >= '3.10') and extra == 'langgraph'
35
- Requires-Dist: networkx>=3.0; extra == 'langgraph'
36
- Provides-Extra: llm
37
- Requires-Dist: openai>=1.0; extra == 'llm'
38
- Provides-Extra: report
39
- Requires-Dist: fpdf2>=2.7; extra == 'report'
40
- Requires-Dist: matplotlib>=3.5; extra == 'report'
41
- Requires-Dist: networkx>=3.0; extra == 'report'
42
- Description-Content-Type: text/markdown
43
-
44
1
  <div align="center">
45
2
 
46
3
  # auditable
47
4
 
48
- **Audit any agent decision across its past, present, and future, on one typed graph.**
5
+ **`auditable` is an open-source system of record for AI-agent decisions: it captures what each decision relied on, replays it against live state, and rolls back the committed action when it no longer holds.**
49
6
 
50
7
  *Your logs show what the agent did. `auditable` shows what it relied on, replays it under live state, and rolls it back when it no longer holds.*
51
8
 
@@ -125,9 +82,11 @@ pip install "auditable[langgraph]"
125
82
  python examples/example_langgraph_capture.py # a real LangGraph run -> observed=100%, keystone named
126
83
  ```
127
84
 
85
+ The same wrapped builder also yields **replayable** records, so the LIVE pillar runs on the captured run, not a hand-built dict. `builder.to_records(decisions={"approve": "vendor_payment"})` lowers a marked decision node into a `DecisionRecord` carrying the state it relied on, and `replay(record, live_state=...)` re-decides it under state that is live now and routes a rollback. See [`example_langgraph_live_replay.py`](examples/example_langgraph_live_replay.py): one real LangGraph run, captured, ranked (POST), then replayed and reversed under a drifted budget (LIVE).
86
+
128
87
  Want a real model in the loop? [`example_langgraph_llm_agent.py`](examples/example_langgraph_llm_agent.py) runs the same capture with live LLM nodes against any OpenAI-compatible endpoint (a plain OpenAI key, a gateway, or a local vLLM or Ollama server). The captured edges are identical, because the capture sees the state channels a node read and wrote, not the model call inside it.
129
88
 
130
- Not on LangGraph? The framework-agnostic `TouchRecorder` captures the same observed edges from any loop (a raw OpenAI or Anthropic agent, your own scheduler) by declaring each step's `reads()` and `writes()`. See [`examples/example_touch_capture.py`](examples/example_touch_capture.py). **Roadmap:** LangChain, CrewAI, MCP, and OpenTelemetry.
89
+ Not on LangGraph? A one-line callback captures any LangChain runnable (`auditable.integrations.langchain`), an in-place `instrument` records every tool call at an MCP server boundary (`auditable.integrations.mcp`), and the framework-agnostic `TouchRecorder` captures the same observed edges from any loop (a raw OpenAI or Anthropic agent, your own scheduler) by declaring each step's `reads()` and `writes()`. See [`examples/example_touch_capture.py`](examples/example_touch_capture.py). **Roadmap:** CrewAI and OpenTelemetry.
131
90
 
132
91
  ## Examples and Integrations
133
92
 
@@ -137,13 +96,17 @@ Point `auditable` at a scenario and it builds the same typed graph. Each row lin
137
96
  |---|---|---|---|
138
97
  | Capture a real LangGraph agent | LIVE → POST | a live `StateGraph` run (`instrument`) | [`example_langgraph_capture.py`](examples/example_langgraph_capture.py) |
139
98
  | Capture a real LLM agent (live model) | LIVE → POST | a model-driven `StateGraph` run (`instrument` + OpenAI-compatible) | [`example_langgraph_llm_agent.py`](examples/example_langgraph_llm_agent.py) |
99
+ | Capture a real LangChain runnable | LIVE → POST | a callback on any runnable (`LangChainCallbackHandler`) | [`example_langchain_capture.py`](examples/example_langchain_capture.py) |
140
100
  | Capture any tool loop by hand | LIVE → POST | declared resource touches (`TouchRecorder`) | [`example_touch_capture.py`](examples/example_touch_capture.py) |
101
+ | Capture MCP tool traffic at the server boundary | LIVE → POST | an instrumented FastMCP server (`instrument`) | [`example_mcp_capture.py`](examples/example_mcp_capture.py) |
102
+ | Capture a LangGraph agent, then replay and reverse it | LIVE → POST | a live `StateGraph` run lowered to replayable records (`instrument` + `to_records`) | [`example_langgraph_live_replay.py`](examples/example_langgraph_live_replay.py) |
141
103
  | Lint a declared plan before deploy | PRE | a framework-agnostic plan dict (`declared_plan_v1`) | [`example_pre_lint_plan.py`](examples/example_pre_lint_plan.py) |
142
104
  | Recover a payment as the budget drifts | LIVE | a decision captured live (`audit` + `replay`) | [`example_live_replay.py`](examples/example_live_replay.py) |
105
+ | Monitor a run as it streams, name the keystone live | LIVE | a run scored prefix by prefix (`LiveSession`) | [`example_live_monitor.py`](examples/example_live_monitor.py) |
143
106
  | Rank a tau-bench run, name the keystone | POST | a tau-bench trajectory (`tau_bench_prior_db_reads_v1`) | [`example_post_rank_run.py`](examples/example_post_rank_run.py) |
144
107
  | Walk one payment through every pillar | PRE, LIVE, POST | the full lifecycle (`own_record_v1` for POST) | [`example_end_to_end.py`](examples/example_end_to_end.py) |
145
108
 
146
- The first three rows capture a real run; see [Plug In Your Agent](#plug-in-your-agent) for the two-line setup. **Roadmap:** LangChain, CrewAI, MCP, and OpenTelemetry.
109
+ The first six rows capture a real run; see [Plug In Your Agent](#plug-in-your-agent) for the two-line setup. **Roadmap:** CrewAI and OpenTelemetry.
147
110
 
148
111
  ## The Lifecycle
149
112
 
@@ -165,9 +128,11 @@ The core is dependency-free and torch-free. Structural-graph analysis (`analyze_
165
128
 
166
129
  ```bash
167
130
  pip install "auditable[graph]"
131
+ pip install "auditable[anomaly]"
168
132
  ```
169
133
 
170
- The LIVE example runs on the core install alone.
134
+ The graph extra adds NetworkX. The anomaly extra adds the fitted `DataAuditor` path
135
+ through PyOD; freshness fallback and the LIVE example run on the core install alone.
171
136
 
172
137
  Docs: https://auditable-ai.readthedocs.io/
173
138
 
@@ -293,7 +258,12 @@ These standalone auditors are inputs to the record, not the headline. The compos
293
258
 
294
259
  **What ships today.** The full signed chain, replay under live state, executed recovery through a rail-neutral gate, two sinks (in-memory and append-only JSONL), the POST `analyze_run` ranking, the PRE `analyze_plan` lints plus preflight coverage report, and real-run capture into observed dependency edges (LangGraph via `instrument`, any loop via `TouchRecorder`).
295
260
 
296
- The release does **not** yet claim a learned data-anomaly method, a calibrated model-trust score, calibrated cross-layer risk, or live incremental scoring. The POST structural score is an uncalibrated ranking; the compound report is a transparent, explicitly uncalibrated debug bundle. Everything beyond this list is on the [roadmap](#roadmap).
261
+ The fitted data-anomaly path ships as an unpromoted module API, and `LiveSession`
262
+ prefix scoring ships. The release does **not** yet claim benchmark-grade detector
263
+ evidence, a calibrated model-trust score, or calibrated cross-layer risk. The POST
264
+ structural score is an uncalibrated ranking; the compound report is a transparent,
265
+ explicitly uncalibrated debug bundle. Calibrated compound scoring remains on the
266
+ [roadmap](#roadmap).
297
267
 
298
268
  </details>
299
269
 
@@ -304,17 +274,22 @@ The release does **not** yet claim a learned data-anomaly method, a calibrated m
304
274
 
305
275
  <br>
306
276
 
307
- Shipping today: the full capture, replay, and recover chain; PRE plan lints; POST run ranking; signed, hash-chained records; two sinks. Next:
277
+ Shipping today: the full capture, replay, and recover chain; PRE plan lints; POST run ranking; `LiveSession` prefix scoring; signed, hash-chained records; two sinks. The fitted PyOD detector over dependency snapshots also ships as an unpromoted module API, with freshness as the fallback; promotion waits for benchmark-grade evidence. Next:
308
278
 
309
- - **Smarter drift detection.** Learn what healthy dependency state looks like and flag anomalies, not just staleness (PyOD backend, with freshness as the fallback).
310
- - **One calibrated risk score.** Combine the data, model, and action signals into a single calibrated score, with the model treated as a first-class node in the graph.
311
- - **Live scoring.** Score decisions on the prefix graph as the agent runs, not only after the run completes. (Capturing what each step read and wrote already ships for LangGraph and the `TouchRecorder`.)
279
+ - **One calibrated compound score.** Combine the data, model, and action signals into a single calibrated score, with the model treated as a first-class node in the graph.
312
280
  - **Automatic fixes.** Refresh stale data, quarantine bad inputs, fall back to a safer model, or require human sign-off.
313
281
  - **Built-in CI checks.** A table-stakes OWASP-Agentic and CWE rule floor for continuous integration.
314
- - **Integrations (1.0).** LangGraph capture ships now (`instrument`); next are one-line hooks for LangChain and CrewAI, an MCP server, export to OpenTelemetry and LangSmith, downloadable evidence bundles, and a stable public API.
282
+ - **Integrations (1.0).** LangGraph capture (`instrument`), the LangChain callback, and MCP tool-boundary capture ship now; next are a CrewAI hook, export to OpenTelemetry and LangSmith, downloadable evidence bundles, and a stable public API.
315
283
 
316
284
  </details>
317
285
 
286
+ ## Related Projects
287
+
288
+ `auditable` is part of a small family of open agent-reliability projects:
289
+
290
+ - **[awesome-auditable-ai](https://github.com/yzhao062/awesome-auditable-ai)** — a curated reading list on AI agent reliability and auditing.
291
+ - **[GRADE](https://arxiv.org/abs/2606.22741)** — the research behind the typed two-layer graph this library is built on (also in [References](#references)).
292
+
318
293
  ## References
319
294
 
320
295
  <details>
@@ -21,16 +21,23 @@ the langgraph extra):
21
21
  pip install "auditable[graph]"
22
22
  pip install "auditable[langgraph]" # for example_langgraph_capture.py
23
23
  pip install "auditable[langgraph,llm]" # for example_langgraph_llm_agent.py (a real model; needs an OpenAI-compatible key)
24
+ pip install "auditable[graph,langchain]" # for example_langchain_capture.py
25
+ pip install "auditable[graph,mcp]" # for example_mcp_capture.py (Python >= 3.10)
24
26
  ```
25
27
 
26
28
  | Pillar | Example | When it runs | Benchmark number |
27
29
  |--------|---------|--------------|------------------|
28
30
  | PRE | `example_pre_lint_plan.py` | Design time, before any step executes | None (capability demo) |
29
31
  | LIVE | `example_live_replay.py` | At decision time, on a committed action | None (capability demo) |
32
+ | LIVE | `example_live_monitor.py` | While a run streams, before it finishes | None (capability demo) |
30
33
  | POST | `example_post_rank_run.py` | After a run finishes | GRADE corpus results (see below) |
31
34
  | Capture | `example_langgraph_capture.py` | Capture a real LangGraph run, then rank it | None (capability demo) |
32
35
  | Capture | `example_langgraph_llm_agent.py` | Capture a real LLM-driven LangGraph run, then rank it | None (capability demo) |
36
+ | Capture | `example_langchain_capture.py` | Capture a real LangChain runnable, then rank it | None (capability demo) |
37
+ | Capture + LIVE | `example_langgraph_live_replay.py` | Capture a real LangGraph run, then replay and reverse its decision under drift | None (capability demo) |
38
+ | Capture + Data | `example_state_anomaly.py` | Capture clean state history, fit from it, then score synthetic faults | None (concept demo) |
33
39
  | Capture | `example_touch_capture.py` | Capture any tool loop by hand, then rank it | None (capability demo) |
40
+ | Capture | `example_mcp_capture.py` | Capture MCP tool traffic at the server boundary (ops-side mount) into an honest zero-coverage report | None (capability demo) |
34
41
 
35
42
  ## PRE: Lint a Declared Plan Before Deploy
36
43
 
@@ -81,6 +88,29 @@ Run it:
81
88
  python examples/example_live_replay.py
82
89
  ```
83
90
 
91
+ ## LIVE: Monitor a Run as It Streams
92
+
93
+ `example_live_monitor.py` is the other LIVE capability: a running structural score while
94
+ the run is still going. Where `example_live_replay.py` re-decides one committed action,
95
+ `LiveSession` watches the whole run grow. Feed each step as it happens
96
+ (`live.observe(step)`) and the running report names the keystone and its blast share over
97
+ the run seen so far, marked `completeness=prefix`, so a caller can flag, gate, or
98
+ checkpoint a high-blast step before the run ends. It is the same structural kernel as
99
+ POST, run on a growing graph.
100
+
101
+ The example replays the retail trajectory one step at a time so the score change is
102
+ visible: the first read carries no blast when it happens, becomes the keystone once the
103
+ first write lands on it, and its blast share then moves as later steps arrive. That
104
+ movement is the honest part. A prefix score is a live triage and gating signal, not a
105
+ validated early-warning probability; the blast structure can resolve late, so the keystone
106
+ may shift. This is a capability demo with NO benchmark percentage.
107
+
108
+ Run it:
109
+
110
+ ```
111
+ python examples/example_live_monitor.py
112
+ ```
113
+
84
114
  ## POST: Rank a Finished Run and Name the Keystone
85
115
 
86
116
  `example_post_rank_run.py` reads a run after it completes, builds one typed
@@ -144,10 +174,35 @@ a pure function or a frontier model, which is the point the example makes. It re
144
174
  plain OpenAI key or any compatible gateway or local server, and prints a notice and
145
175
  exits cleanly when no key is set. It needs `pip install "auditable[langgraph,llm]"`.
146
176
 
177
+ `example_langgraph_live_replay.py` carries one real LangGraph run through both
178
+ pillars. The same wrapped builder that yields the POST ranking also lowers the
179
+ marked decision node into a replayable `DecisionRecord`. `to_records` takes the
180
+ budget the approval relied on into the snapshot, and `action_args` / `action_costs`
181
+ lift the recipient and the real payment cost onto the action itself, so the lowered
182
+ record is the executable payment rather than a state-only stub:
183
+
184
+ ```python
185
+ record = builder.to_records(
186
+ decisions={"approve": "vendor_payment"},
187
+ action_args={"approve": {"recipient": "recipient"}},
188
+ action_costs={"approve": "amount"},
189
+ )[0]
190
+ ```
191
+
192
+ After capture, the example lowers the live budget below the approved amount (fault
193
+ injection, a controlled post-capture change, not drift the corpus supplied) and
194
+ `replay` re-decides on the live state with a cost-based policy: the action's cost
195
+ exceeds the budget that is live now, so it routes a `ROLLBACK` that the `ActionGate`
196
+ executes through a reference ledger. It needs `pip install "auditable[langgraph]"`.
197
+ It is a capability demo on one run, not the comparative replay-versus-baseline
198
+ experiment.
199
+
147
200
  `example_touch_capture.py` does the same for a plain tool loop with no framework,
148
201
  using the generic `TouchRecorder`: each step declares its `reads()` and `writes()`
149
- and the same matcher produces the observed edges. This is the framework-agnostic
150
- path for a raw OpenAI or Anthropic loop, or any scheduler.
202
+ and the same matcher produces the observed edges. The recorder also carries the
203
+ relied-on values (`reads(..., value=...)`) and a `decides(...)` action, so
204
+ `rec.to_records()` lowers a consequential step into the same replayable record. This
205
+ is the framework-agnostic path for a raw OpenAI or Anthropic loop, or any scheduler.
151
206
 
152
207
  Both are capability demos: the keystone ranking is a triage signal, not a
153
208
  calibrated probability, and the OBSERVED grade is an observed touch match, not a
@@ -0,0 +1,100 @@
1
+ """Plug in a real LangChain runnable: capture its run into the typed decision graph.
2
+
3
+ What this shows, in plain terms: you attach one `auditable` callback handler to
4
+ an actual LangChain runnable you already built, run it as usual, and get back the
5
+ typed two-layer decision graph for that run. Chain/runnable and model callbacks
6
+ become decision steps, tool callbacks become tool-call steps, and `analyze_run`
7
+ ranks the run and names the keystone, exactly as the POST pillar does for corpus
8
+ traces.
9
+
10
+ This is a real-agent capture path: no hand-written plan dict, no corpus fixture,
11
+ no network, and no API key. The only change to your runnable call is the callback,
12
+ followed by one analysis line:
13
+
14
+ handler = LangChainCallbackHandler() # 1) make once
15
+ chain.invoke(request, config={"callbacks": [handler]}) # 2) attach
16
+ report = analyze_run(handler, adapter=handler) # 3) analyze
17
+
18
+ The scenario is a small payment approver. `fetch_budget` prepares the amount and
19
+ budget, the `approve_payment` tool decides whether the payment fits, and
20
+ `record_ledger` records the result. LangChain's RunnableSequence callback tags
21
+ declare both handoffs, and the adjacent output/input payload digests match, so
22
+ those two State-A value-flow edges are graded OBSERVED.
23
+
24
+ Honesty holds in the output. Callbacks expose State A -- runnable/model reasoning
25
+ and tool flow -- but not State B, the live version or contents of the external
26
+ systems behind a tool. This example therefore makes no observed claim about an
27
+ account database; its OBSERVED edges mean exact callback-level value handoffs,
28
+ not external-state reads or precise causality. The score is a triage ranking, not
29
+ a calibrated probability. The corpus-scale GRADE numbers (arXiv:2606.22741) are
30
+ not produced here; this is a capability demo on one offline run.
31
+
32
+ Needs the graph + langchain extras: pip install "auditable[graph,langchain]"
33
+ Run: python examples/example_langchain_capture.py
34
+ """
35
+ try:
36
+ from langchain_core.runnables import RunnableLambda
37
+ from langchain_core.tools import tool
38
+ except ImportError:
39
+ print(
40
+ 'example skipped: install langchain-core with pip install "auditable[graph,langchain]"'
41
+ )
42
+ raise SystemExit(0)
43
+
44
+ from auditable import analyze_run
45
+ from auditable.integrations.langchain import LangChainCallbackHandler
46
+
47
+
48
+ def fetch_budget(request):
49
+ """Prepare the payment and the illustrative budget it will be checked against."""
50
+ return {"amount": request["amount"], "budget": 5_000}
51
+
52
+
53
+ @tool
54
+ def approve_payment(amount: int, budget: int) -> bool:
55
+ """Approve a payment only when it fits within the supplied budget."""
56
+ return amount <= budget
57
+
58
+
59
+ def record_ledger(approved):
60
+ """Record the tool's approval result."""
61
+ return {"approved": approved, "paid": approved}
62
+
63
+
64
+ def main():
65
+ chain = (
66
+ RunnableLambda(fetch_budget, name="fetch_budget")
67
+ | approve_payment
68
+ | RunnableLambda(record_ledger, name="record_ledger")
69
+ ).with_config(run_name="payment_chain")
70
+
71
+ handler = LangChainCallbackHandler()
72
+ final = chain.invoke({"amount": 4_200}, config={"callbacks": [handler]})
73
+ print(f"agent result: approved={final['approved']}, paid={final['paid']}\n")
74
+
75
+ # One public call: the callback handler is its own source and adapter.
76
+ report = analyze_run(handler, adapter=handler)
77
+ print(report)
78
+
79
+ print("\nDependency edges captured from the live run:")
80
+ steps = {step.idx: step for step in handler.steps()}
81
+ for step in steps.values():
82
+ for edge in step.deps:
83
+ source = steps[edge.src_idx]
84
+ print(
85
+ f" {step.agent} -> {source.agent} "
86
+ f"({edge.grade.value}, {edge.evidence['relation']})"
87
+ )
88
+
89
+ keystone = report.keystone
90
+ if keystone is not None:
91
+ print(
92
+ f"\nKeystone: step {keystone.idx} "
93
+ f"({keystone.node_attrs.get('runnable') or keystone.label}) -- "
94
+ "the approval and ledger result both rest on this captured handoff, "
95
+ "so it is the first step to review."
96
+ )
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()
@@ -0,0 +1,154 @@
1
+ """Capture a real LangGraph agent, then replay and reverse its decision under drift.
2
+
3
+ This joins the two halves auditable shipped separately. ``example_langgraph_capture.py``
4
+ captures a real LangGraph run for POST analysis (observed dependency edges). This
5
+ example does that AND lowers the captured decision into a replayable record, so the
6
+ LIVE pillar (``replay`` + routed recovery) runs on a real captured agent rather than
7
+ a hand-built dict. Wrap once, get both:
8
+
9
+ builder = instrument(StateGraph(State)) # 1) wrap; build / compile / invoke normally
10
+ report = analyze_run(builder, adapter=builder) # POST: rank the run, name the keystone
11
+ records = builder.to_records( # LIVE: replayable records
12
+ decisions={"approve": "vendor_payment"},
13
+ action_args={"approve": {"recipient": "recipient"}}, # action payload from captured fields
14
+ action_costs={"approve": "amount"}, # action cost from the captured amount
15
+ )
16
+
17
+ The scenario is a payment approver: ``fetch_budget`` reads the account budget,
18
+ ``approve`` decides a vendor payment against it, and ``record_ledger`` rests on the
19
+ approval. The agent is real (a LangGraph ``StateGraph`` of plain function nodes); the
20
+ relied-on budget and the decision are captured from the run, not hand-written.
21
+
22
+ The drift is fault injection, stated plainly: after the run is captured, we lower the
23
+ live budget below the approved amount and ``replay`` re-decides on that live state.
24
+ This is a controlled post-capture change, not a fault the corpus supplied (real agent
25
+ corpora carry no intra-run state drift). It is a capability demo on one run, not the
26
+ comparative replay-versus-baseline experiment, which is not shown here. No API key, no
27
+ network.
28
+
29
+ Needs: pip install "auditable[langgraph]"
30
+ Run: python examples/example_langgraph_live_replay.py
31
+ """
32
+ from typing import TypedDict
33
+
34
+ from auditable import Action, ActionGate, ReferenceLedger, analyze_run, replay
35
+ from auditable.integrations.langgraph import instrument
36
+
37
+ try:
38
+ from langgraph.graph import END, START, StateGraph
39
+ except ImportError as exc: # pragma: no cover - exercised only without the extra
40
+ raise SystemExit('This example needs langgraph: pip install "auditable[langgraph]"') from exc
41
+
42
+
43
+ class PaymentState(TypedDict):
44
+ invoice: str
45
+ recipient: str
46
+ amount: int
47
+ budget: int
48
+ allow_list: list
49
+ approved: bool
50
+ recorded: bool
51
+
52
+
53
+ def budget_policy(state, action):
54
+ """Re-decide the payment: the action's recipient must be allow-listed, and the action's
55
+ cost must fit the budget that is live now. The recipient and cost ride on the captured
56
+ action (``to_records(action_args=..., action_costs=...)``); the allow-list and budget come
57
+ from the dependency state. A cost-based check like this is why the lowered action must
58
+ carry the real cost, not ``0``."""
59
+ recipient = action.arguments.get("recipient")
60
+ allow_list = state.get("allow_list", [])
61
+ budget = state.get("budget", 0)
62
+ if recipient not in allow_list:
63
+ return False, f"recipient {recipient!r} not allow-listed"
64
+ if action.cost > budget:
65
+ return False, f"payment ${action.cost:,.0f} exceeds budget ${budget:,.0f}"
66
+ return True, f"payment ${action.cost:,.0f} within budget ${budget:,.0f}"
67
+
68
+
69
+ def build_agent():
70
+ """A real LangGraph payment approver of plain function nodes, instrumented once."""
71
+ builder = instrument(StateGraph(PaymentState))
72
+
73
+ def fetch_budget(state):
74
+ """Read the account budget the approval will rest on (the keystone source).
75
+ The allow-list is static config (read free in the initial state), so the budget
76
+ is the single fetched dependency the approval rests on."""
77
+ return {"budget": 10000}
78
+
79
+ def approve(state):
80
+ """Decide the payment against the budget and allow-list this node read."""
81
+ ok = state["recipient"] in state["allow_list"] and state["amount"] <= state["budget"]
82
+ return {"approved": ok}
83
+
84
+ def record_ledger(state):
85
+ """Rest on the approval the approve node wrote."""
86
+ return {"recorded": state["approved"]}
87
+
88
+ builder.add_node("fetch_budget", fetch_budget)
89
+ builder.add_node("approve", approve)
90
+ builder.add_node("record_ledger", record_ledger)
91
+ builder.add_edge(START, "fetch_budget")
92
+ builder.add_edge("fetch_budget", "approve")
93
+ builder.add_edge("approve", "record_ledger")
94
+ builder.add_edge("record_ledger", END)
95
+ return builder
96
+
97
+
98
+ def main():
99
+ builder = build_agent()
100
+ graph = builder.compile()
101
+ final = graph.invoke(
102
+ {
103
+ "invoice": "INV-4471",
104
+ "recipient": "acme-supplies",
105
+ "amount": 4200,
106
+ "budget": 0,
107
+ "allow_list": ["acme-supplies"],
108
+ "approved": False,
109
+ "recorded": False,
110
+ }
111
+ )
112
+ print(f"agent run: approved={final['approved']}\n")
113
+
114
+ # POST: the same captured run, ranked. fetch_budget is the structural keystone.
115
+ report = analyze_run(builder, adapter=builder)
116
+ print(report)
117
+ k = report.keystone
118
+ if k is not None:
119
+ print(f"\nPOST keystone: step {k.idx} ({k.node_attrs.get('langgraph_node')})\n")
120
+
121
+ # LIVE: lower the captured approval into a replayable record. The relied-on budget,
122
+ # amount, recipient, and allow-list become its DependencySnapshot; action_args and
123
+ # action_costs lift the recipient and the real cost onto the action itself, so the
124
+ # lowered record is the executable payment, not a state-only stub.
125
+ record = builder.to_records(
126
+ decisions={"approve": "vendor_payment"},
127
+ action_args={"approve": {"recipient": "recipient"}},
128
+ action_costs={"approve": "amount"},
129
+ )[0]
130
+ snap = record.data.snapshot.state
131
+ print("captured decision record:")
132
+ print(f" action: {record.harness.action_type} -> {record.harness.arguments}")
133
+ print(f" cost: ${record.harness.cost:,.0f} (the captured amount)")
134
+ print(f" relied-on budget: ${snap['budget']:,}; amount: ${snap['amount']:,}\n")
135
+
136
+ # Commit the captured payment through a reference ledger (a demo rail, not a production
137
+ # rail). The action committed is the one the record carries, not a hand-rebuilt one.
138
+ ledger = ReferenceLedger(balance=10000)
139
+ gate = ActionGate(ledger)
140
+ pay = Action(record.harness.action_type, dict(record.harness.arguments), cost=record.harness.cost)
141
+ receipt = gate.commit(pay)
142
+ print(f"committed ${pay.cost:,.0f}; ledger balance now ${ledger.balance:,.0f}")
143
+
144
+ # Fault injection: after capture, the live budget drops below the approved amount.
145
+ # replay re-decides on the live state and routes a rollback; the gate executes it.
146
+ live_state = {**snap, "budget": 3000}
147
+ verdict = replay(record, live_state=live_state, policy=budget_policy)
148
+ gate.enforce_post_commit(verdict, receipt=receipt)
149
+ print(f"replay under live budget $3,000: {verdict.action.value.upper()} -- {verdict.reason}")
150
+ print(f"recovery executed; ledger balance restored to ${ledger.balance:,.0f}")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()