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,25 @@
1
+ """Agent failure analyzers — operate on a FailureCase's Trajectory.
2
+
3
+ Build order (cheapest/most reliable first): deterministic heuristics
4
+ (loop_detect, ignored_obs) → LLM-judge (first_error_judge, trajectory_rubric)
5
+ → intervention probes that RE-RUN the agent (counterfactual = perturb a step,
6
+ reliability_probe = pass@k over k reps, tool_shap = Shapley over tool subsets).
7
+ """
8
+
9
+ from evalrx.analyzers.agent.counterfactual import CounterfactualReplay
10
+ from evalrx.analyzers.agent.first_error_judge import FirstErrorJudge
11
+ from evalrx.analyzers.agent.ignored_obs import IgnoredObservationDetector
12
+ from evalrx.analyzers.agent.loop_detect import LoopDetector
13
+ from evalrx.analyzers.agent.reliability import ReliabilityProbe
14
+ from evalrx.analyzers.agent.tool_shap import ToolShap
15
+ from evalrx.analyzers.agent.trajectory_rubric import TrajectoryRubricJudge
16
+
17
+ __all__ = [
18
+ "LoopDetector",
19
+ "IgnoredObservationDetector",
20
+ "FirstErrorJudge",
21
+ "TrajectoryRubricJudge",
22
+ "CounterfactualReplay",
23
+ "ReliabilityProbe",
24
+ "ToolShap",
25
+ ]
@@ -0,0 +1,84 @@
1
+ """Counterfactual replay — causal step attribution for agent trajectories.
2
+
3
+ For each decision step, re-run the trajectory forked at that step and see whether
4
+ the outcome flips: a high flip-rate ⇒ the step is causally influential. The actual
5
+ re-run is INJECTED as ``rerun_fn(trajectory, step_idx, seed) -> bool`` (it wraps a
6
+ live Agent + verifier), keeping this analyzer decoupled and testable.
7
+
8
+ Caveat (built into the design): with temperature > 0, run ``n_replays`` per step
9
+ and read the flip-RATE — a single replay conflates "this step was causal" with
10
+ model noise. Prefer temperature=0 replay or a larger ``n_replays``.
11
+
12
+ Causal framework:
13
+ "Causality: Models, Reasoning and Inference"
14
+ Pearl (2000/2009), Cambridge University Press — do-calculus, interventions
15
+
16
+ Applied to neural NLP (causal mediation / interchange intervention):
17
+ "Causal Mediation Analysis for Interpreting Neural NLP: The Case of Gender Bias"
18
+ Vig et al., 2020 — https://arxiv.org/abs/2004.12265
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import TYPE_CHECKING, Callable
24
+
25
+ from evalrx.core.analyzer import Analyzer
26
+ from evalrx.core.capability import Capability
27
+ from evalrx.core.case import Label
28
+ from evalrx.core.registry import register_analyzer
29
+ from evalrx.core.result import Result
30
+
31
+ if TYPE_CHECKING:
32
+ from evalrx.core.case import CaseBatch
33
+ from evalrx.core.model import Model
34
+
35
+
36
+ @register_analyzer("counterfactual")
37
+ class CounterfactualReplay(Analyzer):
38
+ """Rank a trajectory's tool-call steps by how often re-running flips the outcome."""
39
+
40
+ name = "counterfactual"
41
+ requires = frozenset({Capability.TOOL_CALLS})
42
+ #: TOOL_CALLS says the model CAN call tools; this says the cases ARE agent
43
+ #: runs. A chat model whose template renders tools declares the former.
44
+ requires_trajectories = True
45
+ applies_to_modalities = frozenset({"text", "image"}) # trajectory analysis is modality-agnostic
46
+
47
+ def __init__(self, rerun_fn: Callable, n_replays: int = 3) -> None:
48
+ super().__init__(rerun_fn=rerun_fn, n_replays=n_replays)
49
+ self.rerun_fn = rerun_fn
50
+
51
+ def _run(self, model: "Model", cases: "CaseBatch") -> Result:
52
+ per_case = []
53
+ for case in cases:
54
+ traj = case.trajectory
55
+ if traj is None:
56
+ continue
57
+ original_success = traj.outcome == Label.PASS
58
+ candidates = [s for s in traj.steps if s.tool_call]
59
+ step_scores = []
60
+ for s in candidates:
61
+ flips = sum(
62
+ 1 for seed in range(self.n_replays)
63
+ if bool(self.rerun_fn(traj, s.idx, seed)) != original_success
64
+ )
65
+ step_scores.append({
66
+ "step": s.idx,
67
+ "action": s.tool_call.get("name"),
68
+ "flip_rate": round(flips / self.n_replays, 4) if self.n_replays else 0.0,
69
+ })
70
+ step_scores.sort(key=lambda d: -d["flip_rate"])
71
+ per_case.append({
72
+ "sample_id": traj.sample_id,
73
+ "original_success": original_success,
74
+ "most_influential_step": step_scores[0] if step_scores else None,
75
+ "steps": step_scores,
76
+ })
77
+ return Result(
78
+ analyzer=self.name, model=repr(model), cases=cases,
79
+ findings={
80
+ "n_trajectories": len(per_case),
81
+ "per_case": per_case,
82
+ "_caveat": "flip-rate ≈ causal influence; use temperature=0 or larger n_replays to separate from noise.",
83
+ },
84
+ )
@@ -0,0 +1,96 @@
1
+ """First-error attribution — LLM-as-judge (the Who&When 'all-at-once' protocol).
2
+
3
+ Given a trajectory, ask a JUDGE model which step first introduced the error.
4
+ The judge is a separate handle (judge != system-under-test, to avoid
5
+ self-preference bias). Writes ``is_first_error`` onto the implicated step.
6
+
7
+ Reliability caveats (bake into any reporting): LLM judges show position/length
8
+ bias and self-preference; calibrate against a human-gold subset and report
9
+ step-accuracy/F1. As reported by Zhang et al. (Who&When), even strong judges
10
+ reach only ~14% step-level accuracy — treat outputs as weak signal, not truth.
11
+
12
+ References:
13
+ - Which Agent Causes Task Failures and When? Automated Failure Attribution (Who&When)
14
+ Zhang et al., ICML 2025 — arXiv:2505.00212
15
+ - Failure-mode vocabulary — MAST, Cemri et al., 2025 — arXiv:2503.13657
16
+ - LLM-as-judge biases — Judging LLM-as-a-Judge, Zheng et al., NeurIPS 2023 — arXiv:2306.05685
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from typing import TYPE_CHECKING, Optional
23
+
24
+ from evalrx.core.analyzer import Analyzer
25
+ from evalrx.core.registry import register_analyzer
26
+ from evalrx.core.result import Result
27
+
28
+ if TYPE_CHECKING:
29
+ from evalrx.core.case import CaseBatch, Trajectory
30
+ from evalrx.core.model import Model
31
+
32
+ _PROMPT = """You are debugging an AI agent trajectory. The agent failed to achieve the goal.
33
+ Goal: {goal}
34
+
35
+ Steps:
36
+ {steps}
37
+
38
+ Which step index FIRST introduced the error that led to failure?
39
+ Answer with exactly one line: "STEP: <index>" (use -1 if no step is at fault)."""
40
+
41
+
42
+ def _render(traj: "Trajectory") -> str:
43
+ lines = []
44
+ for s in traj.steps:
45
+ piece = f"[{s.idx}] {s.role.value}"
46
+ if s.tool_call:
47
+ piece += f" calls {s.tool_call.get('name')}({s.tool_call.get('args')})"
48
+ if s.observation is not None:
49
+ piece += f" -> obs: {str(s.observation)[:200]}"
50
+ if s.content:
51
+ piece += f" : {str(s.content)[:200]}"
52
+ lines.append(piece)
53
+ return "\n".join(lines)
54
+
55
+
56
+ @register_analyzer("first_error_judge")
57
+ class FirstErrorJudge(Analyzer):
58
+ """Attribute the first-erroneous step via an LLM judge (all-at-once protocol)."""
59
+
60
+ name = "first_error_judge"
61
+ requires = frozenset() # the analysed model is irrelevant; the judge does the work
62
+ #: Reads agent runs. The model is not what makes this applicable —
63
+ #: the DATA is, which is why `requires` stays empty (these run on
64
+ #: trajectories loaded from disk, with no model at all).
65
+ requires_trajectories = True
66
+ applies_to_modalities = frozenset({"text", "image"}) # trajectory analysis is modality-agnostic
67
+
68
+ def __init__(self, judge: Optional["Model"] = None) -> None:
69
+ super().__init__(judge=judge)
70
+
71
+ def _run(self, model: "Model", cases: "CaseBatch") -> Result:
72
+ judge = self.judge if self.judge is not None else model
73
+ if judge is None or not hasattr(judge, "generate"):
74
+ raise ValueError("FirstErrorJudge needs a judge model with generate(); pass judge=...")
75
+ per_case = []
76
+ for case in cases:
77
+ traj = case.trajectory
78
+ if traj is None:
79
+ continue
80
+ raw = judge.generate(_PROMPT.format(goal=traj.goal, steps=_render(traj)))
81
+ m = re.search(r"STEP:\s*(-?\d+)", str(raw))
82
+ idx = int(m.group(1)) if m else -1
83
+ if 0 <= idx < len(traj.steps):
84
+ traj.steps[idx].is_first_error = True
85
+ per_case.append({"sample_id": traj.sample_id, "first_error_step": idx, "judge_raw": str(raw)[:200]})
86
+ return Result(
87
+ analyzer=self.name,
88
+ model=repr(model),
89
+ cases=cases,
90
+ findings={
91
+ "n_trajectories": len(per_case),
92
+ "judge": repr(judge),
93
+ "per_case": per_case,
94
+ "_caveat": "LLM-judge: position/length/self-preference bias; calibrate vs human gold (Who&When).",
95
+ },
96
+ )
@@ -0,0 +1,81 @@
1
+ """Ignored-observation detection — deterministic trajectory heuristic.
2
+
3
+ Flags the pattern: a tool returns an error/empty signal, yet the next action
4
+ repeats the *same* tool call unchanged (the agent didn't adapt to the
5
+ observation). MAST "ignored tool output" / failure-to-recover mode.
6
+
7
+ References:
8
+ - Failure-mode taxonomy — Why Do Multi-Agent LLM Systems Fail? (MAST)
9
+ Cemri et al., 2025 — arXiv:2503.13657 (ignored-tool-output / fail-to-recover modes)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from typing import TYPE_CHECKING
16
+
17
+ from evalrx.core.analyzer import Analyzer
18
+ from evalrx.core.case import StepRole
19
+ from evalrx.core.registry import register_analyzer
20
+ from evalrx.core.result import Result
21
+
22
+ if TYPE_CHECKING:
23
+ from evalrx.core.case import CaseBatch
24
+ from evalrx.core.model import Model
25
+
26
+
27
+ @register_analyzer("ignored_obs")
28
+ class IgnoredObservationDetector(Analyzer):
29
+ """Detect error observations that the agent then ignored (repeated the same call)."""
30
+
31
+ name = "ignored_obs"
32
+ requires = frozenset()
33
+ #: Reads agent runs. The model is not what makes this applicable —
34
+ #: the DATA is, which is why `requires` stays empty (these run on
35
+ #: trajectories loaded from disk, with no model at all).
36
+ requires_trajectories = True
37
+ applies_to_modalities = frozenset({"text", "image"}) # trajectory analysis is modality-agnostic
38
+
39
+ DEFAULT_MARKERS = ("error", "not found", "failed", "no result", "none", "invalid", "empty", "exception")
40
+
41
+ def __init__(self, markers: tuple[str, ...] | None = None) -> None:
42
+ super().__init__(markers=markers or self.DEFAULT_MARKERS)
43
+
44
+ def _is_error(self, observation) -> bool:
45
+ text = str(observation).lower()
46
+ return any(m in text for m in self.markers)
47
+
48
+ @staticmethod
49
+ def _sig(tool_call: dict | None) -> str | None:
50
+ if not tool_call:
51
+ return None
52
+ return json.dumps([tool_call.get("name"), tool_call.get("args", {})], sort_keys=True, default=str)
53
+
54
+ def _run(self, model: "Model", cases: "CaseBatch") -> Result:
55
+ per_case = []
56
+ for case in cases:
57
+ traj = case.trajectory
58
+ if traj is None:
59
+ continue
60
+ steps = traj.steps
61
+ flags = []
62
+ for i, s in enumerate(steps):
63
+ if s.role is not StepRole.TOOL or s.observation is None or not self._is_error(s.observation):
64
+ continue
65
+ # the actor that produced this observation is the previous step;
66
+ # the next actor that repeats the same call ignored the error.
67
+ prev_sig = self._sig(steps[i - 1].tool_call) if i > 0 else None
68
+ nxt = next((x for x in steps[i + 1:] if x.role is StepRole.ACTOR), None)
69
+ if nxt is not None and prev_sig is not None and self._sig(nxt.tool_call) == prev_sig:
70
+ flags.append({"error_obs_step": s.idx, "repeated_at_step": nxt.idx})
71
+ per_case.append({"sample_id": traj.sample_id, "n_ignored": len(flags), "ignored": flags})
72
+ return Result(
73
+ analyzer=self.name,
74
+ model=repr(model),
75
+ cases=cases,
76
+ findings={
77
+ "n_trajectories": len(per_case),
78
+ "n_with_ignored_obs": sum(1 for c in per_case if c["n_ignored"]),
79
+ "per_case": per_case,
80
+ },
81
+ )
@@ -0,0 +1,79 @@
1
+ """Loop detection — a deterministic trajectory heuristic (no model, no LLM).
2
+
3
+ Flags repeated ``(tool, normalised args)`` actions in an agent trajectory — the
4
+ MAST "step repetition" failure mode. Cheap, deterministic, and the highest-ROI
5
+ agent signal to compute first.
6
+
7
+ References:
8
+ - Failure-mode taxonomy — Why Do Multi-Agent LLM Systems Fail? (MAST)
9
+ Cemri et al., 2025 — arXiv:2503.13657 (step-repetition mode)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from collections import Counter
16
+ from typing import TYPE_CHECKING
17
+
18
+ from evalrx.core.analyzer import Analyzer
19
+ from evalrx.core.registry import register_analyzer
20
+ from evalrx.core.result import Result
21
+
22
+ if TYPE_CHECKING:
23
+ from evalrx.core.case import CaseBatch
24
+ from evalrx.core.model import Model
25
+
26
+
27
+ def _sig(tool_call: dict) -> tuple[str, str]:
28
+ return (tool_call.get("name", ""), json.dumps(tool_call.get("args", {}), sort_keys=True, default=str))
29
+
30
+
31
+ @register_analyzer("loop_detect")
32
+ class LoopDetector(Analyzer):
33
+ """Detect repeated tool actions in a trajectory.
34
+
35
+ Hyper-parameters:
36
+ min_repeats: how many times an identical action must occur to count as a loop.
37
+ """
38
+
39
+ name = "loop_detect"
40
+ requires = frozenset() # pure heuristic over Trajectory — no model needed
41
+ #: Reads agent runs. The model is not what makes this applicable —
42
+ #: the DATA is, which is why `requires` stays empty (these run on
43
+ #: trajectories loaded from disk, with no model at all).
44
+ requires_trajectories = True
45
+ applies_to_modalities = frozenset({"text", "image"}) # trajectory analysis is modality-agnostic
46
+
47
+ def __init__(self, min_repeats: int = 2) -> None:
48
+ super().__init__(min_repeats=min_repeats)
49
+
50
+ def _run(self, model: "Model", cases: "CaseBatch") -> Result:
51
+ per_case = []
52
+ for case in cases:
53
+ traj = case.trajectory
54
+ if traj is None:
55
+ continue
56
+ sigs = [_sig(s.tool_call) for s in traj.steps if s.tool_call]
57
+ counts = Counter(sigs)
58
+ loops = [
59
+ {"action": name, "args": args, "count": n}
60
+ for (name, args), n in counts.items()
61
+ if n >= self.min_repeats
62
+ ]
63
+ consecutive = any(sigs[i] == sigs[i + 1] for i in range(len(sigs) - 1))
64
+ per_case.append({
65
+ "sample_id": traj.sample_id,
66
+ "has_loop": bool(loops),
67
+ "consecutive_repeat": consecutive,
68
+ "loops": loops,
69
+ })
70
+ return Result(
71
+ analyzer=self.name,
72
+ model=repr(model),
73
+ cases=cases,
74
+ findings={
75
+ "n_trajectories": len(per_case),
76
+ "n_with_loops": sum(c["has_loop"] for c in per_case),
77
+ "per_case": per_case,
78
+ },
79
+ )
@@ -0,0 +1,165 @@
1
+ """Reliability probe — pass@k / pass^k over repeated agent runs.
2
+
3
+ A single run hides the capability/stability split: "reliably fails" and
4
+ "flaky" are different diagnoses with different fixes (prompt/tool surgery vs
5
+ decoding/consistency scaffolds). This probe re-runs each case *k* times
6
+ through an injected runner and reports, per case, the pass@k family plus
7
+ answer- and trajectory-consistency measures.
8
+
9
+ Reference: Pass@k (capability: at least one of k passes) vs Pass^k
10
+ (reliability: all k pass) as popularised by agent-eval harnesses.
11
+
12
+ The runner is INJECTED — the analyzer never builds an agent itself::
13
+
14
+ def runs_fn(case, k):
15
+ # e.g. a temperature>0 endpoint, or per-rep seeds; use run_batch for concurrency
16
+ return [Agent(handle, tools_factory(case), system=SYS).run(case) for _ in range(k)]
17
+
18
+ probe = ReliabilityProbe(runs_fn=runs_fn, k=5)
19
+ result = probe.run(model, cases)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ import statistics
26
+ from collections import Counter
27
+ from typing import TYPE_CHECKING, Any, Callable, Optional
28
+
29
+ from evalrx.core.analyzer import Analyzer
30
+ from evalrx.core.registry import register_analyzer
31
+ from evalrx.core.result import Result
32
+
33
+ if TYPE_CHECKING:
34
+ from evalrx.core.case import CaseBatch, FailureCase, Trajectory
35
+ from evalrx.core.model import Model
36
+
37
+ _YESNO = re.compile(r"\b(yes|no)\b")
38
+
39
+
40
+ def default_grader(trajectory: "Trajectory", case: "FailureCase") -> Optional[bool]:
41
+ """Grade one run against ``case.expected`` (``None`` = ungradable).
42
+
43
+ yes/no expectations match the FIRST yes/no token in the answer; other
44
+ expectations match as a case-insensitive substring. Inject a task-specific
45
+ grader for anything richer.
46
+ """
47
+ expected = case.expected
48
+ if expected is None or trajectory.final_answer is None:
49
+ return None
50
+ answer = str(trajectory.final_answer).strip().lower()
51
+ exp = str(expected).strip().lower()
52
+ if exp in ("yes", "no"):
53
+ m = _YESNO.search(answer)
54
+ return bool(m and m.group(1) == exp)
55
+ return exp in answer
56
+
57
+
58
+ def _normalized_answer(trajectory: "Trajectory") -> str:
59
+ return " ".join(str(trajectory.final_answer or "").lower().split())[:200]
60
+
61
+
62
+ def _tool_sequence(trajectory: "Trajectory") -> tuple:
63
+ return tuple(s.tool_call.get("name", "") for s in trajectory.steps if s.tool_call)
64
+
65
+
66
+ @register_analyzer("reliability_probe")
67
+ class ReliabilityProbe(Analyzer):
68
+ """Re-run each case k times and measure pass@k / pass^k / consistency.
69
+
70
+ Hyper-parameters:
71
+ runs_fn: ``callable(case, k) -> list[Trajectory]`` — REQUIRED. The
72
+ caller owns sampling variation (temperature/seeds) and
73
+ concurrency; deterministic runners make every rep identical
74
+ and the probe degenerates (it will say so in the caveat).
75
+ k: repetitions per case.
76
+ grader: ``callable(trajectory, case) -> bool | None``; defaults to
77
+ :func:`default_grader`.
78
+ max_cases: label-balanced cap on probed cases (k runs each is costly).
79
+ """
80
+
81
+ name = "reliability_probe"
82
+ requires = frozenset() # the injected runner encapsulates the model
83
+ applies_to_modalities = frozenset({"text", "image"})
84
+
85
+ def __init__(
86
+ self,
87
+ runs_fn: Callable[["FailureCase", int], "list[Trajectory]"],
88
+ k: int = 5,
89
+ grader: Optional[Callable[["Trajectory", "FailureCase"], Optional[bool]]] = None,
90
+ max_cases: Optional[int] = None,
91
+ ) -> None:
92
+ super().__init__(k=k, max_cases=max_cases)
93
+ # stored under the ctor names so sklearn-style get_params() reflection works
94
+ self.runs_fn = runs_fn
95
+ self.grader = grader or default_grader
96
+
97
+ def _run(self, model: "Model", cases: "CaseBatch") -> Result:
98
+ selected = cases.stratified_head(self.max_cases) if self.max_cases else list(cases)
99
+ per_case: list[dict[str, Any]] = []
100
+ for case in selected:
101
+ trajectories = list(self.runs_fn(case, self.k) or [])
102
+ if not trajectories:
103
+ continue
104
+ n_runs = len(trajectories)
105
+ grades = [self.grader(t, case) for t in trajectories]
106
+ graded = [g for g in grades if g is not None]
107
+
108
+ answers = Counter(_normalized_answer(t) for t in trajectories)
109
+ agreement = answers.most_common(1)[0][1] / n_runs
110
+ sequences = Counter(_tool_sequence(t) for t in trajectories)
111
+ calls = [
112
+ int(t.metrics.get("n_tool_calls", sum(1 for s in t.steps if s.tool_call)))
113
+ for t in trajectories
114
+ ]
115
+
116
+ entry: dict[str, Any] = {
117
+ "sample_id": case.id,
118
+ "n_runs": n_runs,
119
+ "answer_agreement": round(agreement, 4),
120
+ "answer_consistent": 1 if len(answers) == 1 else 0,
121
+ "tool_seq_diversity": round(len(sequences) / n_runs, 4),
122
+ "n_tool_calls_mean": round(statistics.fmean(calls), 3) if calls else 0.0,
123
+ "n_tool_calls_std": round(statistics.pstdev(calls), 3) if len(calls) > 1 else 0.0,
124
+ }
125
+ if graded:
126
+ n_pass = sum(graded)
127
+ entry.update(
128
+ {
129
+ "n_graded": len(graded),
130
+ "n_pass": n_pass,
131
+ "success_rate": round(n_pass / len(graded), 4),
132
+ "pass_at_k": 1 if n_pass > 0 else 0,
133
+ "pass_all_k": 1 if n_pass == len(graded) else 0,
134
+ "flaky": 1 if 0 < n_pass < len(graded) else 0,
135
+ }
136
+ )
137
+ per_case.append(entry)
138
+
139
+ rates = [c["success_rate"] for c in per_case if "success_rate" in c]
140
+ all_consistent = per_case and all(c["answer_consistent"] == 1 for c in per_case)
141
+ findings: dict[str, Any] = {
142
+ "n_trajectories": len(per_case),
143
+ "k": self.k,
144
+ "n_graded_cases": len(rates),
145
+ "mean_success_rate": round(statistics.fmean(rates), 4) if rates else None,
146
+ "frac_flaky": (
147
+ round(sum(c.get("flaky", 0) for c in per_case) / len(rates), 4) if rates else None
148
+ ),
149
+ "per_case": per_case,
150
+ "_caveat": (
151
+ "Ungraded cases (no expected answer) report consistency only. "
152
+ + (
153
+ "Every case produced k identical answers — the runner looks "
154
+ "deterministic; use temperature>0 or per-rep seeds for a "
155
+ "meaningful reliability read. "
156
+ if all_consistent
157
+ else ""
158
+ )
159
+ + "pass@k measures capability, pass^k reliability; the gap is the "
160
+ "instability mass. These are INTERVENTIONAL columns: held-out "
161
+ "verification must RE-RUN the k repetitions on the held-out "
162
+ "cases — never reuse exploration-set values."
163
+ ),
164
+ }
165
+ return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)