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,15 @@
1
+ """Prompt templates for M4 hypothesis testing."""
2
+
3
+ _CONSISTENCY_PROMPT = """\
4
+ Experiment protocol:
5
+ {protocol_text}
6
+
7
+ Hypothesis under review:
8
+ Statement: {statement}
9
+ Predicted failure mode: {failure_mode}
10
+
11
+ Does this hypothesis address a failure mode that is relevant to the experiment protocol above?
12
+ Answer YES or NO on the first line, then give a one-sentence reason.
13
+
14
+ Answer YES if the hypothesis explains a failure that the protocol would care about.
15
+ Answer NO if the hypothesis is about something the protocol does not mention or test."""
@@ -0,0 +1,38 @@
1
+ """Prompt templates for natural-language diagnosis scaffolding."""
2
+
3
+ _AGENT_PROMPT_TEMPLATE = """\
4
+ Generate a complete `run.py` for an EvalRX diagnosis experiment.
5
+
6
+ DESCRIPTION OF FAILURE TO DIAGNOSE
7
+ ===================================
8
+ {description}
9
+
10
+ TARGET MODEL
11
+ ============
12
+ {model_key}
13
+
14
+ REQUIREMENTS
15
+ ============
16
+ 1. Import from `evalrx` and `evalrx.eval_agent`.
17
+ 2. Create an `ExperimentProtocol` whose `description` field captures the
18
+ failure described above (verbatim or paraphrased).
19
+ 3. Build a `CaseBatch` with 6-10 `FailureCase` objects that probe the
20
+ described failure. Include at least two "easy" control cases that the
21
+ model should PASS and several "hard" cases that expose the failure.
22
+ Use `Inputs(prompt=..., image=...)` — image is optional for text-only failures.
23
+ 4. Load the model with `compose("{model_key}", "hf_local", ...)`.
24
+ 5. Run `VLDiagnoseLoop` with `ProbeAgent`, `StatsAnalysisAgent`,
25
+ `DiagnosisAgent`, and `AgyModel` as judge (with a try/except fallback
26
+ when the agy binary is absent).
27
+ 6. Accept CLI flags: --model, --device, --dtype, --max-cycles,
28
+ --max-analyzers, --smoke-test, --run-dir.
29
+ 7. Write outputs via `RunContext(args.run_dir)` (from `evalrx.eval_agent`),
30
+ defaulting `--run-dir` to `Path(__file__).parent / "outputs"`. This
31
+ defaults to the V2 logger (`run.json` + `M1..M5/log.json`, no flat
32
+ `run_log.jsonl`, no `README.txt`/`manifest.json`) — don't assume those
33
+ V1-only files exist when printing a completion summary.
34
+ 8. Include a `--smoke-test` path with a `_SmokeModel` stand-in so the
35
+ script can be verified without a GPU.
36
+
37
+ Write ONLY `run.py` — nothing else. No markdown, no explanation.
38
+ """
@@ -0,0 +1,25 @@
1
+ """Prompt templates for M1 probe-agent analyzer selection."""
2
+
3
+ _SELECTION_PROMPT_TMPL = """\
4
+ You are selecting diagnostic analyzers for a model evaluation experiment.
5
+ Choose the analyzers that would surface the most useful evidence given the \
6
+ researcher's description.
7
+
8
+ EXPERIMENT DESCRIPTION:
9
+ {description}
10
+ {task_domain_section}
11
+ {success_criteria_section}
12
+ {failure_patterns_section}{cases_section}
13
+ MODEL TYPE: {model_kind}
14
+
15
+ AVAILABLE ANALYZERS ({n_available} compatible with this model):
16
+ {analyzer_list}
17
+ {failed_section}{prior_hypotheses_section}
18
+ Select up to {max_n} analyzers. Return ONLY a JSON object, no other text:
19
+ {{"analyzers": ["name1", "name2", ...], "rationale": "one sentence explaining the selection"}}
20
+
21
+ If the analyzers you can actually rely on (excluding any listed as failed)
22
+ cannot adequately surface the suspected mechanism, additionally include
23
+ "need_custom": "<one line describing the probe to generate>" in the JSON —
24
+ a bespoke probe will be code-generated and run in a sandbox.
25
+ """
@@ -0,0 +1,14 @@
1
+ """Prompt for ProbeLLM-style Macro/Micro question paraphrasing (VLM probe search)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ PARAPHRASE_PROMPT = """\
6
+ You are helping stress-test a vision-language model. Below is a question that
7
+ was originally asked about an image. Rewrite it as {style}, while preserving
8
+ its exact meaning and its correct answer — do not change what is being asked
9
+ about, only how it is phrased.
10
+
11
+ Original question: {question}
12
+
13
+ Reply with ONLY the rewritten question text — no quotes, no explanation, no
14
+ "Question:" prefix."""
@@ -0,0 +1,35 @@
1
+ """Prompt templates for generated code-writing stages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _INPUT_FILENAME = "m1_probe_input.json"
6
+ _RESULT_MARKER = "PROBE_RESULT_JSON="
7
+ _MAX_OUTPUT_CHARS = 4000
8
+
9
+ _GENERATE_PROMPT = """\
10
+ You are writing a self-contained Python script that probes a model's text outputs
11
+ for a specific failure pattern.
12
+
13
+ GOAL (what to probe for):
14
+ {need}
15
+
16
+ DATA: a JSON file named "{input_filename}" sits in the current working directory:
17
+ {{
18
+ "cases": [
19
+ {{"id": "<case id>", "prompt": "<input prompt>", "expected": <expected or null>,
20
+ "label": "pass"|"fail"|"unknown"|null, "output": "<the model's text output>"}}
21
+ ]
22
+ }}
23
+
24
+ REQUIREMENTS:
25
+ - Read "{input_filename}" from the current directory; do NOT hardcode the data.
26
+ - Compute, per case, a numeric or boolean signal for the failure pattern above
27
+ (e.g. 1 if the output refuses / drifts language / breaks format, else 0).
28
+ - Standard library + `import numpy` only. No network, no file writes, no model calls.
29
+ - The LAST line of stdout MUST be exactly:
30
+ {marker}{{"findings": {{"<aggregate_metric>": <number>}}, "per_case": [{{"sample_id": "<case id>", "<signal_name>": <number_or_bool>}}, ...]}}
31
+ - Every per_case entry MUST carry "sample_id" equal to the case "id".
32
+ - Print NOTHING after that line. Keep the script under ~60 lines.
33
+
34
+ Return ONLY the Python code{fences_hint}."""
35
+
@@ -0,0 +1,38 @@
1
+ """Prompt templates for generated code-writing stages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _MANIFEST = "m1_whitebox_manifest.json"
6
+ _DUMP_DIR = "m1_whitebox"
7
+ _RESULT_MARKER = "PROBE_RESULT_JSON="
8
+
9
+ _GENERATE_PROMPT = """\
10
+ You are writing a self-contained Python script that probes a vision-language
11
+ model's INTERNAL attention for a specific failure mechanism.
12
+
13
+ GOAL (what mechanism to probe for):
14
+ {need}
15
+
16
+ DATA in the current working directory:
17
+ - "{manifest}": {{"cases": [{{"id": str, "label": "pass"|"fail"|null,
18
+ "seq_len": int, "n_layers": int, "tokens": [str, ...]}}, ...]}}
19
+ - "{dump_dir}/<case_id>.npz" per case with arrays:
20
+ - attn_last: float16 (n_layers, seq) — head-averaged attention from
21
+ the LAST query position to every key position, per layer
22
+ - image_token_mask: bool (seq,) — True at image-patch token positions
23
+ The "tokens" list in the manifest gives the token string at each position —
24
+ use it to locate structural/special tokens (e.g. positions whose token
25
+ contains "im_start", "image_pad", "vision").
26
+
27
+ REQUIREMENTS:
28
+ - numpy only (`import numpy as np`); no network, no other file writes.
29
+ - Compute ONE per-case numeric signal for the mechanism above (e.g. attention
30
+ mass on structural tokens, image-vs-text attention ratio at a layer,
31
+ attention entropy inside the image region).
32
+ - The LAST line of stdout MUST be exactly:
33
+ {marker}{{"findings": {{"<aggregate_metric>": <number>}}, "per_case": [{{"sample_id": "<case id>", "<signal_name>": <number>}}, ...]}}
34
+ - Every per_case entry MUST use "sample_id" equal to the case "id".
35
+ - Print NOTHING after that line. Keep the script under ~60 lines.
36
+
37
+ Return ONLY the Python code{fences_hint}."""
38
+
@@ -0,0 +1,58 @@
1
+ """Diagnostic report — turn a statistical verdict (+ its pre-registration) into output.
2
+
3
+ Carries the corrected decision, effect size + CI, e-value, the underpowered flag,
4
+ and the pre-registration hash that proves the hypothesis preceded the test.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional
10
+
11
+ from evalrx.stats import StatResult
12
+
13
+
14
+ class DiagnosticReport:
15
+ """Format an A/B / hypothesis-test conclusion as a dict or markdown."""
16
+
17
+ @staticmethod
18
+ def from_comparison(
19
+ stat: StatResult,
20
+ *,
21
+ title: str = "A/B comparison",
22
+ hypothesis=None,
23
+ prereg_hash: Optional[str] = None,
24
+ split: Optional[str] = None,
25
+ ) -> dict:
26
+ return {
27
+ "title": title,
28
+ "decision": "REJECT H0" if stat.reject else "inconclusive",
29
+ "effect": stat.effect,
30
+ "ci": list(stat.ci),
31
+ "e_value": stat.e_value,
32
+ "underpowered": stat.underpowered,
33
+ "method": stat.method,
34
+ "alpha": stat.alpha,
35
+ "split": split,
36
+ "preregistered": prereg_hash is not None,
37
+ "prereg_hash": prereg_hash,
38
+ "hypothesis": getattr(hypothesis, "statement", None) or getattr(hypothesis, "predicate", None),
39
+ "details": stat.details,
40
+ }
41
+
42
+ @staticmethod
43
+ def to_markdown(report: dict) -> str:
44
+ lines = [
45
+ f"## {report['title']}",
46
+ f"- **Decision**: {report['decision']}"
47
+ + (" ⚠️ underpowered" if report.get("underpowered") else ""),
48
+ f"- **Effect (B−A)**: {report['effect']:+.4f} CI {report['ci'][0]:+.4f}..{report['ci'][1]:+.4f}",
49
+ f"- **e-value**: {report['e_value']:.3g}" if report.get("e_value") is not None else "- e-value: n/a",
50
+ f"- **Method**: {report['method']} (α={report['alpha']})",
51
+ ]
52
+ if report.get("hypothesis"):
53
+ lines.append(f"- **Hypothesis**: {report['hypothesis']}")
54
+ if report.get("preregistered"):
55
+ lines.append(f"- **Pre-registered**: yes (hash `{report['prereg_hash']}`, split `{report.get('split')}`)")
56
+ else:
57
+ lines.append("- **Pre-registered**: NO — exploratory, not confirmatory")
58
+ return "\n".join(lines)
@@ -0,0 +1,354 @@
1
+ """RunContext — single owner of one diagnosis run's output directory.
2
+
3
+ Historically each ``examples/*/run.py`` glued together four independent output
4
+ producers by hand: the example wrote report files at the run root, the run
5
+ logger was buried under a ``logs/`` subdir, :class:`StatsAnalysisAgent`
6
+ figures went to a hand-built ``logs/figures/`` path, and the sandbox wrote
7
+ experiment code into an ephemeral temp dir. ``RunContext`` replaces that
8
+ gluing with one library-owned object that owns the whole run directory and
9
+ hands every producer its subdirectory.
10
+
11
+ The logger is :class:`~evalrx.eval_agent.run_logger_v2.RunLoggerV2` (see
12
+ ``evalrx/eval_agent/RUN_LOGGER_V2.md``): one ``run.json`` plus one
13
+ ``M1/log.json``..``M5/log.json`` per stage, no flat event file, no persisted
14
+ ``prompts/``/``experiments/``/``tools/``/``workspace/``/``fixes/`` — generated
15
+ text/code is captured inline into the relevant stage's JSON via an ephemeral
16
+ runtime tree that :meth:`finalize` deletes, and no ``manifest.json``/
17
+ ``README.txt`` is written (the manifest lives inside ``run.json`` instead).
18
+
19
+ Layout::
20
+
21
+ <root>/
22
+ ├── run.json run-wide events: run_start, cases, diagnose_reports, manifest, …
23
+ ├── M1/log.json … one JSON document per stage, plus each stage's own artifacts/
24
+ │ M5/log.json
25
+ ├── contract/ one validated JSON per stage (see evalrx.contract), if emitted
26
+ ├── artifacts/ M1 heavy numeric data (.npy / .json) written outside any stage
27
+ └── langfuse_trace.json
28
+
29
+ ``fixes/`` and ``experiments/`` attempts (:meth:`RunContext.new_trial`) — one
30
+ numbered attempt per fix candidate or M5 experiment — live under an ephemeral
31
+ :attr:`runtime_root` instead: generated code, the sandbox it ran in, and
32
+ judge prompt/output are inlined into the owning stage's JSON, and the
33
+ runtime tree is deleted at :meth:`finalize`, so "what did attempt #14 do" is
34
+ one entry in that stage's log, not a filename-slug hunt across loose folders.
35
+
36
+ Usage::
37
+
38
+ from evalrx.eval_agent import RunContext, VLDiagnoseLoop
39
+
40
+ with RunContext("examples/foo/outputs", verbose=True) as ctx:
41
+ stats_agent = StatsAnalysisAgent(judge=judge, figure_dir=str(ctx.figures_dir))
42
+ loop = VLDiagnoseLoop(..., run_logger=ctx.logger)
43
+ report = loop.run(cases)
44
+ ctx.write_diagnose_report(report, cases, discovery=discovery_rows)
45
+ # run.json/M*/log.json are flushed incrementally throughout the run;
46
+ # finalize() just closes them out and inlines the runtime tree.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import json
52
+ import re
53
+ import shutil
54
+ import tempfile
55
+ from datetime import datetime
56
+ from pathlib import Path
57
+ from typing import TYPE_CHECKING, Any
58
+
59
+ if TYPE_CHECKING:
60
+ from evalrx.eval_agent.run_logger_v2 import RunLoggerV2
61
+
62
+
63
+ class Trial:
64
+ """One self-contained attempt — a fix candidate or an M5 experiment.
65
+
66
+ Returned by :meth:`RunContext.new_trial`. Everything about this one
67
+ attempt (generated code, the sandbox it ran in, judge prompt/output, and
68
+ its result/record) is written under :attr:`root`, so reviewing "what did
69
+ attempt #14 do" never requires hopping across run-global category dirs.
70
+ :attr:`root` lives under :attr:`RunContext.runtime_root` — an ephemeral
71
+ tree captured into the owning stage's JSON and deleted at
72
+ :meth:`RunContext.finalize`, not a permanent directory.
73
+
74
+ Directory creation is lazy: nothing is written to disk until the first
75
+ call to :meth:`write` / :attr:`workspace`, so an attempt that is discarded
76
+ before producing anything leaves no empty folder behind.
77
+ """
78
+
79
+ def __init__(self, root: Path) -> None:
80
+ self.root = root
81
+ self._workspace: "Path | None" = None
82
+
83
+ @property
84
+ def workspace(self) -> Path:
85
+ """``<trial>/workspace/`` — where the sandbox for this attempt runs."""
86
+ if self._workspace is None:
87
+ self._workspace = self.root / "workspace"
88
+ self._workspace.mkdir(parents=True, exist_ok=True)
89
+ return self._workspace
90
+
91
+ def write(self, name: str, content: "str | bytes") -> Path:
92
+ """Write *content* to ``<trial>/<name>``; return the path."""
93
+ self.root.mkdir(parents=True, exist_ok=True)
94
+ path = self.root / name
95
+ if isinstance(content, bytes):
96
+ path.write_bytes(content)
97
+ else:
98
+ path.write_text(content, encoding="utf-8")
99
+ return path
100
+
101
+ def write_record(self, markdown: str) -> Path:
102
+ """Write the human-readable summary to ``<trial>/record.md``."""
103
+ return self.write("record.md", markdown)
104
+
105
+ def write_result(self, data: "dict[str, Any]") -> Path:
106
+ """Write the machine-readable outcome to ``<trial>/result.json``."""
107
+ return self.write("result.json", json.dumps(data, indent=2, default=str))
108
+
109
+ def __repr__(self) -> str:
110
+ return f"Trial(root={str(self.root)!r})"
111
+
112
+
113
+ class RunContext:
114
+ """Owns one run's output directory and every producer's subdirectory.
115
+
116
+ Args:
117
+ root: Run root directory. Created if missing. Defaults to
118
+ ``runs/<YYYYMMDD_HHMMSS>/`` relative to cwd.
119
+ run_id: Optional identifier recorded in the manifest; defaults to the
120
+ root directory name.
121
+ verbose: Forwarded to the logger (human-readable stdout).
122
+ narrate: Forwarded to the logger — live, aligned M1-M5 terminal
123
+ narration instead of `verbose`'s raw one-liner (see
124
+ :class:`evalrx.eval_agent.narration.LoopNarrator`).
125
+ Supersedes *verbose* rather than stacking with it.
126
+ config: Optional run-configuration dict recorded verbatim in the
127
+ manifest (model, judge, protocol, …).
128
+
129
+ The logger allocates producer sandboxes in an external ephemeral runtime
130
+ tree; their text is inlined into JSON and their media is copied before
131
+ finalization removes the tree.
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ root: "str | Path | None" = None,
137
+ *,
138
+ run_id: "str | None" = None,
139
+ verbose: bool = False,
140
+ narrate: bool = False,
141
+ config: "dict[str, Any] | None" = None,
142
+ observability_mode: str | None = None,
143
+ ) -> None:
144
+ if root is None:
145
+ root = Path("runs") / datetime.now().strftime("%Y%m%d_%H%M%S")
146
+ # Resolved to absolute: sandbox subprocesses run with cwd=<some
147
+ # workdir under root> *and* a script path built from that same
148
+ # workdir (see ExperimentSandbox._run_script / run_coded_pipeline) —
149
+ # if root stayed relative, the child process would resolve that
150
+ # script path a second time relative to its new cwd, doubling it.
151
+ self.root = Path(root).resolve()
152
+ self.root.mkdir(parents=True, exist_ok=True)
153
+ self.run_id = run_id or self.root.name
154
+ self.config = dict(config or {})
155
+ self._verbose = verbose
156
+ self._narrate = narrate
157
+ self._observability_mode = observability_mode
158
+ self._logger: "RunLoggerV2 | None" = None
159
+ self._workdir_seq = 0
160
+ self._trial_seq: "dict[str, int]" = {}
161
+ self._runtime_root: "Path | None" = None
162
+ self._finalized = False
163
+
164
+ @property
165
+ def runtime_root(self) -> Path:
166
+ """Ephemeral execution workspace used by producers.
167
+
168
+ Generated text/code is captured by the logger before this tree is
169
+ removed; it never becomes part of the portable run artifact.
170
+ """
171
+ if self._runtime_root is None:
172
+ self._runtime_root = Path(tempfile.mkdtemp(prefix=f"evalrx-{self.run_id}-"))
173
+ return self._runtime_root
174
+
175
+ # ------------------------------------------------------------------
176
+ # Directory properties — each lazily created on first access.
177
+ # ------------------------------------------------------------------
178
+
179
+ def _sub(self, name: str) -> Path:
180
+ d = self.root / name
181
+ d.mkdir(parents=True, exist_ok=True)
182
+ return d
183
+
184
+ @property
185
+ def figures_dir(self) -> Path:
186
+ return self._sub("M2/artifacts")
187
+
188
+ @property
189
+ def explore_dir(self) -> Path:
190
+ """The in-cycle explore step's report, tables and rendered figures
191
+ (``VLDiagnoseLoop(explorer=..., explore_dir=ctx.explore_dir)``; the
192
+ loop derives the same path from ``ctx.logger`` when not given).
193
+ Ephemeral — captured into ``M2/log.json`` and deleted at
194
+ :meth:`finalize`, not a permanent directory."""
195
+ d = self.runtime_root / "explore"
196
+ d.mkdir(parents=True, exist_ok=True)
197
+ return d
198
+
199
+ @property
200
+ def artifacts_dir(self) -> Path:
201
+ return self._sub("artifacts")
202
+
203
+ # ------------------------------------------------------------------
204
+ # Logging component
205
+ # ------------------------------------------------------------------
206
+
207
+ @property
208
+ def logger(self) -> "RunLoggerV2":
209
+ """The logger bound to this context (created on first use)."""
210
+ if self._logger is None:
211
+ from evalrx.eval_agent.run_logger_v2 import RunLoggerV2
212
+
213
+ self._logger = RunLoggerV2(
214
+ run_dir=self.root, verbose=self._verbose, narrate=self._narrate,
215
+ observability_mode=self._observability_mode,
216
+ context=self,
217
+ )
218
+ return self._logger
219
+
220
+ # ------------------------------------------------------------------
221
+ # Producer-facing path allocation
222
+ # ------------------------------------------------------------------
223
+
224
+ def new_workdir(self, label: str) -> Path:
225
+ """Return a fresh sandbox working directory under the runtime tree.
226
+
227
+ Replaces ``tempfile.mkdtemp()`` so the experiment code the agent
228
+ writes is captured with the rest of the run instead of vanishing
229
+ untracked. *label* is slugified; a monotonic counter guarantees
230
+ uniqueness.
231
+ """
232
+ self._workdir_seq += 1
233
+ slug = re.sub(r"[^a-zA-Z0-9]+", "_", label).strip("_") or "work"
234
+ d = self.runtime_root / "workspace" / f"{self._workdir_seq:02d}_{slug}"
235
+ d.mkdir(parents=True, exist_ok=True)
236
+ return d
237
+
238
+ def new_trial(self, category: str, label: str) -> "Trial":
239
+ """Allocate one self-contained attempt folder under the runtime tree.
240
+
241
+ A *trial* is one fix candidate or one M5 experiment: its generated
242
+ code, the sandbox it actually ran in, judge prompt/output, and its
243
+ result/record all live together under one numbered folder.
244
+ *category* is ``"fixes"`` or ``"experiments"``; numbering is
245
+ monotonic per category.
246
+
247
+ The trial's own folder (and its ``workspace/``) is created lazily on
248
+ first write — a candidate discarded before producing anything (e.g. a
249
+ deduped fix proposal) leaves no empty folder on disk; the numbering
250
+ still advances, so a gap in the sequence honestly means "proposed,
251
+ then discarded," not a missing record.
252
+ """
253
+ if category not in ("fixes", "experiments"):
254
+ raise ValueError(
255
+ f"new_trial: unknown category {category!r} "
256
+ "(expected 'fixes' or 'experiments')"
257
+ )
258
+ seq = self._trial_seq.get(category, 0) + 1
259
+ self._trial_seq[category] = seq
260
+ slug = re.sub(r"[^a-zA-Z0-9]+", "_", label).strip("_") or "trial"
261
+ return Trial(self.runtime_root / category / f"{seq:02d}_{slug}")
262
+
263
+ def figure_path(self, name: str) -> Path:
264
+ """Return ``figures_dir/<name>`` (created if needed)."""
265
+ if not name.lower().endswith((".png", ".jpg", ".jpeg", ".svg", ".pdf")):
266
+ name = f"{name}.png"
267
+ return self.figures_dir / name
268
+
269
+ # ------------------------------------------------------------------
270
+ # Report API — absorbs the per-example boilerplate.
271
+ # ------------------------------------------------------------------
272
+
273
+ def write_diagnose_report(
274
+ self,
275
+ report: Any,
276
+ cases: "list[Any]",
277
+ *,
278
+ discovery: "list[dict[str, Any]] | None" = None,
279
+ ) -> None:
280
+ """Inline the standard post-diagnosis report into ``run.json``.
281
+
282
+ *report* is an :class:`AutoDiagnoseReport` (all three loops return
283
+ this one unified class — ``VLDiagnoseReport`` is an alias for it).
284
+ *discovery* (optional) is a list of already-serialised case rows —
285
+ examples that compute task-specific columns (e.g. parsed yes/no)
286
+ build the rows themselves. Delegates to
287
+ :meth:`~evalrx.eval_agent.run_logger_v2.RunLoggerV2.log_diagnose_report`.
288
+ """
289
+ self.logger.log_diagnose_report(report, cases, discovery=discovery)
290
+
291
+ def publish_report(
292
+ self,
293
+ *,
294
+ model: "Any | None" = None,
295
+ example_dir: "str | Path | None" = None,
296
+ ) -> "Any":
297
+ """Publish the dynamic report after all diagnostic/fix stages finish."""
298
+ from evalrx.reporting.dynamic import publish_report
299
+
300
+ return publish_report(
301
+ self.root, example_dir=example_dir, model=model, run_logger=self._logger,
302
+ )
303
+
304
+ # ------------------------------------------------------------------
305
+ # Lifecycle
306
+ # ------------------------------------------------------------------
307
+
308
+ def finalize(self) -> None:
309
+ """Close the logger, inline the runtime tree, and delete it. Idempotent."""
310
+ if self._finalized:
311
+ return
312
+ if self._logger is not None or self._runtime_root is not None:
313
+ logger = self.logger
314
+ if self._runtime_root is not None:
315
+ logger.log_runtime_snapshot(self._runtime_root)
316
+ logger.close()
317
+ # close() materializes the trace bundle and every M1-M5 log;
318
+ # index only afterwards so the manifest is complete.
319
+ logger.log_manifest(run_id=self.run_id, config=self.config)
320
+ # write_contract_index() only looks for an on-disk contract/ dir
321
+ # and the logger's trace_id. Without it, a run that emitted
322
+ # contract/ payloads (evalrx.contract.emit, independent of
323
+ # RunContext) never gets the index.json a reader opening the run
324
+ # without its producer needs.
325
+ self.write_contract_index()
326
+ if self._runtime_root is not None:
327
+ shutil.rmtree(self._runtime_root, ignore_errors=True)
328
+ self._finalized = True
329
+
330
+ def write_contract_index(self) -> "Path | None":
331
+ """Write ``contract/index.json`` when any stage emitted a payload.
332
+
333
+ Written here rather than by the emitter so it describes the finished
334
+ directory: a reader opening this run — or a zip of it — gets one file
335
+ naming every payload, its stage and cycle, and the wire model that
336
+ decodes it, without having to infer any of that from filenames.
337
+ """
338
+ if not (self.root / "contract").is_dir():
339
+ return None
340
+ try:
341
+ from evalrx.contract.emit import write_index
342
+ except ImportError: # contract extra not installed
343
+ return None
344
+ trace_id = getattr(self._logger, "trace_id", "") if self._logger else ""
345
+ return write_index(self.root, trace_id=trace_id)
346
+
347
+ def __enter__(self) -> "RunContext":
348
+ return self
349
+
350
+ def __exit__(self, *_: Any) -> None:
351
+ self.finalize()
352
+
353
+ def __repr__(self) -> str:
354
+ return f"RunContext(root={str(self.root)!r})"