trajectory-causal-attribution 1.0.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 (72) hide show
  1. trajectory_causal_attribution-1.0.0/.gitignore +40 -0
  2. trajectory_causal_attribution-1.0.0/LICENSE +21 -0
  3. trajectory_causal_attribution-1.0.0/PKG-INFO +593 -0
  4. trajectory_causal_attribution-1.0.0/README.md +555 -0
  5. trajectory_causal_attribution-1.0.0/benchmarks/README.md +50 -0
  6. trajectory_causal_attribution-1.0.0/benchmarks/whowhen.py +227 -0
  7. trajectory_causal_attribution-1.0.0/examples/connect_any_framework.py +50 -0
  8. trajectory_causal_attribution-1.0.0/examples/quickstart.py +29 -0
  9. trajectory_causal_attribution-1.0.0/examples/test_agent_with_pytest.py +36 -0
  10. trajectory_causal_attribution-1.0.0/huggingface/README.md +25 -0
  11. trajectory_causal_attribution-1.0.0/pyproject.toml +86 -0
  12. trajectory_causal_attribution-1.0.0/src/agent_replay/__init__.py +99 -0
  13. trajectory_causal_attribution-1.0.0/src/agent_replay/_ambient.py +34 -0
  14. trajectory_causal_attribution-1.0.0/src/agent_replay/ablation.py +235 -0
  15. trajectory_causal_attribution-1.0.0/src/agent_replay/adapters/__init__.py +15 -0
  16. trajectory_causal_attribution-1.0.0/src/agent_replay/adapters/langchain.py +103 -0
  17. trajectory_causal_attribution-1.0.0/src/agent_replay/adapters/openai_sdk.py +107 -0
  18. trajectory_causal_attribution-1.0.0/src/agent_replay/aggregate.py +218 -0
  19. trajectory_causal_attribution-1.0.0/src/agent_replay/analyze.py +95 -0
  20. trajectory_causal_attribution-1.0.0/src/agent_replay/attribution.py +420 -0
  21. trajectory_causal_attribution-1.0.0/src/agent_replay/cli.py +555 -0
  22. trajectory_causal_attribution-1.0.0/src/agent_replay/drift.py +347 -0
  23. trajectory_causal_attribution-1.0.0/src/agent_replay/errors.py +26 -0
  24. trajectory_causal_attribution-1.0.0/src/agent_replay/explain.py +329 -0
  25. trajectory_causal_attribution-1.0.0/src/agent_replay/faithfulness.py +193 -0
  26. trajectory_causal_attribution-1.0.0/src/agent_replay/hashing.py +37 -0
  27. trajectory_causal_attribution-1.0.0/src/agent_replay/instrument.py +406 -0
  28. trajectory_causal_attribution-1.0.0/src/agent_replay/interop.py +307 -0
  29. trajectory_causal_attribution-1.0.0/src/agent_replay/multiverse.py +344 -0
  30. trajectory_causal_attribution-1.0.0/src/agent_replay/pytest_plugin.py +206 -0
  31. trajectory_causal_attribution-1.0.0/src/agent_replay/recorder.py +437 -0
  32. trajectory_causal_attribution-1.0.0/src/agent_replay/repair.py +163 -0
  33. trajectory_causal_attribution-1.0.0/src/agent_replay/replayer.py +307 -0
  34. trajectory_causal_attribution-1.0.0/src/agent_replay/report.py +302 -0
  35. trajectory_causal_attribution-1.0.0/src/agent_replay/serve.py +199 -0
  36. trajectory_causal_attribution-1.0.0/src/agent_replay/session.py +81 -0
  37. trajectory_causal_attribution-1.0.0/src/agent_replay/stats.py +131 -0
  38. trajectory_causal_attribution-1.0.0/src/agent_replay/store.py +281 -0
  39. trajectory_causal_attribution-1.0.0/src/agent_replay/types.py +341 -0
  40. trajectory_causal_attribution-1.0.0/tests/_demo_agent.py +106 -0
  41. trajectory_causal_attribution-1.0.0/tests/conftest.py +34 -0
  42. trajectory_causal_attribution-1.0.0/tests/test_ablation.py +59 -0
  43. trajectory_causal_attribution-1.0.0/tests/test_action_observation.py +149 -0
  44. trajectory_causal_attribution-1.0.0/tests/test_adaptive.py +103 -0
  45. trajectory_causal_attribution-1.0.0/tests/test_aggregate.py +83 -0
  46. trajectory_causal_attribution-1.0.0/tests/test_async.py +121 -0
  47. trajectory_causal_attribution-1.0.0/tests/test_attribution.py +89 -0
  48. trajectory_causal_attribution-1.0.0/tests/test_benchmark.py +40 -0
  49. trajectory_causal_attribution-1.0.0/tests/test_branching.py +85 -0
  50. trajectory_causal_attribution-1.0.0/tests/test_cache.py +75 -0
  51. trajectory_causal_attribution-1.0.0/tests/test_cli.py +239 -0
  52. trajectory_causal_attribution-1.0.0/tests/test_drift.py +113 -0
  53. trajectory_causal_attribution-1.0.0/tests/test_end_to_end.py +78 -0
  54. trajectory_causal_attribution-1.0.0/tests/test_explain.py +103 -0
  55. trajectory_causal_attribution-1.0.0/tests/test_faithfulness.py +89 -0
  56. trajectory_causal_attribution-1.0.0/tests/test_instrument.py +180 -0
  57. trajectory_causal_attribution-1.0.0/tests/test_interop.py +127 -0
  58. trajectory_causal_attribution-1.0.0/tests/test_multiverse.py +179 -0
  59. trajectory_causal_attribution-1.0.0/tests/test_openai_adapter.py +61 -0
  60. trajectory_causal_attribution-1.0.0/tests/test_properties.py +110 -0
  61. trajectory_causal_attribution-1.0.0/tests/test_pytest_plugin.py +100 -0
  62. trajectory_causal_attribution-1.0.0/tests/test_recorder.py +47 -0
  63. trajectory_causal_attribution-1.0.0/tests/test_repair.py +38 -0
  64. trajectory_causal_attribution-1.0.0/tests/test_repair_v2.py +70 -0
  65. trajectory_causal_attribution-1.0.0/tests/test_replayer.py +57 -0
  66. trajectory_causal_attribution-1.0.0/tests/test_report.py +64 -0
  67. trajectory_causal_attribution-1.0.0/tests/test_serve.py +76 -0
  68. trajectory_causal_attribution-1.0.0/tests/test_soundness.py +178 -0
  69. trajectory_causal_attribution-1.0.0/tests/test_stats.py +56 -0
  70. trajectory_causal_attribution-1.0.0/tests/test_store.py +92 -0
  71. trajectory_causal_attribution-1.0.0/tests/test_synthetic_scm.py +105 -0
  72. trajectory_causal_attribution-1.0.0/tests/test_virtual_time.py +104 -0
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ env/
11
+
12
+ # Test / coverage
13
+ .pytest_cache/
14
+ .hypothesis/
15
+ .coverage
16
+ .coverage.*
17
+ htmlcov/
18
+ coverage.xml
19
+
20
+ # Tooling
21
+ .ruff_cache/
22
+ .mypy_cache/
23
+
24
+ # Generated artifacts
25
+ *.sqlite
26
+ *.sqlite-journal
27
+ report.html
28
+ report.json
29
+ demo.sqlite
30
+
31
+ # OS / editor
32
+ .DS_Store
33
+ .idea/
34
+ .vscode/
35
+
36
+ # Research source material (not published)
37
+ *.docx
38
+
39
+ # MkDocs build output
40
+ site/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agent-replay contributors
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,593 @@
1
+ Metadata-Version: 2.4
2
+ Name: trajectory-causal-attribution
3
+ Version: 1.0.0
4
+ Summary: Record AI agent trajectories and find which step caused a failure via counterfactual step-ablation replay.
5
+ Project-URL: Homepage, https://github.com/krishddd/Trajectory_Causal_Attribution
6
+ Project-URL: Documentation, https://krishddd.github.io/Trajectory_Causal_Attribution/
7
+ Project-URL: Repository, https://github.com/krishddd/Trajectory_Causal_Attribution
8
+ Project-URL: Issues, https://github.com/krishddd/Trajectory_Causal_Attribution/issues
9
+ Project-URL: Changelog, https://github.com/krishddd/Trajectory_Causal_Attribution/blob/main/CHANGELOG.md
10
+ Author: agent-replay contributors
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: agents,causal-inference,counterfactual,debugging,llm,observability,replay,shapley
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Software Development :: Debuggers
24
+ Requires-Python: >=3.9
25
+ Provides-Extra: dev
26
+ Requires-Dist: build>=1.0; extra == 'dev'
27
+ Requires-Dist: hypothesis>=6.0; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.4; extra == 'dev'
31
+ Provides-Extra: docs
32
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
33
+ Provides-Extra: langchain
34
+ Requires-Dist: langchain-core>=0.1; extra == 'langchain'
35
+ Provides-Extra: openai
36
+ Requires-Dist: openai>=1.0; extra == 'openai'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # agent-replay
40
+
41
+ **Find *which step* caused your AI agent to fail — with causal proof, not correlational guesswork.**
42
+
43
+ `agent-replay` records an AI agent's trajectory (LLM calls, tool calls, memory
44
+ operations) as a checkpointed, deterministically-replayable session, then
45
+ attributes a failure to a specific step by **counterfactual step-ablation**:
46
+ re-run the trajectory with one step perturbed and measure how the failure
47
+ probability shifts.
48
+
49
+ ```
50
+ attribution(step i) = P(fail | step i kept) − P(fail | step i ablated)
51
+ ```
52
+
53
+ This is the software-level realization of the *Trajectory Causal Attribution*
54
+ method: formalize the run as a Structural Causal Model, intervene on one step at
55
+ a time, and use the **Point-of-Commitment Rule** and **Shapley-value attribution**
56
+ to localize the true root cause — instead of blaming the step that mechanically
57
+ executed the harmful action (a category error) or an LLM-as-judge (≈14% accuracy
58
+ on the *Who&When* benchmark).
59
+
60
+ - 🎯 **Causal, not correlational** — real `do()`-calculus interventions on a replayable trajectory.
61
+ - 🧩 **Framework-agnostic** — a tiny decorator/wrapper API that works with *any* Python agent. Optional LangChain / OpenAI-SDK adapters included.
62
+ - 🌿 **Branch-safe** — live replay calls bind to recorded steps by **idempotency key** (kind + name + inputs), so agents whose step sequence depends on earlier outputs are attributed correctly, not just linear ones.
63
+ - 💾 **Checkpointed** — SQLite store with content-addressable, deduplicated blobs and a Merkle-linked step chain.
64
+ - 🔁 **Deterministic replay** — the VCR/cassette pattern: recorded steps are served verbatim; only ablated steps re-run.
65
+ - 📊 **Rigorous** — Wilson score + bootstrap confidence intervals, antithetic Shapley sampling, no coalition caching.
66
+ - 🛠 **Actionable** — searches for a *minimal counterfactual repair* and emits an HTML + JSON failure-attribution report.
67
+ - 🔎 **Honest** — attributing a *passing* run raises by default (or runs the symmetric **credit** analysis: which step secured success); observation-only / non-resamplable steps are flagged, never silently scored zero.
68
+ - 🪶 **Zero runtime dependencies** — pure Python standard library.
69
+
70
+ 📖 **[Documentation site](https://krishddd.github.io/Trajectory_Causal_Attribution/)** · [Changelog](CHANGELOG.md)
71
+
72
+ ---
73
+
74
+ ## Install
75
+
76
+ ```bash
77
+ pip install trajectory-causal-attribution
78
+ # optional integrations:
79
+ pip install "trajectory-causal-attribution[langchain]"
80
+ pip install "trajectory-causal-attribution[openai]"
81
+ ```
82
+
83
+ The distribution is named `trajectory-causal-attribution`; the **import package
84
+ and CLI stay `agent_replay` / `agent-replay`** (like `scikit-learn` → `sklearn`):
85
+
86
+ ```python
87
+ import agent_replay # after `pip install trajectory-causal-attribution`
88
+ ```
89
+
90
+ Prefer installing straight from GitHub (no PyPI needed)? Both work today:
91
+
92
+ ```bash
93
+ # from the tagged source
94
+ pip install "git+https://github.com/krishddd/Trajectory_Causal_Attribution.git@v1.0.0"
95
+ # or from the release wheel
96
+ pip install https://github.com/krishddd/Trajectory_Causal_Attribution/releases/download/v1.0.0/trajectory_causal_attribution-1.0.0-py3-none-any.whl
97
+ ```
98
+
99
+ Requires Python 3.9+.
100
+
101
+ ---
102
+
103
+ ## Quickstart (20-line integration)
104
+
105
+ ```python
106
+ from agent_replay import Session, attribute
107
+
108
+ def my_agent(ctx, question):
109
+ plan = ctx.llm("plan", produce=lambda: {"q": question}, prompt=question)
110
+ hits = ctx.tool("search", produce=lambda: ("bug" if ctx.rng.random() < 0.7 else "ok"), q=plan["q"])
111
+ draft = ctx.llm("write", produce=lambda: hits, context=hits)
112
+ return {"answer": draft, "ok": draft == "ok"}
113
+
114
+ def verifier(result): # 1.0 == success, 0.0 == failure
115
+ return 1.0 if result["ok"] else 0.0
116
+
117
+ with Session("demo.sqlite") as session:
118
+ traj = session.record(my_agent, {"question": "why did it fail?"}, seed=3, verifier=verifier)
119
+ result = attribute(traj, my_agent, verifier, rollouts=60, method="both", repair=True)
120
+ result.to_html("report.html")
121
+ result.to_json("report.json")
122
+ print("culprit step:", result.culprit_index) # -> the step that caused the failure
123
+ ```
124
+
125
+ The only thing your agent has to do is route its non-deterministic work through
126
+ the context handle — `ctx.llm(...)`, `ctx.tool(...)`, `ctx.memory(...)` — passing
127
+ a `produce` callable that *is* the policy for that step, and drawing any
128
+ randomness from `ctx.rng`. The **same function** is used for recording and for
129
+ every counterfactual rollout; that is what makes attribution possible.
130
+
131
+ ---
132
+
133
+ ## The CLI
134
+
135
+ The CLI points at **your own** agent and verifier via `module:function`
136
+ entrypoints — the package ships no bundled agents. Given a `myproject/agents.py`
137
+ that exposes an agent (routing its work through `ctx`, as in the Quickstart) and a
138
+ verifier:
139
+
140
+ ```bash
141
+ # 1. record a factual run into a checkpoint store
142
+ agent-replay record --db demo.sqlite --session run1 \
143
+ --agent myproject.agents:support_agent \
144
+ --verifier myproject.agents:answered_correctly --seed 1
145
+
146
+ # 2. deterministically replay it (fast-forward through the recorded decisions)
147
+ agent-replay replay --db demo.sqlite --session run1 \
148
+ --agent myproject.agents:support_agent \
149
+ --verifier myproject.agents:answered_correctly
150
+
151
+ # 3. attribute the failure + generate reports + propose a repair
152
+ agent-replay attribute --db demo.sqlite --session run1 \
153
+ --agent myproject.agents:support_agent \
154
+ --verifier myproject.agents:answered_correctly \
155
+ --rollouts 60 --method both --repair --out report
156
+
157
+ # 4. regenerate the HTML report from a saved JSON report
158
+ agent-replay report --json report.json --out report.html
159
+ ```
160
+
161
+ Sample output:
162
+
163
+ ```
164
+ ============================================================
165
+ TRAJECTORY CAUSAL ATTRIBUTION REPORT
166
+ ============================================================
167
+ Session: run1
168
+ Total steps: 6
169
+ Outcome: FAILED (verifier score: 0.000)
170
+ Method: both (60 rollouts/step)
171
+ ------------------------------------------------------------
172
+ Point-of-Commitment: step 3
173
+ [RESULT] Failure attributed to step 3 (tool tool_step_3) with score 0.700.
174
+ CI [0.560, 0.820]
175
+ [REPAIR] step 3: 'BAD' -> 'OK' (valid, minimality 0.400, P(fail)->0.000)
176
+ ============================================================
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Architecture
182
+
183
+ The end-to-end pipeline: record a factual run once, then re-run it many times
184
+ under counterfactual interventions to localize — and repair — the failure.
185
+
186
+ ```mermaid
187
+ flowchart TD
188
+ A["Your agent<br/>ctx.llm / ctx.tool / ctx.memory"]
189
+ A -->|record| R["RecordContext<br/>runs the policy, captures steps"]
190
+ R --> T["Trajectory<br/>SCM: state → action → obs → outcome<br/>Merkle-linked step chain"]
191
+ T -->|"save / load"| S[("CheckpointStore<br/>SQLite + CAS blobs, WAL")]
192
+ T -->|"replay, per rollout"| RP["ReplayContext<br/>serve cassette by idempotency key<br/>OR resample per ReplayPlan"]
193
+ RP --> E["AblationEngine<br/>N stochastic rollouts / intervention<br/>serial or parallel (max_workers)"]
194
+ E --> SC["Attribution scorer"]
195
+ SC --> P1["Phase 1 — contrastive estimator<br/>+ Point-of-Commitment Rule"]
196
+ SC --> P2["Phase 2 — Shapley<br/>antithetic, adaptive stopping"]
197
+ P1 --> RES["AttributionResult<br/>Wilson + bootstrap CIs"]
198
+ P2 --> RES
199
+ RES --> REP["Repair search<br/>minimal counterfactual fix"]
200
+ RES --> OUT["HTML + JSON report<br/>+ plain-language explanation"]
201
+ REP --> OUT
202
+
203
+ classDef store fill:#123020,stroke:#1e4a33,color:#e6e9ef;
204
+ classDef out fill:#3a1618,stroke:#5a2226,color:#ffd7d7;
205
+ class S store;
206
+ class OUT,REP out;
207
+ ```
208
+
209
+ **The replay decision — how each live call binds to the cassette.** This is the
210
+ branch-safe core: a held step is served from the recording only when the *same*
211
+ operation actually recurs; otherwise the timeline has diverged and the call is
212
+ resampled live.
213
+
214
+ ```mermaid
215
+ flowchart LR
216
+ Call["live replay call<br/>kind + name + inputs"] --> Match{"idempotency key<br/>matches an unconsumed<br/>recorded step?"}
217
+ Match -->|no| Diverge["timeline diverged<br/>→ resample live"]
218
+ Match -->|yes| Plan{"ReplayPlan.decision(i)"}
219
+ Plan -->|hold| Serve["serve recorded output<br/>(cassette)"]
220
+ Plan -->|"force / do()"| Force["inject forced action"]
221
+ Plan -->|remove| Rm["REMOVED sentinel"]
222
+ Plan -->|resample| Res["re-run policy<br/>fresh stochastic draw"]
223
+ ```
224
+
225
+ ### How the causal attribution works
226
+
227
+ 1. **Record** the factual run. Each step's inputs/outputs are stored (content-addressed, deduplicated) and chained into a Merkle-style hash sequence.
228
+ 2. **Phase 1 — single-step contrastive estimation.** For every step `i`, hold steps `< i` at their factual recorded actions, **resample** step `i` and everything downstream, and run forward `N` times. Compute `attribution(i) = P(fail|kept) − P(fail|ablated)` with a Wilson interval on the ablated failure rate and a bootstrap interval on the difference.
229
+ 3. **Point-of-Commitment Rule.** Because resampling an early step re-rolls the fatal late step too (a butterfly-effect confound), *magnitude alone blames early, irrelevant steps*. Instead we take the **latest** step whose interval still strictly excludes zero — the final juncture at which re-deciding can still rescue the run. That is the true causal locus.
230
+ 4. **Phase 2 — Shapley attribution.** For interacting (AND/OR) failures, single-step ablation double-counts or zeroes-out credit. Shapley values split responsibility fairly by averaging each step's marginal contribution over sampled permutations, using **antithetic reverse-permutation pairing** for variance reduction. Coalition values are deliberately **never cached** (that would collapse marginal variance and yield falsely narrow intervals) and **no truncation** is used (it would skip pivotal late steps).
231
+ 5. **Repair.** The culprit step's action is swapped for candidate repairs via a `do()` intervention; a candidate that flips the failure rate below threshold and has maximum **minimality** (least behavioural drift) is reported as the validated counterfactual repair.
232
+
233
+ ### Scope note
234
+
235
+ The source research (*The Chronos Protocol* / *Agent Time-Travel Debugger*)
236
+ describes OS-level substrates — DeltaFS/DeltaCR millisecond checkpoints, CRIU,
237
+ Firecracker microVMs, WASI-Virt — for capturing full process/filesystem state.
238
+ `agent-replay` implements the **framework-agnostic, application-level** essence
239
+ of that vision: deterministic record/replay via recorded cassettes plus the full
240
+ causal-attribution mathematics, with zero external dependencies. The exotic
241
+ kernel primitives are intentionally out of scope; the attribution algorithm does
242
+ not depend on them.
243
+
244
+ ---
245
+
246
+ ## Public API
247
+
248
+ | Symbol | Purpose |
249
+ |---|---|
250
+ | `Session(db_path)` | Record and persist agent runs to a SQLite store. |
251
+ | `session.record(agent_fn, task, seed, verifier)` | Capture one factual `Trajectory`. |
252
+ | `attribute(traj, agent_fn, verifier, rollouts, method, repair)` | Run the attribution pipeline → `AttributionResult`. |
253
+ | `AttributionResult.to_html(path)` / `.to_json(path)` | Emit the failure-attribution report. |
254
+ | `record(...)` | Low-level one-shot recording (no store). |
255
+ | `replay(agent_fn, traj, plan, seed)` | Deterministic replay under a `ReplayPlan`. |
256
+ | `ReplayPlan.factual / .ablate_from / .coalition` | Build intervention plans. |
257
+ | `AblationEngine` | Run stochastic rollouts for a plan. |
258
+ | `find_minimal_repair(engine, step)` | Search for a minimal counterfactual repair. |
259
+ | `interop.from_otel_spans / from_jsonl / from_steps` | Import a `Trajectory` from an external trace. |
260
+ | `interop.replayable_agent(traj, resample_fns)` | Make an imported trace attributable. |
261
+ | `aggregate_runs(trajectories, agent, verifier)` | Pool attribution across runs → systematic weak step. |
262
+ | `CheckpointStore` | The SQLite checkpoint / content-addressable store. |
263
+
264
+ `method` is `"contrastive"` (Phase 1), `"shapley"` (Phase 2), or `"both"`.
265
+
266
+ ---
267
+
268
+ ## Connect any framework
269
+
270
+ Three ways to feed steps in, from explicit to fully automatic — all produce the
271
+ same `Trajectory`. Full guide: [`docs/frameworks.md`](docs/frameworks.md).
272
+
273
+ **Decorate any callable** (no `ctx` threading — uses an ambient context):
274
+
275
+ ```python
276
+ from agent_replay import instrument
277
+
278
+ @instrument.tool
279
+ def search(q): ...
280
+ @instrument.llm
281
+ def answer(ctx): ...
282
+
283
+ def agent(question): # no ctx parameter
284
+ return {"answer": answer(search(question))}
285
+
286
+ traj = instrument.record_agent(agent, {"question": "..."}, session_id="s", verifier=v)
287
+ result = attribute(traj, agent, v, pass_context=False)
288
+ ```
289
+
290
+ **Auto-instrument an unmodified SDK** via the data-only recipe registry
291
+ (OpenAI, Anthropic, Cohere, Google GenAI, Mistral, LiteLLM, LangChain,
292
+ LlamaIndex, CrewAI, AutoGen — best-effort, absent SDKs skipped):
293
+
294
+ ```python
295
+ from agent_replay import instrument
296
+ instrument.available_frameworks() # -> the list above
297
+ with instrument.installed("openai", "langchain"):
298
+ traj = instrument.record_agent(run_my_crew, {...}, session_id="s", verifier=v)
299
+ ```
300
+
301
+ Not in the registry? Patch any dotted callable — this is how the recipes work,
302
+ so it covers every framework: `instrument.patch("my_fw.LLM.complete", kind="llm")`.
303
+
304
+ ## Import a trace recorded anywhere
305
+
306
+ Already have traces in LangSmith / Langfuse / AgentOps, an OpenTelemetry export,
307
+ or a JSONL log? `agent_replay.interop` turns them into a first-class `Trajectory`
308
+ you can diff, serve, hash — and attribute. Attribution needs to *re-run* a step's
309
+ policy, which a recorded trace doesn't contain, so you supply a resample policy
310
+ per step kind (or name); steps without one stay observation-only.
311
+
312
+ ```python
313
+ from agent_replay import attribute, interop
314
+
315
+ traj = interop.from_otel_spans(otel_spans, session_id="prod-run-42") # or from_jsonl(...)
316
+
317
+ # Make it attributable: give the steps you want perturbed a resample policy.
318
+ def llm_policy(ctx, inputs): # re-draw this step's output from its recorded inputs
319
+ return call_your_model(inputs["messages"], temperature=0.7)
320
+
321
+ agent = interop.replayable_agent(traj, resample_fns={"llm": llm_policy})
322
+ result = attribute(traj, agent, my_verifier, rollouts=60)
323
+ print(result.culprit_index) # which step caused the failure — on an imported trace
324
+ ```
325
+
326
+ `from_otel_spans` follows the OpenTelemetry **GenAI** semantic conventions
327
+ (`gen_ai.*` attributes → `llm` steps, `execute_tool` / `gen_ai.tool.name` →
328
+ `tool` steps) and is deliberately tolerant — unknown spans import as opaque tool
329
+ steps rather than being dropped.
330
+
331
+ ## Proof: the Who&When benchmark
332
+
333
+ Localizing the *responsible step* in a failed trajectory is the *Who&When* task
334
+ (arXiv:2505.00212), where the strongest LLM-as-judge attributor reaches only
335
+ **~14.2%** step accuracy. `benchmarks/whowhen.py` measures this tool on
336
+ ground-truth-labelled trajectories:
337
+
338
+ ```bash
339
+ python benchmarks/whowhen.py
340
+ ```
341
+
342
+ ```
343
+ causal attribution (this tool) : 100.0% (8/8)
344
+ max-magnitude (no PoC rule) : 12.5% (1/8)
345
+ last-step baseline : 37.5% (3/8)
346
+ LLM-as-judge (Who&When lit.) : ~14.2% (arXiv:2505.00212)
347
+ ```
348
+
349
+ The `max-magnitude` row — highest-|attribution| step *without* the
350
+ Point-of-Commitment rule — lands near the judge baseline, quantifying exactly
351
+ what the PoC rule buys. The harness ships a deterministic synthetic generator so
352
+ it runs offline; point `evaluate()` at `interop`-imported trajectories to run the
353
+ real dataset. See [`benchmarks/README.md`](benchmarks/README.md).
354
+
355
+ ## Find your agent's *systematic* weak step
356
+
357
+ Attributing one failure tells you which step broke *that* run. `aggregate` pools
358
+ attribution across many failing runs of the same task and ranks steps by **name**
359
+ (the stable identity of an operation) — so you can tell bad luck (culprit in 1 of
360
+ 20 runs) from a design flaw (culprit in 15 of 20):
361
+
362
+ ```python
363
+ from agent_replay import aggregate_runs
364
+
365
+ agg = aggregate_runs(failing_trajectories, my_agent, my_verifier, rollouts=50)
366
+ print(agg.systematic_culprit) # e.g. "tool:web_search" — the step most consistently to blame
367
+ print(agg.to_text())
368
+ ```
369
+
370
+ ```
371
+ Aggregate attribution for 'support-agent': 18 failing runs
372
+ Systematic weak step: tool:web_search
373
+ step culprit mean attr [95% CI] poc
374
+ tool:web_search 15/18 83% 0.71 [ 0.55, 0.86] 15/18
375
+ llm:plan 2/18 11% 0.14 [ 0.02, 0.29] 2/18
376
+ llm:write 1/18 6% 0.05 [-0.03, 0.14] 1/18
377
+ ```
378
+
379
+ Or over a whole checkpoint store from the CLI:
380
+
381
+ ```bash
382
+ agent-replay aggregate --db runs.sqlite --agent myproj:agent --verifier myproj:ok --rollouts 60
383
+ ```
384
+
385
+ ## Explainable output
386
+
387
+ Every attribution can be rendered as a traceable, plain-language explanation —
388
+ **what** went wrong, **where**, **why**, and **how to fix** it — with a per-step
389
+ causal trace from the first action to the point of no return. The estimators are
390
+ unchanged; this is a presentation layer.
391
+
392
+ ```python
393
+ explanation = result.explain(traj)
394
+ print(explanation.to_text()) # ASCII-safe narrative
395
+ result.to_html("report.html", explanation=explanation) # adds an Explanation panel
396
+ ```
397
+
398
+ ```
399
+ WHAT: The run failed (score 0.00). The decisive error is step 3. Its action was 'BAD'.
400
+ WHERE: Step 3 - tool:tool_step_3.
401
+ WHY: Keeping step 3 fails 1.00 of the time; re-deciding it drops failure to 0.78
402
+ (rescue 0.22). It is the latest step where re-deciding still changes the
403
+ outcome; the 2 steps after it stay failing, so the run is locked in beyond here.
404
+ FIX: Constrain step 3 from 'BAD' toward '' (validated repair, P(fail)->0.00).
405
+
406
+ Causal trace (first action -> point of no return):
407
+ + step 0 [llm:reason_step_0] contributing (butterfly effect; blame resolves later)
408
+ >> step 3 [tool:tool_step_3] decisive (the point of commitment)
409
+ x step 4 [llm:reason_step_4] locked-in (outcome already committed)
410
+ ```
411
+
412
+ The CLI prints this automatically (`--no-explain` to suppress) and embeds it in
413
+ the HTML/JSON reports.
414
+
415
+ ---
416
+
417
+ ## Test your agent (pytest)
418
+
419
+ Gate CI on agent reliability. `assert_agent_passes` runs the agent many times
420
+ (agents are stochastic — one green run is not a pass), fails the test if the
421
+ failure rate exceeds budget, and on failure puts a counterfactual attribution —
422
+ *which step broke, why, and the minimal fix* — into the test output.
423
+
424
+ ```python
425
+ from agent_replay.pytest_plugin import assert_agent_passes
426
+
427
+ def test_my_agent():
428
+ assert_agent_passes(my_agent, {"q": "..."}, my_verifier,
429
+ rollouts=40, p_fail_max=0.05,
430
+ report_path="agent_failure.html") # HTML artifact on failure
431
+ ```
432
+
433
+ ## Faster, cheaper attribution
434
+
435
+ `adaptive=True` uses sequential stopping — rollouts accrue only until each step's
436
+ interval is tight enough (decisive steps stop early), typically several-fold
437
+ fewer rollouts with the same verdict:
438
+
439
+ ```python
440
+ result = attribute(traj, agent, verifier, rollouts=200, adaptive=True, target_ci_width=0.2)
441
+ ```
442
+
443
+ ## Analyze once, pay once
444
+
445
+ `attribute`, `drift` and `faithfulness` re-run the same prefix-hold rollouts on a
446
+ run. Share a `RolloutCache` so the second and third analysis reuse the first's —
447
+ or use the `analyze` wrapper, which does it for you:
448
+
449
+ ```python
450
+ from agent_replay import analyze
451
+
452
+ out = analyze(traj, agent, verifier, rollouts=50, state_scorer=my_health_fn)
453
+ out["attribution"].culprit_index
454
+ out["drift"].commitment_index
455
+ out["cache"].hits # rollouts reused instead of recomputed
456
+ ```
457
+
458
+ Shapley coalition rollouts are never cached (they must stay independent draws for
459
+ valid variance); only the prefix-hold / factual plans are shared. Results are
460
+ byte-identical to running each analysis on its own.
461
+
462
+ ## Async agents
463
+
464
+ `async def` agents just work — `record` / `replay` / `attribute` detect coroutine
465
+ agents and run them through the same pipeline (Shapley, repair, branch-safety
466
+ included):
467
+
468
+ ```python
469
+ async def agent(ctx, q):
470
+ plan = await ctx.llm("plan", produce=lambda: call_model(q))
471
+ ...
472
+ traj = record(agent, {"q": "..."}, session_id="s", verifier=v) # auto-detected
473
+ result = attribute(traj, agent, v) # sync API, async agent
474
+ ```
475
+
476
+ ## The intervention algebra
477
+
478
+ Attribution rests on `do()`-calculus interventions on one step. The full set the
479
+ research describes is available, both as `ReplayPlan`s and through `fork`:
480
+
481
+ | Intervention | What it changes | How |
482
+ |---|---|---|
483
+ | **resample** | re-draw the step's action from its policy | default |
484
+ | **do / force** | override the step's **action** | `fork(..., do=value)` |
485
+ | **remove** | drop the step entirely | `fork(..., remove=True)` |
486
+ | **mock-observe** | override the **observation**, keep the recorded action | `fork(..., observe=value)` |
487
+ | **swap-model** | run the step under a different model | `fork(..., model="gpt-5")` |
488
+
489
+ The SCM separates the policy's *action* from the *observation* it yields. Record
490
+ both when they differ — the agent's chosen call is the action, the environment's
491
+ return is the observation:
492
+
493
+ ```python
494
+ def agent(ctx, q):
495
+ result = ctx.tool("search",
496
+ produce=lambda: choose_query(q), # the action
497
+ observe=lambda call: run_search(call)) # the observation
498
+ return {"answer": result}
499
+ ```
500
+
501
+ Then `mock-observe` tests memory/context reliance (keep the action, inject a
502
+ different observation), and `swap-model` asks whether a different model would have
503
+ recovered — a policy reads `ctx.model_hint` to switch:
504
+
505
+ ```python
506
+ from agent_replay import fork
507
+ mocked = fork(agent, traj, at_step=1, observe="empty search results")
508
+ upgraded = fork(agent, traj, at_step=1, model="gpt-5") # policy reads ctx.model_hint
509
+ ```
510
+
511
+ ## The Multiverse: fork, resume, diff, faithfulness, drift
512
+
513
+ Beyond attributing a single failure, agent-replay realizes the *Agent Multiverse*
514
+ vision at the application level (see [`docs/MULTIVERSE_GAPS.md`](docs/MULTIVERSE_GAPS.md)):
515
+
516
+ ```mermaid
517
+ flowchart TD
518
+ Base["Recorded trajectory<br/>factual, failing"]
519
+ Base -->|"fork(at_step=i, do= / remove=)"| Child["Child branch<br/>held prefix + intervention<br/>+ live continuation"]
520
+ Base -->|"resume()"| Live["Continue live<br/>past the recorded horizon"]
521
+ Base --> DIFF{{"diff(base, child)"}}
522
+ Child --> DIFF
523
+ DIFF --> Div["first divergence<br/>+ per-step state diff"]
524
+ Base -. "parent_session / fork_step (CAS-deduped prefix)" .-> Child
525
+ ```
526
+
527
+
528
+ ```python
529
+ from agent_replay import fork, resume, diff, faithfulness, drift
530
+
531
+ # Fork an alternate timeline at step 3 (force or remove the step's action),
532
+ # recorded as a first-class child branch (shared prefix deduped via the CAS store).
533
+ alt = fork(agent, traj, at_step=3, do="OK", session_id="fixed")
534
+ print(diff(traj, alt)["first_divergence"]) # where the timelines split
535
+
536
+ # Durable resume: fast-forward the recorded prefix, continue live past the horizon.
537
+ live = resume(agent, traj)
538
+
539
+ # Step-level faithfulness: does the reasoning actually drive the answer?
540
+ fr = faithfulness(traj, agent, verifier)
541
+ print(fr.quadrant) # correct/wrong x faithful/unfaithful; flags post-hoc rationalization
542
+
543
+ # Drift / entropy-of-autonomy curve: chart a run's health as it unfolds.
544
+ dr = drift(traj, agent, verifier, state_scorer=my_health_fn) # state_scorer optional
545
+ print(dr.commitment_index) # step where the outcome's fate seals (entropy collapses)
546
+ dr.to_html("drift.html") # standalone SVG curve
547
+ ```
548
+
549
+ `drift` needs only the outcome verifier for the entropy curve (how *open* the run's
550
+ fate still is at each step); an optional `state_scorer(step) -> [0,1]` adds an
551
+ alignment-health overlay and flags **silent decay** — internal health degrading
552
+ before the outcome reflects it. Browse it all in the zero-dependency console:
553
+
554
+ ```bash
555
+ agent-replay serve --db demo.sqlite # sessions, frozen per-step state, branch graph
556
+ agent-replay fork --db demo.sqlite --session run1 --agent … --at-step 3 --do '"OK"'
557
+ agent-replay drift --db demo.sqlite --session run1 --agent … --verifier … \
558
+ --state-scorer mypkg:health --out drift.html
559
+ ```
560
+
561
+ ## Deployable step-wise fixes
562
+
563
+ The validated repair becomes a runtime guard and training data:
564
+
565
+ ```python
566
+ result = attribute(traj, agent, verifier, repair=True,
567
+ repair_propose_fn=my_llm_proposer) # optional: LLM-proposed candidates
568
+ print(result.repair.to_guard()) # deploy-time recovery snippet
569
+
570
+ from agent_replay import export_contrastive_pairs
571
+ export_contrastive_pairs([result], "pairs.jsonl") # (wrong -> fix) pairs for DPO
572
+ ```
573
+
574
+ ---
575
+
576
+ ## Development
577
+
578
+ ```bash
579
+ pip install -e ".[dev]"
580
+ pytest -q # full suite (mock agent with a known-culprit step)
581
+ ruff check . && ruff format --check .
582
+ python -m build # sdist + wheel
583
+ python examples/quickstart.py # generates report.html / report.json
584
+ ```
585
+
586
+ CI (GitHub Actions) runs ruff lint + format checks, the pytest suite on Python
587
+ 3.9–3.12 with coverage, and a distribution build.
588
+
589
+ ---
590
+
591
+ ## License
592
+
593
+ MIT © agent-replay contributors. See [LICENSE](LICENSE).