evalrx 0.1.2__py3-none-any.whl

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 (339) hide show
  1. evalrx/__init__.py +139 -0
  2. evalrx/agent_assets/__init__.py +2 -0
  3. evalrx/agent_assets/skills/README.md +28 -0
  4. evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
  5. evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
  6. evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
  7. evalrx/agent_assets/skills/nature-figure/README.md +412 -0
  8. evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
  9. evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
  10. evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
  11. evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
  12. evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
  13. evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
  14. evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
  15. evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
  16. evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
  17. evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
  18. evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
  19. evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
  20. evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
  21. evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
  22. evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
  23. evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
  24. evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
  25. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
  26. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
  27. evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
  28. evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
  29. evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
  30. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
  31. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
  32. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
  33. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
  34. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
  35. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
  36. evalrx/agent_assets/skills.py +27 -0
  37. evalrx/agent_runtime/__init__.py +78 -0
  38. evalrx/agent_runtime/_docker_runner.py +89 -0
  39. evalrx/agent_runtime/cli_runtime.py +103 -0
  40. evalrx/agent_runtime/cli_transcript.py +138 -0
  41. evalrx/agent_runtime/cli_types.py +68 -0
  42. evalrx/agent_runtime/codegen/__init__.py +5 -0
  43. evalrx/agent_runtime/codegen/runner.py +94 -0
  44. evalrx/agent_runtime/experiment_harness.py +117 -0
  45. evalrx/agent_runtime/factory.py +102 -0
  46. evalrx/agent_runtime/json_shape.py +44 -0
  47. evalrx/agent_runtime/judges/__init__.py +28 -0
  48. evalrx/agent_runtime/judges/agy.py +179 -0
  49. evalrx/agent_runtime/judges/autodetect.py +135 -0
  50. evalrx/agent_runtime/judges/claude.py +159 -0
  51. evalrx/agent_runtime/judges/codex.py +120 -0
  52. evalrx/agent_runtime/providers/__init__.py +21 -0
  53. evalrx/agent_runtime/providers/antigravity.py +31 -0
  54. evalrx/agent_runtime/providers/base.py +145 -0
  55. evalrx/agent_runtime/providers/claude_code.py +49 -0
  56. evalrx/agent_runtime/providers/codex.py +37 -0
  57. evalrx/agent_runtime/providers/gemini_cli.py +26 -0
  58. evalrx/agent_runtime/providers/kimi_cli.py +27 -0
  59. evalrx/agent_runtime/providers/opencode.py +27 -0
  60. evalrx/agent_runtime/providers/registry.py +58 -0
  61. evalrx/agent_runtime/sandbox.py +517 -0
  62. evalrx/agent_runtime/skill_audit.py +143 -0
  63. evalrx/agent_runtime/skills/__init__.py +19 -0
  64. evalrx/agent_runtime/skills/installer.py +68 -0
  65. evalrx/agent_runtime/skills/prompt_policy.py +86 -0
  66. evalrx/agent_runtime/skills/resolver.py +19 -0
  67. evalrx/analysis/__init__.py +132 -0
  68. evalrx/analysis/adjudicate.py +154 -0
  69. evalrx/analysis/analysis_module.py +361 -0
  70. evalrx/analysis/api.py +171 -0
  71. evalrx/analysis/case_studio.py +651 -0
  72. evalrx/analysis/cli.py +114 -0
  73. evalrx/analysis/dashboard.py +350 -0
  74. evalrx/analysis/eval_case_matrix.py +118 -0
  75. evalrx/analysis/eval_viz_theme.py +833 -0
  76. evalrx/analysis/explore_run.py +333 -0
  77. evalrx/analysis/explorer.py +1276 -0
  78. evalrx/analysis/failure_modes.py +607 -0
  79. evalrx/analysis/fused_pipeline.py +489 -0
  80. evalrx/analysis/holdout.py +300 -0
  81. evalrx/analysis/hypothesis_agent.py +230 -0
  82. evalrx/analysis/narration.py +177 -0
  83. evalrx/analysis/operationalize.py +442 -0
  84. evalrx/analysis/plain_language.py +42 -0
  85. evalrx/analysis/planner.py +283 -0
  86. evalrx/analysis/probe_search.py +203 -0
  87. evalrx/analysis/profile.py +268 -0
  88. evalrx/analysis/prompts/__init__.py +0 -0
  89. evalrx/analysis/prompts/explorer.py +417 -0
  90. evalrx/analysis/prompts/failure_modes.py +33 -0
  91. evalrx/analysis/prompts/holdout.py +27 -0
  92. evalrx/analysis/prompts/hypothesis_agent.py +78 -0
  93. evalrx/analysis/prompts/run_codebase.py +47 -0
  94. evalrx/analysis/prompts/stats_agent.py +72 -0
  95. evalrx/analysis/prompts/stats_tool_generator.py +43 -0
  96. evalrx/analysis/result_marker.py +47 -0
  97. evalrx/analysis/run_codebase.py +242 -0
  98. evalrx/analysis/run_view.py +205 -0
  99. evalrx/analysis/stage_views.py +93 -0
  100. evalrx/analysis/stats_agent.py +944 -0
  101. evalrx/analysis/stats_tool_agent.py +261 -0
  102. evalrx/analysis/stats_tool_generator.py +415 -0
  103. evalrx/analysis/stats_tools.py +1153 -0
  104. evalrx/analysis/trajectory_records.py +193 -0
  105. evalrx/analysis/workbench.py +431 -0
  106. evalrx/analyzers/__init__.py +42 -0
  107. evalrx/analyzers/agent/__init__.py +25 -0
  108. evalrx/analyzers/agent/counterfactual.py +84 -0
  109. evalrx/analyzers/agent/first_error_judge.py +96 -0
  110. evalrx/analyzers/agent/ignored_obs.py +81 -0
  111. evalrx/analyzers/agent/loop_detect.py +79 -0
  112. evalrx/analyzers/agent/reliability.py +165 -0
  113. evalrx/analyzers/agent/tool_shap.py +225 -0
  114. evalrx/analyzers/agent/trajectory_rubric.py +168 -0
  115. evalrx/analyzers/attention/__init__.py +19 -0
  116. evalrx/analyzers/attention/relative_attn.py +610 -0
  117. evalrx/analyzers/attention/rollout.py +73 -0
  118. evalrx/analyzers/attention/sink.py +56 -0
  119. evalrx/analyzers/attention/summary.py +190 -0
  120. evalrx/analyzers/attribution/__init__.py +6 -0
  121. evalrx/analyzers/attribution/generic_attn.py +31 -0
  122. evalrx/analyzers/attribution/gradcam.py +30 -0
  123. evalrx/analyzers/base.py +12 -0
  124. evalrx/analyzers/geometry/__init__.py +6 -0
  125. evalrx/analyzers/geometry/cka.py +70 -0
  126. evalrx/analyzers/geometry/linear_probe.py +157 -0
  127. evalrx/analyzers/hallucination/__init__.py +9 -0
  128. evalrx/analyzers/hallucination/chair.py +78 -0
  129. evalrx/analyzers/hallucination/opera.py +29 -0
  130. evalrx/analyzers/hallucination/pope.py +119 -0
  131. evalrx/analyzers/hallucination/selfcheck.py +155 -0
  132. evalrx/analyzers/hallucination/vcd.py +29 -0
  133. evalrx/analyzers/lens/__init__.py +7 -0
  134. evalrx/analyzers/lens/layer_contrast.py +133 -0
  135. evalrx/analyzers/lens/logit_lens.py +138 -0
  136. evalrx/analyzers/lens/tuned_lens.py +30 -0
  137. evalrx/analyzers/patching/__init__.py +5 -0
  138. evalrx/analyzers/patching/causal_trace.py +30 -0
  139. evalrx/analyzers/perturbation/__init__.py +23 -0
  140. evalrx/analyzers/perturbation/_shapley.py +54 -0
  141. evalrx/analyzers/perturbation/context_shap.py +174 -0
  142. evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
  143. evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
  144. evalrx/analyzers/perturbation/mm_shap.py +146 -0
  145. evalrx/analyzers/perturbation/modality_ablation.py +196 -0
  146. evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
  147. evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
  148. evalrx/analyzers/perturbation/rise.py +94 -0
  149. evalrx/analyzers/perturbation/vl_shap.py +102 -0
  150. evalrx/analyzers/reasoning/__init__.py +33 -0
  151. evalrx/analyzers/reasoning/_text.py +328 -0
  152. evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
  153. evalrx/analyzers/reasoning/arith_audit.py +226 -0
  154. evalrx/analyzers/reasoning/contamination.py +214 -0
  155. evalrx/analyzers/reasoning/knowledge_split.py +253 -0
  156. evalrx/analyzers/reasoning/self_repair.py +246 -0
  157. evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
  158. evalrx/analyzers/reasoning/termination_audit.py +258 -0
  159. evalrx/analyzers/uncertainty/__init__.py +18 -0
  160. evalrx/analyzers/uncertainty/calibration.py +174 -0
  161. evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
  162. evalrx/analyzers/uncertainty/entropy.py +90 -0
  163. evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
  164. evalrx/analyzers/uncertainty/self_consistency.py +204 -0
  165. evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
  166. evalrx/cli.py +411 -0
  167. evalrx/config.py +77 -0
  168. evalrx/contract/__init__.py +179 -0
  169. evalrx/contract/common.py +452 -0
  170. evalrx/contract/emit.py +948 -0
  171. evalrx/contract/export.py +237 -0
  172. evalrx/contract/m1.py +325 -0
  173. evalrx/contract/m2.py +317 -0
  174. evalrx/contract/m3.py +165 -0
  175. evalrx/contract/m4.py +130 -0
  176. evalrx/contract/m5.py +292 -0
  177. evalrx/contract/methodology.py +76 -0
  178. evalrx/contract/pre_m1.py +58 -0
  179. evalrx/contract/typescript.py +140 -0
  180. evalrx/core/__init__.py +85 -0
  181. evalrx/core/analyzer.py +174 -0
  182. evalrx/core/capability.py +54 -0
  183. evalrx/core/case.py +443 -0
  184. evalrx/core/experiment.py +106 -0
  185. evalrx/core/model.py +198 -0
  186. evalrx/core/pipeline.py +42 -0
  187. evalrx/core/registry.py +142 -0
  188. evalrx/core/result.py +64 -0
  189. evalrx/core/spec.py +173 -0
  190. evalrx/core/tokentype.py +165 -0
  191. evalrx/core/tool.py +92 -0
  192. evalrx/datasets/__init__.py +41 -0
  193. evalrx/datasets/base.py +68 -0
  194. evalrx/datasets/gui_os.py +52 -0
  195. evalrx/datasets/llm_qa.py +57 -0
  196. evalrx/datasets/pure_qa.py +12 -0
  197. evalrx/datasets/vlm_qa.py +695 -0
  198. evalrx/datasets/web_search_qa.py +52 -0
  199. evalrx/eval_agent/__init__.py +341 -0
  200. evalrx/eval_agent/_tools.py +81 -0
  201. evalrx/eval_agent/ab_runner.py +50 -0
  202. evalrx/eval_agent/agentic/__init__.py +43 -0
  203. evalrx/eval_agent/agentic/actions.py +216 -0
  204. evalrx/eval_agent/agentic/board.py +107 -0
  205. evalrx/eval_agent/agentic/loop.py +190 -0
  206. evalrx/eval_agent/agentic/tools.py +538 -0
  207. evalrx/eval_agent/checkpoint.py +57 -0
  208. evalrx/eval_agent/cli_agent.py +59 -0
  209. evalrx/eval_agent/cli_skills.py +5 -0
  210. evalrx/eval_agent/evolution.py +396 -0
  211. evalrx/eval_agent/git_manager.py +215 -0
  212. evalrx/eval_agent/hypothesis.py +172 -0
  213. evalrx/eval_agent/label_quarantine.py +209 -0
  214. evalrx/eval_agent/legacy.py +530 -0
  215. evalrx/eval_agent/log_schema.py +497 -0
  216. evalrx/eval_agent/loop.py +2159 -0
  217. evalrx/eval_agent/loop_reports.py +116 -0
  218. evalrx/eval_agent/model_instrumentation.py +282 -0
  219. evalrx/eval_agent/narration.py +193 -0
  220. evalrx/eval_agent/nl_runner.py +460 -0
  221. evalrx/eval_agent/orchestrator.py +61 -0
  222. evalrx/eval_agent/preregister.py +93 -0
  223. evalrx/eval_agent/prompts/__init__.py +1 -0
  224. evalrx/eval_agent/prompts/agentic.py +46 -0
  225. evalrx/eval_agent/prompts/case_discovery.py +25 -0
  226. evalrx/eval_agent/prompts/diagnosis.py +125 -0
  227. evalrx/eval_agent/prompts/experiment_writer.py +265 -0
  228. evalrx/eval_agent/prompts/explore_step.py +37 -0
  229. evalrx/eval_agent/prompts/fix_agent.py +257 -0
  230. evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
  231. evalrx/eval_agent/prompts/nl_runner.py +38 -0
  232. evalrx/eval_agent/prompts/probe_agent.py +25 -0
  233. evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
  234. evalrx/eval_agent/prompts/probe_generator.py +35 -0
  235. evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
  236. evalrx/eval_agent/report.py +58 -0
  237. evalrx/eval_agent/run_context.py +354 -0
  238. evalrx/eval_agent/run_log.schema.json +1215 -0
  239. evalrx/eval_agent/run_logger_v2.py +1764 -0
  240. evalrx/eval_agent/run_metadata.py +208 -0
  241. evalrx/eval_agent/stages/__init__.py +56 -0
  242. evalrx/eval_agent/stages/case_discovery.py +293 -0
  243. evalrx/eval_agent/stages/diagnosis.py +1017 -0
  244. evalrx/eval_agent/stages/experiment_writer.py +1634 -0
  245. evalrx/eval_agent/stages/fix_agent.py +3916 -0
  246. evalrx/eval_agent/stages/fix_internals.py +499 -0
  247. evalrx/eval_agent/stages/fix_pipeline.py +725 -0
  248. evalrx/eval_agent/stages/fix_tiers.py +187 -0
  249. evalrx/eval_agent/stages/fix_tools.py +1034 -0
  250. evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
  251. evalrx/eval_agent/stages/probe.py +439 -0
  252. evalrx/eval_agent/stages/probe_agent.py +1079 -0
  253. evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
  254. evalrx/eval_agent/stages/probe_generator.py +326 -0
  255. evalrx/eval_agent/stages/probe_search_agent.py +106 -0
  256. evalrx/eval_agent/stages/protocol.py +112 -0
  257. evalrx/eval_agent/stages/repair_catalog.py +273 -0
  258. evalrx/eval_agent/stages/surgery.py +524 -0
  259. evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
  260. evalrx/eval_agent/store.py +231 -0
  261. evalrx/logging_utils.py +112 -0
  262. evalrx/models/__init__.py +161 -0
  263. evalrx/models/_discover.py +101 -0
  264. evalrx/models/agent.py +380 -0
  265. evalrx/models/backends/__init__.py +58 -0
  266. evalrx/models/backends/api.py +169 -0
  267. evalrx/models/backends/base.py +57 -0
  268. evalrx/models/backends/gemini_compat.py +579 -0
  269. evalrx/models/backends/hf_local.py +2074 -0
  270. evalrx/models/backends/openai_compat.py +301 -0
  271. evalrx/models/backends/vllm_offline.py +116 -0
  272. evalrx/models/base.py +24 -0
  273. evalrx/models/blackbox/__init__.py +4 -0
  274. evalrx/models/blackbox/agent.py +31 -0
  275. evalrx/models/blackbox/base.py +29 -0
  276. evalrx/models/blackbox/gemini.py +279 -0
  277. evalrx/models/blackbox/llm_api.py +17 -0
  278. evalrx/models/blackbox/vlm_api.py +17 -0
  279. evalrx/models/compose.py +66 -0
  280. evalrx/models/inference.py +88 -0
  281. evalrx/models/paper_methods/__init__.py +8 -0
  282. evalrx/models/paper_methods/aad.py +53 -0
  283. evalrx/models/paper_methods/ifcd.py +204 -0
  284. evalrx/models/paper_methods/pai.py +164 -0
  285. evalrx/models/paper_methods/tcd.py +202 -0
  286. evalrx/models/paper_methods/vcd.py +45 -0
  287. evalrx/models/paper_methods/vicrop.py +137 -0
  288. evalrx/models/toolcodec.py +143 -0
  289. evalrx/models/tools/__init__.py +20 -0
  290. evalrx/models/tools/perception.py +300 -0
  291. evalrx/models/tools/visual.py +174 -0
  292. evalrx/models/whitebox/__init__.py +26 -0
  293. evalrx/models/whitebox/agent.py +31 -0
  294. evalrx/models/whitebox/base.py +24 -0
  295. evalrx/models/whitebox/qwen.py +61 -0
  296. evalrx/models/whitebox/qwen2_5_omni.py +29 -0
  297. evalrx/models/whitebox/qwen2_audio.py +25 -0
  298. evalrx/models/whitebox/qwen_omni.py +53 -0
  299. evalrx/models/whitebox/qwen_vl.py +62 -0
  300. evalrx/observability/__init__.py +21 -0
  301. evalrx/observability/envelope.py +122 -0
  302. evalrx/observability/outbox.py +111 -0
  303. evalrx/observability/tracer.py +882 -0
  304. evalrx/reporting/__init__.py +28 -0
  305. evalrx/reporting/case_study.py +947 -0
  306. evalrx/reporting/compiler.py +587 -0
  307. evalrx/reporting/dynamic.py +1882 -0
  308. evalrx/reporting/html_report.py +2225 -0
  309. evalrx/reporting/langfuse_exporter.py +38 -0
  310. evalrx/reporting/langfuse_source.py +155 -0
  311. evalrx/reporting/model.py +151 -0
  312. evalrx/reporting/run_events.py +184 -0
  313. evalrx/reporting/server.py +557 -0
  314. evalrx/reporting/stages.py +58 -0
  315. evalrx/reporting/static_export.py +142 -0
  316. evalrx/reporting/web_dist/index.html +146 -0
  317. evalrx/specs.py +727 -0
  318. evalrx/stats/__init__.py +47 -0
  319. evalrx/stats/api.py +192 -0
  320. evalrx/stats/bootstrap.py +86 -0
  321. evalrx/stats/ebh.py +27 -0
  322. evalrx/stats/evalue.py +98 -0
  323. evalrx/stats/friedman.py +138 -0
  324. evalrx/stats/mcnemar.py +40 -0
  325. evalrx/stats/multiplicity.py +159 -0
  326. evalrx/stats/subset_sampling.py +55 -0
  327. evalrx/term_links.py +43 -0
  328. evalrx/viz/__init__.py +7 -0
  329. evalrx/viz/labels.py +77 -0
  330. evalrx/viz/prompts.py +39 -0
  331. evalrx/viz/renderer.py +590 -0
  332. evalrx/viz/schema.py +36 -0
  333. evalrx/viz/style.py +134 -0
  334. evalrx-0.1.2.dist-info/METADATA +532 -0
  335. evalrx-0.1.2.dist-info/RECORD +339 -0
  336. evalrx-0.1.2.dist-info/WHEEL +5 -0
  337. evalrx-0.1.2.dist-info/entry_points.txt +3 -0
  338. evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
  339. evalrx-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,102 @@
1
+ """Sandbox factory — create the appropriate sandbox backend from config.
2
+
3
+ Mirrors ``researchclaw/experiment/factory.py``.
4
+
5
+ Supported modes:
6
+ ``"subprocess"`` (default) — :class:`ExperimentSandbox` running in a local
7
+ subprocess. No extra dependencies required.
8
+ ``"docker"`` — Docker-isolated sandbox. Falls back to the subprocess
9
+ backend when Docker is not available or the DockerSandbox import fails.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+
18
+ from evalrx.agent_runtime.sandbox import ExperimentSandbox, SandboxProtocol
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Configuration dataclasses
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class SandboxConfig:
30
+ """Configuration for the subprocess sandbox backend.
31
+
32
+ Attributes:
33
+ python_path: Python executable to use. Empty string → ``sys.executable``.
34
+ timeout_sec: Default timeout for sandbox runs (seconds).
35
+ """
36
+
37
+ python_path: str = ""
38
+ timeout_sec: int = 60
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class SandboxFactoryConfig:
43
+ """Top-level factory configuration.
44
+
45
+ Attributes:
46
+ mode: Backend to use: ``"subprocess"`` (default) or ``"docker"``.
47
+ sandbox: Settings for the subprocess backend.
48
+ """
49
+
50
+ mode: str = "subprocess"
51
+ sandbox: SandboxConfig = field(default_factory=SandboxConfig)
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Factory
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ def create_sandbox(config: SandboxFactoryConfig, workdir: Path) -> SandboxProtocol:
60
+ """Instantiate the sandbox backend specified by *config.mode*.
61
+
62
+ Args:
63
+ config: Factory configuration.
64
+ workdir: Directory where the sandbox writes scripts and project copies.
65
+
66
+ Returns:
67
+ A :class:`~evalrx.agent_runtime.sandbox.SandboxProtocol` implementation.
68
+ Falls back to :class:`ExperimentSandbox` if the requested backend is
69
+ unavailable.
70
+ """
71
+ if config.mode == "docker":
72
+ sandbox = _try_create_docker_sandbox(workdir)
73
+ if sandbox is not None:
74
+ return sandbox
75
+ logger.warning(
76
+ "Docker sandbox unavailable — falling back to subprocess sandbox"
77
+ )
78
+
79
+ python_path = config.sandbox.python_path or None
80
+ return ExperimentSandbox(workdir=workdir, python_path=python_path)
81
+
82
+
83
+ def _try_create_docker_sandbox(workdir: Path) -> SandboxProtocol | None:
84
+ """Attempt to create a DockerSandbox; return None if not available."""
85
+ try:
86
+ import subprocess as _sp
87
+ result = _sp.run(
88
+ ["docker", "info"],
89
+ capture_output=True,
90
+ timeout=5,
91
+ check=False,
92
+ )
93
+ if result.returncode != 0:
94
+ logger.debug("Docker not running (docker info returned %s)", result.returncode)
95
+ return None
96
+ except Exception as exc: # noqa: BLE001
97
+ logger.debug("Docker check failed: %s", exc)
98
+ return None
99
+
100
+ # DockerSandbox is not yet implemented in evalrx — stub falls through.
101
+ logger.debug("Docker is available but DockerSandbox is not yet implemented")
102
+ return None
@@ -0,0 +1,44 @@
1
+ """Minimal JSON schema validator shared by anything that parses LLM JSON output.
2
+
3
+ No external deps (not even ``jsonschema``) — only handles type/required/
4
+ minLength, which is enough to catch the common LLM mistakes (missing field,
5
+ empty string, wrong top-level type). Used by M3's hypothesis parser, the
6
+ agentic loop's action parser, and failure-mode cluster naming.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ def validate_json_shape(data: object, schema: dict) -> list[str]:
13
+ """Returns a list of error strings; empty means valid."""
14
+ errors: list[str] = []
15
+
16
+ def _check(node: object, s: dict, path: str) -> None:
17
+ expected_type = s.get("type")
18
+ if expected_type == "array":
19
+ if not isinstance(node, list):
20
+ errors.append(f"{path}: expected array, got {type(node).__name__}")
21
+ return
22
+ item_schema = s.get("items", {})
23
+ for i, item in enumerate(node):
24
+ _check(item, item_schema, f"{path}[{i}]")
25
+ elif expected_type == "object":
26
+ if not isinstance(node, dict):
27
+ errors.append(f"{path}: expected object, got {type(node).__name__}")
28
+ return
29
+ for req in s.get("required", []):
30
+ if req not in node:
31
+ errors.append(f"{path}: missing required field '{req}'")
32
+ for prop, prop_schema in s.get("properties", {}).items():
33
+ if prop in node:
34
+ _check(node[prop], prop_schema, f"{path}.{prop}")
35
+ elif expected_type == "string":
36
+ if not isinstance(node, str):
37
+ errors.append(f"{path}: expected string, got {type(node).__name__}")
38
+ elif "minLength" in s and len(node) < s["minLength"]:
39
+ errors.append(
40
+ f"{path}: string too short ({len(node)} < {s['minLength']})"
41
+ )
42
+
43
+ _check(data, schema, "$")
44
+ return errors
@@ -0,0 +1,28 @@
1
+ """CLI-backed judge model wrappers."""
2
+
3
+ from evalrx.agent_runtime.judges.agy import AgyModel, scan_agy_log
4
+ from evalrx.agent_runtime.judges.autodetect import (
5
+ DEFAULT_AGY_CANDIDATES,
6
+ DEFAULT_CLAUDE_CANDIDATES,
7
+ ResolvedJudge,
8
+ pick_agy_model,
9
+ pick_claude_model,
10
+ pick_live_model,
11
+ resolve_cli_judge,
12
+ )
13
+ from evalrx.agent_runtime.judges.claude import ClaudeModel
14
+ from evalrx.agent_runtime.judges.codex import CodexModel
15
+
16
+ __all__ = [
17
+ "AgyModel",
18
+ "ClaudeModel",
19
+ "CodexModel",
20
+ "scan_agy_log",
21
+ "DEFAULT_AGY_CANDIDATES",
22
+ "DEFAULT_CLAUDE_CANDIDATES",
23
+ "ResolvedJudge",
24
+ "pick_agy_model",
25
+ "pick_claude_model",
26
+ "pick_live_model",
27
+ "resolve_cli_judge",
28
+ ]
@@ -0,0 +1,179 @@
1
+ """Antigravity CLI wrapped as a text-generation judge model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import pathlib
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ import warnings
12
+ from pathlib import Path
13
+
14
+ AGY_ERROR_MARKERS = (
15
+ "RESOURCE_EXHAUSTED",
16
+ "code 429",
17
+ "quota",
18
+ "ineligible",
19
+ "PERMISSION_DENIED",
20
+ "UNAUTHENTICATED",
21
+ "exhausted",
22
+ )
23
+
24
+
25
+ def safe_unlink(path: str) -> None:
26
+ try:
27
+ os.unlink(path)
28
+ except OSError:
29
+ pass
30
+
31
+
32
+ def scan_agy_log(path: str) -> str:
33
+ """Return the last agy-log error line, or ``""`` if none."""
34
+ try:
35
+ with open(path, encoding="utf-8", errors="replace") as fh:
36
+ lines = fh.readlines()[-300:]
37
+ except OSError:
38
+ return ""
39
+ for line in reversed(lines):
40
+ if any(marker.lower() in line.lower() for marker in AGY_ERROR_MARKERS):
41
+ msg = line.strip()
42
+ idx = msg.rfind("] ")
43
+ return (msg[idx + 2:] if idx != -1 else msg)[:240]
44
+ return ""
45
+
46
+
47
+ class AgyModel:
48
+ """agy CLI wrapped as a judge model."""
49
+
50
+ key = "agy"
51
+
52
+ def __init__(
53
+ self,
54
+ binary_path: str = "",
55
+ timeout_sec: int = 120,
56
+ model: str = "",
57
+ ) -> None:
58
+ from evalrx.core.capability import Capability
59
+
60
+ binary = binary_path or shutil.which("agy") or ""
61
+ if not binary or not os.path.isfile(binary) or not os.access(binary, os.X_OK):
62
+ raise RuntimeError(
63
+ "AgyModel: 'agy' binary not found or not executable. "
64
+ "Set AGY_PATH=$(which agy) and re-run, or pass binary_path= explicitly."
65
+ )
66
+ self._binary = binary
67
+ self._timeout_sec = timeout_sec
68
+ self._model = model
69
+ self.capabilities = frozenset({Capability.GENERATE})
70
+ self.modalities = frozenset({"text"})
71
+
72
+ # ``-p`` passes the prompt as a literal argv element. M2/M3 judge prompts
73
+ # in this framework routinely embed a full per-case evidence dump (every
74
+ # M1 analyzer's findings across every EXPLORE-split case) and can exceed
75
+ # the kernel's argv+envp size limit -- observed in practice as
76
+ # ``OSError: [Errno 7] Argument list too long`` on exec, which the caller
77
+ # (StatsAnalysisAgent/DiagnosisAgent) then silently treats as "judge
78
+ # returned nothing" and falls back to a threshold-only conclusion / zero
79
+ # hypotheses. Conservative threshold: well under any real ARG_MAX
80
+ # (2 MiB is typical on Linux, shared with the environment).
81
+ _LARGE_PROMPT_BYTES = 60_000
82
+
83
+ def generate(
84
+ self,
85
+ inputs: object,
86
+ *,
87
+ images: "list[Path] | None" = None,
88
+ **kwargs: object,
89
+ ) -> str:
90
+ """Run ``agy -p <inputs>`` and return the text response."""
91
+ fd, log_path = tempfile.mkstemp(prefix="agy_", suffix=".log")
92
+ os.close(fd)
93
+ workspace_dir: str | None = None
94
+ try:
95
+ prompt_text = str(inputs)
96
+ img_names: list[str] = []
97
+ if images:
98
+ valid = [p for p in images if isinstance(p, pathlib.Path) and p.exists()]
99
+ if valid:
100
+ workspace_dir = workspace_dir or tempfile.mkdtemp(prefix="agy_ws_")
101
+ for path in valid:
102
+ shutil.copy2(path, pathlib.Path(workspace_dir) / path.name)
103
+ img_names = [path.name for path in valid]
104
+
105
+ # Above the threshold, spill the prompt to a file in the same
106
+ # isolated per-call workspace instead of passing it inline --
107
+ # this exposes nothing the inline prompt didn't already contain,
108
+ # it just relocates it out of argv.
109
+ if len(prompt_text.encode("utf-8", errors="replace")) > self._LARGE_PROMPT_BYTES:
110
+ workspace_dir = workspace_dir or tempfile.mkdtemp(prefix="agy_ws_")
111
+ prompt_path = pathlib.Path(workspace_dir) / "prompt.txt"
112
+ prompt_path.write_text(prompt_text, encoding="utf-8")
113
+ cli_prompt = (
114
+ "Your full task instructions are in the file `prompt.txt` "
115
+ "in this workspace (too large to pass inline). Read it "
116
+ "completely and respond exactly as it instructs -- do not "
117
+ "summarize, truncate, or skip any part of it."
118
+ )
119
+ else:
120
+ cli_prompt = prompt_text
121
+ if img_names:
122
+ names = ", ".join(img_names)
123
+ cli_prompt = f"Images available in workspace: {names}\n\n{cli_prompt}"
124
+
125
+ cmd = [
126
+ self._binary,
127
+ "-p",
128
+ cli_prompt,
129
+ # A judge receives the evidence explicitly in its prompt. It
130
+ # must not inspect repository files, benchmark manifests, or
131
+ # held-out labels through terminal tools.
132
+ "--sandbox",
133
+ "--dangerously-skip-permissions",
134
+ "--log-file",
135
+ log_path,
136
+ ]
137
+ if workspace_dir:
138
+ cmd += ["--add-dir", workspace_dir]
139
+ if self._model:
140
+ cmd += ["--model", self._model]
141
+
142
+ try:
143
+ proc = subprocess.run(
144
+ cmd,
145
+ capture_output=True,
146
+ timeout=self._timeout_sec,
147
+ text=True,
148
+ encoding="utf-8",
149
+ errors="replace",
150
+ env={**os.environ},
151
+ )
152
+ except subprocess.TimeoutExpired as exc:
153
+ raise RuntimeError(
154
+ f"AgyModel: agy timed out after {self._timeout_sec}s"
155
+ ) from exc
156
+
157
+ output = proc.stdout.strip()
158
+ if proc.returncode != 0 and not output:
159
+ reason = scan_agy_log(log_path) or (proc.stderr or "").strip()[:240]
160
+ raise RuntimeError(f"AgyModel: agy exited {proc.returncode}: {reason}")
161
+
162
+ output = re.sub(r"<think>.*?</think>", "", output, flags=re.DOTALL).strip()
163
+ if not output:
164
+ reason = scan_agy_log(log_path)
165
+ warnings.warn(
166
+ "AgyModel: agy returned an empty response"
167
+ + (f" -- {reason}" if reason else "")
168
+ + ". agy is likely rate-limited or quota-exhausted; the caller "
169
+ "will fall back to a non-LLM path.",
170
+ stacklevel=2,
171
+ )
172
+ return output
173
+ finally:
174
+ safe_unlink(log_path)
175
+ if workspace_dir:
176
+ shutil.rmtree(workspace_dir, ignore_errors=True)
177
+
178
+ def __repr__(self) -> str:
179
+ return f"AgyModel(binary={self._binary!r})"
@@ -0,0 +1,135 @@
1
+ """Judge liveness-probe autodetection: find a CLI judge that actually responds.
2
+
3
+ A quota-exhausted CLI model often exits 0 with an empty response rather than
4
+ an error, so a tiny generation probe is the only reliable availability check.
5
+ Promoted from the ``_pick_agy_model``/``_pick_claude_model``/``_resolve_judge``
6
+ helpers duplicated across the ``VLDiagnoseLoop`` examples (``examples/m1_m3/*``,
7
+ ``examples/m1_m5/*``).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import warnings
13
+ from dataclasses import dataclass
14
+ from typing import Any, Callable, Sequence
15
+
16
+ from evalrx.agent_runtime.judges.agy import AgyModel
17
+ from evalrx.agent_runtime.judges.claude import ClaudeModel
18
+
19
+ _DEFAULT_PROBE_PROMPT = "Reply with exactly the word OK"
20
+
21
+ # Probe order: cheap/fast candidates first; quotas are per-model and reset on
22
+ # independent clocks, so whichever responds first is "the available version".
23
+ DEFAULT_AGY_CANDIDATES: tuple[str, ...] = (
24
+ "Gemini 3.1 Pro (Low)",
25
+ "Claude Sonnet 4.6 (Thinking)",
26
+ "GPT-OSS 120B (Medium)",
27
+ "Gemini 3.5 Flash (Low)",
28
+ "Claude Opus 4.6 (Thinking)",
29
+ )
30
+ # Claude judge candidates: the user's session model first (Fable), then
31
+ # cheaper aliases.
32
+ DEFAULT_CLAUDE_CANDIDATES: tuple[str, ...] = ("claude-fable-5", "sonnet", "haiku")
33
+
34
+
35
+ def pick_live_model(
36
+ factory: "Callable[[str], Any]",
37
+ candidates: "Sequence[str]",
38
+ *,
39
+ probe_prompt: str = _DEFAULT_PROBE_PROMPT,
40
+ ) -> str:
41
+ """Return the first candidate name whose model actually answers *probe_prompt*.
42
+
43
+ *factory* builds a model from a candidate name (e.g. ``lambda name:
44
+ AgyModel(model=name, timeout_sec=60)``). A quota-exhausted model typically
45
+ returns an empty string rather than raising, so an empty response is
46
+ treated the same as a failed candidate. Raises ``RuntimeError`` (with every
47
+ attempt's failure reason) when all candidates are dead.
48
+ """
49
+ errors: list[str] = []
50
+ for name in candidates:
51
+ try:
52
+ probe = factory(name)
53
+ with warnings.catch_warnings():
54
+ warnings.simplefilter("ignore") # quota warnings are expected here
55
+ out = probe.generate(probe_prompt)
56
+ if str(out).strip():
57
+ return name
58
+ errors.append(f"{name!r}: empty response (likely quota-exhausted)")
59
+ except RuntimeError as exc:
60
+ errors.append(f"{name!r}: {exc}")
61
+ raise RuntimeError(
62
+ f"no model responded to the availability probe ({len(candidates)} tried): "
63
+ + "; ".join(errors)
64
+ )
65
+
66
+
67
+ def pick_agy_model(
68
+ candidates: "Sequence[str]" = DEFAULT_AGY_CANDIDATES, *, timeout_sec: int = 60,
69
+ ) -> str:
70
+ """Return the first agy model name that answers a probe prompt."""
71
+ return pick_live_model(
72
+ lambda name: AgyModel(model=name, timeout_sec=timeout_sec), candidates,
73
+ )
74
+
75
+
76
+ def pick_claude_model(
77
+ candidates: "Sequence[str]" = DEFAULT_CLAUDE_CANDIDATES, *, timeout_sec: int = 90,
78
+ ) -> str:
79
+ """Return the first claude model name that answers a probe prompt."""
80
+ return pick_live_model(
81
+ lambda name: ClaudeModel(model=name, timeout_sec=timeout_sec), candidates,
82
+ )
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class ResolvedJudge:
87
+ """A CLI judge resolved by :func:`resolve_cli_judge`, with its provenance."""
88
+
89
+ judge: Any
90
+ provider: str # "agy" | "claude"
91
+ model: str
92
+
93
+
94
+ def resolve_cli_judge(
95
+ provider: str = "auto",
96
+ model: str = "auto",
97
+ *,
98
+ effort: str = "",
99
+ timeout_sec: int = 240,
100
+ ) -> ResolvedJudge:
101
+ """Resolve a live CLI judge.
102
+
103
+ ``provider="auto"`` tries agy first (its quota errors fail fast), then
104
+ claude. ``model="auto"`` probes :data:`DEFAULT_AGY_CANDIDATES` /
105
+ :data:`DEFAULT_CLAUDE_CANDIDATES` for a live one; an explicit model name
106
+ is used directly (constructed, not probed). Raises ``RuntimeError``
107
+ (aggregating every attempt's failure) when nothing is available; raises
108
+ ``ValueError`` for an unrecognized *provider*.
109
+ """
110
+
111
+ def _try_agy() -> ResolvedJudge:
112
+ name = pick_agy_model() if model == "auto" else model
113
+ return ResolvedJudge(AgyModel(model=name, timeout_sec=timeout_sec), "agy", name)
114
+
115
+ def _try_claude() -> ResolvedJudge:
116
+ name = pick_claude_model() if model == "auto" else model
117
+ return ResolvedJudge(
118
+ ClaudeModel(model=name, timeout_sec=timeout_sec, effort=effort), "claude", name,
119
+ )
120
+
121
+ try:
122
+ chain = {"agy": (_try_agy,), "claude": (_try_claude,),
123
+ "auto": (_try_agy, _try_claude)}[provider]
124
+ except KeyError:
125
+ raise ValueError(
126
+ f"resolve_cli_judge: unknown provider {provider!r} (agy/claude/auto)"
127
+ ) from None
128
+
129
+ errors: list[str] = []
130
+ for attempt in chain:
131
+ try:
132
+ return attempt()
133
+ except RuntimeError as exc:
134
+ errors.append(str(exc))
135
+ raise RuntimeError("no CLI judge available: " + "; ".join(errors))
@@ -0,0 +1,159 @@
1
+ """Claude Code CLI wrapped as a text-generation judge model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import pathlib
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ import warnings
12
+ from pathlib import Path
13
+
14
+ #: The Claude Code CLI's limit notices (plan / session / spend / usage), which
15
+ #: arrive on stdout with exit 0 exactly like a real answer.
16
+ _LIMIT_RE = re.compile(
17
+ r"hit your (monthly |weekly |session )?(spend|usage|rate) limit|"
18
+ r"(spend|usage) limit (reached|exceeded)|out of (extra )?usage|"
19
+ r"limit resets", re.I)
20
+
21
+
22
+ class ClaudeModel:
23
+ """Claude Code CLI wrapped as a judge model."""
24
+
25
+ key = "claude"
26
+
27
+ def __init__(
28
+ self,
29
+ binary_path: str = "",
30
+ timeout_sec: int = 240,
31
+ model: str = "",
32
+ effort: str = "",
33
+ ) -> None:
34
+ from evalrx.core.capability import Capability
35
+
36
+ binary = binary_path or shutil.which("claude") or ""
37
+ if not binary or not os.path.isfile(binary) or not os.access(binary, os.X_OK):
38
+ raise RuntimeError(
39
+ "ClaudeModel: 'claude' binary not found or not executable. "
40
+ "Set CLAUDE_PATH=$(which claude) and re-run, or pass "
41
+ "binary_path= explicitly."
42
+ )
43
+ self._binary = binary
44
+ self._timeout_sec = timeout_sec
45
+ self._model = model
46
+ self._effort = effort
47
+ self.capabilities = frozenset({Capability.GENERATE})
48
+ self.modalities = frozenset({"text"})
49
+
50
+ def generate(
51
+ self,
52
+ inputs: object,
53
+ *,
54
+ images: "list[Path] | None" = None,
55
+ **kwargs: object,
56
+ ) -> str:
57
+ """Run ``claude -p`` with the prompt on STDIN and return the response.
58
+
59
+ The prompt must not be an argv entry. Linux caps a SINGLE argument at
60
+ ``MAX_ARG_STRLEN`` = 32 pages = 131,072 bytes — a limit ``ulimit -s`` and
61
+ ``ARG_MAX`` do not describe and no configuration raises — and execve
62
+ fails outright with ``OSError: [Errno 7] Argument list too long``.
63
+
64
+ M2's prompt is the analyzers' findings JSON, so its size grows with how
65
+ many analyzers M1 selected. Measured on qwen3.5-2b / bbh_word_sorting:
66
+ 9 analyzers produced 127,856 bytes and worked, 12 analyzers produced
67
+ ~137,600 and did not. The size that fails is therefore a property of one
68
+ judge's analyzer selection, which is exactly the thing that varies run to
69
+ run — the same batch, the same code, and one run crosses the cliff.
70
+
71
+ Stdin has no equivalent limit; 144,050 bytes round-trips fine.
72
+ """
73
+ img_dir: str | None = None
74
+ try:
75
+ prompt_text = str(inputs)
76
+ if images:
77
+ valid = [p for p in images if isinstance(p, pathlib.Path) and p.exists()]
78
+ if valid:
79
+ img_dir = tempfile.mkdtemp(prefix="claude_imgs_")
80
+ for path in valid:
81
+ shutil.copy2(path, pathlib.Path(img_dir) / path.name)
82
+ names = ", ".join(path.name for path in valid)
83
+ prompt_text = f"Images available in workspace: {names}\n\n{prompt_text}"
84
+
85
+ # A judge generates text; it does not act. Asking for the blanket
86
+ # permission bypass was more than that needs, and it made the judge
87
+ # unrunnable anywhere nothing wraps it: the CLI refuses
88
+ # `--dangerously-skip-permissions` under root unless IS_SANDBOX=1,
89
+ # which our compose files set (see _common/compose/base.yml) and a
90
+ # bare root box — Colab, a plain container, CI — does not. There
91
+ # every judge call exited 1 before reaching the model.
92
+ #
93
+ # `--tools ""` is the accurate request for a text completion: no
94
+ # tools, therefore nothing to permission. An image-bearing call is
95
+ # the one exception — the figures are staged in a temp dir and the
96
+ # model has to Read them — so that call allows exactly Read, scoped
97
+ # to the directory just added, and nothing else.
98
+ cmd = [
99
+ self._binary,
100
+ "-p",
101
+ "--output-format",
102
+ "text",
103
+ ]
104
+ if img_dir:
105
+ cmd += ["--add-dir", img_dir, "--allowed-tools", "Read"]
106
+ else:
107
+ cmd += ["--tools", ""]
108
+ if self._model:
109
+ cmd += ["--model", self._model]
110
+ if self._effort:
111
+ cmd += ["--effort", self._effort]
112
+
113
+ try:
114
+ proc = subprocess.run(
115
+ cmd,
116
+ input=prompt_text,
117
+ capture_output=True,
118
+ timeout=self._timeout_sec,
119
+ text=True,
120
+ encoding="utf-8",
121
+ errors="replace",
122
+ env={**os.environ},
123
+ )
124
+ except subprocess.TimeoutExpired as exc:
125
+ raise RuntimeError(
126
+ f"ClaudeModel: claude timed out after {self._timeout_sec}s"
127
+ ) from exc
128
+
129
+ output = (proc.stdout or "").strip()
130
+ # The CLI reports an exhausted plan/session/spend limit as a one-line
131
+ # *answer* on stdout with exit 0 ("You've hit your monthly spend
132
+ # limit ..."). Returned as a judge reply it parses to zero
133
+ # hypotheses and the run "finishes" with an empty diagnosis
134
+ # (qwen2-audio/mmau 2026-09-07, all three demo128 cells
135
+ # 2026-09-09). Fail loudly instead.
136
+ if len(output) < 400 and _LIMIT_RE.search(output):
137
+ raise RuntimeError(f"ClaudeModel: account limit reached, not a judge answer: {output!r}")
138
+ if proc.returncode != 0 and not output:
139
+ reason = (proc.stderr or "").strip()[:240]
140
+ raise RuntimeError(
141
+ f"ClaudeModel: claude exited {proc.returncode}: {reason}"
142
+ )
143
+ if not output:
144
+ warnings.warn(
145
+ "ClaudeModel: claude returned an empty response -- likely "
146
+ "rate-limited or out of quota; the caller will fall back "
147
+ "to a non-LLM path.",
148
+ stacklevel=2,
149
+ )
150
+ return output
151
+ finally:
152
+ if img_dir:
153
+ shutil.rmtree(img_dir, ignore_errors=True)
154
+
155
+ def __repr__(self) -> str:
156
+ return (
157
+ f"ClaudeModel(binary={self._binary!r}, model={self._model!r}, "
158
+ f"effort={self._effort!r})"
159
+ )