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,530 @@
1
+ """Legacy loop controllers, kept for backward compatibility.
2
+
3
+ ``SelfEvolveLoop``
4
+ Original Stage-1 skeleton.
5
+
6
+ ``AutoDiagnoseLoop``
7
+ Concrete M1→M2→M3→M5 implementation (original architecture):
8
+
9
+ ┌───────────────────────────────────────────────────────────────────┐
10
+ │ M1 · ProbeAgent select analyzers + execute (direct / Docker) │
11
+ │ M2 · AnalysisModule interpret results → structured report │
12
+ │ M3 · DiagnosisAgent Gemini reads report → hypotheses │
13
+ │ M5 · SurgeryAgent operate + verify; stop or refocus data │
14
+ └───────────────────────────────────────────────────────────────────┘
15
+ ↑__________________________│ (repeat)
16
+
17
+ Prefer :class:`~evalrx.eval_agent.loop.VLDiagnoseLoop` (M1→M2→M3→M4, M5
18
+ post-loop) or :class:`~evalrx.eval_agent.agentic.AgenticDiagnoseLoop`
19
+ (judge-decided) for new work — this module is the pre-2026-06-05 architecture,
20
+ kept for existing callers and its resumable run_dir/checkpoint infrastructure.
21
+
22
+ Run-directory infrastructure (mirrors AutoResearchClaw):
23
+
24
+ When ``run_dir`` is supplied:
25
+ - ``run_dir/artifacts/{run_id}/`` — per-run artifact staging
26
+ - ``run_dir/checkpoint.json`` — atomic resume state (temp+rename)
27
+ - ``run_dir/heartbeat.json`` — per-cycle liveness signal
28
+ - ``run_dir/evolution/`` — JSONL lesson store (auto-created)
29
+
30
+ Git integration:
31
+ - When the repo is detected, each resolved run is committed on
32
+ ``eval/{run_id}``; unresolved runs are discarded with git reset.
33
+
34
+ Usage::
35
+
36
+ loop = AutoDiagnoseLoop(model=my_model, run_dir=Path("./runs"))
37
+ report = loop.run(failure_cases)
38
+ print(report.resolved, report.final_hypotheses)
39
+
40
+ # Resume an interrupted run
41
+ report = AutoDiagnoseLoop.resume(Path("./runs"), model=my_model, data=cases)
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import logging
47
+ import time
48
+ from datetime import datetime, timezone
49
+ from pathlib import Path
50
+ from typing import TYPE_CHECKING, Any
51
+
52
+ from evalrx.eval_agent.checkpoint import read_checkpoint, write_checkpoint, write_heartbeat
53
+ from evalrx.eval_agent.hypothesis import HypothesisStatus
54
+ from evalrx.eval_agent.loop_reports import AutoDiagnoseReport
55
+ from evalrx.eval_agent.run_metadata import _attach_run_logger, _log_generated_tools, _run_config
56
+ from evalrx.eval_agent.store import InMemoryStore, Store
57
+
58
+ if TYPE_CHECKING:
59
+ from evalrx.analysis.analysis_module import AnalysisModule
60
+ from evalrx.core.case import CaseBatch
61
+ from evalrx.core.model import Model
62
+ from evalrx.eval_agent.evolution import EvolutionStore
63
+ from evalrx.eval_agent.git_manager import ExperimentGitManager
64
+ from evalrx.eval_agent.hypothesis import Hypothesis, HypothesisGenerator
65
+ from evalrx.eval_agent.run_logger_v2 import RunLoggerV2
66
+ from evalrx.eval_agent.stages.diagnosis import DiagnosisAgent
67
+ from evalrx.eval_agent.stages.probe_agent import ProbeAgent
68
+ from evalrx.eval_agent.stages.surgery import SurgeryAgent
69
+
70
+ logger = logging.getLogger(__name__)
71
+
72
+
73
+ class SelfEvolveLoop:
74
+ """Original Stage-1 skeleton — kept for backward compatibility.
75
+
76
+ Args:
77
+ generator: Proposes and mutates hypotheses.
78
+ store: Persistent memory; defaults to an in-memory store.
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ generator: "HypothesisGenerator | None" = None,
84
+ store: Store | None = None,
85
+ runner: Any = None, # kept for signature compat
86
+ ) -> None:
87
+ from evalrx.core.experiment import ExperimentRunner
88
+
89
+ self.generator = generator
90
+ self.store = store or InMemoryStore()
91
+ self.runner = runner if runner is not None else ExperimentRunner()
92
+
93
+ def step(self) -> list:
94
+ if self.generator is None:
95
+ raise ValueError("SelfEvolveLoop needs a generator (e.g. ManualHypothesisGenerator).")
96
+ proposed = self.generator.propose(self.store.summarize())
97
+ for h in proposed:
98
+ self.store.add_hypothesis(h)
99
+ return list(proposed)
100
+
101
+ def run(self, max_cycles: int = 10) -> list:
102
+ history: list = []
103
+ for _ in range(max_cycles):
104
+ proposed = self.step()
105
+ history.append(proposed)
106
+ if not proposed:
107
+ break
108
+ return history
109
+
110
+
111
+ # ──────────────────────────────────────────────────────────────────────────────
112
+ # AutoDiagnoseLoop — M1 → M2 → M3 → M5
113
+ # ──────────────────────────────────────────────────────────────────────────────
114
+
115
+
116
+ class AutoDiagnoseLoop:
117
+ """Automated M1→M2→M3→M5 diagnosis loop.
118
+
119
+ Args:
120
+ model: The model under evaluation.
121
+ probe_agent: M1 — selects and executes analyzers (direct or Docker).
122
+ Defaults to ``ProbeAgent()``.
123
+ analysis_module: M2 — interprets raw Results into an AnalysisReport.
124
+ Defaults to ``AnalysisModule()``.
125
+ diagnosis_agent: M3 — reads the AnalysisReport and proposes hypotheses via
126
+ Gemini (default when ``GEMINI_API_KEY`` is set).
127
+ Pass ``None`` to run in *analysis-only* mode (M1+M2 only).
128
+ surgery_agent: M5 — operates on each hypothesis to verify or refute it.
129
+ Defaults to ``SurgeryAgent()``.
130
+ store: Persistent memory. Defaults to ``InMemoryStore()``.
131
+ max_cycles: Hard cap on M1→M5 iterations.
132
+ run_logger: Optional :class:`~evalrx.eval_agent.run_logger_v2.RunLoggerV2`
133
+ that logs the run's events and saves analyzer artifacts.
134
+ run_dir: Optional root directory for run infrastructure.
135
+ When set, enables checkpoints, heartbeat, and the
136
+ ``EvolutionStore``.
137
+ git_manager: Optional :class:`~evalrx.eval_agent.git_manager.ExperimentGitManager`.
138
+ ``None`` → auto-detect git repo when *run_dir* is given.
139
+ evolution_store: Optional :class:`~evalrx.eval_agent.evolution.EvolutionStore`.
140
+ ``None`` → auto-create under ``run_dir/evolution/`` when
141
+ *run_dir* is given.
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ model: "Model",
147
+ probe_agent: "ProbeAgent | None" = None,
148
+ analysis_module: "AnalysisModule | None" = None,
149
+ diagnosis_agent: "DiagnosisAgent | None" = None,
150
+ surgery_agent: "SurgeryAgent | None" = None,
151
+ store: Store | None = None,
152
+ max_cycles: int = 5,
153
+ run_logger: "RunLoggerV2 | None" = None,
154
+ # --- run-directory infrastructure (new) ---
155
+ run_dir: "Path | None" = None,
156
+ git_manager: "ExperimentGitManager | None" = None,
157
+ evolution_store: "EvolutionStore | None" = None,
158
+ # --- token / cost budget ---
159
+ token_budget: int = 0,
160
+ _run_id_override: str | None = None,
161
+ ) -> None:
162
+ from evalrx.analysis.analysis_module import AnalysisModule
163
+ from evalrx.eval_agent.stages.probe_agent import ProbeAgent
164
+ from evalrx.eval_agent.stages.surgery import SurgeryAgent
165
+
166
+ self.model = model
167
+ self.probe_agent = probe_agent or ProbeAgent()
168
+ self.analysis_module = analysis_module or AnalysisModule()
169
+ self.diagnosis_agent = diagnosis_agent
170
+ self.surgery_agent = surgery_agent or SurgeryAgent()
171
+ self.store = store or InMemoryStore()
172
+ self.max_cycles = max_cycles
173
+ self.run_logger = run_logger
174
+ self.token_budget = token_budget
175
+ self._tokens_used: int = 0
176
+ self._timings: dict[str, float] = {}
177
+
178
+ # --- run-directory setup ---
179
+ self._run_dir: Path | None = None
180
+ self._artifacts_dir: Path | None = None
181
+ self._checkpoint_path: Path | None = None
182
+ self._heartbeat_path: Path | None = None
183
+ self._run_id: str = ""
184
+
185
+ if run_dir is not None:
186
+ run_dir = Path(run_dir)
187
+ ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
188
+ self._run_id = _run_id_override or ts
189
+ self._run_dir = run_dir
190
+ self._artifacts_dir = run_dir / "artifacts" / ts
191
+ self._artifacts_dir.mkdir(parents=True, exist_ok=True)
192
+ self._checkpoint_path = run_dir / "checkpoint.json"
193
+ self._heartbeat_path = run_dir / "heartbeat.json"
194
+
195
+ # EvolutionStore: auto-create when run_dir is set
196
+ self.evolution_store: "EvolutionStore | None" = evolution_store
197
+ if self.evolution_store is None and run_dir is not None:
198
+ try:
199
+ from evalrx.eval_agent.evolution import EvolutionStore
200
+ self.evolution_store = EvolutionStore(run_dir / "evolution")
201
+ except Exception as exc:
202
+ logger.debug("Could not create EvolutionStore: %s", exc)
203
+
204
+ # ExperimentGitManager: auto-detect when run_dir is set
205
+ self.git_manager: "ExperimentGitManager | None" = git_manager
206
+ if self.git_manager is None and run_dir is not None:
207
+ try:
208
+ from evalrx.eval_agent.git_manager import ExperimentGitManager
209
+ _gm = ExperimentGitManager(run_dir)
210
+ if _gm.is_git_repo():
211
+ self.git_manager = _gm
212
+ except Exception as exc:
213
+ logger.debug("Git manager auto-detect failed: %s", exc)
214
+
215
+ # ──────────────────────────────────────────────────────────────────
216
+ # Public API
217
+ # ──────────────────────────────────────────────────────────────────
218
+
219
+ def publish_report(
220
+ self,
221
+ *,
222
+ model: "Any | None" = None,
223
+ example_dir: "str | Path | None" = None,
224
+ ) -> "Any":
225
+ """Compose the completed-run UI, reusing the diagnosis judge."""
226
+ if self.run_logger is None:
227
+ raise RuntimeError("publish_report needs a run_logger with a durable run directory")
228
+ if model is None:
229
+ model = getattr(self.diagnosis_agent, "judge", None)
230
+ from evalrx.reporting.dynamic import publish_report
231
+
232
+ return publish_report(
233
+ self.run_logger.run_dir,
234
+ example_dir=example_dir,
235
+ model=model,
236
+ run_logger=self.run_logger,
237
+ )
238
+
239
+ def run(self, data: "CaseBatch") -> AutoDiagnoseReport:
240
+ """Drive the M1→M2→M3→M5 loop until resolved or *max_cycles* reached.
241
+
242
+ Args:
243
+ data: Cases to analyse. Refocused to unexplained cases when M5
244
+ returns a SUPPORTED finding with ``new_data``.
245
+
246
+ Returns:
247
+ :class:`AutoDiagnoseReport` with the final state.
248
+ """
249
+ all_hypotheses: list[Any] = []
250
+ final_results: dict[str, Any] = {}
251
+ final_analysis = None
252
+
253
+ # Determine start cycle from checkpoint (resume support).
254
+ # Only honor the checkpoint when run_id matches — this means the
255
+ # caller explicitly requested a resume of a specific prior run
256
+ # (via AutoDiagnoseLoop.resume() or _run_id_override). A fresh
257
+ # AutoDiagnoseLoop always gets a new run_id and starts from 0.
258
+ start_cycle = 0
259
+ if self._checkpoint_path is not None:
260
+ cp = read_checkpoint(self._checkpoint_path)
261
+ if cp is not None and cp.get("run_id") == self._run_id:
262
+ start_cycle = cp.get("last_completed_cycle", -1) + 1
263
+ if start_cycle > 0:
264
+ logger.info(
265
+ "Resuming from checkpoint: skipping cycles 0–%d",
266
+ start_cycle - 1,
267
+ )
268
+
269
+ prior_cycles: list[dict[str, Any]] = []
270
+ outstanding_modes: list[str] = []
271
+ completed_cycles = 0
272
+ # Per-cycle max confidence scores: detect flat progress early
273
+ _confidence_history: list[float] = []
274
+ self._tokens_used = 0
275
+
276
+ # Per-stage wall-clock totals (seconds), surfaced in loop_end.
277
+ self._timings = {}
278
+
279
+ # Forward the RunLogger so the probe generators emit tool_codegen events.
280
+ _attach_run_logger(self.run_logger, self.probe_agent)
281
+ if self.run_logger is not None:
282
+ self.run_logger.log_run_start(
283
+ _run_config(self, data, loop_name="AutoDiagnoseLoop")
284
+ )
285
+ self.run_logger.log_cases(data)
286
+
287
+ for cycle in range(start_cycle, self.max_cycles):
288
+ if self.run_logger is not None:
289
+ self.run_logger.current_cycle = cycle
290
+ # Budget guard: stop before consuming more tokens than allowed
291
+ if self.token_budget > 0 and self._tokens_used >= self.token_budget:
292
+ logger.warning(
293
+ "Token budget %d exhausted (%d used) — stopping loop after %d cycles",
294
+ self.token_budget, self._tokens_used, completed_cycles,
295
+ )
296
+ break
297
+ completed_cycles = cycle + 1
298
+
299
+ # ── M1: probe ───────────────────────────────────────────────
300
+ _t0 = time.monotonic()
301
+ probe_results = self.probe_agent.probe(
302
+ self.model, data,
303
+ hint_failure_modes=outstanding_modes or None,
304
+ )
305
+ _dt = time.monotonic() - _t0
306
+ self._timings["m1"] = self._timings.get("m1", 0.0) + _dt
307
+ if self.run_logger:
308
+ self.run_logger.log_probe(
309
+ cycle, probe_results, schema=self.probe_agent.last_schema,
310
+ judge_prompt=getattr(self.probe_agent, "last_selection_prompt", ""),
311
+ judge_raw=getattr(self.probe_agent, "last_selection_raw", ""),
312
+ duration_sec=_dt,
313
+ failed_analyzers=getattr(self.probe_agent, "_failed_analyzers", None) or None,
314
+ )
315
+ _log_generated_tools(self.run_logger, cycle, "m1_probe", self.probe_agent)
316
+ if not probe_results:
317
+ break
318
+ final_results = probe_results
319
+ for r in probe_results.values():
320
+ self.store.add_result(r)
321
+
322
+ # ── M2: analyze ─────────────────────────────────────────────
323
+ _t0 = time.monotonic()
324
+ analysis = self.analysis_module.analyze(probe_results, repr(self.model))
325
+ _dt = time.monotonic() - _t0
326
+ self._timings["m2"] = self._timings.get("m2", 0.0) + _dt
327
+ final_analysis = analysis
328
+ if self.run_logger:
329
+ self.run_logger.log_analysis(cycle, analysis, duration_sec=_dt)
330
+
331
+ if self.diagnosis_agent is None:
332
+ self._on_cycle_complete(cycle, all_hypotheses)
333
+ break # analysis-only mode
334
+
335
+ # ── M3: diagnose ─────────────────────────────────────────────
336
+ _t0 = time.monotonic()
337
+ diag = self.diagnosis_agent.diagnose(
338
+ analysis, prior_cycles=prior_cycles or None
339
+ )
340
+ _dt = time.monotonic() - _t0
341
+ self._timings["m3"] = self._timings.get("m3", 0.0) + _dt
342
+ # Track token usage from the diagnosis LLM call.
343
+ # Use response metadata when available; otherwise estimate from
344
+ # output length (1 token ≈ 4 chars) so the budget is always counted.
345
+ _diag_tokens = getattr(diag, "tokens_used", None)
346
+ if _diag_tokens is None:
347
+ _diag_tokens = max(1, len(diag.raw_judge_output) // 4)
348
+ self._tokens_used += _diag_tokens
349
+ if self.run_logger:
350
+ self.run_logger.log_diagnosis(cycle, diag, duration_sec=_dt)
351
+ if not diag.hypotheses:
352
+ self._on_cycle_complete(cycle, all_hypotheses)
353
+ break
354
+ for h in diag.hypotheses:
355
+ self.store.add_hypothesis(h)
356
+ all_hypotheses.extend(diag.hypotheses)
357
+
358
+ # ── M5: surgery ──────────────────────────────────────────────
359
+ outstanding_modes = []
360
+ cycle_max_confidence = 0.0
361
+ for h in diag.hypotheses:
362
+ _t0 = time.monotonic()
363
+ iv = self.surgery_agent.operate(h, self.model, probe_results, data)
364
+ _dt = time.monotonic() - _t0
365
+ self._timings["m5"] = self._timings.get("m5", 0.0) + _dt
366
+ h.status = iv.status
367
+ cycle_max_confidence = max(cycle_max_confidence, iv.confidence_score)
368
+ if self.run_logger:
369
+ self.run_logger.log_surgery(cycle, h, iv, duration_sec=_dt)
370
+ # When surgery wrote and ran an experiment, also persist it
371
+ # (script, output, thinking, workspace snapshot).
372
+ if getattr(iv, "experiment", None):
373
+ self.run_logger.log_experiment(cycle, h, iv, module="m5")
374
+ if iv.fixed:
375
+ report = AutoDiagnoseReport(
376
+ cycles=completed_cycles,
377
+ resolved=True,
378
+ final_hypotheses=all_hypotheses,
379
+ final_results=final_results,
380
+ final_analysis=final_analysis,
381
+ store=self.store,
382
+ _run_id=self._run_id,
383
+ )
384
+ self._on_cycle_complete(cycle, all_hypotheses)
385
+ self._on_loop_end(report)
386
+ return report
387
+ if iv.status == HypothesisStatus.INCONCLUSIVE:
388
+ outstanding_modes.append(h.predicted_failure_mode)
389
+ if iv.new_data is not None and len(iv.new_data) > 0:
390
+ data = iv.new_data
391
+
392
+ # Track confidence per cycle and stop early when progress stalls:
393
+ # if the last 2 cycles both show confidence < 0.1 the hypotheses
394
+ # are not gaining traction — continuing burns budget without insight.
395
+ _confidence_history.append(cycle_max_confidence)
396
+ if len(_confidence_history) >= 2 and all(
397
+ c < 0.1 for c in _confidence_history[-2:]
398
+ ):
399
+ logger.info(
400
+ "Stopping early: confidence flat at %.3f for 2 consecutive cycles",
401
+ cycle_max_confidence,
402
+ )
403
+ break
404
+
405
+ # Record cycle for M3 context
406
+ prior_cycles.append({
407
+ "cycle": cycle,
408
+ "severity": analysis.severity,
409
+ "hypotheses": [
410
+ {
411
+ "statement": h.statement,
412
+ "failure_mode": h.predicted_failure_mode,
413
+ "status": h.status.value if h.status else "pending",
414
+ }
415
+ for h in diag.hypotheses
416
+ ],
417
+ })
418
+
419
+ # Write checkpoint + heartbeat after each full cycle
420
+ self._on_cycle_complete(cycle, all_hypotheses)
421
+
422
+ report = AutoDiagnoseReport(
423
+ cycles=completed_cycles,
424
+ resolved=False,
425
+ final_hypotheses=all_hypotheses,
426
+ final_results=final_results,
427
+ final_analysis=final_analysis,
428
+ store=self.store,
429
+ _run_id=self._run_id,
430
+ )
431
+ self._on_loop_end(report)
432
+ return report
433
+
434
+ @classmethod
435
+ def resume(
436
+ cls,
437
+ run_dir: Path,
438
+ model: "Model",
439
+ data: "CaseBatch",
440
+ **kwargs: Any,
441
+ ) -> "AutoDiagnoseReport":
442
+ """Resume a loop from a checkpoint.
443
+
444
+ Reads ``run_dir/checkpoint.json``, skips already-completed cycles,
445
+ and continues from ``last_completed_cycle + 1``.
446
+
447
+ Args:
448
+ run_dir: Directory that contains ``checkpoint.json``.
449
+ model: The model under evaluation.
450
+ data: Original :class:`CaseBatch` to continue from.
451
+ **kwargs: Forwarded to :class:`AutoDiagnoseLoop.__init__`.
452
+ ``run_dir`` is set automatically — do not pass it.
453
+ """
454
+ # Read the stored run_id so the checkpoint match succeeds and
455
+ # the loop correctly skips already-completed cycles.
456
+ run_id_override: str | None = None
457
+ cp = read_checkpoint(Path(run_dir) / "checkpoint.json")
458
+ if cp is not None:
459
+ run_id_override = cp.get("run_id")
460
+ instance = cls(
461
+ model=model, run_dir=run_dir, _run_id_override=run_id_override, **kwargs
462
+ )
463
+ return instance.run(data)
464
+
465
+ # ──────────────────────────────────────────────────────────────────
466
+ # Per-cycle and end-of-loop hooks
467
+ # ──────────────────────────────────────────────────────────────────
468
+
469
+ def _on_cycle_complete(
470
+ self, cycle: int, hypotheses: list["Hypothesis"]
471
+ ) -> None:
472
+ """Write checkpoint and heartbeat after each completed cycle."""
473
+ statuses = [
474
+ h.status.value if h.status else "pending"
475
+ for h in hypotheses
476
+ ]
477
+ if self._checkpoint_path is not None:
478
+ write_checkpoint(
479
+ self._checkpoint_path, cycle=cycle, run_id=self._run_id,
480
+ hypothesis_statuses=statuses,
481
+ )
482
+ if self._heartbeat_path is not None:
483
+ write_heartbeat(self._heartbeat_path, cycle=cycle, run_id=self._run_id)
484
+
485
+ def _on_loop_end(self, report: AutoDiagnoseReport) -> None:
486
+ """Append lessons to EvolutionStore and commit/discard via git."""
487
+ if self.run_logger:
488
+ self.run_logger.log_loop_end(
489
+ report,
490
+ tokens_used=self._tokens_used,
491
+ timings=getattr(self, "_timings", None) or None,
492
+ )
493
+
494
+ # EvolutionStore lesson extraction
495
+ if self.evolution_store is not None:
496
+ try:
497
+ from evalrx.eval_agent.evolution import extract_lessons
498
+ lessons = extract_lessons(report)
499
+ self.evolution_store.append_many(lessons)
500
+ logger.debug(
501
+ "EvolutionStore: appended %d lesson(s) for run %s",
502
+ len(lessons), self._run_id,
503
+ )
504
+ except Exception as exc:
505
+ logger.warning("Failed to record evolution lessons: %s", exc)
506
+
507
+ # Git integration
508
+ if self.git_manager is not None and self._run_id:
509
+ hyp_statuses = {
510
+ h.statement[:80]: (h.status.value if h.status else "pending")
511
+ for h in report.final_hypotheses
512
+ }
513
+ try:
514
+ if report.resolved:
515
+ self.git_manager.commit_experiment(
516
+ self._run_id,
517
+ report.cycles,
518
+ hyp_statuses,
519
+ "resolved",
520
+ )
521
+ logger.info(
522
+ "Git: committed resolved run %s on eval/%s",
523
+ self._run_id, self._run_id,
524
+ )
525
+ else:
526
+ self.git_manager.discard_experiment(
527
+ self._run_id, "not resolved"
528
+ )
529
+ except Exception as exc:
530
+ logger.warning("Git integration failed: %s", exc)