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,530 @@
|
|
|
1
|
+
"""Legacy loop controllers, kept for backward compatibility.
|
|
2
|
+
|
|
3
|
+
``SelfEvolveLoop``
|
|
4
|
+
Original Stage-1 skeleton.
|
|
5
|
+
|
|
6
|
+
``AutoDiagnoseLoop``
|
|
7
|
+
Concrete M1→M2→M3→M5 implementation (original architecture):
|
|
8
|
+
|
|
9
|
+
┌───────────────────────────────────────────────────────────────────┐
|
|
10
|
+
│ M1 · ProbeAgent select analyzers + execute (direct / Docker) │
|
|
11
|
+
│ M2 · AnalysisModule interpret results → structured report │
|
|
12
|
+
│ M3 · DiagnosisAgent Gemini reads report → hypotheses │
|
|
13
|
+
│ M5 · SurgeryAgent operate + verify; stop or refocus data │
|
|
14
|
+
└───────────────────────────────────────────────────────────────────┘
|
|
15
|
+
↑__________________________│ (repeat)
|
|
16
|
+
|
|
17
|
+
Prefer :class:`~evalrx.eval_agent.loop.VLDiagnoseLoop` (M1→M2→M3→M4, M5
|
|
18
|
+
post-loop) or :class:`~evalrx.eval_agent.agentic.AgenticDiagnoseLoop`
|
|
19
|
+
(judge-decided) for new work — this module is the pre-2026-06-05 architecture,
|
|
20
|
+
kept for existing callers and its resumable run_dir/checkpoint infrastructure.
|
|
21
|
+
|
|
22
|
+
Run-directory infrastructure (mirrors AutoResearchClaw):
|
|
23
|
+
|
|
24
|
+
When ``run_dir`` is supplied:
|
|
25
|
+
- ``run_dir/artifacts/{run_id}/`` — per-run artifact staging
|
|
26
|
+
- ``run_dir/checkpoint.json`` — atomic resume state (temp+rename)
|
|
27
|
+
- ``run_dir/heartbeat.json`` — per-cycle liveness signal
|
|
28
|
+
- ``run_dir/evolution/`` — JSONL lesson store (auto-created)
|
|
29
|
+
|
|
30
|
+
Git integration:
|
|
31
|
+
- When the repo is detected, each resolved run is committed on
|
|
32
|
+
``eval/{run_id}``; unresolved runs are discarded with git reset.
|
|
33
|
+
|
|
34
|
+
Usage::
|
|
35
|
+
|
|
36
|
+
loop = AutoDiagnoseLoop(model=my_model, run_dir=Path("./runs"))
|
|
37
|
+
report = loop.run(failure_cases)
|
|
38
|
+
print(report.resolved, report.final_hypotheses)
|
|
39
|
+
|
|
40
|
+
# Resume an interrupted run
|
|
41
|
+
report = AutoDiagnoseLoop.resume(Path("./runs"), model=my_model, data=cases)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
import time
|
|
48
|
+
from datetime import datetime, timezone
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import TYPE_CHECKING, Any
|
|
51
|
+
|
|
52
|
+
from evalrx.eval_agent.checkpoint import read_checkpoint, write_checkpoint, write_heartbeat
|
|
53
|
+
from evalrx.eval_agent.hypothesis import HypothesisStatus
|
|
54
|
+
from evalrx.eval_agent.loop_reports import AutoDiagnoseReport
|
|
55
|
+
from evalrx.eval_agent.run_metadata import _attach_run_logger, _log_generated_tools, _run_config
|
|
56
|
+
from evalrx.eval_agent.store import InMemoryStore, Store
|
|
57
|
+
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
from evalrx.analysis.analysis_module import AnalysisModule
|
|
60
|
+
from evalrx.core.case import CaseBatch
|
|
61
|
+
from evalrx.core.model import Model
|
|
62
|
+
from evalrx.eval_agent.evolution import EvolutionStore
|
|
63
|
+
from evalrx.eval_agent.git_manager import ExperimentGitManager
|
|
64
|
+
from evalrx.eval_agent.hypothesis import Hypothesis, HypothesisGenerator
|
|
65
|
+
from evalrx.eval_agent.run_logger_v2 import RunLoggerV2
|
|
66
|
+
from evalrx.eval_agent.stages.diagnosis import DiagnosisAgent
|
|
67
|
+
from evalrx.eval_agent.stages.probe_agent import ProbeAgent
|
|
68
|
+
from evalrx.eval_agent.stages.surgery import SurgeryAgent
|
|
69
|
+
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SelfEvolveLoop:
|
|
74
|
+
"""Original Stage-1 skeleton — kept for backward compatibility.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
generator: Proposes and mutates hypotheses.
|
|
78
|
+
store: Persistent memory; defaults to an in-memory store.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
generator: "HypothesisGenerator | None" = None,
|
|
84
|
+
store: Store | None = None,
|
|
85
|
+
runner: Any = None, # kept for signature compat
|
|
86
|
+
) -> None:
|
|
87
|
+
from evalrx.core.experiment import ExperimentRunner
|
|
88
|
+
|
|
89
|
+
self.generator = generator
|
|
90
|
+
self.store = store or InMemoryStore()
|
|
91
|
+
self.runner = runner if runner is not None else ExperimentRunner()
|
|
92
|
+
|
|
93
|
+
def step(self) -> list:
|
|
94
|
+
if self.generator is None:
|
|
95
|
+
raise ValueError("SelfEvolveLoop needs a generator (e.g. ManualHypothesisGenerator).")
|
|
96
|
+
proposed = self.generator.propose(self.store.summarize())
|
|
97
|
+
for h in proposed:
|
|
98
|
+
self.store.add_hypothesis(h)
|
|
99
|
+
return list(proposed)
|
|
100
|
+
|
|
101
|
+
def run(self, max_cycles: int = 10) -> list:
|
|
102
|
+
history: list = []
|
|
103
|
+
for _ in range(max_cycles):
|
|
104
|
+
proposed = self.step()
|
|
105
|
+
history.append(proposed)
|
|
106
|
+
if not proposed:
|
|
107
|
+
break
|
|
108
|
+
return history
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
# AutoDiagnoseLoop — M1 → M2 → M3 → M5
|
|
113
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AutoDiagnoseLoop:
|
|
117
|
+
"""Automated M1→M2→M3→M5 diagnosis loop.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
model: The model under evaluation.
|
|
121
|
+
probe_agent: M1 — selects and executes analyzers (direct or Docker).
|
|
122
|
+
Defaults to ``ProbeAgent()``.
|
|
123
|
+
analysis_module: M2 — interprets raw Results into an AnalysisReport.
|
|
124
|
+
Defaults to ``AnalysisModule()``.
|
|
125
|
+
diagnosis_agent: M3 — reads the AnalysisReport and proposes hypotheses via
|
|
126
|
+
Gemini (default when ``GEMINI_API_KEY`` is set).
|
|
127
|
+
Pass ``None`` to run in *analysis-only* mode (M1+M2 only).
|
|
128
|
+
surgery_agent: M5 — operates on each hypothesis to verify or refute it.
|
|
129
|
+
Defaults to ``SurgeryAgent()``.
|
|
130
|
+
store: Persistent memory. Defaults to ``InMemoryStore()``.
|
|
131
|
+
max_cycles: Hard cap on M1→M5 iterations.
|
|
132
|
+
run_logger: Optional :class:`~evalrx.eval_agent.run_logger_v2.RunLoggerV2`
|
|
133
|
+
that logs the run's events and saves analyzer artifacts.
|
|
134
|
+
run_dir: Optional root directory for run infrastructure.
|
|
135
|
+
When set, enables checkpoints, heartbeat, and the
|
|
136
|
+
``EvolutionStore``.
|
|
137
|
+
git_manager: Optional :class:`~evalrx.eval_agent.git_manager.ExperimentGitManager`.
|
|
138
|
+
``None`` → auto-detect git repo when *run_dir* is given.
|
|
139
|
+
evolution_store: Optional :class:`~evalrx.eval_agent.evolution.EvolutionStore`.
|
|
140
|
+
``None`` → auto-create under ``run_dir/evolution/`` when
|
|
141
|
+
*run_dir* is given.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
model: "Model",
|
|
147
|
+
probe_agent: "ProbeAgent | None" = None,
|
|
148
|
+
analysis_module: "AnalysisModule | None" = None,
|
|
149
|
+
diagnosis_agent: "DiagnosisAgent | None" = None,
|
|
150
|
+
surgery_agent: "SurgeryAgent | None" = None,
|
|
151
|
+
store: Store | None = None,
|
|
152
|
+
max_cycles: int = 5,
|
|
153
|
+
run_logger: "RunLoggerV2 | None" = None,
|
|
154
|
+
# --- run-directory infrastructure (new) ---
|
|
155
|
+
run_dir: "Path | None" = None,
|
|
156
|
+
git_manager: "ExperimentGitManager | None" = None,
|
|
157
|
+
evolution_store: "EvolutionStore | None" = None,
|
|
158
|
+
# --- token / cost budget ---
|
|
159
|
+
token_budget: int = 0,
|
|
160
|
+
_run_id_override: str | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
from evalrx.analysis.analysis_module import AnalysisModule
|
|
163
|
+
from evalrx.eval_agent.stages.probe_agent import ProbeAgent
|
|
164
|
+
from evalrx.eval_agent.stages.surgery import SurgeryAgent
|
|
165
|
+
|
|
166
|
+
self.model = model
|
|
167
|
+
self.probe_agent = probe_agent or ProbeAgent()
|
|
168
|
+
self.analysis_module = analysis_module or AnalysisModule()
|
|
169
|
+
self.diagnosis_agent = diagnosis_agent
|
|
170
|
+
self.surgery_agent = surgery_agent or SurgeryAgent()
|
|
171
|
+
self.store = store or InMemoryStore()
|
|
172
|
+
self.max_cycles = max_cycles
|
|
173
|
+
self.run_logger = run_logger
|
|
174
|
+
self.token_budget = token_budget
|
|
175
|
+
self._tokens_used: int = 0
|
|
176
|
+
self._timings: dict[str, float] = {}
|
|
177
|
+
|
|
178
|
+
# --- run-directory setup ---
|
|
179
|
+
self._run_dir: Path | None = None
|
|
180
|
+
self._artifacts_dir: Path | None = None
|
|
181
|
+
self._checkpoint_path: Path | None = None
|
|
182
|
+
self._heartbeat_path: Path | None = None
|
|
183
|
+
self._run_id: str = ""
|
|
184
|
+
|
|
185
|
+
if run_dir is not None:
|
|
186
|
+
run_dir = Path(run_dir)
|
|
187
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
188
|
+
self._run_id = _run_id_override or ts
|
|
189
|
+
self._run_dir = run_dir
|
|
190
|
+
self._artifacts_dir = run_dir / "artifacts" / ts
|
|
191
|
+
self._artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
self._checkpoint_path = run_dir / "checkpoint.json"
|
|
193
|
+
self._heartbeat_path = run_dir / "heartbeat.json"
|
|
194
|
+
|
|
195
|
+
# EvolutionStore: auto-create when run_dir is set
|
|
196
|
+
self.evolution_store: "EvolutionStore | None" = evolution_store
|
|
197
|
+
if self.evolution_store is None and run_dir is not None:
|
|
198
|
+
try:
|
|
199
|
+
from evalrx.eval_agent.evolution import EvolutionStore
|
|
200
|
+
self.evolution_store = EvolutionStore(run_dir / "evolution")
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
logger.debug("Could not create EvolutionStore: %s", exc)
|
|
203
|
+
|
|
204
|
+
# ExperimentGitManager: auto-detect when run_dir is set
|
|
205
|
+
self.git_manager: "ExperimentGitManager | None" = git_manager
|
|
206
|
+
if self.git_manager is None and run_dir is not None:
|
|
207
|
+
try:
|
|
208
|
+
from evalrx.eval_agent.git_manager import ExperimentGitManager
|
|
209
|
+
_gm = ExperimentGitManager(run_dir)
|
|
210
|
+
if _gm.is_git_repo():
|
|
211
|
+
self.git_manager = _gm
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
logger.debug("Git manager auto-detect failed: %s", exc)
|
|
214
|
+
|
|
215
|
+
# ──────────────────────────────────────────────────────────────────
|
|
216
|
+
# Public API
|
|
217
|
+
# ──────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
def publish_report(
|
|
220
|
+
self,
|
|
221
|
+
*,
|
|
222
|
+
model: "Any | None" = None,
|
|
223
|
+
example_dir: "str | Path | None" = None,
|
|
224
|
+
) -> "Any":
|
|
225
|
+
"""Compose the completed-run UI, reusing the diagnosis judge."""
|
|
226
|
+
if self.run_logger is None:
|
|
227
|
+
raise RuntimeError("publish_report needs a run_logger with a durable run directory")
|
|
228
|
+
if model is None:
|
|
229
|
+
model = getattr(self.diagnosis_agent, "judge", None)
|
|
230
|
+
from evalrx.reporting.dynamic import publish_report
|
|
231
|
+
|
|
232
|
+
return publish_report(
|
|
233
|
+
self.run_logger.run_dir,
|
|
234
|
+
example_dir=example_dir,
|
|
235
|
+
model=model,
|
|
236
|
+
run_logger=self.run_logger,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def run(self, data: "CaseBatch") -> AutoDiagnoseReport:
|
|
240
|
+
"""Drive the M1→M2→M3→M5 loop until resolved or *max_cycles* reached.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
data: Cases to analyse. Refocused to unexplained cases when M5
|
|
244
|
+
returns a SUPPORTED finding with ``new_data``.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
:class:`AutoDiagnoseReport` with the final state.
|
|
248
|
+
"""
|
|
249
|
+
all_hypotheses: list[Any] = []
|
|
250
|
+
final_results: dict[str, Any] = {}
|
|
251
|
+
final_analysis = None
|
|
252
|
+
|
|
253
|
+
# Determine start cycle from checkpoint (resume support).
|
|
254
|
+
# Only honor the checkpoint when run_id matches — this means the
|
|
255
|
+
# caller explicitly requested a resume of a specific prior run
|
|
256
|
+
# (via AutoDiagnoseLoop.resume() or _run_id_override). A fresh
|
|
257
|
+
# AutoDiagnoseLoop always gets a new run_id and starts from 0.
|
|
258
|
+
start_cycle = 0
|
|
259
|
+
if self._checkpoint_path is not None:
|
|
260
|
+
cp = read_checkpoint(self._checkpoint_path)
|
|
261
|
+
if cp is not None and cp.get("run_id") == self._run_id:
|
|
262
|
+
start_cycle = cp.get("last_completed_cycle", -1) + 1
|
|
263
|
+
if start_cycle > 0:
|
|
264
|
+
logger.info(
|
|
265
|
+
"Resuming from checkpoint: skipping cycles 0–%d",
|
|
266
|
+
start_cycle - 1,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
prior_cycles: list[dict[str, Any]] = []
|
|
270
|
+
outstanding_modes: list[str] = []
|
|
271
|
+
completed_cycles = 0
|
|
272
|
+
# Per-cycle max confidence scores: detect flat progress early
|
|
273
|
+
_confidence_history: list[float] = []
|
|
274
|
+
self._tokens_used = 0
|
|
275
|
+
|
|
276
|
+
# Per-stage wall-clock totals (seconds), surfaced in loop_end.
|
|
277
|
+
self._timings = {}
|
|
278
|
+
|
|
279
|
+
# Forward the RunLogger so the probe generators emit tool_codegen events.
|
|
280
|
+
_attach_run_logger(self.run_logger, self.probe_agent)
|
|
281
|
+
if self.run_logger is not None:
|
|
282
|
+
self.run_logger.log_run_start(
|
|
283
|
+
_run_config(self, data, loop_name="AutoDiagnoseLoop")
|
|
284
|
+
)
|
|
285
|
+
self.run_logger.log_cases(data)
|
|
286
|
+
|
|
287
|
+
for cycle in range(start_cycle, self.max_cycles):
|
|
288
|
+
if self.run_logger is not None:
|
|
289
|
+
self.run_logger.current_cycle = cycle
|
|
290
|
+
# Budget guard: stop before consuming more tokens than allowed
|
|
291
|
+
if self.token_budget > 0 and self._tokens_used >= self.token_budget:
|
|
292
|
+
logger.warning(
|
|
293
|
+
"Token budget %d exhausted (%d used) — stopping loop after %d cycles",
|
|
294
|
+
self.token_budget, self._tokens_used, completed_cycles,
|
|
295
|
+
)
|
|
296
|
+
break
|
|
297
|
+
completed_cycles = cycle + 1
|
|
298
|
+
|
|
299
|
+
# ── M1: probe ───────────────────────────────────────────────
|
|
300
|
+
_t0 = time.monotonic()
|
|
301
|
+
probe_results = self.probe_agent.probe(
|
|
302
|
+
self.model, data,
|
|
303
|
+
hint_failure_modes=outstanding_modes or None,
|
|
304
|
+
)
|
|
305
|
+
_dt = time.monotonic() - _t0
|
|
306
|
+
self._timings["m1"] = self._timings.get("m1", 0.0) + _dt
|
|
307
|
+
if self.run_logger:
|
|
308
|
+
self.run_logger.log_probe(
|
|
309
|
+
cycle, probe_results, schema=self.probe_agent.last_schema,
|
|
310
|
+
judge_prompt=getattr(self.probe_agent, "last_selection_prompt", ""),
|
|
311
|
+
judge_raw=getattr(self.probe_agent, "last_selection_raw", ""),
|
|
312
|
+
duration_sec=_dt,
|
|
313
|
+
failed_analyzers=getattr(self.probe_agent, "_failed_analyzers", None) or None,
|
|
314
|
+
)
|
|
315
|
+
_log_generated_tools(self.run_logger, cycle, "m1_probe", self.probe_agent)
|
|
316
|
+
if not probe_results:
|
|
317
|
+
break
|
|
318
|
+
final_results = probe_results
|
|
319
|
+
for r in probe_results.values():
|
|
320
|
+
self.store.add_result(r)
|
|
321
|
+
|
|
322
|
+
# ── M2: analyze ─────────────────────────────────────────────
|
|
323
|
+
_t0 = time.monotonic()
|
|
324
|
+
analysis = self.analysis_module.analyze(probe_results, repr(self.model))
|
|
325
|
+
_dt = time.monotonic() - _t0
|
|
326
|
+
self._timings["m2"] = self._timings.get("m2", 0.0) + _dt
|
|
327
|
+
final_analysis = analysis
|
|
328
|
+
if self.run_logger:
|
|
329
|
+
self.run_logger.log_analysis(cycle, analysis, duration_sec=_dt)
|
|
330
|
+
|
|
331
|
+
if self.diagnosis_agent is None:
|
|
332
|
+
self._on_cycle_complete(cycle, all_hypotheses)
|
|
333
|
+
break # analysis-only mode
|
|
334
|
+
|
|
335
|
+
# ── M3: diagnose ─────────────────────────────────────────────
|
|
336
|
+
_t0 = time.monotonic()
|
|
337
|
+
diag = self.diagnosis_agent.diagnose(
|
|
338
|
+
analysis, prior_cycles=prior_cycles or None
|
|
339
|
+
)
|
|
340
|
+
_dt = time.monotonic() - _t0
|
|
341
|
+
self._timings["m3"] = self._timings.get("m3", 0.0) + _dt
|
|
342
|
+
# Track token usage from the diagnosis LLM call.
|
|
343
|
+
# Use response metadata when available; otherwise estimate from
|
|
344
|
+
# output length (1 token ≈ 4 chars) so the budget is always counted.
|
|
345
|
+
_diag_tokens = getattr(diag, "tokens_used", None)
|
|
346
|
+
if _diag_tokens is None:
|
|
347
|
+
_diag_tokens = max(1, len(diag.raw_judge_output) // 4)
|
|
348
|
+
self._tokens_used += _diag_tokens
|
|
349
|
+
if self.run_logger:
|
|
350
|
+
self.run_logger.log_diagnosis(cycle, diag, duration_sec=_dt)
|
|
351
|
+
if not diag.hypotheses:
|
|
352
|
+
self._on_cycle_complete(cycle, all_hypotheses)
|
|
353
|
+
break
|
|
354
|
+
for h in diag.hypotheses:
|
|
355
|
+
self.store.add_hypothesis(h)
|
|
356
|
+
all_hypotheses.extend(diag.hypotheses)
|
|
357
|
+
|
|
358
|
+
# ── M5: surgery ──────────────────────────────────────────────
|
|
359
|
+
outstanding_modes = []
|
|
360
|
+
cycle_max_confidence = 0.0
|
|
361
|
+
for h in diag.hypotheses:
|
|
362
|
+
_t0 = time.monotonic()
|
|
363
|
+
iv = self.surgery_agent.operate(h, self.model, probe_results, data)
|
|
364
|
+
_dt = time.monotonic() - _t0
|
|
365
|
+
self._timings["m5"] = self._timings.get("m5", 0.0) + _dt
|
|
366
|
+
h.status = iv.status
|
|
367
|
+
cycle_max_confidence = max(cycle_max_confidence, iv.confidence_score)
|
|
368
|
+
if self.run_logger:
|
|
369
|
+
self.run_logger.log_surgery(cycle, h, iv, duration_sec=_dt)
|
|
370
|
+
# When surgery wrote and ran an experiment, also persist it
|
|
371
|
+
# (script, output, thinking, workspace snapshot).
|
|
372
|
+
if getattr(iv, "experiment", None):
|
|
373
|
+
self.run_logger.log_experiment(cycle, h, iv, module="m5")
|
|
374
|
+
if iv.fixed:
|
|
375
|
+
report = AutoDiagnoseReport(
|
|
376
|
+
cycles=completed_cycles,
|
|
377
|
+
resolved=True,
|
|
378
|
+
final_hypotheses=all_hypotheses,
|
|
379
|
+
final_results=final_results,
|
|
380
|
+
final_analysis=final_analysis,
|
|
381
|
+
store=self.store,
|
|
382
|
+
_run_id=self._run_id,
|
|
383
|
+
)
|
|
384
|
+
self._on_cycle_complete(cycle, all_hypotheses)
|
|
385
|
+
self._on_loop_end(report)
|
|
386
|
+
return report
|
|
387
|
+
if iv.status == HypothesisStatus.INCONCLUSIVE:
|
|
388
|
+
outstanding_modes.append(h.predicted_failure_mode)
|
|
389
|
+
if iv.new_data is not None and len(iv.new_data) > 0:
|
|
390
|
+
data = iv.new_data
|
|
391
|
+
|
|
392
|
+
# Track confidence per cycle and stop early when progress stalls:
|
|
393
|
+
# if the last 2 cycles both show confidence < 0.1 the hypotheses
|
|
394
|
+
# are not gaining traction — continuing burns budget without insight.
|
|
395
|
+
_confidence_history.append(cycle_max_confidence)
|
|
396
|
+
if len(_confidence_history) >= 2 and all(
|
|
397
|
+
c < 0.1 for c in _confidence_history[-2:]
|
|
398
|
+
):
|
|
399
|
+
logger.info(
|
|
400
|
+
"Stopping early: confidence flat at %.3f for 2 consecutive cycles",
|
|
401
|
+
cycle_max_confidence,
|
|
402
|
+
)
|
|
403
|
+
break
|
|
404
|
+
|
|
405
|
+
# Record cycle for M3 context
|
|
406
|
+
prior_cycles.append({
|
|
407
|
+
"cycle": cycle,
|
|
408
|
+
"severity": analysis.severity,
|
|
409
|
+
"hypotheses": [
|
|
410
|
+
{
|
|
411
|
+
"statement": h.statement,
|
|
412
|
+
"failure_mode": h.predicted_failure_mode,
|
|
413
|
+
"status": h.status.value if h.status else "pending",
|
|
414
|
+
}
|
|
415
|
+
for h in diag.hypotheses
|
|
416
|
+
],
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
# Write checkpoint + heartbeat after each full cycle
|
|
420
|
+
self._on_cycle_complete(cycle, all_hypotheses)
|
|
421
|
+
|
|
422
|
+
report = AutoDiagnoseReport(
|
|
423
|
+
cycles=completed_cycles,
|
|
424
|
+
resolved=False,
|
|
425
|
+
final_hypotheses=all_hypotheses,
|
|
426
|
+
final_results=final_results,
|
|
427
|
+
final_analysis=final_analysis,
|
|
428
|
+
store=self.store,
|
|
429
|
+
_run_id=self._run_id,
|
|
430
|
+
)
|
|
431
|
+
self._on_loop_end(report)
|
|
432
|
+
return report
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def resume(
|
|
436
|
+
cls,
|
|
437
|
+
run_dir: Path,
|
|
438
|
+
model: "Model",
|
|
439
|
+
data: "CaseBatch",
|
|
440
|
+
**kwargs: Any,
|
|
441
|
+
) -> "AutoDiagnoseReport":
|
|
442
|
+
"""Resume a loop from a checkpoint.
|
|
443
|
+
|
|
444
|
+
Reads ``run_dir/checkpoint.json``, skips already-completed cycles,
|
|
445
|
+
and continues from ``last_completed_cycle + 1``.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
run_dir: Directory that contains ``checkpoint.json``.
|
|
449
|
+
model: The model under evaluation.
|
|
450
|
+
data: Original :class:`CaseBatch` to continue from.
|
|
451
|
+
**kwargs: Forwarded to :class:`AutoDiagnoseLoop.__init__`.
|
|
452
|
+
``run_dir`` is set automatically — do not pass it.
|
|
453
|
+
"""
|
|
454
|
+
# Read the stored run_id so the checkpoint match succeeds and
|
|
455
|
+
# the loop correctly skips already-completed cycles.
|
|
456
|
+
run_id_override: str | None = None
|
|
457
|
+
cp = read_checkpoint(Path(run_dir) / "checkpoint.json")
|
|
458
|
+
if cp is not None:
|
|
459
|
+
run_id_override = cp.get("run_id")
|
|
460
|
+
instance = cls(
|
|
461
|
+
model=model, run_dir=run_dir, _run_id_override=run_id_override, **kwargs
|
|
462
|
+
)
|
|
463
|
+
return instance.run(data)
|
|
464
|
+
|
|
465
|
+
# ──────────────────────────────────────────────────────────────────
|
|
466
|
+
# Per-cycle and end-of-loop hooks
|
|
467
|
+
# ──────────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
def _on_cycle_complete(
|
|
470
|
+
self, cycle: int, hypotheses: list["Hypothesis"]
|
|
471
|
+
) -> None:
|
|
472
|
+
"""Write checkpoint and heartbeat after each completed cycle."""
|
|
473
|
+
statuses = [
|
|
474
|
+
h.status.value if h.status else "pending"
|
|
475
|
+
for h in hypotheses
|
|
476
|
+
]
|
|
477
|
+
if self._checkpoint_path is not None:
|
|
478
|
+
write_checkpoint(
|
|
479
|
+
self._checkpoint_path, cycle=cycle, run_id=self._run_id,
|
|
480
|
+
hypothesis_statuses=statuses,
|
|
481
|
+
)
|
|
482
|
+
if self._heartbeat_path is not None:
|
|
483
|
+
write_heartbeat(self._heartbeat_path, cycle=cycle, run_id=self._run_id)
|
|
484
|
+
|
|
485
|
+
def _on_loop_end(self, report: AutoDiagnoseReport) -> None:
|
|
486
|
+
"""Append lessons to EvolutionStore and commit/discard via git."""
|
|
487
|
+
if self.run_logger:
|
|
488
|
+
self.run_logger.log_loop_end(
|
|
489
|
+
report,
|
|
490
|
+
tokens_used=self._tokens_used,
|
|
491
|
+
timings=getattr(self, "_timings", None) or None,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# EvolutionStore lesson extraction
|
|
495
|
+
if self.evolution_store is not None:
|
|
496
|
+
try:
|
|
497
|
+
from evalrx.eval_agent.evolution import extract_lessons
|
|
498
|
+
lessons = extract_lessons(report)
|
|
499
|
+
self.evolution_store.append_many(lessons)
|
|
500
|
+
logger.debug(
|
|
501
|
+
"EvolutionStore: appended %d lesson(s) for run %s",
|
|
502
|
+
len(lessons), self._run_id,
|
|
503
|
+
)
|
|
504
|
+
except Exception as exc:
|
|
505
|
+
logger.warning("Failed to record evolution lessons: %s", exc)
|
|
506
|
+
|
|
507
|
+
# Git integration
|
|
508
|
+
if self.git_manager is not None and self._run_id:
|
|
509
|
+
hyp_statuses = {
|
|
510
|
+
h.statement[:80]: (h.status.value if h.status else "pending")
|
|
511
|
+
for h in report.final_hypotheses
|
|
512
|
+
}
|
|
513
|
+
try:
|
|
514
|
+
if report.resolved:
|
|
515
|
+
self.git_manager.commit_experiment(
|
|
516
|
+
self._run_id,
|
|
517
|
+
report.cycles,
|
|
518
|
+
hyp_statuses,
|
|
519
|
+
"resolved",
|
|
520
|
+
)
|
|
521
|
+
logger.info(
|
|
522
|
+
"Git: committed resolved run %s on eval/%s",
|
|
523
|
+
self._run_id, self._run_id,
|
|
524
|
+
)
|
|
525
|
+
else:
|
|
526
|
+
self.git_manager.discard_experiment(
|
|
527
|
+
self._run_id, "not resolved"
|
|
528
|
+
)
|
|
529
|
+
except Exception as exc:
|
|
530
|
+
logger.warning("Git integration failed: %s", exc)
|