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,452 @@
1
+ """Shared wire types for the M1-M4 stage contract.
2
+
3
+ These models describe the **serialized** shape crossing a stage boundary — what
4
+ lands on disk and what a frontend reads — not the in-memory Python objects.
5
+ The two are deliberately different: ``AnalysisReport.to_dict()`` drops
6
+ ``raw_results`` and flattens ``findings`` to strings, so a contract written
7
+ against the in-memory dataclass would not describe what a reader actually gets.
8
+
9
+ Design rules enforced here (see ``docs/stage_io.md``):
10
+
11
+ 1. **Envelope strict, payload free.** Fields the pipeline joins/aggregates on are
12
+ required and typed; analyzer- and dataset-specific content stays open
13
+ (``extra="allow"``) so a new analyzer never requires a contract change.
14
+ 2. **One join key.** Every per-case row carries ``sample_id`` and it equals
15
+ :attr:`FailureCaseWire.id`. A broken join is reported, never silently
16
+ degraded into "no effect found" (see :class:`JoinReport`).
17
+ 3. **Modalities are slots + sets, never combination enums.** Adding a modality
18
+ must not multiply the type space.
19
+ 4. **Absent / null / empty / zero / false are five different things.** Every
20
+ optional field is explicitly ``| None`` so a reader can tell them apart; never
21
+ collapse them with a truthiness fallback.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from enum import Enum
27
+ from typing import Any, Literal, TypeVar, Union
28
+
29
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
30
+
31
+ #: Bumped only when a field's *meaning* changes. Adding fields is additive and
32
+ #: does NOT bump it — readers must ignore unknown fields, not fail on them.
33
+ #:
34
+ #: 4: ``AnalyzerSelection.model_kind`` (a combination enum) replaced by modality
35
+ #: slots + ``is_agent``; ``StepWire.role`` realigned to ``core.case.StepRole``.
36
+ #: 5: stage ids M4 and M5 swapped so numbering follows execution order —
37
+ #: hypothesis verification is now ``m4`` (was ``m5``), surgery + fix are now
38
+ #: ``m5_surgery``/``m5_fix`` (were ``m4_surgery``/``m4_fix``).
39
+ SCHEMA_VERSION = 5
40
+
41
+
42
+ class WireModel(BaseModel):
43
+ """Base for every model in this contract: strict envelope, no coercion."""
44
+
45
+ model_config = ConfigDict(
46
+ extra="forbid", # envelope fields are closed by default
47
+ strict=False, # allow int->float, but not str->int
48
+ validate_assignment=True,
49
+ frozen=False,
50
+ )
51
+
52
+
53
+ class OpenWireModel(WireModel):
54
+ """Base for payload areas whose content is producer-defined.
55
+
56
+ Unknown keys are kept (not dropped) so the raw inspector can show them and a
57
+ newer producer never loses data against an older reader.
58
+ """
59
+
60
+ model_config = ConfigDict(extra="allow")
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Enums — closed value sets the UI branches on
65
+ # ---------------------------------------------------------------------------
66
+
67
+ class Label(str, Enum):
68
+ """Per-case outcome. Not a statistical verdict."""
69
+
70
+ PASS = "pass"
71
+ FAIL = "fail"
72
+ UNKNOWN = "unknown"
73
+
74
+
75
+ class Source(str, Enum):
76
+ HUMAN = "human"
77
+ DATASET = "dataset"
78
+ AGENT = "agent"
79
+
80
+
81
+ class HypothesisStatus(str, Enum):
82
+ PROPOSED = "proposed"
83
+ TESTING = "testing"
84
+ SUPPORTED = "supported"
85
+ REFUTED = "refuted"
86
+ INCONCLUSIVE = "inconclusive"
87
+
88
+
89
+ class EvidenceGrade(str, Enum):
90
+ """Strength tier of the deciding evidence.
91
+
92
+ ``INTERVENTION`` beats ``OBSERVATIONAL``: a paired contrast changed one
93
+ variable and re-ran, so it carries causal weight an association does not.
94
+ """
95
+
96
+ INTERVENTION = "intervention"
97
+ OBSERVATIONAL = "observational"
98
+ NONE = "none"
99
+
100
+
101
+ class StageState(str, Enum):
102
+ """Per-stage UI state. A single generic "N/A" loses information the reader
103
+ needs — in particular ``EMPTY`` (ran, produced nothing) and ``FAILED``
104
+ (did not run correctly) must never render the same."""
105
+
106
+ NOT_STARTED = "not_started"
107
+ RUNNING = "running"
108
+ SUCCEEDED = "succeeded"
109
+ EMPTY = "empty"
110
+ ABSTAINED = "abstained"
111
+ PARTIAL = "partial"
112
+ FAILED = "failed"
113
+ SKIPPED = "skipped"
114
+ UNAVAILABLE = "unavailable"
115
+
116
+
117
+ #: Open string set on purpose — a new modality must be one more member of a set,
118
+ #: never a new combination enum (VA / TIA / ... is 2^n and does not compose).
119
+ #:
120
+ #: LLM / VLM / ALM / AVLM are not four types in this contract; they are four
121
+ #: subsets of this set. An AVLM is ``{"text", "image", "audio"}`` and needs no
122
+ #: schema change to exist.
123
+ Modality = Literal["text", "image", "audio", "video"]
124
+
125
+ #: Every modality slot other than ``text``. Text is always present (a prompt is
126
+ #: mandatory), so it is never a *slot* — it is the floor.
127
+ MEDIA_SLOTS: tuple[Modality, ...] = ("image", "audio", "video")
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # References — things that are NOT inline payload
132
+ # ---------------------------------------------------------------------------
133
+
134
+ class ExternalRef(WireModel):
135
+ """Pointer replacing an inline value that exceeded the size budget.
136
+
137
+ Detect by shape (``kind == "external_ref"``), lazy-load ``path``, and keep
138
+ ``n_items`` visible while loading. Never render the pointer as one item.
139
+ """
140
+
141
+ kind: Literal["external_ref"] = "external_ref"
142
+ path: str = Field(description="Relative to runRoot. Never an absolute host path.")
143
+ n_items: int | None = Field(default=None, description="Item count in the target, when countable.")
144
+ bytes: int = Field(description="Serialized size of the externalized value.")
145
+
146
+
147
+ T = TypeVar("T")
148
+ #: An inline value that may have been swapped for a pointer. Readers must handle
149
+ #: both arms; the discriminator is ``kind == "external_ref"``.
150
+ Externalizable = Union[T, ExternalRef]
151
+
152
+
153
+ class MediaRef(WireModel):
154
+ """A media *reference*, never media bytes.
155
+
156
+ ``kind`` tells the reader whether a preview is even possible: an in-memory
157
+ PIL image that was never persisted degrades to ``descriptor`` (e.g.
158
+ ``"<image 640x480>"``), and offering a broken preview for it is a bug.
159
+
160
+ A ``path`` must be RELATIVE TO THE RUN ROOT, which means the run has to carry
161
+ the file: the media a case was evaluated on is part of the evidence, not an
162
+ external dependency. An absolute path is only meaningful on the machine that
163
+ produced it, so a run recording one is readable nowhere else — every audio
164
+ and video preview 404s and an audio-visual report silently becomes text. The
165
+ run logger copies case media under ``artifacts/case_media/`` for exactly this
166
+ reason; point at that copy, and keep the producer's original path (if it is
167
+ worth keeping at all) as provenance that nothing resolves against.
168
+ """
169
+
170
+ kind: Literal["path", "url", "descriptor"]
171
+ value: str = Field(
172
+ description="Relative to runRoot when kind is 'path'. Never an absolute host path.",
173
+ )
174
+ mime: str | None = None
175
+ n_bytes: int | None = None
176
+
177
+ @classmethod
178
+ def coerce(cls, value: Any) -> "MediaRef | None":
179
+ """Build a ref from whatever a producer put in a modality slot.
180
+
181
+ ``FailureCase.to_dict()`` runs every slot through ``_json_safe``, which
182
+ yields a bare string: a path/URL when the slot held one, or a descriptor
183
+ like ``"<image 640x480>"`` for a PIL object that was never persisted.
184
+ Both are legitimate producer output, so the contract classifies them here
185
+ rather than rejecting the case — a case is not invalid because its image
186
+ lived in memory.
187
+ """
188
+ if value is None or isinstance(value, MediaRef):
189
+ return value
190
+ if isinstance(value, dict):
191
+ return cls.model_validate(value)
192
+ text = str(value).strip()
193
+ if not text:
194
+ return None
195
+ if text.startswith("<") and text.endswith(">"):
196
+ return cls(kind="descriptor", value=text)
197
+ if text.startswith(("http://", "https://", "data:", "s3://", "gs://")):
198
+ return cls(kind="url", value=text)
199
+ return cls(kind="path", value=text)
200
+
201
+
202
+ class ArtifactRef(WireModel):
203
+ """A heavy artifact (``.npy``, ``.png``, workspace dir) left on disk.
204
+
205
+ Heavy arrays never go inline: a browser must not parse a ``.npy``, and a
206
+ JSONL event must not carry an attention map.
207
+ """
208
+
209
+ path: str = Field(description="Relative to runRoot.")
210
+ media_type: Literal["npy", "png", "jpg", "json", "text", "dir", "other"] = "other"
211
+ bytes: int | None = None
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Case data — the raw rows every stage joins back to
216
+ # ---------------------------------------------------------------------------
217
+
218
+ class InputsWire(OpenWireModel):
219
+ """The question, plus optional modality slots.
220
+
221
+ Slots are independent and may be filled in any combination. There is
222
+ deliberately no "modality" discriminator field: ``prompt + image`` and
223
+ ``prompt + image + audio`` are the same type with different slots filled.
224
+ """
225
+
226
+ prompt: str
227
+ image: MediaRef | None = None
228
+ audio: MediaRef | None = None
229
+ video: MediaRef | None = None
230
+
231
+ @field_validator("image", "audio", "video", mode="before")
232
+ @classmethod
233
+ def _coerce_media(cls, v: Any) -> Any:
234
+ return MediaRef.coerce(v)
235
+
236
+ def modalities(self) -> set[Modality]:
237
+ """Modalities actually present on this case."""
238
+ present: set[Modality] = {"text"}
239
+ for slot in MEDIA_SLOTS:
240
+ if getattr(self, slot) is not None:
241
+ present.add(slot)
242
+ return present
243
+
244
+
245
+ #: Legacy role names this contract shipped before it was aligned to
246
+ #: :class:`evalrx.core.case.StepRole`. Mapped rather than rejected so a
247
+ #: trajectory written by an older producer still validates.
248
+ _LEGACY_STEP_ROLES = {"env": "tool", "critic": "verifier"}
249
+
250
+
251
+ class StepWire(OpenWireModel):
252
+ """One step of an agent trajectory.
253
+
254
+ ``role`` is exactly :class:`evalrx.core.case.StepRole`. It was not, and
255
+ the divergence was not cosmetic: ``planner`` / ``verifier`` / ``memory`` /
256
+ ``system`` are values the producer emits every multi-agent run, and every one
257
+ of them failed validation here, while ``env`` / ``critic`` named steps no
258
+ producer can emit.
259
+ """
260
+
261
+ idx: int
262
+ role: Literal[
263
+ "user", "planner", "actor", "tool", "verifier", "memory", "system"
264
+ ] = "actor"
265
+ content: Any | None = None
266
+ agent_id: str = "main"
267
+ tool_call: dict[str, Any] | None = None
268
+ observation: Any | None = None
269
+ span: dict[str, Any] = Field(default_factory=dict, description="tokens / latency_ms / cost / model")
270
+ is_first_error: bool | None = None
271
+ failure_mode: str | None = None
272
+ judge_confidence: float | None = None
273
+
274
+ @field_validator("role", mode="before")
275
+ @classmethod
276
+ def _map_legacy_role(cls, v: Any) -> Any:
277
+ if isinstance(v, str):
278
+ return _LEGACY_STEP_ROLES.get(v, v)
279
+ return getattr(v, "value", v)
280
+
281
+
282
+ class TrajectoryWire(OpenWireModel):
283
+ """Multi-step agent run. Present only for agent cases.
284
+
285
+ A trajectory is a *data* property, not a model capability: trajectory
286
+ analyzers run with ``model=None`` on rows loaded from disk, so eligibility
287
+ is gated on the batch in hand, not on what the model can do.
288
+ """
289
+
290
+ sample_id: str = Field(description="Must equal the owning FailureCaseWire.id.")
291
+ goal: str = ""
292
+ steps: list[StepWire] = Field(default_factory=list)
293
+ final_answer: Any | None = None
294
+ ground_truth: Any | None = None
295
+ outcome: Label = Label.UNKNOWN
296
+ metrics: dict[str, Any] = Field(default_factory=dict)
297
+
298
+
299
+ class ProvenanceWire(OpenWireModel):
300
+ source: Source = Source.HUMAN
301
+ metadata: dict[str, Any] = Field(default_factory=dict)
302
+
303
+
304
+ class FailureCaseWire(OpenWireModel):
305
+ """One evaluated case. ``id`` is the pipeline's single join key.
306
+
307
+ ``expected`` / ``observed`` are intentionally ``Any``: a gold answer may be a
308
+ string, a bounding box, or a structured object depending on the benchmark.
309
+ Render strings directly and objects in a structured/raw view.
310
+ """
311
+
312
+ id: str = Field(min_length=1, description="Stable join key. Every per-case row points here.")
313
+ inputs: InputsWire
314
+ expected: Any | None = None
315
+ observed: Any | None = None
316
+ trajectory: TrajectoryWire | None = None
317
+ label: Label = Label.UNKNOWN
318
+ tags: list[str] = Field(default_factory=list, description="Order is not semantically meaningful.")
319
+ provenance: ProvenanceWire = Field(default_factory=ProvenanceWire)
320
+ metadata: dict[str, Any] = Field(
321
+ default_factory=dict,
322
+ description="Benchmark-specific columns. Never assume a fixed schema here.",
323
+ )
324
+
325
+ @field_validator("trajectory")
326
+ @classmethod
327
+ def _trajectory_id_matches(cls, v: TrajectoryWire | None, info) -> TrajectoryWire | None:
328
+ # A trajectory whose sample_id differs from the case id gets registered
329
+ # under BOTH keys downstream, which double-counts that case in every
330
+ # group comparison. Reject the divergence at the boundary instead.
331
+ case_id = (info.data or {}).get("id")
332
+ if v is not None and case_id and v.sample_id != case_id:
333
+ raise ValueError(
334
+ f"trajectory.sample_id={v.sample_id!r} != case id={case_id!r}; "
335
+ "a divergent id double-counts this case in stats group splits"
336
+ )
337
+ return v
338
+
339
+
340
+ class CaseBatchWire(WireModel):
341
+ """Serialized ``CaseBatch``. The in-memory class has no ``to_dict()``, so
342
+ this shape is the contract for anything that crosses a process boundary."""
343
+
344
+ schema_version: int = SCHEMA_VERSION
345
+ n_cases: int
346
+ cases: list[FailureCaseWire]
347
+
348
+ @field_validator("cases")
349
+ @classmethod
350
+ def _ids_unique(cls, v: list[FailureCaseWire]) -> list[FailureCaseWire]:
351
+ ids = [c.id for c in v]
352
+ dupes = {i for i in ids if ids.count(i) > 1}
353
+ if dupes:
354
+ raise ValueError(f"duplicate case ids break every downstream join: {sorted(dupes)[:5]}")
355
+ return v
356
+
357
+ def probed_modalities(self) -> set[Modality]:
358
+ """Slots this batch actually fills — the union over its cases.
359
+
360
+ Distinct from what the model *declares*, and it is the distinction that
361
+ decides analyzer routing: an omni model evaluated on an audio benchmark
362
+ declares image too, and routing on the declaration sends image analyzers
363
+ at a batch with no images in it.
364
+ """
365
+ present: set[Modality] = set()
366
+ for case in self.cases:
367
+ present |= case.inputs.modalities()
368
+ return present or {"text"}
369
+
370
+
371
+ class CaseBatchRef(WireModel):
372
+ """A batch referenced by file rather than inlined (the normal case)."""
373
+
374
+ path: str
375
+ n_cases: int
376
+ split: Literal["all", "explore", "confirm", "discovery"] | None = None
377
+
378
+
379
+ # ---------------------------------------------------------------------------
380
+ # Cross-cutting diagnostics
381
+ # ---------------------------------------------------------------------------
382
+
383
+ class JoinReport(WireModel):
384
+ """Health of one ``sample_id`` <-> ``FailureCase.id`` join.
385
+
386
+ This exists because a broken join and a real null result are indistinguishable
387
+ downstream: unmatched ids are skipped, both groups come out empty, and the
388
+ tool reports "no significant association" — the same output as a model with
389
+ no defect. Emitting coverage makes "the pipeline is broken" a separate,
390
+ visible state from "the model is fine".
391
+ """
392
+
393
+ left: str = Field(description="e.g. 'attention.findings.per_case'")
394
+ right: str = Field(description="e.g. 'CaseBatch(explore)'")
395
+ n_left: int
396
+ n_right: int
397
+ n_matched: int
398
+ coverage: float = Field(ge=0.0, le=1.0, description="n_matched / n_left")
399
+ unmatched_sample: list[str] = Field(
400
+ default_factory=list, max_length=10, description="First few unmatched ids, for triage."
401
+ )
402
+
403
+ @property
404
+ def is_broken(self) -> bool:
405
+ """Zero overlap is a plumbing failure, never a scientific finding."""
406
+ return self.n_left > 0 and self.n_matched == 0
407
+
408
+
409
+ class StageStatus(WireModel):
410
+ """Terminal state of one stage in one cycle.
411
+
412
+ ``EMPTY`` and ``ABSTAINED`` are successes with nothing to show; ``FAILED``
413
+ and ``UNAVAILABLE`` are not. Collapsing them loses the only signal a reader
414
+ has for whether to trust the absence of a result.
415
+ """
416
+
417
+ stage: Literal["pre_m1", "m1", "explore", "m2", "m3", "m5_surgery", "m5_fix", "m4"]
418
+ state: StageState
419
+ cycle: int = Field(description="Normal cycles start at 0; the post-loop fix uses -1.")
420
+ reason: str | None = Field(
421
+ default=None,
422
+ description="Why, for any state that is not plain success — the skip rule, the error, "
423
+ "the reason for abstaining. One field, because a reader wants one answer.",
424
+ )
425
+ duration_sec: float | None = None
426
+ joins: list[JoinReport] = Field(default_factory=list)
427
+
428
+
429
+ class StageEnvelope(WireModel):
430
+ """Common header on every persisted stage output.
431
+
432
+ Which stage this is comes from ``status.stage`` — carrying it twice invites
433
+ the two copies to disagree, and the reader has no way to tell which is right.
434
+ """
435
+
436
+ schema_version: int = SCHEMA_VERSION
437
+ trace_id: str = Field(description="Run-level correlation id.")
438
+ span_id: str | None = Field(default=None, description="e.g. 'c0.m1'.")
439
+ cycle: int = 0
440
+ produced_at: str = Field(description="ISO-8601 UTC.")
441
+ status: StageStatus
442
+
443
+
444
+ __all__ = [
445
+ "SCHEMA_VERSION", "WireModel", "OpenWireModel",
446
+ "Label", "Source", "HypothesisStatus", "EvidenceGrade", "StageState",
447
+ "Modality", "MEDIA_SLOTS",
448
+ "ExternalRef", "Externalizable", "MediaRef", "ArtifactRef",
449
+ "InputsWire", "StepWire", "TrajectoryWire", "ProvenanceWire",
450
+ "FailureCaseWire", "CaseBatchWire", "CaseBatchRef",
451
+ "JoinReport", "StageStatus", "StageEnvelope",
452
+ ]