crashlab 0.0.1__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 (166) hide show
  1. crashlab-0.0.1/.gitignore +224 -0
  2. crashlab-0.0.1/ARCHITECTURE.md +379 -0
  3. crashlab-0.0.1/CHANGELOG.md +46 -0
  4. crashlab-0.0.1/CONTRIBUTING.md +17 -0
  5. crashlab-0.0.1/LICENSE +201 -0
  6. crashlab-0.0.1/PKG-INFO +447 -0
  7. crashlab-0.0.1/README.md +401 -0
  8. crashlab-0.0.1/SECURITY.md +19 -0
  9. crashlab-0.0.1/bench/README.md +169 -0
  10. crashlab-0.0.1/docs/case-studies/README.md +22 -0
  11. crashlab-0.0.1/docs/case-studies/crewai-integration.md +24 -0
  12. crashlab-0.0.1/docs/case-studies/groq-output-parse-failed-note.md +18 -0
  13. crashlab-0.0.1/docs/case-studies/groq-sqlv4-note.md +24 -0
  14. crashlab-0.0.1/docs/case-studies/openai-agents-DRAFT_ISSUE.md +55 -0
  15. crashlab-0.0.1/docs/case-studies/openai-agents-customer-service.md +67 -0
  16. crashlab-0.0.1/docs/case-studies/refund-html502-verify-note.md +54 -0
  17. crashlab-0.0.1/docs/case-studies/refund-misleading-desc-note.md +51 -0
  18. crashlab-0.0.1/docs/case-studies/refund-policy-fork-note.md +47 -0
  19. crashlab-0.0.1/docs/case-studies/smolagents-DRAFT_ISSUE.md +79 -0
  20. crashlab-0.0.1/docs/case-studies/smolagents-PR2475-comment-DRAFT.md +44 -0
  21. crashlab-0.0.1/docs/case-studies/smolagents-text-to-sql.md +58 -0
  22. crashlab-0.0.1/docs/case-studies/sql-20b-fault-smokes-note.md +21 -0
  23. crashlab-0.0.1/docs/case-studies/worker-c-live-scoreboard.md +55 -0
  24. crashlab-0.0.1/docs/demo/README.md +32 -0
  25. crashlab-0.0.1/docs/demo/demoscript.sh +18 -0
  26. crashlab-0.0.1/docs/demo/quickstart-offline.sh +24 -0
  27. crashlab-0.0.1/docs/demo/smolagents-text-to-sql.gif +0 -0
  28. crashlab-0.0.1/docs/demo/terminal.html +37 -0
  29. crashlab-0.0.1/docs/demo/transcript.txt +15 -0
  30. crashlab-0.0.1/examples/__init__.py +0 -0
  31. crashlab-0.0.1/examples/integrations/README.md +13 -0
  32. crashlab-0.0.1/examples/integrations/crewai.md +34 -0
  33. crashlab-0.0.1/examples/integrations/openai-agents.md +61 -0
  34. crashlab-0.0.1/examples/integrations/smolagents.md +31 -0
  35. crashlab-0.0.1/examples/refund_bot/README.md +198 -0
  36. crashlab-0.0.1/examples/refund_bot/__init__.py +1 -0
  37. crashlab-0.0.1/examples/refund_bot/__main__.py +3 -0
  38. crashlab-0.0.1/examples/refund_bot/agent.py +572 -0
  39. crashlab-0.0.1/examples/refund_bot/demo/clean_a17.jsonl +6 -0
  40. crashlab-0.0.1/examples/refund_bot/demo/html_502_loop_a17.jsonl +8 -0
  41. crashlab-0.0.1/examples/refund_bot/demo/local_bad_args_llama.jsonl +4 -0
  42. crashlab-0.0.1/examples/refund_bot/demo/local_breaker_qwen.jsonl +4 -0
  43. crashlab-0.0.1/examples/refund_bot/demo/policy_drop_c03.jsonl +6 -0
  44. crashlab-0.0.1/examples/refund_bot/fork_demo.py +111 -0
  45. crashlab-0.0.1/examples/refund_bot/restore_policy.json +10 -0
  46. crashlab-0.0.1/examples/refund_bot/restore_policy.yaml +7 -0
  47. crashlab-0.0.1/examples/refund_bot/tests/__init__.py +0 -0
  48. crashlab-0.0.1/examples/refund_bot/tests/cassettes/refund_bot_01m5gtk1fr.jsonl +6 -0
  49. crashlab-0.0.1/examples/refund_bot/tests/cassettes/refund_bot_01m5gtk1fr_fixed.jsonl +4 -0
  50. crashlab-0.0.1/examples/refund_bot/tests/test_refund_bot_01m5gtk1fr.py +38 -0
  51. crashlab-0.0.1/examples/sql_analyst/__init__.py +0 -0
  52. crashlab-0.0.1/examples/sql_analyst/__main__.py +3 -0
  53. crashlab-0.0.1/examples/sql_analyst/agent.py +457 -0
  54. crashlab-0.0.1/examples/sql_analyst/shop.sql +20 -0
  55. crashlab-0.0.1/pyproject.toml +117 -0
  56. crashlab-0.0.1/scripts/check.sh +22 -0
  57. crashlab-0.0.1/scripts/scan.sh +15 -0
  58. crashlab-0.0.1/src/crashlab/__init__.py +144 -0
  59. crashlab-0.0.1/src/crashlab/__main__.py +3 -0
  60. crashlab-0.0.1/src/crashlab/_ids.py +51 -0
  61. crashlab-0.0.1/src/crashlab/_version.py +1 -0
  62. crashlab-0.0.1/src/crashlab/bench/__init__.py +18 -0
  63. crashlab-0.0.1/src/crashlab/bench/catalog.py +117 -0
  64. crashlab-0.0.1/src/crashlab/bench/harness.py +423 -0
  65. crashlab-0.0.1/src/crashlab/capture/__init__.py +52 -0
  66. crashlab-0.0.1/src/crashlab/capture/_chat.py +297 -0
  67. crashlab-0.0.1/src/crashlab/capture/_proxy.py +64 -0
  68. crashlab-0.0.1/src/crashlab/capture/_steps.py +176 -0
  69. crashlab-0.0.1/src/crashlab/capture/_util.py +57 -0
  70. crashlab-0.0.1/src/crashlab/capture/agent.py +347 -0
  71. crashlab-0.0.1/src/crashlab/capture/anthropic.py +100 -0
  72. crashlab-0.0.1/src/crashlab/capture/litellm.py +107 -0
  73. crashlab-0.0.1/src/crashlab/capture/normalize.py +780 -0
  74. crashlab-0.0.1/src/crashlab/capture/openai.py +120 -0
  75. crashlab-0.0.1/src/crashlab/capture/streams.py +397 -0
  76. crashlab-0.0.1/src/crashlab/capture/tools.py +179 -0
  77. crashlab-0.0.1/src/crashlab/checks/__init__.py +40 -0
  78. crashlab-0.0.1/src/crashlab/checks/config.py +231 -0
  79. crashlab-0.0.1/src/crashlab/checks/detectors.py +619 -0
  80. crashlab-0.0.1/src/crashlab/checks/engine.py +240 -0
  81. crashlab-0.0.1/src/crashlab/checks/schema.py +115 -0
  82. crashlab-0.0.1/src/crashlab/cli.py +1791 -0
  83. crashlab-0.0.1/src/crashlab/compare.py +213 -0
  84. crashlab-0.0.1/src/crashlab/context.py +332 -0
  85. crashlab-0.0.1/src/crashlab/diagnose/__init__.py +42 -0
  86. crashlab-0.0.1/src/crashlab/diagnose/compact.py +155 -0
  87. crashlab-0.0.1/src/crashlab/diagnose/diagnoser.py +324 -0
  88. crashlab-0.0.1/src/crashlab/diagnose/evidence.py +141 -0
  89. crashlab-0.0.1/src/crashlab/diagnose/fields.py +123 -0
  90. crashlab-0.0.1/src/crashlab/diagnose/fixes.py +399 -0
  91. crashlab-0.0.1/src/crashlab/diagnose/heuristics.py +171 -0
  92. crashlab-0.0.1/src/crashlab/diagnose/prompt.py +59 -0
  93. crashlab-0.0.1/src/crashlab/diagnose/taxonomy.py +59 -0
  94. crashlab-0.0.1/src/crashlab/export.py +215 -0
  95. crashlab-0.0.1/src/crashlab/integrations/__init__.py +1 -0
  96. crashlab-0.0.1/src/crashlab/integrations/litellm.py +5 -0
  97. crashlab-0.0.1/src/crashlab/models.py +384 -0
  98. crashlab-0.0.1/src/crashlab/patch.py +798 -0
  99. crashlab-0.0.1/src/crashlab/pricing.py +330 -0
  100. crashlab-0.0.1/src/crashlab/py.typed +0 -0
  101. crashlab-0.0.1/src/crashlab/pytest_plugin.py +70 -0
  102. crashlab-0.0.1/src/crashlab/replay/__init__.py +26 -0
  103. crashlab-0.0.1/src/crashlab/replay/divergence.py +192 -0
  104. crashlab-0.0.1/src/crashlab/replay/engine.py +687 -0
  105. crashlab-0.0.1/src/crashlab/replay/rebuild.py +230 -0
  106. crashlab-0.0.1/src/crashlab/replay/synth.py +267 -0
  107. crashlab-0.0.1/src/crashlab/report_html.py +250 -0
  108. crashlab-0.0.1/src/crashlab/store/__init__.py +185 -0
  109. crashlab-0.0.1/src/crashlab/store/cassette.py +69 -0
  110. crashlab-0.0.1/src/crashlab/store/redact.py +129 -0
  111. crashlab-0.0.1/src/crashlab/store/sqlite.py +344 -0
  112. crashlab-0.0.1/src/crashlab/verify/__init__.py +24 -0
  113. crashlab-0.0.1/src/crashlab/verify/oracles.py +124 -0
  114. crashlab-0.0.1/src/crashlab/verify/runner.py +295 -0
  115. crashlab-0.0.1/src/crashlab/verify/stats.py +128 -0
  116. crashlab-0.0.1/tests/__init__.py +0 -0
  117. crashlab-0.0.1/tests/_transport.py +48 -0
  118. crashlab-0.0.1/tests/conftest.py +34 -0
  119. crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5kpm44v.jsonl +12 -0
  120. crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5kpm44v_fixed.jsonl +4 -0
  121. crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5kq4eee.jsonl +6 -0
  122. crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5ksfg22.jsonl +6 -0
  123. crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5ksj8sf.jsonl +6 -0
  124. crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5ksmj8q.jsonl +6 -0
  125. crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5kpm44v.py +39 -0
  126. crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5kq4eee.py +22 -0
  127. crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5ksfg22.py +22 -0
  128. crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5ksj8sf.py +22 -0
  129. crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5ksmj8q.py +22 -0
  130. crashlab-0.0.1/tests/fakes.py +560 -0
  131. crashlab-0.0.1/tests/replay_agents.py +99 -0
  132. crashlab-0.0.1/tests/test_anthropic_raw_response.py +184 -0
  133. crashlab-0.0.1/tests/test_async_client_reuse.py +36 -0
  134. crashlab-0.0.1/tests/test_bench.py +221 -0
  135. crashlab-0.0.1/tests/test_capture.py +349 -0
  136. crashlab-0.0.1/tests/test_checks.py +261 -0
  137. crashlab-0.0.1/tests/test_cli.py +114 -0
  138. crashlab-0.0.1/tests/test_compat_providers.py +163 -0
  139. crashlab-0.0.1/tests/test_diagnose.py +375 -0
  140. crashlab-0.0.1/tests/test_diff.py +93 -0
  141. crashlab-0.0.1/tests/test_docs_demo_assets.py +75 -0
  142. crashlab-0.0.1/tests/test_export.py +204 -0
  143. crashlab-0.0.1/tests/test_findings_cli.py +226 -0
  144. crashlab-0.0.1/tests/test_fixes.py +265 -0
  145. crashlab-0.0.1/tests/test_fork.py +606 -0
  146. crashlab-0.0.1/tests/test_litellm.py +145 -0
  147. crashlab-0.0.1/tests/test_models.py +111 -0
  148. crashlab-0.0.1/tests/test_normalize.py +505 -0
  149. crashlab-0.0.1/tests/test_openai_raw_response.py +231 -0
  150. crashlab-0.0.1/tests/test_openai_transport.py +116 -0
  151. crashlab-0.0.1/tests/test_patch.py +469 -0
  152. crashlab-0.0.1/tests/test_pricing.py +149 -0
  153. crashlab-0.0.1/tests/test_providers.py +329 -0
  154. crashlab-0.0.1/tests/test_redact.py +165 -0
  155. crashlab-0.0.1/tests/test_replay.py +345 -0
  156. crashlab-0.0.1/tests/test_replay_providers.py +296 -0
  157. crashlab-0.0.1/tests/test_replay_sdk.py +122 -0
  158. crashlab-0.0.1/tests/test_runtime_checks.py +436 -0
  159. crashlab-0.0.1/tests/test_sdk_transport.py +198 -0
  160. crashlab-0.0.1/tests/test_server_address.py +112 -0
  161. crashlab-0.0.1/tests/test_sql_analyst.py +208 -0
  162. crashlab-0.0.1/tests/test_store.py +121 -0
  163. crashlab-0.0.1/tests/test_thread_fallback.py +85 -0
  164. crashlab-0.0.1/tests/test_threads.py +104 -0
  165. crashlab-0.0.1/tests/test_verify.py +283 -0
  166. crashlab-0.0.1/tests/test_wrap_detection.py +48 -0
@@ -0,0 +1,224 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ *.lcov
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ # Pipfile.lock
97
+
98
+ # uv
99
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # uv.lock
103
+
104
+ # poetry
105
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109
+ # poetry.lock
110
+ # poetry.toml
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
115
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
116
+ # pdm.lock
117
+ # pdm.toml
118
+ .pdm-python
119
+ .pdm-build/
120
+
121
+ # pixi
122
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
123
+ # pixi.lock
124
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
125
+ # in the .venv directory. It is recommended not to include this directory in version control.
126
+ .pixi/*
127
+ !.pixi/config.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule*
134
+ celerybeat.pid
135
+
136
+ # Redis
137
+ *.rdb
138
+ *.aof
139
+ *.pid
140
+
141
+ # RabbitMQ
142
+ mnesia/
143
+ rabbitmq/
144
+ rabbitmq-data/
145
+
146
+ # ActiveMQ
147
+ activemq-data/
148
+
149
+ # SageMath parsed files
150
+ *.sage.py
151
+
152
+ # Environments
153
+ .env
154
+ .envrc
155
+ .venv
156
+ env/
157
+ venv/
158
+ ENV/
159
+ env.bak/
160
+ venv.bak/
161
+
162
+ # Spyder project settings
163
+ .spyderproject
164
+ .spyproject
165
+
166
+ # Rope project settings
167
+ .ropeproject
168
+
169
+ # mkdocs/Zensical documentation
170
+ /site
171
+
172
+ # mypy
173
+ .mypy_cache/
174
+ .dmypy.json
175
+ dmypy.json
176
+
177
+ # Pyre type checker
178
+ .pyre/
179
+
180
+ # pytype static type analyzer
181
+ .pytype/
182
+
183
+ # Cython debug symbols
184
+ cython_debug/
185
+
186
+ # PyCharm
187
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
188
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
189
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
190
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
191
+ # .idea/
192
+
193
+ # Abstra
194
+ # Abstra is an AI-powered process automation framework.
195
+ # Ignore directories containing user credentials, local state, and settings.
196
+ # Learn more at https://abstra.io/docs
197
+ .abstra/
198
+
199
+ # Visual Studio Code
200
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
201
+ # can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
202
+ # and can be added to the global gitignore or merged into this file. However, if you prefer, you
203
+ # could uncomment the following to ignore the entire vscode folder
204
+ # .vscode/
205
+ # Temporary file for partial code execution
206
+ tempCodeRunnerFile.py
207
+
208
+ # Ruff stuff:
209
+ .ruff_cache/
210
+
211
+ # PyPI configuration file
212
+ .pypirc
213
+
214
+ # Marimo
215
+ marimo/_static/
216
+ marimo/_lsp/
217
+ __marimo__/
218
+
219
+ # Streamlit
220
+ .streamlit/secrets.toml
221
+
222
+ # crashlab local run store and dev venv
223
+ .crashlab/
224
+ .venv/
@@ -0,0 +1,379 @@
1
+ # crashlab architecture (v0.1)
2
+
3
+ **Crash-test your AI agents.** A local-first, framework-agnostic Python library that records an agent run, replays it exactly, diagnoses the failing step and its cause with verbatim evidence, verifies a proposed fix, and exports the failure as a pytest regression test.
4
+
5
+ Users keep their own agent loop. They only wrap their LLM client and decorate their tools.
6
+
7
+ ## v0.1 scope (keep it tight)
8
+
9
+ - Providers: OpenAI and Anthropic clients (sync + async), LiteLLM via patch. No streaming replay beyond assembled responses.
10
+ - Failure labels (6 + UNKNOWN): `TOOL_ERROR_MISHANDLED`, `BAD_TOOL_ARGUMENTS`, `WRONG_TOOL_SELECTION`, `STEP_REPETITION`, `TASK_SPEC_VIOLATION`, `EXTERNAL_FAILURE`, `UNKNOWN`.
11
+ - Fault types for the seed benchmark (4): HTML/garbage tool result, persistent tool error, ambiguous tool descriptions, removed system-prompt constraint. Plus clean runs for false positives.
12
+ - Demo agents (2): `refund_bot` (order DB + refund tool + policy), `sql_analyst` (SQLite schema/query tools).
13
+ - Deferred: MCP proxy, LangGraph adapter, memory correction, auto-apply fixes, hosted version, web app.
14
+
15
+ ## Data flow
16
+
17
+ ```mermaid
18
+ flowchart LR
19
+ subgraph APP["User's agent (unchanged)"]
20
+ A["@crashlab.agent entry fn"] --> L["crashlab.wrap(client)"]
21
+ A --> T["@crashlab.tool functions"]
22
+ end
23
+ L --> CAP["Capture: run context + redaction + runtime checks"]
24
+ T --> CAP
25
+ CAP --> ST[(".crashlab/ index.sqlite + runs/ID/cassette.jsonl")]
26
+ ST --> RP["Replay engine: replay / fork at step N + patch"]
27
+ ST --> DG["Diagnose: compact -> heuristics -> LLM -> evidence check -> fix proposals"]
28
+ DG --> VF["Verify: k reruns with fix + regression on past good runs"]
29
+ VF --> RP
30
+ VF --> OUT["CLI, HTML report, pytest export"]
31
+ DG --> OUT
32
+ ```
33
+
34
+ ## Modules (build in this order)
35
+
36
+ | # | Module | Responsibility |
37
+ |---|---|---|
38
+ | 1 | `models.py` | Pydantic v2: `Run`, `Step`, `Flag`, `Patch`, `Diagnosis`, `Evidence`, `Fix`, `Verification` |
39
+ | 2 | `context.py` | `contextvars` current run, monotonic step counter, mode (`record` / `replay` / `fork`) |
40
+ | 3 | `store/` | `sqlite.py` index (runs, steps metadata, flags, diagnoses), `cassette.py` JSONL read/write, `redact.py` strips keys/headers + user regex hook |
41
+ | 4 | `capture/` | `normalize.py` (provider -> common request/response shape + request hash), `openai.py`, `anthropic.py`, `litellm.py` proxies, `_chat.py` (single LLM-call interception point), `streams.py` (pass-through stream wrappers + assemblers), `tools.py` (`@tool(side_effects=...)`), `agent.py` (`@agent`, `record()` context manager); `integrations/litellm.py` exposes `patch()` / `unpatch()` |
42
+ | 5 | `cli.py` | Typer + Rich: `ls`, `show`, `replay`, `check`, `diagnose`, `fix`, `report`, `fork`, `diff`, `verify`, `export-test`, `report --html`; later `bench` |
43
+ | 6 | `replay/`, `patch.py`, `compare.py` | `engine.py` (strict replay; fork = cassette for steps < N, patch from N, live after), `patch.py` (typed patches: `SystemPrompt` (optional `when` scopes an edit to one agent in a multi-agent shared client), `Message`, `InjectMessage`, `ToolResult`, `ToolDescription`, `ToolSchema`, `ModelParams`), `divergence.py` (hash mismatch -> `DivergenceError` with diff), `compare.py` (run diff) |
44
+ | 7 | `checks/` | Inline findings (built, see below): `loop` (same tool+args >= 3 in last 8 steps), `tool_error_streak` (>= 3), `bad_tool_args`, `suspicious_tool_result` (HTML/empty where JSON expected), `budget`, `length_cutoff` |
45
+ | 8 | `diagnose/` | `compact.py` step table with truncation handles, `heuristics.py` candidate steps from flags/errors, `diagnoser.py` one LLM call returning JSON (failing_step, label, evidence, explanation, confidence), `evidence.py` drops quotes not found verbatim -> UNKNOWN if none, `fixes.py` separate LLM call producing 1-3 typed fixes, `taxonomy.py` enum + definitions |
46
+ | 9 | `verify/` | `runner.py` k forks with fix + reruns of past passing runs, `oracles.py` (`@check` fns > "same failure didn't recur" > LLM judge, labelled weaker), `stats.py` pass^k + Wilson CI + verdict |
47
+ | 10 | `export/` + `pytest_plugin.py` | Write `tests/crashlab/test_<agent>_<id>.py` + copied cassette; plugin adds `crashlab_replay` fixture and `--crashlab-live` |
48
+ | 11 | `report/` | Single-file HTML report (Jinja2): timeline, failing step, evidence, fix diff, verification table |
49
+ | 12 | `bench/` | Fault injectors, seed runs, metrics: step localization (exact / within window), label accuracy, fix success, false positive rate, cost |
50
+
51
+ ## Capture details (as built)
52
+
53
+ - **Common shape.** Every provider maps to `{"system", "messages", "tools", "params"}` requests
54
+ and `{"messages", "tool_calls", "finish_reason", "usage", ...}` responses. Messages carry
55
+ typed `parts` (`text`, `tool_call`, `tool_call_response`, `image`/`audio`/`file`
56
+ placeholders, `reasoning`), loosely following the OTel `gen_ai.input.messages` schema.
57
+ Tool-call arguments are stored parsed (invalid JSON stays a raw string). Anthropic
58
+ `tool_result` blocks become `tool` messages, so OpenAI and Anthropic runs line up.
59
+ - **Request hash.** sha256 of canonical JSON (sorted keys) of the redacted normalized
60
+ request. Transport-only kwargs (headers, timeouts, `stream`, API keys) are excluded, so a
61
+ streamed and a plain call with the same inputs hash the same.
62
+ - **One interception point.** All LLM calls go through `capture/_chat.invoke()`; replay
63
+ will substitute cassette responses there.
64
+ - **Streaming.** Streams are wrapped, not consumed: chunks pass through unchanged and the
65
+ assembled final response is recorded on end / error / close (`crashlab.stream.complete`).
66
+ - **OpenAI-compatible quirks.** `reasoning` / `reasoning_content` on assistant messages and
67
+ stream deltas (Groq, DeepSeek, vLLM, LiteLLM) become `reasoning` parts. `finish_reason`
68
+ `"stop"` on a turn that has tool calls (Gemini streams) normalizes to `"tool_calls"`; the raw
69
+ value stays in `provider_finish_reason`. The provider's `total_tokens` is kept
70
+ (`crashlab.usage.total_tokens`) and summed into the run total, because it can exceed
71
+ input+output (Gemini thinking tokens).
72
+ - **Opaque tokens survive.** Media elision only touches `data:` URLs and base64 under media
73
+ keys (`data`, `b64_json`, `file_data`...). Replay tokens such as Gemini `thought_signature`,
74
+ Anthropic `signature` / `redacted_thinking`, and OpenAI `encrypted_content` are stored
75
+ verbatim, since the provider needs them back.
76
+ - **Threads.** Context vars do not cross plain threads; `crashlab.bind_context`,
77
+ `ContextThreadPoolExecutor` and `run_in_thread` carry the active run into workers. A worker thread
78
+ that was not bound (e.g. smolagents' CodeAgent runs each code block in a fresh
79
+ `ThreadPoolExecutor`) falls back to the only live run in the process, never when two runs
80
+ are live; `CRASHLAB_THREAD_FALLBACK=0` disables this.
81
+
82
+ ## Replay and checks (as built)
83
+
84
+ - **Strict replay** (`replay/engine.py`). `crashlab.replay(run)` imports the recorded entry
85
+ point, calls it with the recorded input (plus `overrides=`), and puts the new run in
86
+ `mode="replay"` with a `Replayer` on the run context. There is no second code path: the
87
+ capture points ask the replayer before doing anything live.
88
+ - `capture/_chat.invoke()` (the single LLM interception point) never calls the SDK. It
89
+ returns the recorded response rebuilt as the recorded class (`crashlab.response.type`,
90
+ e.g. `ChatCompletion`, `anthropic` `Message`, `litellm.ModelResponse`; `ReplayObject`
91
+ fallback). Streams become a synthetic stream rebuilt from the assembled response, and
92
+ Anthropic `messages.stream()` yields the SDK's own `MessageStream` over synthetic events.
93
+ Proxies pass `is_async` explicitly, since SDK async methods do not look like coroutines.
94
+ - `@crashlab.tool` never runs the function in replay: it returns the recorded result or
95
+ re-raises the recorded exception (same type name and message, so the next request is
96
+ identical). `side_effects=True` tools are additionally guarded in non-record modes.
97
+ - Recorded LLM errors are re-raised the same way (dynamic subclass of the original type,
98
+ `status_code` kept).
99
+ - **Matching.** A call must be the next step by `idx` with the same operation and name, and
100
+ its redacted normalized request must hash to the recorded `request_hash`. At run end the
101
+ replayer also checks that no recorded step was skipped, that the output matches, and that
102
+ the run raised the same exception (or none) as the recording.
103
+ - **DivergenceError** (`replay/divergence.py`) carries `kind` (`request`, `step`,
104
+ `extra_step`, `missing_steps`, `output`, `exception`, `network`), `step_idx`, `fields`
105
+ (dotted paths such as `system` or `args.order_id`) and a unified diff per differing
106
+ field, rendered YAML-style so prompts diff line by line. It is sticky: user code that
107
+ swallows it (`except Exception`) cannot hide it, and `replay()` re-raises at the end. The
108
+ diverged replay is still stored (`status="error"`) for `crashlab show`.
109
+ - **Offline guards.** During replay outbound socket connections raise (and count as a
110
+ `network` divergence), and unset `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` get a placeholder
111
+ so SDK clients can be constructed. Nothing is ever sent.
112
+ - **Provenance.** Replay runs get `replay_of` (indexed column, schema v2 with an automatic
113
+ v1 migration) and the `replay` tag; served steps have `source="cassette"` and
114
+ `crashlab.replay.source_step`.
115
+ - **Outcome oracles** (`verify/oracles.py`). `@crashlab.check("agent")` registers a function
116
+ of the finished `Run`. Returning `True` / `None` passes; `False`, a non-empty message, or
117
+ an `AssertionError` fails; any other exception is `error`. Checks run in `record.__exit__`
118
+ (steps are kept in memory for this) and set `outcome` / `outcome_reason` unless the user
119
+ set an outcome by hand. Results are stored in `run.checks`. `crashlab check` re-runs them.
120
+ - **Known limits.** Streamed calls replay with different chunk boundaries (same final
121
+ content). Tool results come back as their recorded JSON (a tool that returned a dataclass
122
+ replays a dict). Runs recorded with `crashlab.record(...)` need `replay(run, fn=main)`.
123
+ Strict order means concurrently started tool calls must start in the recorded order.
124
+
125
+ ## Fork, patches and diff (as built)
126
+
127
+ - **Fork** (`replay/engine.py`, `Forker`). `crashlab.fork(run, at_step=N, patch=..., client=...)`
128
+ re-runs the recorded entry point (same path as replay) in `mode="fork"`. Steps `< N` must
129
+ match the cassette exactly, as in strict replay (a mismatch is a `DivergenceError`). From
130
+ `N` on, LLM calls go to the real SDK (or the `client=` / `fn=` you pass) and tools follow
131
+ `tools="cassette_if_match"` (a call with the same tool and equal arguments as an unused
132
+ parent call gets the recorded result; otherwise it runs) or `tools="live"`.
133
+ `side_effects=True` tools never run live unless `allow_live_side_effects=True`: an
134
+ identical parent call is served once, anything else raises `SideEffectBlockedError` and
135
+ the step is stored with `source="blocked"` so checks still see the attempt. The network is
136
+ not blocked. Agent exceptions after `N` are stored on the fork (`status="error"`), not
137
+ raised, unless `raise_errors=True`. The fork is stored with `forked_from`, `fork_step`,
138
+ `patch` and `patch_description`; flags note a fork point never reached or a patch never
139
+ applied. `at_step` can be negative (from the end); patches bound to steps before `N` and
140
+ patches that do not fit the recorded requests (unknown tool, no such message) fail with
141
+ `ForkError` / `PatchError` before anything runs.
142
+ - **Patches** (`patch.py`). Pydantic models with a `kind` discriminator, combined in a
143
+ `PatchSet` (`a + b`, JSON/YAML via `crashlab.patch.load(path)`). Request-wide patches
144
+ (system prompt, tool description / schema, model params) apply to every LLM request with
145
+ `idx >= max(N, from_step)`; step-bound ones (`MessagePatch`, `InjectMessagePatch`: sticky,
146
+ so later requests that resend the history keep the edit; `ToolResultPatch`: replaces one
147
+ tool step's result or raises a `PatchedToolError`) bind to a step. They edit the native
148
+ SDK kwargs before the request is normalized and hashed, through one `RequestEditor` per
149
+ API family (chat / LiteLLM, Anthropic with `system` string or blocks, Responses
150
+ `instructions`); message indexes refer to the normalized `request.messages`. Patched
151
+ steps get `source="patched"` and the `crashlab.fork.patches` attribute.
152
+ `PatchSet.render_diff(run, N)` previews the change against the recorded requests.
153
+ - **Diff** (`compare.py`). `crashlab.diff(a, b)` -> `RunDiff`: relation (fork / replay /
154
+ unrelated), outcome and per-check changes, usage deltas, and a per-step status (`same`,
155
+ `request`, `response`, `different` call, `only_a`, `only_b`) comparing the normalized
156
+ request and response with volatile fields (ids, signatures, usage) removed, plus the
157
+ first divergence with a field diff.
158
+
159
+ ## Runtime checks and cost (as built)
160
+
161
+ - **Findings** (`models.Finding`; `Flag` is an alias): `kind`, `severity` (`info` < `warn` <
162
+ `error` < `critical`), `step` / `steps`, `message`, `evidence` (verbatim quotes with step
163
+ and field), a merge `key` and `stopped_run`. Stored in `run.json` and indexed in the
164
+ `flags` table; `runs.max_severity` and `runs.cost_usd` back `ls --flagged`. Runtime
165
+ checks are separate from `@crashlab.check` oracles: findings say how a run behaved,
166
+ oracles say whether it passed. A finding never changes the outcome, except a
167
+ circuit-breaker stop.
168
+ - **Detectors** (`checks/detectors.py`) are small stateful objects with `on_step` and
169
+ `on_end`. `checks/engine.py` `CheckEngine` runs them inline (`RunContext.add_step`) and
170
+ `checks.evaluate(run, config)` runs the same code over a stored run. Findings with the
171
+ same `(kind, key)` are merged: steps extend, severity only goes up, evidence is capped.
172
+ `suspicious_tool_result` drift compares a result's shape (keys and JSON types) with
173
+ earlier results of the same tool in the run and in the last 5 ok runs of the same agent
174
+ (majority keys).
175
+ Repetition after a failing result is `warn`, after differing results `info`, and
176
+ `critical` at 5. HTML or error bodies count as tool failures for `tool_error_streak`.
177
+ - **Config** (`checks/config.py`, `ChecksConfig`): defaults < `[tool.crashlab.checks]` in
178
+ the nearest `pyproject.toml` < `record(checks=...)` / `agent(checks=...)` (a dict or a
179
+ `ChecksConfig`; `False` disables). The resolved config is stored in `run.check_config`, and
180
+ replay / fork / `crashlab check` start from it, so a stored run keeps its limits.
181
+ `CRASHLAB_NO_PYPROJECT=1` ignores pyproject.
182
+ - **Circuit breaker** (opt-in): a new critical finding raises `CircuitBreakerError`
183
+ (a `BaseException`, so `except Exception` in agent code does not swallow it) right after
184
+ the step is stored. For streams it is raised at the next step, so the stream itself
185
+ completes. End-of-run findings never trip it. The finding gets `stopped_run=True`; the
186
+ run ends with `status=error`, `outcome=fail`. A strict replay re-raises at the same step
187
+ from the stored finding, and `crashlab check --save` keeps that marker. A fork uses the
188
+ config as it is now.
189
+ - **Cost** (`pricing.py`): the provider comes from `server.address` (known API hosts; localhost
190
+ / `*.local` = local = $0) or the LiteLLM model prefix. Prices come from custom
191
+ (`set_price`, `[tool.crashlab.prices]`), then the built-in dated table (`PRICES_AS_OF`),
192
+ then LiteLLM's bundled `model_cost` (never fetched). OpenAI-style input includes cached
193
+ tokens; Anthropic cache reads and writes are separate. Total tokens beyond input + output
194
+ (thinking) are billed as output. Unknown model or no usage reported means `None`, and one
195
+ unknown call makes the run total `None`. Replayed steps keep the recorded cost.
196
+ - **Export / import** (`store.export_run` / `import_run`): one JSONL file, a
197
+ `{"type": "crashlab.run"}` header line with the run, then the redacted cassette lines.
198
+ The demo runs in `examples/refund_bot/demo/` are such files.
199
+
200
+ ## Diagnosis (as built, v1)
201
+
202
+ `crashlab.diagnose(run, model=...)` / `crashlab diagnose RUN...` answers "which step went
203
+ wrong, and why" with one LLM call over a compacted trace, then checks the answer against
204
+ the trace.
205
+
206
+ 1. **Candidates** (`heuristics.py`): cheap signals, strongest first: a failed LLM API call
207
+ (rate limit, 5xx), runtime findings (`bad_tool_args`, `step_repetition` at its first
208
+ repeat, `tool_error_streak`, `suspicious_tool_result`; schema drift counts as
209
+ external), failed `@crashlab.check` oracles (the `step N` in their message), errors
210
+ and blocked side effects. `heuristic_raw(run)` turns them into a rule-table diagnosis:
211
+ the `--heuristic` baseline, no LLM.
212
+ 2. **Compaction** (`compact.py`): task input, result, checks, findings and candidates as
213
+ hints; the system prompt and tool signatures once; then per step only what is new (a
214
+ chat step's new messages since the agent's previous turn, its reasoning, reply and
215
+ tool calls; a tool step's args, result or error). Long fields are cut with
216
+ `…[truncated: X is N chars; only the part above can be quoted]`. `hide_input` drops
217
+ run input keys (the eval hides fault switches, model and provider).
218
+ 3. **One call** (`diagnoser.py`, prompt `diagnose-v1`): JSON with `label`,
219
+ `failing_step`, `root_cause`, `evidence[{step, quote}]`, `secondary`, `confidence`.
220
+ Labels: `TOOL_ERROR_MISHANDLED`, `BAD_TOOL_ARGUMENTS`, `WRONG_TOOL_SELECTION`,
221
+ `STEP_REPETITION`, `TASK_SPEC_VIOLATION`, `EXTERNAL_FAILURE`, `UNKNOWN`, plus the verdict
222
+ `NO_FAILURE`. `response_format=json_object` is asked for and dropped if the server
223
+ rejects it; bare, fenced or embedded JSON is accepted.
224
+ 4. **Validation** (`evidence.py`): a quote is kept only if it appears verbatim in a field
225
+ of the step it cites (after NFKC, plain quotes and dashes, whitespace collapsing and
226
+ stripping leading / trailing ellipses; case-sensitive, at least 3 characters). A label
227
+ outside the taxonomy, a missing or nonexistent step, or a failure label with no
228
+ surviving quote becomes `UNKNOWN` (confidence capped at 0.2, the claimed label kept in
229
+ `secondary`, the reason in `notes`). `NO_FAILURE` on a run whose outcome is `fail` also
230
+ becomes `UNKNOWN`.
231
+ 5. **Storage**: `Run.diagnosis` (shown by `crashlab show`, in `--json`) plus a history row
232
+ in the `diagnoses` table (schema v4) keyed by `cache_key` = sha256(prompt version,
233
+ model, messages): an unchanged run is not asked again; a changed trace, check or
234
+ finding is. The diagnoser's own LLM calls are recorded as `crashlab-diagnoser` runs in
235
+ `<store>/diagnoser/`, never among the user's runs, and `diagnose()` refuses to run
236
+ inside an active run.
237
+ 6. **Models**: `base_url=` (or `$CRASHLAB_DIAGNOSER_BASE_URL`) for any OpenAI-compatible
238
+ endpoint (Groq, Gemini, Ollama, vLLM), LiteLLM model strings (`groq/`, `gemini/`,
239
+ `ollama/` ...), `claude-*` via Anthropic, else OpenAI; `client=` for your own client,
240
+ `fn=` for any callable (not recorded). The CLI takes `--api-key-env VAR`, never a key.
241
+
242
+ Culprit step convention (used by the prompt and the eval): the tool step that executed a
243
+ wrong action; if no tool ran, the chat step whose reply was wrong; for an external
244
+ failure the step where it shows; none for `NO_FAILURE`. `bench/diagnose_eval.py` scores
245
+ label and step accuracy on 31 hand-labelled live runs (`bench/diagnose_cases.json`).
246
+ Prompt v3 adds two rules learned from the Day 6 misses: a tool error / HTML page is
247
+ never `NO_FAILURE` (`EXTERNAL_FAILURE` if handled, `TOOL_ERROR_MISHANDLED` if not), and a
248
+ loop is blamed on its first repeat, not on the step where a budget check fails.
249
+
250
+ ## Fix proposals and the report (as built)
251
+
252
+ `crashlab.propose_fixes(run_or_diagnosis, model=...)` / `crashlab fix RUN` (`fixes.py`,
253
+ prompt `fix-v1`) is a second LLM call, never merged with the diagnosis. Input: the
254
+ diagnosis (label, step, root cause, evidence) and the same compacted trace. Output:
255
+ `{"fixes": [{"patch": {...}, "rationale", "expected_effect", "literal_justification"?}]}`,
256
+ 1-3 ranked. Each proposal is validated and kept as a `Fix` on `run.diagnosis.fixes`; every
257
+ rejection is a line in `diagnosis.fix_notes`:
258
+
259
+ 1. it parses into a known patch type (system_prompt, tool_description, tool_schema,
260
+ tool_result, model_params, message, inject_message);
261
+ 2. its target exists: the tool is in the run's tool list, the step exists and has the
262
+ right kind, and a dry run (`preview_patches`) against the recorded requests applies
263
+ and changes something (anchors found, lines in range);
264
+ 3. it is minimal: at most 700 characters of patch text, at most 4 new prompt lines, no
265
+ `replace` / `prepend` of the whole system prompt;
266
+ 4. leakage guard: literals of this task instance (ids and 2+ digit numbers from the run
267
+ input and the tool arguments / results, minus those already in the system prompt or
268
+ tool descriptions) must not appear in a request patch unless the proposal gives a
269
+ `literal_justification` (kept with a note). `tool_result` patches are counterfactuals
270
+ for testing a hypothesis: allowed, flagged `test_only`, never `--try`'d.
271
+
272
+ Kept fixes are saved as `<store>/runs/<id>/fixes/fix-N.yaml` (JSON without PyYAML) for
273
+ `crashlab fork --patch`, and cached by a hash of the fix prompt. `try_fix(run, fix)` /
274
+ `--try` forks once from the step the patch applies to (the first LLM call for request
275
+ patches) with the agent's own client and stores `fix.tried` (fork run id, outcome, failed
276
+ checks). Repeated verification is `crashlab verify` (below).
277
+
278
+ `crashlab report RUN` renders the run header, checks, findings, the diagnosis panel, the
279
+ fixes with their diffs, rejections and `--try` result, and next-command hints;
280
+ `crashlab diagnose --fix [--try]` prints the same report.
281
+
282
+ ## Fix verification (as built)
283
+
284
+ `crashlab.verify(run, fix, k=5, control=True, regress=None, max_cost_usd=None)`
285
+ (`verify/runner.py`, `verify/stats.py`):
286
+
287
+ - **Patched arm:** k forks of the failing run with the fix, live from the step it applies
288
+ to. **Control arm:** k forks from the same step with no patch: the noise floor.
289
+ Trials interleave (patched, control, ...) so a budget stop keeps the arms balanced.
290
+ - **Regression:** the patch on past good runs, forked at their first LLM call
291
+ (default: the newest 10 passing, non-fork runs of the same agent; or ids / `tag:X` /
292
+ `none`). Step-bound patches and patches whose anchor a run lacks are `skipped` with a
293
+ note, which blocks VERIFIED but is not a regression.
294
+ - **Errored:** a child run that died on a provider error (429/5xx/overload/connection) or
295
+ a fork that failed to start is `errored`, excluded from n.
296
+ - **Stats:** per-arm passes/fails/errored, rate, Wilson 95% CI, pass^k (tau-bench
297
+ estimator C(c,k)/C(n,k)). Verdict rules are in `stats.verdict`.
298
+ - **Storage:** `runs/<id>/verifications/<vid>.json` (all trials with child run ids,
299
+ cost, tokens), a summary appended to `run.fix_attempts`, child runs tagged
300
+ `verify:<vid>` and `verify-patched|control|regression`. Cost counts live chat steps only.
301
+
302
+ ## Test export and the HTML report (as built)
303
+
304
+ - `export.py`: `export_test(run, fix=None|"fix-N"|"auto", out_dir)` writes
305
+ `test_<agent>_<id>.py` plus `cassettes/<slug>.jsonl` (and `<slug>_fixed.jsonl`, the
306
+ newest passing patched child from `verify` or `--try`). Store/home paths are replaced.
307
+ The generated module is ruff-clean and asserts the outcome and every check status;
308
+ the fixed test asserts `patch_description` (the pinned fix) and all checks pass.
309
+ - Replaying a fork (`PatchedReplayer`) re-applies the fork's patch from its fork step,
310
+ so patched runs reproduce offline (before Day 9 this diverged).
311
+ - `pytest_plugin.py` (`pytest11` entry point): `--crashlab` selects marked tests;
312
+ `crashlab_store` isolates `CRASHLAB_DIR`; `crashlab_replay(path)` imports and strictly
313
+ replays (network blocked); a missing provider SDK skips.
314
+ - `report_html.py`: `crashlab report RUN --html FILE`, escaped, inline CSS only, times in
315
+ local time.
316
+
317
+ ## Key design rules
318
+
319
+ 1. Never record secrets. Redact before writing.
320
+ 2. Tools marked `side_effects=True` never run live during replay or fork; they return the recorded result or a stub that raises.
321
+ 3. Fixes are suggestions. Never auto-apply to user code.
322
+ 4. Every diagnosis must cite verbatim evidence, or it says UNKNOWN.
323
+ 5. Diagnosis and fix proposal are separate LLM calls.
324
+ 6. Show estimated cost before verification; default k=3; early stop when the target fails twice.
325
+ 7. Core installs with no provider SDKs; providers are extras (`crashlab[openai]`, `[anthropic]`, `[litellm]`).
326
+ 8. No telemetry.
327
+
328
+ ## Public API target
329
+
330
+ ```python
331
+ import crashlab
332
+ from openai import OpenAI
333
+
334
+ client = crashlab.wrap(OpenAI()) # or anthropic.Anthropic(); LiteLLM: crashlab.integrations.litellm.patch()
335
+
336
+ @crashlab.tool()
337
+ def get_order(order_id: str) -> dict: ...
338
+
339
+ @crashlab.tool(side_effects=True)
340
+ def issue_refund(order_id: str, amount: float) -> dict: ...
341
+
342
+ @crashlab.agent(name="refund-bot")
343
+ def run_agent(user_msg: str) -> str: ... # existing loop, unchanged
344
+
345
+ @crashlab.check("refund-bot")
346
+ def refund_not_above_total(run) -> bool: ...
347
+
348
+ run = crashlab.load("last")
349
+ crashlab.replay(run) # offline, exact
350
+ b = crashlab.fork(run, at_step=2, patch=crashlab.SystemPromptPatch(op="append", text="..."))
351
+ crashlab.diff(run, b) # checks, usage, step divergence
352
+ d = crashlab.diagnose(run) # failing_step, label, evidence, fixes
353
+ v = crashlab.verify(run, fix=d.fixes[0], k=3) # verdict, pass^k, CI
354
+ crashlab.export_test(run, fix=d.fixes[0])
355
+ ```
356
+
357
+ ```bash
358
+ crashlab ls --flagged
359
+ crashlab diagnose last
360
+ crashlab verify last --fix 1 -k 3
361
+ crashlab export-test last --fix 1 && pytest tests/crashlab -q
362
+ crashlab report last --open
363
+ ```
364
+
365
+ ## Repo layout
366
+
367
+ ```
368
+ src/crashlab/{__init__,models,context,cli,pytest_plugin}.py
369
+ src/crashlab/{store,capture,integrations,checks,replay,diagnose,verify,export,report}/
370
+ examples/{refund_bot,sql_analyst}/
371
+ bench/{faults.py,run_bench.py,results/}
372
+ tests/
373
+ .github/workflows/{ci.yml,release.yml}
374
+ pyproject.toml LICENSE README.md ARCHITECTURE.md
375
+ ```
376
+
377
+ ## Stack
378
+
379
+ Python >= 3.10, Pydantic v2, stdlib sqlite3, Typer + Rich, Jinja2, hatchling, uv, ruff, pytest + pytest-asyncio, GitHub Actions (3.10 to 3.13), PyPI Trusted Publishing, Apache-2.0.
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1 (pre-alpha, unreleased on PyPI)
4
+
5
+ First public-shaped cut. Not on PyPI yet.
6
+
7
+ ### Record / replay
8
+ - `crashlab.wrap` for OpenAI, Anthropic, LiteLLM (sync + async + streaming assembled)
9
+ - OpenAI `chat.completions.with_raw_response.create` and `responses.with_raw_response.create` recorded (CrewAI path)
10
+ - `crashlab.wrap` accepts responses-only OpenAI-compatible clients (no chat.completions)
11
+ - Anthropic `messages.with_raw_response.create` recorded
12
+ - `@crashlab.agent`, `@crashlab.tool`, `@crashlab.check`
13
+ - Strict offline replay and fork-at-step with typed patches
14
+ - Thread fallback: unbound worker threads attach to the only live run (smolagents CodeAgent)
15
+
16
+ ### Diagnose / fix / verify
17
+ - Heuristic + LLM diagnosis (prompt `diagnose-v3`) with verbatim evidence
18
+ - Fix proposals (`fix-v2`) with typed patches; `system_prompt.when` scopes multi-agent edits
19
+ - `crashlab verify` (k patched vs control + regression); Wilson CIs; VERIFIED / LIKELY /
20
+ INCONCLUSIVE / REJECTED; errored provider trials excluded; thin arms → INCONCLUSIVE
21
+
22
+ ### Export / report / bench
23
+ - `export-test` + pytest plugin (`--crashlab`)
24
+ - Self-contained HTML report
25
+ - Seed fault-injection bench (`crashlab bench`) with offline rescoring
26
+ - Export scrub of cwd / install paths from cassette stack traces (editable `$CRASHLAB_REPO` vs wheel `$SITE_PACKAGES`)
27
+
28
+ ### Examples / packaging / hygiene
29
+ - `sql_analyst`: recover answer from Groq gpt-oss `tool_use_failed` / `output_parse_failed` / `failed_generation` (and empty-reply nudge)
30
+ - `scripts/check.sh` / `scripts/scan.sh` for local pre-push (key-shaped suffixes only)
31
+ - SECURITY.md + CONTRIBUTING secret-scan notes
32
+ - `py.typed` shipped (`Typing :: Typed`); sdist includes docs/, CHANGELOG, scripts/
33
+ - Redactor value patterns include Groq `gsk_` and DashScope-shaped `AQ.Ab8…`
34
+
35
+ ### Evidence (Day 10–11)
36
+ - Post-sqlv4 Groq clean smoke: **3/3 pass** after failed_generation recovery
37
+ - Seed bench on held-out `sql-analyst` (Gemini flash-lite): LLM label ok 6/6 triggered
38
+ (CI 61–100%), small n — see `bench/README.md`
39
+ - Groq sqlv4 (after agent nudges): LLM label ok 3/11; 1/3 clean pass; 0/3 fixes verified
40
+ - Real bug in huggingface/smolagents text-to-sql (empty `:memory:` SQLite across threads);
41
+ StaticPool fix verified with no-LLM A/B — `docs/case-studies/`
42
+ - OpenAI Agents SDK customer_service: genuine triage follow-up failure; two prompt fixes
43
+ correctly REJECTED for regression
44
+
45
+ ### Not in 0.0.1
46
+ - PyPI publish, LangGraph/CrewAI first-class adapters, MCP proxy, hosted UI
@@ -0,0 +1,17 @@
1
+ # Contributing
2
+
3
+ crashlab is pre-alpha. Useful contributions right now:
4
+
5
+ 1. **Integration reports** — wrap a real agent, add `@crashlab.check` oracles, open an
6
+ issue with the cassette id and what broke (or worked). See `examples/integrations/`.
7
+ 2. **Offline tests** — pure unit tests under `tests/`, no live keys. Run
8
+ `pytest` and `ruff check .` before a PR.
9
+ 3. **Docs** — fix quickstart pain; keep claims honest (CIs, small-n, quotas). Case
10
+ studies under `docs/case-studies/` must not invent results.
11
+
12
+ Please do **not** commit API keys, `.crashlab/` live stores, or `crashlab-live/`.
13
+ Do not publish to PyPI from a fork. Do not force-push `main`.
14
+
15
+ Dev setup: `python -m venv .venv && .venv/bin/pip install -e ".[dev,openai,anthropic,litellm]"`.
16
+ Pre-push: `./scripts/check.sh` (ruff + offline pytest + secret-scan), or at least
17
+ `./scripts/scan.sh` before every push.