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,172 @@
|
|
|
1
|
+
"""Hypotheses — the unit the self-evolving agent proposes, tests, and mutates.
|
|
2
|
+
|
|
3
|
+
A hypothesis is a falsifiable claim about *when/why* a model fails (e.g. "Qwen
|
|
4
|
+
mis-binds entities when two names share a surname"). The loop turns it into
|
|
5
|
+
cases + an experiment, runs it, and updates its status from the findings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HypothesisStatus(str, Enum):
|
|
17
|
+
PROPOSED = "proposed" # generated, not yet tested
|
|
18
|
+
TESTING = "testing" # experiment in flight
|
|
19
|
+
SUPPORTED = "supported" # evidence backs it
|
|
20
|
+
REFUTED = "refuted" # evidence contradicts it
|
|
21
|
+
INCONCLUSIVE = "inconclusive"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Hypothesis:
|
|
26
|
+
"""A falsifiable claim about a model's failure behaviour.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
statement: Natural-language claim (LLM-generated/readable).
|
|
30
|
+
target_model: Registered model name the claim is about.
|
|
31
|
+
predicted_failure_mode: Tag/description of the expected failure.
|
|
32
|
+
plain_statement: Plain-language explanation understandable by a layperson.
|
|
33
|
+
test_design: How to verify this claim — analyzer / per-case
|
|
34
|
+
signal / strategy-contrast keywords proposed by
|
|
35
|
+
M3 (e.g. ``"relative_attention.max_relative_weight"``,
|
|
36
|
+
``"prompt_contrast describe_first"``). M4 uses
|
|
37
|
+
it to route evidence deterministically and M1
|
|
38
|
+
uses it for cycle-2 targeted probing.
|
|
39
|
+
expected_association: Pre-registered direction for the named test:
|
|
40
|
+
``higher_on_failures`` or ``lower_on_failures``.
|
|
41
|
+
This prevents a protective-valued signal (for
|
|
42
|
+
example ``n_correct``) from being interpreted
|
|
43
|
+
backwards after its effect is observed.
|
|
44
|
+
status: Lifecycle state (:class:`HypothesisStatus`).
|
|
45
|
+
parent_id: Hypothesis this one was mutated from, if any.
|
|
46
|
+
id: Stable identifier.
|
|
47
|
+
evidence: Result/case ids accumulated while testing.
|
|
48
|
+
metadata: Free-form extras.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
statement: str
|
|
52
|
+
target_model: str
|
|
53
|
+
predicted_failure_mode: str
|
|
54
|
+
plain_statement: str = ""
|
|
55
|
+
test_design: str = ""
|
|
56
|
+
expected_association: str = ""
|
|
57
|
+
status: HypothesisStatus = HypothesisStatus.PROPOSED
|
|
58
|
+
parent_id: str | None = None
|
|
59
|
+
id: str = ""
|
|
60
|
+
evidence: list[str] = field(default_factory=list)
|
|
61
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class HypothesisGenerator:
|
|
65
|
+
"""Proposes new hypotheses — from scratch or by mutating prior ones.
|
|
66
|
+
|
|
67
|
+
Stage 2: an LLM-backed generator that reads the store (past supported/refuted
|
|
68
|
+
hypotheses and their evidence) and proposes the next ones to test.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def propose(self, context: Any = None) -> list[Hypothesis]:
|
|
72
|
+
raise NotImplementedError("HypothesisGenerator is planned for Stage 2.")
|
|
73
|
+
|
|
74
|
+
def mutate(self, hypothesis: Hypothesis, feedback: Any = None) -> list[Hypothesis]:
|
|
75
|
+
"""Derive refined/adjacent hypotheses from one that was tested."""
|
|
76
|
+
raise NotImplementedError("HypothesisGenerator.mutate is planned for Stage 2.")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Serialization helpers (needed by JsonlStore and loop checkpointing)
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def hypothesis_to_dict(h: Hypothesis) -> dict[str, Any]:
|
|
85
|
+
"""Serialize a Hypothesis to a JSON-compatible dict."""
|
|
86
|
+
plain = h.plain_statement or (h.metadata.get("plain_statement", "") if h.metadata else "")
|
|
87
|
+
return {
|
|
88
|
+
"statement": h.statement,
|
|
89
|
+
"plain_statement": plain,
|
|
90
|
+
"target_model": h.target_model,
|
|
91
|
+
"predicted_failure_mode": h.predicted_failure_mode,
|
|
92
|
+
"test_design": h.test_design,
|
|
93
|
+
"expected_association": h.expected_association,
|
|
94
|
+
"status": h.status.value if h.status else HypothesisStatus.PROPOSED.value,
|
|
95
|
+
"parent_id": h.parent_id,
|
|
96
|
+
"id": h.id,
|
|
97
|
+
"evidence": list(h.evidence),
|
|
98
|
+
"metadata": dict(h.metadata),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def hypothesis_from_dict(data: dict[str, Any]) -> Hypothesis:
|
|
103
|
+
"""Deserialize a Hypothesis from a dict (e.g., loaded from JSONL)."""
|
|
104
|
+
raw_status = data.get("status", HypothesisStatus.PROPOSED.value)
|
|
105
|
+
try:
|
|
106
|
+
status = HypothesisStatus(raw_status)
|
|
107
|
+
except ValueError:
|
|
108
|
+
status = HypothesisStatus.PROPOSED
|
|
109
|
+
plain = data.get("plain_statement") or (data.get("metadata", {}).get("plain_statement", "") if isinstance(data.get("metadata"), dict) else "")
|
|
110
|
+
return Hypothesis(
|
|
111
|
+
statement=str(data.get("statement", "")),
|
|
112
|
+
target_model=str(data.get("target_model", "")),
|
|
113
|
+
predicted_failure_mode=str(data.get("predicted_failure_mode", "")),
|
|
114
|
+
plain_statement=plain,
|
|
115
|
+
test_design=str(data.get("test_design", "")),
|
|
116
|
+
expected_association=str(data.get("expected_association", "")),
|
|
117
|
+
status=status,
|
|
118
|
+
parent_id=data.get("parent_id"),
|
|
119
|
+
id=data.get("id", ""),
|
|
120
|
+
evidence=list(data.get("evidence", [])),
|
|
121
|
+
metadata=dict(data.get("metadata", {})),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _hyp_val(obj: Any, name: str, default: Any = None) -> Any:
|
|
126
|
+
"""Read an attribute or mapping key, whichever *obj* is."""
|
|
127
|
+
if isinstance(obj, dict):
|
|
128
|
+
return obj.get(name, default)
|
|
129
|
+
return getattr(obj, name, default)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def hypothesis_id(hypothesis: Any) -> str:
|
|
133
|
+
"""Stable join key for one hypothesis, shared by every M3/M4/M5 log entry.
|
|
134
|
+
|
|
135
|
+
``Hypothesis.id`` defaults to ``""`` and nothing in the M1-M5 loop fills it
|
|
136
|
+
in, so every stage that names a hypothesis has to derive one — deriving it
|
|
137
|
+
per call site is how the same object ends up with two different names from
|
|
138
|
+
two different stages, and the join between a claim and its verdict goes
|
|
139
|
+
silently empty. Falls back to a hash of the statement, mirroring
|
|
140
|
+
``eval_agent.loop._hyp_key`` — deterministic, so two stages holding the
|
|
141
|
+
same hypothesis always agree. ``evalrx.contract.emit.hypothesis_id``
|
|
142
|
+
delegates here so the optional contract sidecar and the primary log never
|
|
143
|
+
disagree on the id.
|
|
144
|
+
"""
|
|
145
|
+
hid = str(_hyp_val(hypothesis, "id", "") or "").strip()
|
|
146
|
+
if hid:
|
|
147
|
+
return hid
|
|
148
|
+
statement = str(_hyp_val(hypothesis, "statement", "") or "").strip()
|
|
149
|
+
if statement:
|
|
150
|
+
return "h-" + hashlib.sha1(statement.encode("utf-8")).hexdigest()[:12]
|
|
151
|
+
return "unknown"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ManualHypothesisGenerator(HypothesisGenerator):
|
|
155
|
+
"""A non-LLM generator: drains a fixed queue, or calls an injected ``proposer``.
|
|
156
|
+
|
|
157
|
+
Lets the loop run + be unit-tested without an LLM; swap in an LLM-backed
|
|
158
|
+
generator later without touching the loop.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, hypotheses: list[Hypothesis] | None = None, proposer: Any = None) -> None:
|
|
162
|
+
self._queue: list[Hypothesis] = list(hypotheses or [])
|
|
163
|
+
self._proposer = proposer
|
|
164
|
+
|
|
165
|
+
def propose(self, context: Any = None) -> list[Hypothesis]:
|
|
166
|
+
if self._proposer is not None:
|
|
167
|
+
return list(self._proposer(context))
|
|
168
|
+
out, self._queue = self._queue, []
|
|
169
|
+
return out
|
|
170
|
+
|
|
171
|
+
def mutate(self, hypothesis: Hypothesis, feedback: Any = None) -> list[Hypothesis]:
|
|
172
|
+
return []
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Hold a run directory's label-bearing artifacts in memory while the fix
|
|
2
|
+
stage runs, so nothing the repair coder or its sandbox can read from disk
|
|
3
|
+
reveals gold answers or per-case pass/fail.
|
|
4
|
+
|
|
5
|
+
Why this exists
|
|
6
|
+
---------------
|
|
7
|
+
The fix stage already withholds labels at the *prompt* level: the proposer sees
|
|
8
|
+
EXPLORE cases only, the coded-pipeline payload carries ``id`` / ``prompt`` /
|
|
9
|
+
``baseline_output`` and nothing else, and every FixAgent prompt says so. But the
|
|
10
|
+
coder CLI runs with ``Bash Edit Write Read`` and the pipeline sandbox is a plain
|
|
11
|
+
subprocess, both rooted *inside* the run directory — where, by the time the fix
|
|
12
|
+
stage starts, M1..M4 have written the same labels many times over:
|
|
13
|
+
|
|
14
|
+
* ``baseline.json`` and ``logs/report/discovery_cases.json`` — every case with
|
|
15
|
+
``expected`` and ``label``, CONFIRM cases included;
|
|
16
|
+
* ``logs/run.json`` — ``case_record`` events with ``expected``, ``label``
|
|
17
|
+
and ``metadata.gold``;
|
|
18
|
+
* ``logs/artifacts/*.result.json``, ``logs/contract/c*.m1.json``,
|
|
19
|
+
``explore/records.json`` — per-case analyzer signals, among them
|
|
20
|
+
``gold_yes``, which *is* the gold answer on a yes/no task;
|
|
21
|
+
* ``logs/workspace/post_m5/cases.json`` — the M5 experiment's cases with gold.
|
|
22
|
+
|
|
23
|
+
``cat ../../../baseline.json`` from a fix workspace is two directory levels
|
|
24
|
+
away. A candidate that joins its cases to any of these by id can hard-code the
|
|
25
|
+
held-out answers; the frozen-model control catches the crude lookup-table form
|
|
26
|
+
but not one that also manufactures consensus support through the bridge.
|
|
27
|
+
|
|
28
|
+
What it does
|
|
29
|
+
------------
|
|
30
|
+
:func:`quarantine_run_dir` is a context manager. On entry it reads every
|
|
31
|
+
regular file under the run directory into memory and unlinks it (directory
|
|
32
|
+
names stay; they carry nothing). Files handed in as ``append_logs`` — the
|
|
33
|
+
structured event log, which a :class:`logging.FileHandler` holds open in
|
|
34
|
+
append mode — are truncated in place instead, so events the fix stage writes
|
|
35
|
+
still land in the same file. On exit (also on an exception) everything is put
|
|
36
|
+
back byte-for-byte; an append log becomes ``old + what the fix stage wrote``;
|
|
37
|
+
a path the fix stage re-created with new content keeps the new content and
|
|
38
|
+
the pre-fix bytes are saved beside it as ``<name>.pre_fix`` — nothing
|
|
39
|
+
is ever lost. A ``fix_quarantine.json`` manifest is written on exit so the run
|
|
40
|
+
records exactly which files were hidden.
|
|
41
|
+
|
|
42
|
+
What it cannot do
|
|
43
|
+
-----------------
|
|
44
|
+
It only governs what *this run wrote*. A dataset manifest on a bind mount
|
|
45
|
+
(``/app/work/data/<dataset>/manifest.json`` in the benchmark containers)
|
|
46
|
+
carries ``gold`` too and cannot be hidden from a root process in the same
|
|
47
|
+
container — that needs uid separation or host-side label delivery, which is
|
|
48
|
+
out of scope here. The gap is deliberate and documented, not forgotten.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import json
|
|
54
|
+
import logging
|
|
55
|
+
import os
|
|
56
|
+
from collections.abc import Iterable, Iterator
|
|
57
|
+
from contextlib import contextmanager
|
|
58
|
+
from dataclasses import dataclass, field
|
|
59
|
+
from datetime import datetime, timezone
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
MANIFEST_NAME = "fix_quarantine.json"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class QuarantineReport:
|
|
69
|
+
"""What was hidden and how it was restored (also written as JSON)."""
|
|
70
|
+
|
|
71
|
+
root: Path
|
|
72
|
+
hidden: list[str] = field(default_factory=list)
|
|
73
|
+
truncated: list[str] = field(default_factory=list)
|
|
74
|
+
kept: list[str] = field(default_factory=list)
|
|
75
|
+
restored: list[str] = field(default_factory=list)
|
|
76
|
+
merged: list[str] = field(default_factory=list)
|
|
77
|
+
conflicts: list[str] = field(default_factory=list)
|
|
78
|
+
n_bytes: int = 0
|
|
79
|
+
|
|
80
|
+
def to_json(self) -> dict:
|
|
81
|
+
return {
|
|
82
|
+
"root": str(self.root),
|
|
83
|
+
"n_hidden": len(self.hidden),
|
|
84
|
+
"n_bytes": self.n_bytes,
|
|
85
|
+
"hidden": sorted(self.hidden),
|
|
86
|
+
"truncated_in_place": sorted(self.truncated),
|
|
87
|
+
"kept_visible": sorted(self.kept),
|
|
88
|
+
"merged_append_logs": sorted(self.merged),
|
|
89
|
+
"conflicts_saved_as_pre_fix": sorted(self.conflicts),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _under(rel: str, prefixes: tuple[str, ...]) -> bool:
|
|
94
|
+
return any(rel == p or rel.startswith(p.rstrip("/") + "/") for p in prefixes)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _rel(path: Path, root: Path) -> str:
|
|
98
|
+
return path.relative_to(root).as_posix()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@contextmanager
|
|
102
|
+
def quarantine_run_dir(
|
|
103
|
+
run_dir: "str | os.PathLike[str]",
|
|
104
|
+
*,
|
|
105
|
+
append_logs: Iterable["str | os.PathLike[str]"] = (),
|
|
106
|
+
rewrite_logs: Iterable["str | os.PathLike[str]"] = (),
|
|
107
|
+
keep: Iterable[str] = (),
|
|
108
|
+
write_manifest: bool = True,
|
|
109
|
+
) -> Iterator[QuarantineReport]:
|
|
110
|
+
"""Hide every file under *run_dir* for the duration of the block.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
run_dir: The run directory (``baseline.json``, ``logs/``, ...).
|
|
114
|
+
append_logs: Files that a live handler holds open in append mode.
|
|
115
|
+
They are truncated to zero length instead of unlinked
|
|
116
|
+
and restored as ``old + new``. RunLoggerV2 rewrites its
|
|
117
|
+
documents atomically instead of appending, so its
|
|
118
|
+
callers pass ``rewrite_logs`` (see
|
|
119
|
+
``RunLoggerV2.managed_json_paths``), not this.
|
|
120
|
+
keep: Relative path prefixes (POSIX style) to leave visible,
|
|
121
|
+
e.g. ``("logs/fixes",)`` when resuming a fix search.
|
|
122
|
+
write_manifest: Write ``fix_quarantine.json`` into *run_dir* on exit.
|
|
123
|
+
|
|
124
|
+
Yields the :class:`QuarantineReport`, filled in as the block runs.
|
|
125
|
+
"""
|
|
126
|
+
root = Path(run_dir).resolve()
|
|
127
|
+
keep_prefixes = tuple(k.strip("/") for k in keep if k.strip("/"))
|
|
128
|
+
append_rel = set()
|
|
129
|
+
for p in append_logs:
|
|
130
|
+
p = Path(p).resolve()
|
|
131
|
+
try:
|
|
132
|
+
append_rel.add(_rel(p, root))
|
|
133
|
+
except ValueError:
|
|
134
|
+
logger.warning("label quarantine: append log %s is outside %s; ignored", p, root)
|
|
135
|
+
rewrite_rel = set()
|
|
136
|
+
for p in rewrite_logs:
|
|
137
|
+
p = Path(p).resolve()
|
|
138
|
+
try:
|
|
139
|
+
rewrite_rel.add(_rel(p, root))
|
|
140
|
+
except ValueError:
|
|
141
|
+
logger.warning("label quarantine: rewrite log %s is outside %s; ignored", p, root)
|
|
142
|
+
|
|
143
|
+
report = QuarantineReport(root=root)
|
|
144
|
+
stash: dict[str, bytes] = {}
|
|
145
|
+
|
|
146
|
+
if root.is_dir():
|
|
147
|
+
for dirpath, _dirnames, filenames in os.walk(root):
|
|
148
|
+
for name in filenames:
|
|
149
|
+
path = Path(dirpath) / name
|
|
150
|
+
if path.is_symlink():
|
|
151
|
+
continue
|
|
152
|
+
rel = _rel(path, root)
|
|
153
|
+
if rel == MANIFEST_NAME or _under(rel, keep_prefixes):
|
|
154
|
+
report.kept.append(rel)
|
|
155
|
+
continue
|
|
156
|
+
data = path.read_bytes()
|
|
157
|
+
stash[rel] = data
|
|
158
|
+
report.n_bytes += len(data)
|
|
159
|
+
if rel in append_rel:
|
|
160
|
+
# A logging.FileHandler appends through an O_APPEND fd:
|
|
161
|
+
# after truncation its next write lands at offset 0.
|
|
162
|
+
with path.open("r+b") as handle:
|
|
163
|
+
handle.truncate(0)
|
|
164
|
+
report.truncated.append(rel)
|
|
165
|
+
else:
|
|
166
|
+
path.unlink()
|
|
167
|
+
report.hidden.append(rel)
|
|
168
|
+
logger.info(
|
|
169
|
+
"label quarantine: hid %d file(s), %d bytes, under %s for the fix stage",
|
|
170
|
+
len(report.hidden), report.n_bytes, root,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
yield report
|
|
175
|
+
finally:
|
|
176
|
+
for rel, old in stash.items():
|
|
177
|
+
path = root / rel
|
|
178
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
if rel in append_rel:
|
|
180
|
+
new = path.read_bytes() if path.exists() else b""
|
|
181
|
+
path.write_bytes(old + new)
|
|
182
|
+
report.merged.append(rel)
|
|
183
|
+
elif rel in rewrite_rel and path.exists() and path.stat().st_size > 0:
|
|
184
|
+
# Atomic JSON loggers rewrite a complete in-memory document;
|
|
185
|
+
# that new document already includes everything in `old`.
|
|
186
|
+
# Keeping it avoids a redundant, non-JSON `.pre_fix` sibling.
|
|
187
|
+
report.merged.append(rel)
|
|
188
|
+
elif path.exists() and path.stat().st_size > 0:
|
|
189
|
+
# Appended AFTER the extension so a `*.json` scan (the contract
|
|
190
|
+
# index) never mistakes the copy for a payload.
|
|
191
|
+
side = path.with_name(path.name + ".pre_fix")
|
|
192
|
+
side.write_bytes(old)
|
|
193
|
+
report.conflicts.append(rel)
|
|
194
|
+
logger.warning(
|
|
195
|
+
"label quarantine: %s was rewritten during the fix stage; "
|
|
196
|
+
"pre-fix content saved as %s", rel, side.name,
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
path.write_bytes(old)
|
|
200
|
+
report.restored.append(rel)
|
|
201
|
+
if write_manifest and root.is_dir():
|
|
202
|
+
payload = report.to_json()
|
|
203
|
+
payload["restored_at"] = datetime.now(timezone.utc).isoformat()
|
|
204
|
+
(root / MANIFEST_NAME).write_text(
|
|
205
|
+
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
__all__ = ["MANIFEST_NAME", "QuarantineReport", "quarantine_run_dir"]
|