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.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- 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,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
|