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,216 @@
1
+ """Judge-decided actions for the agentic diagnosis loop.
2
+
3
+ Each decision turn, the CLI judge returns one strict JSON object describing
4
+ the next tool to call. ``parse_action`` turns that raw text into a validated
5
+ :class:`Action`, using the shared :func:`~evalrx.agent_runtime.json_shape.validate_json_shape`
6
+ (re-exported here for convenience — also used by M3's hypothesis parser and
7
+ failure-mode cluster naming).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import re
15
+ import time
16
+ from dataclasses import dataclass, field
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from evalrx.agent_runtime.json_shape import validate_json_shape
20
+
21
+ if TYPE_CHECKING:
22
+ from evalrx.eval_agent.agentic.board import EvidenceBoard
23
+ from evalrx.eval_agent.agentic.tools import ToolRegistry
24
+ from evalrx.eval_agent.run_logger_v2 import RunLoggerV2
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ ACTION_SCHEMA: dict[str, Any] = {
29
+ "type": "object",
30
+ "required": ["tool", "rationale"],
31
+ "properties": {
32
+ "tool": {"type": "string", "minLength": 1},
33
+ "params": {"type": "object"},
34
+ "rationale": {"type": "string", "minLength": 1},
35
+ },
36
+ }
37
+
38
+
39
+ class ActionParseError(Exception):
40
+ """Raised when the judge's raw output cannot be turned into a valid Action."""
41
+
42
+ def __init__(self, message: str, errors: list[str] | None = None) -> None:
43
+ super().__init__(message)
44
+ self.errors = errors or []
45
+
46
+
47
+ @dataclass
48
+ class Action:
49
+ """One judge-decided tool call."""
50
+
51
+ tool: str
52
+ params: dict[str, Any] = field(default_factory=dict)
53
+ rationale: str = ""
54
+ raw: str = ""
55
+ repair_attempts: int = 0
56
+ valid: bool = True
57
+
58
+
59
+ def _strip_fences(text: str) -> str:
60
+ text = text.strip()
61
+ if text.startswith("```"):
62
+ text = re.sub(r"^```\w*\n?", "", text)
63
+ text = re.sub(r"\n?```\s*$", "", text)
64
+ return text.strip()
65
+
66
+
67
+ def parse_action(raw: str, registry: "ToolRegistry") -> Action:
68
+ """Parse the judge's raw text into a validated :class:`Action`.
69
+
70
+ Raises :class:`ActionParseError` when the text is not JSON, fails the
71
+ action schema, or names a tool the registry doesn't know about.
72
+ """
73
+ text = _strip_fences(raw)
74
+ try:
75
+ data = json.loads(text)
76
+ except json.JSONDecodeError as exc:
77
+ raise ActionParseError(f"not valid JSON: {exc}") from exc
78
+
79
+ errors = validate_json_shape(data, ACTION_SCHEMA)
80
+ if errors:
81
+ raise ActionParseError("action schema validation failed", errors)
82
+
83
+ tool = str(data["tool"])
84
+ if tool not in registry.tool_names():
85
+ raise ActionParseError(
86
+ f"unknown tool {tool!r}", [f"must be one of {sorted(registry.tool_names())}"]
87
+ )
88
+
89
+ params = data.get("params") or {}
90
+ if not isinstance(params, dict):
91
+ raise ActionParseError("params must be an object", [f"$.params: got {type(params).__name__}"])
92
+
93
+ return Action(tool=tool, params=params, rationale=str(data["rationale"]), raw=raw)
94
+
95
+
96
+ def _fallback_action(board: "EvidenceBoard") -> Action:
97
+ """Deterministic next-step heuristic: the first unmet M1->M4 stage.
98
+
99
+ Used when the judge's output is unusable even after one repair prompt, so
100
+ a stuck judge doesn't stall the run — the action is marked ``valid=False``
101
+ so the loop can track a consecutive-failure streak and give up cleanly.
102
+ """
103
+ if not board.probe_findings:
104
+ tool = "run_probe"
105
+ elif not board.stats_findings:
106
+ tool = "run_stats"
107
+ elif not board.hypotheses:
108
+ tool = "propose_hypotheses"
109
+ elif not board.has_supported_hypothesis():
110
+ tool = "test_hypothesis"
111
+ else:
112
+ tool = "stop"
113
+ params = {"resolved": True, "reason": "fallback heuristic"} if tool == "stop" else {}
114
+ return Action(
115
+ tool=tool, params=params,
116
+ rationale="fallback heuristic: judge output unusable", valid=False,
117
+ )
118
+
119
+
120
+ def decide(
121
+ judge: Any,
122
+ board: "EvidenceBoard",
123
+ registry: "ToolRegistry",
124
+ *,
125
+ run_logger: "RunLoggerV2 | None" = None,
126
+ step: int = 0,
127
+ max_repairs: int = 1,
128
+ ) -> Action:
129
+ """Ask the judge for the next action, repairing/falling back as needed.
130
+
131
+ Always returns an :class:`Action` — never raises. When the judge's
132
+ response can't be salvaged after *max_repairs* repair prompts, returns the
133
+ deterministic :func:`_fallback_action` with ``valid=False``.
134
+ """
135
+ from evalrx.eval_agent.prompts.agentic import ACTION_REPAIR_PROMPT, DECISION_PROMPT
136
+
137
+ evidence = board.to_prompt(registry)
138
+ prompt = DECISION_PROMPT.format(evidence=evidence)
139
+ judge_calls: list[dict[str, Any]] = []
140
+ started = time.perf_counter()
141
+ try:
142
+ raw = judge.generate(prompt)
143
+ except Exception as exc: # noqa: BLE001
144
+ judge_calls.append({
145
+ "role": "agent_decision", "operation": "generate", "inputs": prompt,
146
+ "output": None, "error": str(exc),
147
+ "duration_sec": time.perf_counter() - started,
148
+ })
149
+ raw = ""
150
+ action: Action | None = _fallback_action(board)
151
+ else:
152
+ judge_calls.append({
153
+ "role": "agent_decision", "operation": "generate", "inputs": prompt,
154
+ "output": str(raw), "error": None,
155
+ "duration_sec": time.perf_counter() - started,
156
+ })
157
+ try:
158
+ action = parse_action(raw, registry)
159
+ except ActionParseError as exc:
160
+ action = None
161
+ last_raw, last_exc = raw, exc
162
+ if action is None:
163
+ for attempt in range(1, max_repairs + 1):
164
+ repair_prompt = ACTION_REPAIR_PROMPT.format(
165
+ raw=last_raw,
166
+ errors="\n".join(last_exc.errors) or str(last_exc),
167
+ evidence=evidence,
168
+ )
169
+ started = time.perf_counter()
170
+ try:
171
+ last_raw = judge.generate(repair_prompt)
172
+ except Exception as exc: # noqa: BLE001
173
+ judge_calls.append({
174
+ "role": "agent_decision_repair", "operation": "generate",
175
+ "inputs": repair_prompt, "output": None, "error": str(exc),
176
+ "duration_sec": time.perf_counter() - started,
177
+ "metadata": {"attempt": attempt},
178
+ })
179
+ break
180
+ judge_calls.append({
181
+ "role": "agent_decision_repair", "operation": "generate",
182
+ "inputs": repair_prompt, "output": str(last_raw), "error": None,
183
+ "duration_sec": time.perf_counter() - started,
184
+ "metadata": {"attempt": attempt},
185
+ })
186
+ try:
187
+ action = parse_action(last_raw, registry)
188
+ action.repair_attempts = attempt
189
+ break
190
+ except ActionParseError as exc2:
191
+ last_exc = exc2
192
+ action = None
193
+ if action is None:
194
+ action = _fallback_action(board)
195
+ action.raw = raw
196
+
197
+ if run_logger is not None:
198
+ try:
199
+ log_kwargs = dict(
200
+ step=step,
201
+ action=action.tool,
202
+ params=action.params,
203
+ rationale=action.rationale,
204
+ valid=action.valid,
205
+ repair_attempts=action.repair_attempts,
206
+ fallback_used=not action.valid,
207
+ judge_prompt=prompt,
208
+ judge_raw=raw,
209
+ )
210
+ if getattr(run_logger, "preserve_full_model_io", False):
211
+ log_kwargs["judge_calls"] = judge_calls
212
+ run_logger.log_agent_decision(**log_kwargs)
213
+ except Exception: # noqa: BLE001 — logging must never break the loop
214
+ logger.warning("decide: log_agent_decision failed", exc_info=True)
215
+
216
+ return action
@@ -0,0 +1,107 @@
1
+ """The evidence board: everything the judge sees when deciding the next action.
2
+
3
+ The host reconstructs the full decision context from this board on every
4
+ turn — there is no CLI session state carried between judge calls, only what
5
+ is serialized here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+
15
+ @dataclass
16
+ class BudgetState:
17
+ """Guardrails on how long an agentic run may keep deciding actions."""
18
+
19
+ max_actions: int = 12
20
+ token_budget: int = 0
21
+ time_budget_sec: float = 0.0
22
+ actions_taken: int = 0
23
+ tokens_used: int = 0
24
+ _started_monotonic: float = 0.0
25
+
26
+ def start(self) -> None:
27
+ self._started_monotonic = time.monotonic()
28
+
29
+ def exhausted(self) -> str | None:
30
+ """Return which budget tripped (for ``stopped_by``), or ``None``."""
31
+ if self.max_actions > 0 and self.actions_taken >= self.max_actions:
32
+ return "max_actions"
33
+ if self.token_budget > 0 and self.tokens_used >= self.token_budget:
34
+ return "budget"
35
+ if (
36
+ self.time_budget_sec > 0
37
+ and self._started_monotonic > 0
38
+ and (time.monotonic() - self._started_monotonic) >= self.time_budget_sec
39
+ ):
40
+ return "time_budget"
41
+ return None
42
+
43
+
44
+ @dataclass
45
+ class EvidenceBoard:
46
+ """Accumulated evidence the judge reasons over, one decision turn at a time."""
47
+
48
+ protocol_summary: str = ""
49
+ data_summary: dict[str, Any] = field(default_factory=dict)
50
+ probe_findings: list[dict[str, Any]] = field(default_factory=list)
51
+ stats_findings: list[dict[str, Any]] = field(default_factory=list)
52
+ stats_confirmatory: bool = False
53
+ explore_takeaways: list[dict[str, Any]] = field(default_factory=list)
54
+ failure_modes: list[dict[str, Any]] = field(default_factory=list)
55
+ probe_search_findings: list[dict[str, Any]] = field(default_factory=list)
56
+ hypotheses: list[dict[str, Any]] = field(default_factory=list)
57
+ action_log: list[dict[str, Any]] = field(default_factory=list)
58
+ budget: BudgetState = field(default_factory=BudgetState)
59
+
60
+ def has_supported_hypothesis(self) -> bool:
61
+ return any(
62
+ h.get("status") == "supported" and h.get("is_consistent_with_protocol", True)
63
+ for h in self.hypotheses
64
+ )
65
+
66
+ def calls_made(self, tool: str) -> int:
67
+ return sum(1 for a in self.action_log if a.get("tool") == tool)
68
+
69
+ def to_prompt(self, registry: "Any") -> str:
70
+ """Render the board as the decision prompt's evidence section."""
71
+ lines: list[str] = []
72
+ if self.protocol_summary:
73
+ lines.append(f"PROTOCOL:\n{self.protocol_summary}\n")
74
+ if self.data_summary:
75
+ lines.append(f"DATA: {self.data_summary}\n")
76
+
77
+ lines.append(f"PROBE FINDINGS (M1): {self.probe_findings or 'none yet'}")
78
+ lines.append(
79
+ f"STATS FINDINGS (M2, confirmatory={self.stats_confirmatory}): "
80
+ f"{self.stats_findings or 'none yet'}"
81
+ )
82
+ if self.explore_takeaways:
83
+ lines.append(f"EXPLORE TAKEAWAYS: {self.explore_takeaways}")
84
+ if self.failure_modes:
85
+ lines.append(f"FAILURE MODES (clustered): {self.failure_modes}")
86
+ if self.probe_search_findings:
87
+ lines.append(f"PROBE SEARCH (newly synthesized failures): {self.probe_search_findings}")
88
+ lines.append(f"HYPOTHESES: {self.hypotheses or 'none yet'}")
89
+
90
+ if self.action_log:
91
+ lines.append("\nACTIONS TAKEN SO FAR:")
92
+ for a in self.action_log:
93
+ lines.append(
94
+ f" [{a['step']}] {a['tool']}({a.get('params', {})}) "
95
+ f"-> ok={a['ok']}: {a['summary']}"
96
+ )
97
+ else:
98
+ lines.append("\nACTIONS TAKEN SO FAR: none")
99
+
100
+ remaining = (
101
+ self.budget.max_actions - self.budget.actions_taken
102
+ if self.budget.max_actions > 0
103
+ else "unlimited"
104
+ )
105
+ lines.append(f"\nACTIONS REMAINING: {remaining}")
106
+ lines.append(f"\nAVAILABLE TOOLS:\n{registry.catalog_for_prompt(self)}")
107
+ return "\n".join(lines)
@@ -0,0 +1,190 @@
1
+ """AgenticDiagnoseLoop — an LLM-driven decision loop over the M1-M4 stages.
2
+
3
+ ``VLDiagnoseLoop`` runs M1->M2->M3->M4 in a hardcoded sequence every cycle.
4
+ This loop keeps every stage (and the confirm-split / post-loop run_m5/run_fix
5
+ discipline) exactly as-is, but replaces the fixed sequence with a CLI judge
6
+ that decides, one tool call at a time, what to do next — probe, run stats,
7
+ explore the raw data, propose hypotheses, test one, fix, or stop. The host,
8
+ not the judge, enforces call caps, tool preconditions, and the pre-registered
9
+ stopping discipline (no declaring success without a tested, supported,
10
+ protocol-consistent hypothesis).
11
+
12
+ ``VLDiagnoseLoop`` itself is untouched — this is a new mode, not a
13
+ replacement (see the module docstring in ``eval_agent/loop.py``).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from evalrx.eval_agent.agentic.actions import decide
22
+ from evalrx.eval_agent.agentic.board import BudgetState, EvidenceBoard
23
+ from evalrx.eval_agent.agentic.tools import ToolRegistry, _RunState, build_default_registry
24
+ from evalrx.eval_agent.loop import VLDiagnoseLoop
25
+ from evalrx.eval_agent.loop_reports import VLDiagnoseReport
26
+ from evalrx.eval_agent.run_metadata import _attach_run_logger, _data_provenance, _run_config
27
+
28
+ if TYPE_CHECKING:
29
+ from evalrx.core.case import CaseBatch
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ _MAX_INVALID_STREAK = 3
34
+
35
+
36
+ def _protocol_summary(protocol: "Any | None") -> str:
37
+ if protocol is None:
38
+ return ""
39
+ bits = [f"description: {getattr(protocol, 'description', '')}"]
40
+ domain = getattr(protocol, "task_domain", None)
41
+ if domain:
42
+ bits.append(f"task_domain: {domain}")
43
+ patterns = getattr(protocol, "failure_patterns", None)
44
+ if patterns:
45
+ bits.append(f"failure_patterns: {patterns}")
46
+ return "\n".join(bits)
47
+
48
+
49
+ class AgenticDiagnoseLoop(VLDiagnoseLoop):
50
+ """M1-M4 investigation driven by judge decisions instead of a fixed cycle.
51
+
52
+ Reuses :class:`~evalrx.eval_agent.loop.VLDiagnoseLoop`'s constructor,
53
+ stage helpers (``_do_m1``..``_do_m4``), confirm-split, and post-loop
54
+ ``run_m5``/``run_fix`` — only :meth:`run` differs.
55
+
56
+ Args:
57
+ judge: CLI-backed judge that decides the next action each
58
+ turn (e.g. ``ClaudeModel()`` / ``AgyModel()``).
59
+ Required — this is a genuinely new capability, not
60
+ a Gemini-fallback stage judge.
61
+ explorer: Optional ``ExploratoryAnalysisAgent`` — when given,
62
+ registers the ``explore_data`` tool.
63
+ max_actions: Hard cap on judge decision turns (default 12).
64
+ time_budget_sec: Wall-clock cap for the whole run (0 = unlimited).
65
+ registry_factory: Advanced override — ``(loop, run_state) -> ToolRegistry``
66
+ instead of :func:`build_default_registry`.
67
+ **kwargs: Forwarded to ``VLDiagnoseLoop.__init__`` (model,
68
+ protocol, probe_agent, stats_agent, hypothesis_tester,
69
+ surgery_agent, fix_agent, store, token_budget,
70
+ confirm_split, run_logger, ...).
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ model: "Any",
76
+ protocol: "Any",
77
+ *,
78
+ judge: "Any",
79
+ explorer: "Any | None" = None,
80
+ max_actions: int = 12,
81
+ time_budget_sec: float = 0.0,
82
+ registry_factory: "Any | None" = None,
83
+ **kwargs: Any,
84
+ ) -> None:
85
+ super().__init__(model, protocol, **kwargs)
86
+ if judge is None:
87
+ raise ValueError(
88
+ "AgenticDiagnoseLoop requires a decision judge, e.g. "
89
+ "judge=ClaudeModel() or judge=AgyModel()."
90
+ )
91
+ self.judge = judge
92
+ self.explorer = explorer
93
+ self.max_actions = max_actions
94
+ self.time_budget_sec = time_budget_sec
95
+ self._registry_factory = registry_factory
96
+
97
+ def run(self, data: "CaseBatch") -> VLDiagnoseReport:
98
+ """Drive the judge-decided action loop to a stop or a budget limit."""
99
+ self._tokens_used = 0
100
+ timings: dict[str, float] = {}
101
+
102
+ explore, confirm = self._split_explore_confirm(data)
103
+ original_data = data
104
+ if confirm is not None:
105
+ data = explore
106
+
107
+ _attach_run_logger(self.run_logger, self.probe_agent, self.stats_agent)
108
+ if self.run_logger is not None:
109
+ # _run_config records the M3 *stage* judge (loop.diagnosis_agent.judge),
110
+ # not the decision judge driving this loop's own tool-calling — add it
111
+ # explicitly so the dashboard header can show who is actually deciding.
112
+ cfg = _run_config(self, data, loop_name="AgenticDiagnoseLoop")
113
+ cfg["decision_judge"] = repr(self.judge)
114
+ cfg["max_actions"] = self.max_actions
115
+ self.run_logger.log_run_start(cfg)
116
+ self.run_logger.log_cases(data)
117
+
118
+ board = EvidenceBoard(
119
+ protocol_summary=_protocol_summary(self.protocol),
120
+ data_summary=_data_provenance(data),
121
+ budget=BudgetState(
122
+ max_actions=self.max_actions,
123
+ token_budget=self.token_budget,
124
+ time_budget_sec=self.time_budget_sec,
125
+ ),
126
+ )
127
+ board.budget.start()
128
+
129
+ state = _RunState(original_data=original_data, data=data, timings=timings)
130
+ registry: ToolRegistry = (
131
+ self._registry_factory(self, state)
132
+ if self._registry_factory is not None
133
+ else build_default_registry(self, state)
134
+ )
135
+
136
+ stopped_by = "max_actions"
137
+ invalid_streak = 0
138
+ step = 0
139
+ while True:
140
+ reason = board.budget.exhausted()
141
+ if reason:
142
+ stopped_by = reason
143
+ break
144
+
145
+ action = decide(self.judge, board, registry, run_logger=self.run_logger, step=step)
146
+ board.budget.actions_taken += 1
147
+ invalid_streak = 0 if action.valid else invalid_streak + 1
148
+
149
+ outcome = registry.dispatch(action, board)
150
+ board.action_log.append({
151
+ "step": step, "tool": action.tool, "params": action.params,
152
+ "ok": outcome.ok, "summary": outcome.summary,
153
+ })
154
+ if self.run_logger is not None:
155
+ self.run_logger.log_agent_tool(
156
+ step, tool=action.tool, ok=outcome.ok,
157
+ summary=outcome.summary, error=outcome.error,
158
+ )
159
+
160
+ if action.tool == "stop" and outcome.ok:
161
+ stopped_by = "agent_stop"
162
+ step += 1
163
+ break
164
+
165
+ if invalid_streak >= _MAX_INVALID_STREAK:
166
+ logger.warning(
167
+ "AgenticDiagnoseLoop: %d consecutive invalid actions — stopping.",
168
+ invalid_streak,
169
+ )
170
+ stopped_by = "invalid_actions"
171
+ step += 1
172
+ break
173
+
174
+ step += 1
175
+
176
+ verified = self.hypothesis_tester.best_hypotheses(state.all_test_results)
177
+ report = VLDiagnoseReport(
178
+ cycles=step,
179
+ resolved=bool(verified),
180
+ stopped_by=stopped_by,
181
+ verified_hypotheses=verified,
182
+ final_hypotheses=state.all_hypotheses,
183
+ all_test_results=state.all_test_results,
184
+ final_stats_report=state.stats_report,
185
+ store=self.store,
186
+ _run_id=self._run_id,
187
+ )
188
+ if self.run_logger:
189
+ self.run_logger.log_loop_end(report, tokens_used=self._tokens_used, timings=timings)
190
+ return report