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,89 @@
1
+ """Docker worker — runs inside the analyzer container.
2
+
3
+ Invoked as ``python -m evalrx.agent_runtime._docker_runner``.
4
+
5
+ Reads a JSON payload from stdin::
6
+
7
+ {
8
+ "analyzer": "self_consistency",
9
+ "params": {"n": 3},
10
+ "cases": [{"id": "...", "prompt": "...", "label": null, "metadata": {}}],
11
+ "model_env": "GEMINI_API_KEY"
12
+ }
13
+
14
+ Writes the analyzer findings as JSON to stdout::
15
+
16
+ {"findings": {...}}
17
+
18
+ The containerised model is constructed from the ``model_env`` key: the value
19
+ names the environment variable that holds the API key. Currently supports
20
+ Gemini (``GEMINI_API_KEY``) and OpenAI-compatible (``OPENAI_API_KEY``) keys.
21
+ An unrecognised or missing key produces a ``{"error": "..."}`` response.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import sys
29
+
30
+
31
+ def _build_model(model_env: str):
32
+ key = os.getenv(model_env, "")
33
+ if not key:
34
+ raise ValueError(
35
+ f"Environment variable '{model_env}' is not set inside the container."
36
+ )
37
+ if "GEMINI" in model_env.upper():
38
+ from evalrx.models.blackbox.gemini import GeminiModel
39
+
40
+ return GeminiModel(api_key=key)
41
+ raise ValueError(
42
+ f"No model factory for env var '{model_env}'. "
43
+ "Add a case to evalrx/agent_runtime/_docker_runner.py."
44
+ )
45
+
46
+
47
+ def _build_cases(raw_cases: list[dict]):
48
+ from evalrx.core.case import CaseBatch, FailureCase, Inputs, Label
49
+
50
+ cases = []
51
+ for rec in raw_cases:
52
+ label = None
53
+ raw_label = rec.get("label")
54
+ if raw_label is not None:
55
+ try:
56
+ label = Label(raw_label)
57
+ except ValueError:
58
+ pass
59
+ cases.append(
60
+ FailureCase(
61
+ inputs=Inputs(prompt=rec.get("prompt", "")),
62
+ label=label,
63
+ metadata=rec.get("metadata", {}),
64
+ )
65
+ )
66
+ return CaseBatch(cases)
67
+
68
+
69
+ def main() -> None:
70
+ payload = json.loads(sys.stdin.read())
71
+ try:
72
+ model = _build_model(payload.get("model_env", "GEMINI_API_KEY"))
73
+ cases = _build_cases(payload.get("cases", []))
74
+ analyzer_name = payload["analyzer"]
75
+ params = payload.get("params", {})
76
+
77
+ from evalrx.core.registry import registry
78
+
79
+ cls = registry.analyzers.get(analyzer_name)
80
+ analyzer = cls(**params)
81
+ result = analyzer.run(model, cases)
82
+ print(json.dumps({"findings": result.findings}, default=str))
83
+ except Exception as exc:
84
+ print(json.dumps({"error": str(exc), "findings": {}}))
85
+ sys.exit(1)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,103 @@
1
+ """Runtime primitives for CLI coding-agent adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import signal
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ProcessRun:
16
+ """Completed subprocess execution."""
17
+
18
+ returncode: int
19
+ stdout: str
20
+ stderr: str
21
+ elapsed_sec: float
22
+ timed_out: bool = False
23
+
24
+
25
+ def to_text(value: str | bytes | None) -> str:
26
+ if value is None:
27
+ return ""
28
+ if isinstance(value, bytes):
29
+ return value.decode("utf-8", errors="replace")
30
+ return value
31
+
32
+
33
+ def collect_py_files(workdir: Path) -> dict[str, str]:
34
+ """Read top-level Python files produced by a coding agent."""
35
+ files: dict[str, str] = {}
36
+ for pyfile in sorted(workdir.glob("*.py")):
37
+ if pyfile.name.startswith(("_codex_", "_agent_")):
38
+ continue
39
+ try:
40
+ files[pyfile.name] = pyfile.read_text(encoding="utf-8")
41
+ except OSError:
42
+ pass
43
+ return files
44
+
45
+
46
+ class SubprocessRunner:
47
+ """Run CLI agents in a workdir with timeout and environment normalization."""
48
+
49
+ def run(self, cmd: list[str], workdir: Path, timeout_sec: int) -> ProcessRun:
50
+ workdir.mkdir(parents=True, exist_ok=True)
51
+ start = time.monotonic()
52
+ timed_out = False
53
+
54
+ env = {**os.environ}
55
+ bindir = os.path.dirname(sys.executable)
56
+ if bindir:
57
+ env["PATH"] = bindir + os.pathsep + env.get("PATH", "")
58
+
59
+ proc = subprocess.Popen(
60
+ cmd,
61
+ # ``codex exec`` accepts a one-shot prompt on argv but keeps
62
+ # reading stdin for a possible follow-up turn. The runner has no
63
+ # interactive continuation protocol, so leave stdin at EOF. If
64
+ # inherited from the parent it remains open and Codex idles until
65
+ # the wall-clock timeout, withholding its generated artifacts.
66
+ stdin=subprocess.DEVNULL,
67
+ stdout=subprocess.PIPE,
68
+ stderr=subprocess.PIPE,
69
+ cwd=workdir,
70
+ env=env,
71
+ start_new_session=True,
72
+ )
73
+
74
+ try:
75
+ stdout_bytes, stderr_bytes = proc.communicate(timeout=timeout_sec)
76
+ except subprocess.TimeoutExpired:
77
+ timed_out = True
78
+ try:
79
+ os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
80
+ except OSError:
81
+ pass
82
+ try:
83
+ stdout_bytes, stderr_bytes = proc.communicate(timeout=5)
84
+ except subprocess.TimeoutExpired:
85
+ try:
86
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
87
+ except OSError:
88
+ pass
89
+ stdout_bytes, stderr_bytes = b"", b""
90
+ try:
91
+ proc.communicate(timeout=5)
92
+ except Exception: # noqa: BLE001
93
+ pass
94
+
95
+ elapsed = time.monotonic() - start
96
+ rc = proc.returncode if proc.returncode is not None else -1
97
+ return ProcessRun(
98
+ returncode=rc,
99
+ stdout=to_text(stdout_bytes),
100
+ stderr=to_text(stderr_bytes),
101
+ elapsed_sec=elapsed,
102
+ timed_out=timed_out,
103
+ )
@@ -0,0 +1,138 @@
1
+ """Transcript rendering for CLI coding-agent event streams."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ RAW_OUTPUT_CAP = 3000
8
+ STREAM_TEXT_CAP = 4000
9
+ STREAM_TOOL_INPUT_CAP = 2000
10
+ STREAM_TOOL_RESULT_CAP = 2000
11
+ STREAM_FALLBACK_CAP = 8000
12
+
13
+
14
+ def trunc(text: str, cap: int) -> str:
15
+ text = text or ""
16
+ if len(text) <= cap:
17
+ return text
18
+ return text[:cap] + f"\n... [truncated {len(text) - cap} chars]"
19
+
20
+
21
+ def _blocks_text(content: object) -> str:
22
+ if isinstance(content, str):
23
+ return content
24
+ if isinstance(content, list):
25
+ out: list[str] = []
26
+ for block in content:
27
+ if isinstance(block, dict) and block.get("type") == "text":
28
+ out.append(str(block.get("text", "")))
29
+ elif isinstance(block, str):
30
+ out.append(block)
31
+ return "\n".join(out)
32
+ return ""
33
+
34
+
35
+ def _summarise_tool_use(name: str, inp: object) -> str:
36
+ if not isinstance(inp, dict):
37
+ return trunc(str(inp), STREAM_TOOL_INPUT_CAP)
38
+ if name == "Bash":
39
+ return trunc("$ " + str(inp.get("command", "")), STREAM_TOOL_INPUT_CAP)
40
+ if name == "Write":
41
+ content_len = len(str(inp.get("content", "")))
42
+ return f"write {inp.get('file_path', '?')} ({content_len} chars)"
43
+ if name == "Edit":
44
+ return f"edit {inp.get('file_path', '?')}"
45
+ if name == "Read":
46
+ span = ""
47
+ if inp.get("offset") is not None or inp.get("limit") is not None:
48
+ span = f" [offset={inp.get('offset')}, limit={inp.get('limit')}]"
49
+ return f"read {inp.get('file_path', '?')}{span}"
50
+ try:
51
+ return trunc(json.dumps(inp, default=str), STREAM_TOOL_INPUT_CAP)
52
+ except (TypeError, ValueError):
53
+ return trunc(str(inp), STREAM_TOOL_INPUT_CAP)
54
+
55
+
56
+ def render_claude_stream(stdout: str) -> tuple[str, dict | None]:
57
+ """Render ``claude -p --output-format stream-json`` into a coding trajectory."""
58
+ events: list[dict] = []
59
+ for line in stdout.splitlines():
60
+ line = line.strip()
61
+ if not line:
62
+ continue
63
+ try:
64
+ event = json.loads(line)
65
+ except (ValueError, TypeError):
66
+ continue
67
+ if isinstance(event, dict):
68
+ events.append(event)
69
+ if not events:
70
+ return trunc(stdout, STREAM_FALLBACK_CAP), None
71
+
72
+ out: list[str] = []
73
+ usage: dict | None = None
74
+ tool_seq: dict[str, int] = {}
75
+ step = 0
76
+
77
+ for event in events:
78
+ etype = event.get("type")
79
+ if etype == "system" and event.get("subtype") == "init":
80
+ tools = event.get("tools") or []
81
+ out.append(
82
+ f"=== session start | model={event.get('model', '?')} | "
83
+ f"tools={','.join(map(str, tools)) if tools else '-'} ==="
84
+ )
85
+ elif etype == "assistant":
86
+ for block in (event.get("message") or {}).get("content") or []:
87
+ if not isinstance(block, dict):
88
+ continue
89
+ if block.get("type") == "text":
90
+ txt = str(block.get("text", "")).strip()
91
+ if txt:
92
+ out.append(f"[assistant] {trunc(txt, STREAM_TEXT_CAP)}")
93
+ elif block.get("type") == "tool_use":
94
+ step += 1
95
+ if block.get("id"):
96
+ tool_seq[block["id"]] = step
97
+ name = str(block.get("name", "tool"))
98
+ summary = _summarise_tool_use(name, block.get("input"))
99
+ out.append(f"[#{step} {name}] {summary}")
100
+ elif etype == "user":
101
+ for block in (event.get("message") or {}).get("content") or []:
102
+ if isinstance(block, dict) and block.get("type") == "tool_result":
103
+ n = tool_seq.get(block.get("tool_use_id"), "?")
104
+ err = " ERROR" if block.get("is_error") else ""
105
+ out.append(
106
+ f"[#{n} result{err}] "
107
+ f"{trunc(_blocks_text(block.get('content')), STREAM_TOOL_RESULT_CAP)}"
108
+ )
109
+ elif etype == "result":
110
+ token_usage = event.get("usage") or {}
111
+ usage = {
112
+ "cost_usd": event.get("total_cost_usd"),
113
+ "num_turns": event.get("num_turns"),
114
+ "duration_ms": event.get("duration_ms"),
115
+ "input_tokens": token_usage.get("input_tokens"),
116
+ "output_tokens": token_usage.get("output_tokens"),
117
+ "cache_read_input_tokens": token_usage.get("cache_read_input_tokens"),
118
+ "cache_creation_input_tokens": token_usage.get("cache_creation_input_tokens"),
119
+ }
120
+ final = str(event.get("result", "")).strip()
121
+ if final:
122
+ out.append(f"[final] {trunc(final, STREAM_TEXT_CAP)}")
123
+ cost = usage["cost_usd"]
124
+ dur = event.get("duration_ms")
125
+ out.append(
126
+ "=== result: {sub} | turns={turns} | cost={cost} | "
127
+ "tokens in={inp} out={outp} (cache_read={cache}) | wall={wall} ===".format(
128
+ sub=event.get("subtype", "?"),
129
+ turns=usage["num_turns"],
130
+ cost=(f"${cost:.4f}" if isinstance(cost, (int, float)) else "n/a"),
131
+ inp=usage["input_tokens"],
132
+ outp=usage["output_tokens"],
133
+ cache=usage["cache_read_input_tokens"],
134
+ wall=(f"{dur / 1000:.1f}s" if isinstance(dur, (int, float)) else "?"),
135
+ )
136
+ )
137
+
138
+ return "\n".join(out), usage
@@ -0,0 +1,68 @@
1
+ """Public types and provider metadata for CLI coding agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ VALID_PROVIDERS = frozenset(
8
+ {"llm", "claude_code", "codex", "opencode", "gemini_cli", "kimi_cli", "antigravity"}
9
+ )
10
+
11
+ BINARY_DEFAULTS: dict[str, str] = {
12
+ "claude_code": "claude",
13
+ "codex": "codex",
14
+ "opencode": "opencode",
15
+ "gemini_cli": "gemini",
16
+ "kimi_cli": "kimi",
17
+ "antigravity": "agy",
18
+ }
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class CliAgentConfig:
23
+ """Configuration for a CLI-based coding-agent backend.
24
+
25
+ Args:
26
+ provider: Which CLI agent to use. ``"llm"`` means no CLI agent.
27
+ binary_path: Explicit path to the CLI binary. Auto-detected when empty.
28
+ model: Model override flag forwarded to the binary.
29
+ max_budget_usd: Spend cap forwarded to providers that support it.
30
+ timeout_sec: Hard wall-clock limit for the agent subprocess.
31
+ extra_args: Additional flags appended verbatim to the CLI command.
32
+ skills: Paths to Agent-Skill directories containing ``SKILL.md``.
33
+ allow_skills: Enable provider-native skill invocation where supported.
34
+ """
35
+
36
+ provider: str = "llm"
37
+ binary_path: str = ""
38
+ model: str = ""
39
+ max_budget_usd: float = 5.0
40
+ timeout_sec: int = 600
41
+ extra_args: tuple[str, ...] = ()
42
+ skills: tuple[str, ...] = ()
43
+ allow_skills: bool = False
44
+
45
+ @property
46
+ def skills_enabled(self) -> bool:
47
+ return self.allow_skills or bool(self.skills)
48
+
49
+
50
+ @dataclass
51
+ class CliAgentResult:
52
+ """Output of one CLI agent invocation."""
53
+
54
+ files: dict[str, str]
55
+ provider_name: str
56
+ elapsed_sec: float
57
+ raw_output: str = ""
58
+ usage: dict | None = None
59
+ error: str | None = None
60
+ audit: dict | None = None
61
+ # Path (inside the invocation workdir) of the UNTRUNCATED raw agent
62
+ # stream — ``raw_output`` above is a capped rendering for UIs; the full
63
+ # stream (every tool call and result) is persisted here for debugging.
64
+ raw_stream_path: str = ""
65
+
66
+ @property
67
+ def ok(self) -> bool:
68
+ return self.error is None and bool(self.files)
@@ -0,0 +1,5 @@
1
+ """Shared code-generation orchestration helpers."""
2
+
3
+ from evalrx.agent_runtime.codegen.runner import CodegenCodeResult, CodegenRunner
4
+
5
+ __all__ = ["CodegenCodeResult", "CodegenRunner"]
@@ -0,0 +1,94 @@
1
+ """Shared runner for CLI coding-provider invocations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ from evalrx.agent_runtime.cli_types import CliAgentConfig, CliAgentResult
9
+
10
+
11
+ @dataclass
12
+ class CodegenCodeResult:
13
+ """Selected source file plus the underlying CLI run metadata."""
14
+
15
+ code: str
16
+ raw_output: str
17
+ usage: dict | None = None
18
+ files: dict[str, str] = field(default_factory=dict)
19
+ error: str | None = None
20
+ elapsed_sec: float = 0.0
21
+ audit: dict | None = None
22
+ # Path (relative to the invocation workdir) of the UNTRUNCATED raw agent
23
+ # stream persisted by the provider base class.
24
+ raw_stream_path: str = ""
25
+
26
+ @property
27
+ def ok(self) -> bool:
28
+ return bool(self.code) and self.error is None
29
+
30
+
31
+ def _select_py_file(
32
+ files: dict[str, str],
33
+ *,
34
+ preferred: tuple[str, ...] = (),
35
+ ) -> str:
36
+ py_files = {name: body for name, body in files.items() if name.endswith(".py")}
37
+ for name in preferred:
38
+ if name in py_files:
39
+ return py_files[name]
40
+ if not py_files:
41
+ return ""
42
+ return max(py_files.values(), key=len)
43
+
44
+
45
+ class CodegenRunner:
46
+ """Run a configured CLI coding provider and select generated code files."""
47
+
48
+ def __init__(self, cli_config: CliAgentConfig) -> None:
49
+ self._cli_config = cli_config
50
+
51
+ @property
52
+ def provider(self) -> str:
53
+ return self._cli_config.provider
54
+
55
+ def run(
56
+ self,
57
+ prompt: str,
58
+ *,
59
+ workdir: Path,
60
+ timeout_sec: int | None = None,
61
+ ) -> CliAgentResult:
62
+ # Lazy import: this is the sole call site, so importing here avoids
63
+ # pulling every provider module in at CodegenRunner import time.
64
+ from evalrx.agent_runtime.providers.registry import create_cli_agent
65
+
66
+ agent = create_cli_agent(self._cli_config)
67
+ return agent.run(prompt, workdir=workdir, timeout_sec=timeout_sec)
68
+
69
+ def write_code(
70
+ self,
71
+ prompt: str,
72
+ *,
73
+ workdir: Path,
74
+ timeout_sec: int | None = None,
75
+ preferred_filenames: tuple[str, ...] = (),
76
+ include_error_in_raw: bool = False,
77
+ ) -> CodegenCodeResult:
78
+ result = self.run(prompt, workdir=workdir, timeout_sec=timeout_sec)
79
+ raw_output = result.raw_output or (
80
+ (result.error or "") if include_error_in_raw else ""
81
+ )
82
+ code = _select_py_file(result.files, preferred=preferred_filenames)
83
+ if not result.ok:
84
+ code = ""
85
+ return CodegenCodeResult(
86
+ code=code,
87
+ raw_output=raw_output,
88
+ usage=result.usage,
89
+ files=result.files,
90
+ error=result.error,
91
+ elapsed_sec=result.elapsed_sec,
92
+ audit=result.audit,
93
+ raw_stream_path=result.raw_stream_path,
94
+ )
@@ -0,0 +1,117 @@
1
+ """Experiment harness — immutable evaluation infrastructure.
2
+
3
+ This file is injected into the sandbox project directory at execution time.
4
+ The LLM-generated experiment code should import and use this harness for:
5
+ - Time budget management (should_stop)
6
+ - Metric reporting (report_metric)
7
+ - Result finalization (finalize)
8
+ - NaN/divergence detection (built-in)
9
+
10
+ This file is NOT editable by the LLM agent — it provides a trust boundary
11
+ for metric reporting, mirroring AutoResearchClaw's immutable harness pattern.
12
+ """
13
+
14
+ import json
15
+ import math
16
+ import sys
17
+ import time
18
+
19
+
20
+ class ExperimentHarness:
21
+ """Immutable experiment infrastructure for time and metric management."""
22
+
23
+ def __init__(self, time_budget: int = 120):
24
+ self._start = time.time()
25
+ self._time_budget = max(1, int(time_budget))
26
+ self._metrics: dict[str, float] = {}
27
+ self._partial_results: list[dict[str, object]] = []
28
+ self._step_count = 0
29
+ self._nan_count = 0
30
+
31
+ @property
32
+ def elapsed(self) -> float:
33
+ """Seconds elapsed since harness creation."""
34
+ return time.time() - self._start
35
+
36
+ @property
37
+ def progress(self) -> float:
38
+ """Fraction of time budget used (0.0 to 1.0)."""
39
+ return min(self.elapsed / self._time_budget, 1.0)
40
+
41
+ def should_stop(self) -> bool:
42
+ """Return True if approaching 80% of time budget."""
43
+ return self.elapsed >= self._time_budget * 0.8
44
+
45
+ def check_value(self, value: float, name: str = "metric") -> bool:
46
+ """Return True if value is finite. Log warning and count NaN/Inf."""
47
+ if math.isnan(value) or math.isinf(value):
48
+ self._nan_count += 1
49
+ print(
50
+ f"WARNING: {name} = {value} (non-finite, skipped)",
51
+ file=sys.stderr,
52
+ )
53
+ if self._nan_count >= 5:
54
+ print(
55
+ "FAIL: Too many NaN/Inf values detected. "
56
+ "Stopping experiment early.",
57
+ file=sys.stderr,
58
+ )
59
+ self.finalize()
60
+ sys.exit(1)
61
+ return False
62
+ return True
63
+
64
+ def report_metric(self, name: str, value: float) -> None:
65
+ """Report a metric value. Validates and prints in standard format.
66
+
67
+ Non-finite values (NaN, Inf) are rejected and logged as warnings.
68
+ """
69
+ if not isinstance(value, (int, float)):
70
+ try:
71
+ value = float(value)
72
+ except (TypeError, ValueError):
73
+ print(f"WARNING: Cannot convert {name}={value!r} to float", file=sys.stderr)
74
+ return
75
+
76
+ if not self.check_value(value, name):
77
+ return
78
+
79
+ self._metrics[name] = value
80
+ print(f"{name}: {value}")
81
+
82
+ def log_result(self, result_dict: dict[str, object]) -> None:
83
+ """Log a structured result row (e.g., per-condition results)."""
84
+ self._partial_results.append(result_dict)
85
+
86
+ def finalize(self) -> None:
87
+ """Write results.json with all reported metrics and partial results."""
88
+ output: dict[str, object] = {
89
+ "metrics": self._metrics,
90
+ "elapsed_sec": round(self.elapsed, 2),
91
+ "time_budget_sec": self._time_budget,
92
+ "steps_completed": self._step_count,
93
+ "nan_count": self._nan_count,
94
+ }
95
+ if self._partial_results:
96
+ output["results"] = self._partial_results
97
+
98
+ try:
99
+ with open("results.json", "w", encoding="utf-8") as f:
100
+ json.dump(output, f, indent=2, default=str)
101
+ except OSError as e:
102
+ print(f"WARNING: Could not write results.json: {e}", file=sys.stderr)
103
+
104
+ def step(self) -> None:
105
+ """Increment step counter. Call this once per experiment step."""
106
+ self._step_count += 1
107
+
108
+
109
+ _default_harness: ExperimentHarness | None = None
110
+
111
+
112
+ def get_harness(time_budget: int = 120) -> ExperimentHarness:
113
+ """Get or create the default experiment harness."""
114
+ global _default_harness # noqa: PLW0603
115
+ if _default_harness is None:
116
+ _default_harness = ExperimentHarness(time_budget)
117
+ return _default_harness