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.
- crashlab-0.0.1/.gitignore +224 -0
- crashlab-0.0.1/ARCHITECTURE.md +379 -0
- crashlab-0.0.1/CHANGELOG.md +46 -0
- crashlab-0.0.1/CONTRIBUTING.md +17 -0
- crashlab-0.0.1/LICENSE +201 -0
- crashlab-0.0.1/PKG-INFO +447 -0
- crashlab-0.0.1/README.md +401 -0
- crashlab-0.0.1/SECURITY.md +19 -0
- crashlab-0.0.1/bench/README.md +169 -0
- crashlab-0.0.1/docs/case-studies/README.md +22 -0
- crashlab-0.0.1/docs/case-studies/crewai-integration.md +24 -0
- crashlab-0.0.1/docs/case-studies/groq-output-parse-failed-note.md +18 -0
- crashlab-0.0.1/docs/case-studies/groq-sqlv4-note.md +24 -0
- crashlab-0.0.1/docs/case-studies/openai-agents-DRAFT_ISSUE.md +55 -0
- crashlab-0.0.1/docs/case-studies/openai-agents-customer-service.md +67 -0
- crashlab-0.0.1/docs/case-studies/refund-html502-verify-note.md +54 -0
- crashlab-0.0.1/docs/case-studies/refund-misleading-desc-note.md +51 -0
- crashlab-0.0.1/docs/case-studies/refund-policy-fork-note.md +47 -0
- crashlab-0.0.1/docs/case-studies/smolagents-DRAFT_ISSUE.md +79 -0
- crashlab-0.0.1/docs/case-studies/smolagents-PR2475-comment-DRAFT.md +44 -0
- crashlab-0.0.1/docs/case-studies/smolagents-text-to-sql.md +58 -0
- crashlab-0.0.1/docs/case-studies/sql-20b-fault-smokes-note.md +21 -0
- crashlab-0.0.1/docs/case-studies/worker-c-live-scoreboard.md +55 -0
- crashlab-0.0.1/docs/demo/README.md +32 -0
- crashlab-0.0.1/docs/demo/demoscript.sh +18 -0
- crashlab-0.0.1/docs/demo/quickstart-offline.sh +24 -0
- crashlab-0.0.1/docs/demo/smolagents-text-to-sql.gif +0 -0
- crashlab-0.0.1/docs/demo/terminal.html +37 -0
- crashlab-0.0.1/docs/demo/transcript.txt +15 -0
- crashlab-0.0.1/examples/__init__.py +0 -0
- crashlab-0.0.1/examples/integrations/README.md +13 -0
- crashlab-0.0.1/examples/integrations/crewai.md +34 -0
- crashlab-0.0.1/examples/integrations/openai-agents.md +61 -0
- crashlab-0.0.1/examples/integrations/smolagents.md +31 -0
- crashlab-0.0.1/examples/refund_bot/README.md +198 -0
- crashlab-0.0.1/examples/refund_bot/__init__.py +1 -0
- crashlab-0.0.1/examples/refund_bot/__main__.py +3 -0
- crashlab-0.0.1/examples/refund_bot/agent.py +572 -0
- crashlab-0.0.1/examples/refund_bot/demo/clean_a17.jsonl +6 -0
- crashlab-0.0.1/examples/refund_bot/demo/html_502_loop_a17.jsonl +8 -0
- crashlab-0.0.1/examples/refund_bot/demo/local_bad_args_llama.jsonl +4 -0
- crashlab-0.0.1/examples/refund_bot/demo/local_breaker_qwen.jsonl +4 -0
- crashlab-0.0.1/examples/refund_bot/demo/policy_drop_c03.jsonl +6 -0
- crashlab-0.0.1/examples/refund_bot/fork_demo.py +111 -0
- crashlab-0.0.1/examples/refund_bot/restore_policy.json +10 -0
- crashlab-0.0.1/examples/refund_bot/restore_policy.yaml +7 -0
- crashlab-0.0.1/examples/refund_bot/tests/__init__.py +0 -0
- crashlab-0.0.1/examples/refund_bot/tests/cassettes/refund_bot_01m5gtk1fr.jsonl +6 -0
- crashlab-0.0.1/examples/refund_bot/tests/cassettes/refund_bot_01m5gtk1fr_fixed.jsonl +4 -0
- crashlab-0.0.1/examples/refund_bot/tests/test_refund_bot_01m5gtk1fr.py +38 -0
- crashlab-0.0.1/examples/sql_analyst/__init__.py +0 -0
- crashlab-0.0.1/examples/sql_analyst/__main__.py +3 -0
- crashlab-0.0.1/examples/sql_analyst/agent.py +457 -0
- crashlab-0.0.1/examples/sql_analyst/shop.sql +20 -0
- crashlab-0.0.1/pyproject.toml +117 -0
- crashlab-0.0.1/scripts/check.sh +22 -0
- crashlab-0.0.1/scripts/scan.sh +15 -0
- crashlab-0.0.1/src/crashlab/__init__.py +144 -0
- crashlab-0.0.1/src/crashlab/__main__.py +3 -0
- crashlab-0.0.1/src/crashlab/_ids.py +51 -0
- crashlab-0.0.1/src/crashlab/_version.py +1 -0
- crashlab-0.0.1/src/crashlab/bench/__init__.py +18 -0
- crashlab-0.0.1/src/crashlab/bench/catalog.py +117 -0
- crashlab-0.0.1/src/crashlab/bench/harness.py +423 -0
- crashlab-0.0.1/src/crashlab/capture/__init__.py +52 -0
- crashlab-0.0.1/src/crashlab/capture/_chat.py +297 -0
- crashlab-0.0.1/src/crashlab/capture/_proxy.py +64 -0
- crashlab-0.0.1/src/crashlab/capture/_steps.py +176 -0
- crashlab-0.0.1/src/crashlab/capture/_util.py +57 -0
- crashlab-0.0.1/src/crashlab/capture/agent.py +347 -0
- crashlab-0.0.1/src/crashlab/capture/anthropic.py +100 -0
- crashlab-0.0.1/src/crashlab/capture/litellm.py +107 -0
- crashlab-0.0.1/src/crashlab/capture/normalize.py +780 -0
- crashlab-0.0.1/src/crashlab/capture/openai.py +120 -0
- crashlab-0.0.1/src/crashlab/capture/streams.py +397 -0
- crashlab-0.0.1/src/crashlab/capture/tools.py +179 -0
- crashlab-0.0.1/src/crashlab/checks/__init__.py +40 -0
- crashlab-0.0.1/src/crashlab/checks/config.py +231 -0
- crashlab-0.0.1/src/crashlab/checks/detectors.py +619 -0
- crashlab-0.0.1/src/crashlab/checks/engine.py +240 -0
- crashlab-0.0.1/src/crashlab/checks/schema.py +115 -0
- crashlab-0.0.1/src/crashlab/cli.py +1791 -0
- crashlab-0.0.1/src/crashlab/compare.py +213 -0
- crashlab-0.0.1/src/crashlab/context.py +332 -0
- crashlab-0.0.1/src/crashlab/diagnose/__init__.py +42 -0
- crashlab-0.0.1/src/crashlab/diagnose/compact.py +155 -0
- crashlab-0.0.1/src/crashlab/diagnose/diagnoser.py +324 -0
- crashlab-0.0.1/src/crashlab/diagnose/evidence.py +141 -0
- crashlab-0.0.1/src/crashlab/diagnose/fields.py +123 -0
- crashlab-0.0.1/src/crashlab/diagnose/fixes.py +399 -0
- crashlab-0.0.1/src/crashlab/diagnose/heuristics.py +171 -0
- crashlab-0.0.1/src/crashlab/diagnose/prompt.py +59 -0
- crashlab-0.0.1/src/crashlab/diagnose/taxonomy.py +59 -0
- crashlab-0.0.1/src/crashlab/export.py +215 -0
- crashlab-0.0.1/src/crashlab/integrations/__init__.py +1 -0
- crashlab-0.0.1/src/crashlab/integrations/litellm.py +5 -0
- crashlab-0.0.1/src/crashlab/models.py +384 -0
- crashlab-0.0.1/src/crashlab/patch.py +798 -0
- crashlab-0.0.1/src/crashlab/pricing.py +330 -0
- crashlab-0.0.1/src/crashlab/py.typed +0 -0
- crashlab-0.0.1/src/crashlab/pytest_plugin.py +70 -0
- crashlab-0.0.1/src/crashlab/replay/__init__.py +26 -0
- crashlab-0.0.1/src/crashlab/replay/divergence.py +192 -0
- crashlab-0.0.1/src/crashlab/replay/engine.py +687 -0
- crashlab-0.0.1/src/crashlab/replay/rebuild.py +230 -0
- crashlab-0.0.1/src/crashlab/replay/synth.py +267 -0
- crashlab-0.0.1/src/crashlab/report_html.py +250 -0
- crashlab-0.0.1/src/crashlab/store/__init__.py +185 -0
- crashlab-0.0.1/src/crashlab/store/cassette.py +69 -0
- crashlab-0.0.1/src/crashlab/store/redact.py +129 -0
- crashlab-0.0.1/src/crashlab/store/sqlite.py +344 -0
- crashlab-0.0.1/src/crashlab/verify/__init__.py +24 -0
- crashlab-0.0.1/src/crashlab/verify/oracles.py +124 -0
- crashlab-0.0.1/src/crashlab/verify/runner.py +295 -0
- crashlab-0.0.1/src/crashlab/verify/stats.py +128 -0
- crashlab-0.0.1/tests/__init__.py +0 -0
- crashlab-0.0.1/tests/_transport.py +48 -0
- crashlab-0.0.1/tests/conftest.py +34 -0
- crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5kpm44v.jsonl +12 -0
- crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5kpm44v_fixed.jsonl +4 -0
- crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5kq4eee.jsonl +6 -0
- crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5ksfg22.jsonl +6 -0
- crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5ksj8sf.jsonl +6 -0
- crashlab-0.0.1/tests/crashlab/cassettes/refund_bot_01m5ksmj8q.jsonl +6 -0
- crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5kpm44v.py +39 -0
- crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5kq4eee.py +22 -0
- crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5ksfg22.py +22 -0
- crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5ksj8sf.py +22 -0
- crashlab-0.0.1/tests/crashlab/test_refund_bot_01m5ksmj8q.py +22 -0
- crashlab-0.0.1/tests/fakes.py +560 -0
- crashlab-0.0.1/tests/replay_agents.py +99 -0
- crashlab-0.0.1/tests/test_anthropic_raw_response.py +184 -0
- crashlab-0.0.1/tests/test_async_client_reuse.py +36 -0
- crashlab-0.0.1/tests/test_bench.py +221 -0
- crashlab-0.0.1/tests/test_capture.py +349 -0
- crashlab-0.0.1/tests/test_checks.py +261 -0
- crashlab-0.0.1/tests/test_cli.py +114 -0
- crashlab-0.0.1/tests/test_compat_providers.py +163 -0
- crashlab-0.0.1/tests/test_diagnose.py +375 -0
- crashlab-0.0.1/tests/test_diff.py +93 -0
- crashlab-0.0.1/tests/test_docs_demo_assets.py +75 -0
- crashlab-0.0.1/tests/test_export.py +204 -0
- crashlab-0.0.1/tests/test_findings_cli.py +226 -0
- crashlab-0.0.1/tests/test_fixes.py +265 -0
- crashlab-0.0.1/tests/test_fork.py +606 -0
- crashlab-0.0.1/tests/test_litellm.py +145 -0
- crashlab-0.0.1/tests/test_models.py +111 -0
- crashlab-0.0.1/tests/test_normalize.py +505 -0
- crashlab-0.0.1/tests/test_openai_raw_response.py +231 -0
- crashlab-0.0.1/tests/test_openai_transport.py +116 -0
- crashlab-0.0.1/tests/test_patch.py +469 -0
- crashlab-0.0.1/tests/test_pricing.py +149 -0
- crashlab-0.0.1/tests/test_providers.py +329 -0
- crashlab-0.0.1/tests/test_redact.py +165 -0
- crashlab-0.0.1/tests/test_replay.py +345 -0
- crashlab-0.0.1/tests/test_replay_providers.py +296 -0
- crashlab-0.0.1/tests/test_replay_sdk.py +122 -0
- crashlab-0.0.1/tests/test_runtime_checks.py +436 -0
- crashlab-0.0.1/tests/test_sdk_transport.py +198 -0
- crashlab-0.0.1/tests/test_server_address.py +112 -0
- crashlab-0.0.1/tests/test_sql_analyst.py +208 -0
- crashlab-0.0.1/tests/test_store.py +121 -0
- crashlab-0.0.1/tests/test_thread_fallback.py +85 -0
- crashlab-0.0.1/tests/test_threads.py +104 -0
- crashlab-0.0.1/tests/test_verify.py +283 -0
- 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.
|