world-model-optimizer 0.2.0__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.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
wmo/harness/create.py
ADDED
|
@@ -0,0 +1,1191 @@
|
|
|
1
|
+
"""`wmo optimize`: budgeted search over harness deltas, gated by non-regression.
|
|
2
|
+
|
|
3
|
+
Each iteration freezes the current champion, clusters its failures into mechanisms, and asks the
|
|
4
|
+
proposer for a sibling batch of `HarnessDelta` objects against one size-weighted,
|
|
5
|
+
expansion-discounted cluster. Every sibling is applied and evaluated against that same frozen
|
|
6
|
+
champion. After the full batch resolves, at most one gate-eligible sibling becomes the next
|
|
7
|
+
iteration's champion:
|
|
8
|
+
|
|
9
|
+
- **Tier 1 — regression suite**: the child's score on the suite (tasks the search has already
|
|
10
|
+
mastered) must not drop below the champion's. Newly-passing tasks promote into the suite on
|
|
11
|
+
accept, so wins are locked in and later deltas cannot quietly trade them away.
|
|
12
|
+
- **Tier 2 — full split**: the child's overall success rate must be at least the best seen.
|
|
13
|
+
- **Tier 3 — held-out (optional)**: with a holdout task file, the child must also be no worse than
|
|
14
|
+
the champion on tasks the proposer never saw evidence from.
|
|
15
|
+
|
|
16
|
+
Ties pass every gate tier: with k passes per task, scores are coarse, and "no worse" is the
|
|
17
|
+
eligibility contract. When multiple siblings are eligible, full success wins, then assertion
|
|
18
|
+
fraction, then lower proposal index. Every proposed delta, whether selected, rejected, or invalid
|
|
19
|
+
before eval, is recorded in the archive with its verdict. The run as a whole is only as
|
|
20
|
+
reproducible as its providers because proposals and rollouts sample real models at temperature.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import TYPE_CHECKING, Literal
|
|
28
|
+
|
|
29
|
+
from pydantic import BaseModel, Field, model_validator
|
|
30
|
+
|
|
31
|
+
from wmo.engine.world_model import WorldModel
|
|
32
|
+
from wmo.evals.closed_loop import DEFAULT_K, ClosedLoopReport, evaluate_closed_loop
|
|
33
|
+
from wmo.evals.gold import GoldJudge
|
|
34
|
+
from wmo.evals.tasks import TaskSpec
|
|
35
|
+
from wmo.harness.delta import FailureSignature, GateRecord, HarnessDelta, apply_delta
|
|
36
|
+
from wmo.harness.doc import HarnessDoc
|
|
37
|
+
from wmo.harness.e2b_sandbox import SandboxUsage
|
|
38
|
+
from wmo.harness.mutate import render_evidence
|
|
39
|
+
from wmo.harness.proposer import DeltaProposer, ProposalFailure
|
|
40
|
+
from wmo.harness.runtime import (
|
|
41
|
+
HarnessSearchCancelled,
|
|
42
|
+
RuntimeCancelled,
|
|
43
|
+
TokenUsage,
|
|
44
|
+
combine_usage,
|
|
45
|
+
)
|
|
46
|
+
from wmo.providers.base import Provider
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
# Only the annotation: pi_e2b (the optional e2b extra's consumer) is imported lazily where
|
|
50
|
+
# the pool is actually constructed.
|
|
51
|
+
from wmo.harness.pi_e2b import E2BSandboxPool
|
|
52
|
+
|
|
53
|
+
# Non-regression tolerance: fmean over identical verdicts must compare equal, never fail a gate
|
|
54
|
+
# on float noise.
|
|
55
|
+
_TIE_EPS = 1e-9
|
|
56
|
+
|
|
57
|
+
ALL_PASS_MECHANISM = "none: all tasks pass"
|
|
58
|
+
|
|
59
|
+
FailureClusterKey = tuple[str, str, tuple[str, ...]]
|
|
60
|
+
|
|
61
|
+
# Reports (iteration, champion name, success rate, changed); iteration 0 is the seed.
|
|
62
|
+
CreateProgress = Callable[[int, str, float, bool], None]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DeltaArchive(BaseModel):
|
|
66
|
+
"""The full search record: a root snapshot plus every audited delta, in proposal order.
|
|
67
|
+
|
|
68
|
+
Accepted deltas form the lineage (`parent_doc_hash -> delta -> child_doc_hash`); any doc in it
|
|
69
|
+
is reconstructable by folding them from the seed. Rejected and invalid deltas are kept too —
|
|
70
|
+
with their verdicts — because "which kinds of edits fail on which failure classes" is as
|
|
71
|
+
queryable a question as which succeed.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
seed: HarnessDoc
|
|
75
|
+
deltas: list[HarnessDelta] = Field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
def accepted(self) -> list[HarnessDelta]:
|
|
78
|
+
return [d for d in self.deltas if d.verdict is not None and d.verdict.accepted]
|
|
79
|
+
|
|
80
|
+
def reconstruct(self, doc_hash: str) -> HarnessDoc:
|
|
81
|
+
"""Fold accepted deltas from the seed until `doc_hash` is produced."""
|
|
82
|
+
docs = {self.seed.doc_hash: self.seed}
|
|
83
|
+
for delta in self.accepted():
|
|
84
|
+
parent = docs.get(delta.parent_doc_hash)
|
|
85
|
+
if parent is not None:
|
|
86
|
+
child = apply_delta(parent, delta.model_copy(deep=True), parent.name)
|
|
87
|
+
docs[child.doc_hash] = child
|
|
88
|
+
if doc_hash not in docs:
|
|
89
|
+
raise ValueError(f"doc {doc_hash[:12]} is not in this archive's accepted lineage")
|
|
90
|
+
return docs[doc_hash]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ProposalRecord(BaseModel):
|
|
94
|
+
"""One proposal in an iteration's sibling batch, in stable proposal order.
|
|
95
|
+
|
|
96
|
+
A dead proposal (every outcome but ``scored``) ends before a full-split evaluation. Scored
|
|
97
|
+
proposals distinguish gate eligibility from final selection: several siblings may satisfy
|
|
98
|
+
the frozen non-regression gate, but only the best eligible sibling is selected. The
|
|
99
|
+
``champion_score`` is always the frozen pre-iteration champion score, which is the comparison
|
|
100
|
+
baseline and the honest plotting level for a dead proposal.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
iteration: int = Field(ge=1)
|
|
104
|
+
proposal_index: int = Field(ge=1)
|
|
105
|
+
outcome: Literal["scored", "screened", "invalid", "unusable", "proposer_error"]
|
|
106
|
+
candidate: str | None = None
|
|
107
|
+
candidate_doc_hash: str | None = None
|
|
108
|
+
delta_id: str | None = None
|
|
109
|
+
trigger: FailureSignature | None = None
|
|
110
|
+
expected_effect: str | None = None
|
|
111
|
+
ops: list[str] = Field(default_factory=list) # "replace prompt:main" style summaries
|
|
112
|
+
rationales: list[str] = Field(default_factory=list)
|
|
113
|
+
reason: str | None = None
|
|
114
|
+
score: float | None = None # full-suite success rate; scored proposals only
|
|
115
|
+
gate_eligible: bool | None = None # frozen non-regression gate; scored proposals only
|
|
116
|
+
selected: bool = False # true only for the iteration winner
|
|
117
|
+
screen_child: float | None = None # trigger-cluster means; screened attempts only
|
|
118
|
+
screen_parent: float | None = None
|
|
119
|
+
screen_child_fraction: float | None = None # denser assertion-level screen signal
|
|
120
|
+
screen_parent_fraction: float | None = None
|
|
121
|
+
champion_score: float
|
|
122
|
+
|
|
123
|
+
@model_validator(mode="after")
|
|
124
|
+
def _validate_state(self) -> ProposalRecord:
|
|
125
|
+
"""Reject impossible scored, dead, and selected record combinations."""
|
|
126
|
+
if self.outcome == "scored":
|
|
127
|
+
if self.score is None or self.gate_eligible is None:
|
|
128
|
+
raise ValueError("scored proposals require score and gate_eligible")
|
|
129
|
+
if self.candidate is None or self.candidate_doc_hash is None or self.delta_id is None:
|
|
130
|
+
raise ValueError("scored proposals require candidate and delta identities")
|
|
131
|
+
elif self.score is not None or self.gate_eligible is not None or self.selected:
|
|
132
|
+
raise ValueError("dead proposals cannot carry score, gate eligibility, or selection")
|
|
133
|
+
if self.selected and not self.gate_eligible:
|
|
134
|
+
raise ValueError("selected proposals must be gate eligible")
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class CreateResult(BaseModel):
|
|
139
|
+
"""What a create run produced: the champion, its score, and the full search record."""
|
|
140
|
+
|
|
141
|
+
best: HarnessDoc
|
|
142
|
+
best_score: float
|
|
143
|
+
archive: DeltaArchive
|
|
144
|
+
reports: dict[str, ClosedLoopReport] = Field(default_factory=dict) # by doc_hash
|
|
145
|
+
holdout_reports: dict[str, ClosedLoopReport] = Field(default_factory=dict) # by doc_hash
|
|
146
|
+
suite: list[str] = Field(default_factory=list) # final regression suite (task ids)
|
|
147
|
+
skipped: int = 0 # proposals unusable or invalid before evaluation
|
|
148
|
+
proposal_records: list[ProposalRecord] = Field(default_factory=list)
|
|
149
|
+
screened: int = 0 # deltas rejected at the cheap trigger-cluster screen (no full eval spent)
|
|
150
|
+
confirmations: int = 0 # narrow vetoes retried at higher k (see `narrow_failing_tiers`)
|
|
151
|
+
iterations: int = 0
|
|
152
|
+
proposal_batch_size: int = 1
|
|
153
|
+
# Spend meters over the WHOLE search (seed, screens, full splits, holdout, confirmations).
|
|
154
|
+
# worker_usage: worker-LLM tokens from self-metering runtimes (the pi worker path; None on
|
|
155
|
+
# provider-wrapped runtimes, which are metered upstream). sandbox_usage: E2B sandbox count +
|
|
156
|
+
# lifetime seconds (None on the local backend).
|
|
157
|
+
worker_usage: TokenUsage | None = None
|
|
158
|
+
sandbox_usage: SandboxUsage | None = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class _ScoredProposal:
|
|
163
|
+
"""One fully scored sibling awaiting iteration-level winner selection."""
|
|
164
|
+
|
|
165
|
+
proposal_index: int
|
|
166
|
+
child: HarnessDoc
|
|
167
|
+
delta: HarnessDelta
|
|
168
|
+
report: ClosedLoopReport
|
|
169
|
+
gate: GateRecord
|
|
170
|
+
record: ProposalRecord
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cluster_failures(report: ClosedLoopReport, tasks: list[TaskSpec]) -> list[FailureSignature]:
|
|
174
|
+
"""Group failing tasks into mechanisms, deterministically — no LLM, no entropy.
|
|
175
|
+
|
|
176
|
+
Two failing tasks share a mechanism when they share an unmet gold assertion (connected
|
|
177
|
+
components over the task/assertion graph). The cluster's `mechanism` label is its most common
|
|
178
|
+
unmet assertion (ties broken lexicographically); clusters are ordered largest-first so the
|
|
179
|
+
size-weighted, expansion-discounted selector has a stable base order. A failing task whose
|
|
180
|
+
verdicts carry no per-assertion detail (an unparseable judge reply) forms its own cluster.
|
|
181
|
+
"""
|
|
182
|
+
unmet_by_task: dict[str, list[str]] = {}
|
|
183
|
+
for task in tasks:
|
|
184
|
+
outcome = report.per_task.get(task.task_id)
|
|
185
|
+
if outcome is None or outcome.success_rate >= 1.0:
|
|
186
|
+
continue
|
|
187
|
+
seen: set[str] = set()
|
|
188
|
+
unmet: list[str] = []
|
|
189
|
+
for verdict in outcome.verdicts:
|
|
190
|
+
for result in verdict.assertions:
|
|
191
|
+
if not result.passed and result.assertion not in seen:
|
|
192
|
+
seen.add(result.assertion)
|
|
193
|
+
unmet.append(result.assertion)
|
|
194
|
+
unmet_by_task[task.task_id] = unmet
|
|
195
|
+
|
|
196
|
+
clusters: list[FailureSignature] = []
|
|
197
|
+
assigned: set[str] = set()
|
|
198
|
+
for task_id in sorted(unmet_by_task):
|
|
199
|
+
if task_id in assigned:
|
|
200
|
+
continue
|
|
201
|
+
# Flood-fill the component: tasks connected through shared unmet assertions.
|
|
202
|
+
member_ids = {task_id}
|
|
203
|
+
assertions = set(unmet_by_task[task_id])
|
|
204
|
+
grew = True
|
|
205
|
+
while grew:
|
|
206
|
+
grew = False
|
|
207
|
+
for other, other_unmet in unmet_by_task.items():
|
|
208
|
+
if other in member_ids or not assertions.intersection(other_unmet):
|
|
209
|
+
continue
|
|
210
|
+
member_ids.add(other)
|
|
211
|
+
assertions.update(other_unmet)
|
|
212
|
+
grew = True
|
|
213
|
+
assigned.update(member_ids)
|
|
214
|
+
counts: dict[str, int] = {}
|
|
215
|
+
for member in member_ids:
|
|
216
|
+
for assertion in unmet_by_task[member]:
|
|
217
|
+
counts[assertion] = counts.get(assertion, 0) + 1
|
|
218
|
+
# Most common unmet assertion labels the cluster; max() keeps the first (lexicographically
|
|
219
|
+
# smallest) among ties because the candidates are pre-sorted.
|
|
220
|
+
mechanism = (
|
|
221
|
+
max(sorted(counts), key=lambda a: counts[a])
|
|
222
|
+
if counts
|
|
223
|
+
else "run failed without per-assertion verdicts"
|
|
224
|
+
)
|
|
225
|
+
clusters.append(
|
|
226
|
+
FailureSignature(
|
|
227
|
+
mechanism=mechanism,
|
|
228
|
+
task_ids=sorted(member_ids),
|
|
229
|
+
unmet_assertions=sorted(assertions),
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
clusters.sort(key=lambda c: (-len(c.task_ids), c.mechanism))
|
|
233
|
+
return clusters
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def select_failure_cluster(
|
|
237
|
+
clusters: list[FailureSignature],
|
|
238
|
+
expansion_counts: dict[FailureClusterKey, int],
|
|
239
|
+
*,
|
|
240
|
+
parent_doc_hash: str,
|
|
241
|
+
) -> FailureSignature:
|
|
242
|
+
"""Choose a high-impact cluster without getting trapped on one exhausted failure.
|
|
243
|
+
|
|
244
|
+
A cluster's priority is ``task_count / (1 + prior_iterations_on_this_parent)``. Large mechanisms
|
|
245
|
+
still receive proportionally more search budget, but equally sized singleton failures rotate
|
|
246
|
+
after one batch instead of a deterministic ``clusters[0]`` absorbing the entire run. The
|
|
247
|
+
stable mechanism/task ordering resolves exact ties without entropy.
|
|
248
|
+
"""
|
|
249
|
+
if not clusters:
|
|
250
|
+
raise ValueError("cannot select from an empty failure-cluster list")
|
|
251
|
+
|
|
252
|
+
def _priority(cluster: FailureSignature) -> tuple[float, str, tuple[str, ...]]:
|
|
253
|
+
key = _failure_cluster_key(parent_doc_hash, cluster)
|
|
254
|
+
prior_iterations = expansion_counts.get(key, 0)
|
|
255
|
+
return (
|
|
256
|
+
-(len(cluster.task_ids) / (1 + prior_iterations)),
|
|
257
|
+
cluster.mechanism,
|
|
258
|
+
tuple(cluster.task_ids),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return min(clusters, key=_priority)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _failure_cluster_key(parent_doc_hash: str, cluster: FailureSignature) -> FailureClusterKey:
|
|
265
|
+
return parent_doc_hash, cluster.mechanism, tuple(cluster.task_ids)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def narrow_failing_tiers(
|
|
269
|
+
verdict: GateRecord,
|
|
270
|
+
*,
|
|
271
|
+
k: int,
|
|
272
|
+
n_suite: int,
|
|
273
|
+
n_holdout: int,
|
|
274
|
+
margin_attempts: int = 2,
|
|
275
|
+
) -> list[str] | None:
|
|
276
|
+
"""Which tiers vetoed this delta narrowly enough to deserve a re-measurement.
|
|
277
|
+
|
|
278
|
+
Eligible only when the delta strictly won the full split: the question a confirmation
|
|
279
|
+
answers is "was this win vetoed by measurement noise?", not "can a loser get lucky?".
|
|
280
|
+
A tier's veto is narrow when its regression is at most `margin_attempts` single-attempt
|
|
281
|
+
flips wide (one flip changes a tier mean by 1/(k*n)). Returns the narrowly-failing tier
|
|
282
|
+
names, or None when the delta is ineligible (no win, a wide veto, or no veto at all).
|
|
283
|
+
"""
|
|
284
|
+
if verdict.accepted or verdict.full_delta <= _TIE_EPS:
|
|
285
|
+
return None
|
|
286
|
+
# A confirmation may only revisit the explicitly returned binary vetoes. Do not let it erase
|
|
287
|
+
# a separate tied-success dense veto on another tier.
|
|
288
|
+
if abs(verdict.suite_delta) <= _TIE_EPS and verdict.suite_fraction_delta < -_TIE_EPS:
|
|
289
|
+
return None
|
|
290
|
+
if (
|
|
291
|
+
verdict.holdout_delta is not None
|
|
292
|
+
and abs(verdict.holdout_delta) <= _TIE_EPS
|
|
293
|
+
and verdict.holdout_fraction_delta is not None
|
|
294
|
+
and verdict.holdout_fraction_delta < -_TIE_EPS
|
|
295
|
+
):
|
|
296
|
+
return None
|
|
297
|
+
tiers: list[str] = []
|
|
298
|
+
if verdict.suite_delta < -_TIE_EPS:
|
|
299
|
+
if n_suite == 0 or verdict.suite_delta < -(margin_attempts / (k * n_suite)) - _TIE_EPS:
|
|
300
|
+
return None
|
|
301
|
+
tiers.append("suite")
|
|
302
|
+
if verdict.holdout_delta is not None and verdict.holdout_delta < -_TIE_EPS:
|
|
303
|
+
if (
|
|
304
|
+
n_holdout == 0
|
|
305
|
+
or verdict.holdout_delta < -(margin_attempts / (k * n_holdout)) - _TIE_EPS
|
|
306
|
+
):
|
|
307
|
+
return None
|
|
308
|
+
tiers.append("holdout")
|
|
309
|
+
return tiers or None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def gate_delta(
|
|
313
|
+
delta: HarnessDelta,
|
|
314
|
+
*,
|
|
315
|
+
child: ClosedLoopReport,
|
|
316
|
+
champion: ClosedLoopReport,
|
|
317
|
+
best_full: float,
|
|
318
|
+
suite: list[str],
|
|
319
|
+
child_holdout: ClosedLoopReport | None = None,
|
|
320
|
+
champion_holdout: ClosedLoopReport | None = None,
|
|
321
|
+
) -> GateRecord:
|
|
322
|
+
"""Lexicographic success/partial-credit gate plus optional held-out acceptance.
|
|
323
|
+
|
|
324
|
+
End-to-end task success remains the primary objective. When a tier's binary score ties, its
|
|
325
|
+
assertion-level fraction must not regress. This admits useful partial-progress stepping
|
|
326
|
+
stones without promoting a target-local improvement that silently damages more work across
|
|
327
|
+
the full split.
|
|
328
|
+
"""
|
|
329
|
+
suite_delta = _suite_rate(child, suite) - _suite_rate(champion, suite)
|
|
330
|
+
suite_fraction_delta = _suite_fraction(child, suite) - _suite_fraction(champion, suite)
|
|
331
|
+
full_delta = child.success_rate - best_full
|
|
332
|
+
full_fraction_delta = child.mean_fraction - champion.mean_fraction
|
|
333
|
+
holdout_delta = (
|
|
334
|
+
child_holdout.success_rate - champion_holdout.success_rate
|
|
335
|
+
if child_holdout is not None and champion_holdout is not None
|
|
336
|
+
else None
|
|
337
|
+
)
|
|
338
|
+
holdout_fraction_delta = (
|
|
339
|
+
child_holdout.mean_fraction - champion_holdout.mean_fraction
|
|
340
|
+
if child_holdout is not None and champion_holdout is not None
|
|
341
|
+
else None
|
|
342
|
+
)
|
|
343
|
+
failures: list[str] = []
|
|
344
|
+
if suite_delta < -_TIE_EPS:
|
|
345
|
+
failures.append(f"suite regressed by {-suite_delta:.3f}")
|
|
346
|
+
elif abs(suite_delta) <= _TIE_EPS and suite_fraction_delta < -_TIE_EPS:
|
|
347
|
+
failures.append(f"suite assertion fraction regressed by {-suite_fraction_delta:.3f}")
|
|
348
|
+
if full_delta < -_TIE_EPS:
|
|
349
|
+
failures.append(f"full split {child.success_rate:.3f} below best {best_full:.3f}")
|
|
350
|
+
elif abs(full_delta) <= _TIE_EPS and full_fraction_delta < -_TIE_EPS:
|
|
351
|
+
failures.append(f"full-split assertion fraction regressed by {-full_fraction_delta:.3f}")
|
|
352
|
+
if holdout_delta is not None and holdout_delta < -_TIE_EPS:
|
|
353
|
+
failures.append(f"held-out regressed by {-holdout_delta:.3f}")
|
|
354
|
+
elif (
|
|
355
|
+
holdout_delta is not None
|
|
356
|
+
and abs(holdout_delta) <= _TIE_EPS
|
|
357
|
+
and holdout_fraction_delta is not None
|
|
358
|
+
and holdout_fraction_delta < -_TIE_EPS
|
|
359
|
+
):
|
|
360
|
+
failures.append(f"held-out assertion fraction regressed by {-holdout_fraction_delta:.3f}")
|
|
361
|
+
flipped = sum(
|
|
362
|
+
1
|
|
363
|
+
for task_id in delta.trigger.task_ids
|
|
364
|
+
if (outcome := child.per_task.get(task_id)) is not None and outcome.success_rate >= 1.0
|
|
365
|
+
)
|
|
366
|
+
effect = (
|
|
367
|
+
f"trigger cluster: {flipped}/{len(delta.trigger.task_ids)} tasks now pass"
|
|
368
|
+
if delta.trigger.task_ids
|
|
369
|
+
else "no trigger cluster (all-pass parent)"
|
|
370
|
+
)
|
|
371
|
+
accepted = not failures
|
|
372
|
+
reason = ("accepted; " if accepted else "rejected: " + "; ".join(failures) + "; ") + effect
|
|
373
|
+
return GateRecord(
|
|
374
|
+
suite_delta=suite_delta,
|
|
375
|
+
suite_fraction_delta=suite_fraction_delta,
|
|
376
|
+
full_delta=full_delta,
|
|
377
|
+
full_fraction_delta=full_fraction_delta,
|
|
378
|
+
holdout_delta=holdout_delta,
|
|
379
|
+
holdout_fraction_delta=holdout_fraction_delta,
|
|
380
|
+
accepted=accepted,
|
|
381
|
+
reason=reason,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def create_harness(
|
|
386
|
+
name: str,
|
|
387
|
+
seed_doc: HarnessDoc,
|
|
388
|
+
tasks: list[TaskSpec],
|
|
389
|
+
world_model: WorldModel,
|
|
390
|
+
agent_provider: Provider,
|
|
391
|
+
proposer: DeltaProposer,
|
|
392
|
+
judge: GoldJudge,
|
|
393
|
+
*,
|
|
394
|
+
iterations: int = 5,
|
|
395
|
+
proposal_batch_size: int = 1,
|
|
396
|
+
k: int = DEFAULT_K,
|
|
397
|
+
holdout: list[TaskSpec] | None = None,
|
|
398
|
+
confirm_narrow_vetoes: bool = True,
|
|
399
|
+
harness_backend: Literal["local", "e2b"] = "local",
|
|
400
|
+
eval_concurrency: int | None = None,
|
|
401
|
+
e2b_template: str | None = None,
|
|
402
|
+
e2b_metadata: dict[str, str] | None = None,
|
|
403
|
+
on_progress: CreateProgress | None = None,
|
|
404
|
+
on_note: Callable[[str], None] | None = None,
|
|
405
|
+
on_proposal: Callable[[ProposalRecord], None] | None = None,
|
|
406
|
+
on_accept: Callable[[HarnessDoc, HarnessDelta, float], None] | None = None,
|
|
407
|
+
on_sandbox_usage: Callable[[SandboxUsage], None] | None = None,
|
|
408
|
+
should_cancel: Callable[[], bool] | None = None,
|
|
409
|
+
) -> CreateResult:
|
|
410
|
+
"""Search for a better harness under a fixed eval budget; the champion is renamed to `name`.
|
|
411
|
+
|
|
412
|
+
Scores the seed first, then runs ``iterations`` proposal batches. Each iteration asks the
|
|
413
|
+
proposer for ``proposal_batch_size`` siblings against the frozen champion, evaluates every
|
|
414
|
+
sibling against that same snapshot, and selects at most one winner. A dead proposal
|
|
415
|
+
(unusable, invalid, or screened out on its trigger cluster) ends early and cheaply. It is
|
|
416
|
+
counted, recorded in ``proposal_records``, and narrated via ``on_note`` without aborting its
|
|
417
|
+
siblings. ``on_proposal`` receives every final record in stable proposal order after
|
|
418
|
+
selection. ``on_progress`` receives the seed plus exactly one champion point per iteration,
|
|
419
|
+
including an unchanged point when no proposal wins. ``on_accept`` fires at most once per
|
|
420
|
+
iteration with the selected champion, its final delta verdict, and its full-suite score.
|
|
421
|
+
``on_note`` is an eager diagnostic stream, so dead-proposal and feedback-error notes may fire
|
|
422
|
+
while siblings are still evaluating. ``on_proposal`` waits for batch selection and then fires
|
|
423
|
+
in stable proposal order. The iteration's ``on_progress`` checkpoint follows those records.
|
|
424
|
+
|
|
425
|
+
``on_sandbox_usage`` fires after the shared E2B pool is successfully closed on every exit
|
|
426
|
+
path, including failures and cancellation, so callers can persist already-incurred evaluator
|
|
427
|
+
spend without requiring a partial result. An unproven close raises instead of publishing a
|
|
428
|
+
falsely final meter.
|
|
429
|
+
``should_cancel`` is checked before and after every score wave, before each proposal slot,
|
|
430
|
+
and after each batched proposer call. E2B runtimes also poll it while waiting for runner
|
|
431
|
+
frames, so cancellation aborts the active wave without judging partial cells. A provider/tool
|
|
432
|
+
call already in progress remains bounded by that call's own timeout. Cancellation raises
|
|
433
|
+
:class:`HarnessSearchCancelled` while the normal ``finally`` path retires sandbox resources.
|
|
434
|
+
|
|
435
|
+
Every rollout scores against the world-model simulation — the environment is always sim.
|
|
436
|
+
`harness_backend` picks where the harness PROCESS executes: `local` (the default) runs it
|
|
437
|
+
in/from this process exactly as before; `e2b` runs the real pi agent inside E2B sandboxes
|
|
438
|
+
(pi-node seeds only — that harness's context management is the thing under search), with its
|
|
439
|
+
tool calls still answered by the world model host-side. One `E2BSandboxPool` is shared across
|
|
440
|
+
score waves within a proposal batch, then its idle runners are retired before the next batch's
|
|
441
|
+
proposer call. This amortizes bootstrap work across sibling proposals without carrying E2B
|
|
442
|
+
command streams through the potentially long proposal gap between iterations. `eval_concurrency`
|
|
443
|
+
is how many (task, attempt) cells run at once; `None` means the backend default — 1
|
|
444
|
+
(sequential) for local, 0 (every cell at once, one pooled sandbox each) for e2b.
|
|
445
|
+
`e2b_template` names a prebaked sandbox template (node 22 + the pi runner deps) so e2b
|
|
446
|
+
rollouts skip bootstrap installs. `e2b_metadata` tags every sandbox created by the shared
|
|
447
|
+
evaluation pool, including fresh replacements for retired runners.
|
|
448
|
+
|
|
449
|
+
Verification is staged by cost: a child is first SCREENED on its own trigger cluster (the
|
|
450
|
+
2-3 failing tasks its delta claims to fix, k passes). The screen is lexicographic: full-task
|
|
451
|
+
success first, then assertion-level partial credit, so a real partial fix is not flattened
|
|
452
|
+
into a binary tie. If neither signal improves, the delta is rejected and archived for a
|
|
453
|
+
fraction of a full eval's cost. Repeated iterations discount
|
|
454
|
+
their cluster on that parent, preventing one environment-limited singleton from absorbing the
|
|
455
|
+
whole run. Held-out evals run only for children that pass tiers 1-2, so a bad delta costs at
|
|
456
|
+
most one full-split eval. Every judged delta (screened, rejected, or accepted) is fed back to
|
|
457
|
+
the proposer as history, so it iterates instead of re-proposing rejected ideas.
|
|
458
|
+
|
|
459
|
+
Symmetrically, a REJECTION can be noise: with k passes over small tiers, one unlucky attempt
|
|
460
|
+
can veto a genuine win. When `confirm_narrow_vetoes` is set, a delta that strictly won the
|
|
461
|
+
full split but failed suite/holdout within `narrow_failing_tiers`' margin gets that tier
|
|
462
|
+
re-measured — child AND champion, at 2k — and the re-measurement decides. The verdict records
|
|
463
|
+
the retrial either way, so the archive shows which accepts needed confirmation.
|
|
464
|
+
"""
|
|
465
|
+
if harness_backend not in ("local", "e2b"):
|
|
466
|
+
raise ValueError(f"unknown harness_backend {harness_backend!r}; choose local or e2b")
|
|
467
|
+
if proposal_batch_size < 1:
|
|
468
|
+
raise ValueError(f"proposal_batch_size must be positive, got {proposal_batch_size}")
|
|
469
|
+
if harness_backend == "e2b" and seed_doc.runtime_kind() != "pi-node":
|
|
470
|
+
raise ValueError(
|
|
471
|
+
"harness_backend='e2b' runs the pi-node harness process in sandboxes; seed "
|
|
472
|
+
f"runtime kind is {seed_doc.runtime_kind()!r}, which already runs in-process — "
|
|
473
|
+
"use harness_backend='local'"
|
|
474
|
+
)
|
|
475
|
+
sandbox_pool: E2BSandboxPool | None = None
|
|
476
|
+
if harness_backend == "e2b":
|
|
477
|
+
# Lazy: the e2b backend is an optional extra; local searches must import none of it.
|
|
478
|
+
from wmo.harness.pi_e2b import E2BSandboxPool as _Pool
|
|
479
|
+
|
|
480
|
+
sandbox_pool = _Pool(template=e2b_template, metadata=e2b_metadata)
|
|
481
|
+
|
|
482
|
+
cancelled: HarnessSearchCancelled | None = None
|
|
483
|
+
result: CreateResult | None = None
|
|
484
|
+
try:
|
|
485
|
+
|
|
486
|
+
def _check_cancelled() -> None:
|
|
487
|
+
if should_cancel is not None and should_cancel():
|
|
488
|
+
raise HarnessSearchCancelled("harness search cancelled")
|
|
489
|
+
|
|
490
|
+
def _note(message: str) -> None:
|
|
491
|
+
# Narration for iterations that produce NO on_progress event (unusable/invalid/screened
|
|
492
|
+
# proposals): without it a run whose proposals all fail looks like it never iterated.
|
|
493
|
+
if on_note is not None:
|
|
494
|
+
on_note(message)
|
|
495
|
+
|
|
496
|
+
docs: dict[str, HarnessDoc] = {seed_doc.doc_hash: seed_doc}
|
|
497
|
+
worker_usages: list[TokenUsage | None] = []
|
|
498
|
+
reports: dict[str, ClosedLoopReport] = {}
|
|
499
|
+
holdout_reports: dict[str, ClosedLoopReport] = {}
|
|
500
|
+
archive = DeltaArchive(seed=seed_doc)
|
|
501
|
+
failure_cluster_expansions: dict[FailureClusterKey, int] = {}
|
|
502
|
+
skipped = 0
|
|
503
|
+
screened = 0
|
|
504
|
+
confirmations = 0
|
|
505
|
+
|
|
506
|
+
def _score(
|
|
507
|
+
doc: HarnessDoc, split: list[TaskSpec], *, k_override: int | None = None
|
|
508
|
+
) -> ClosedLoopReport:
|
|
509
|
+
_check_cancelled()
|
|
510
|
+
k_eff = k if k_override is None else k_override
|
|
511
|
+
if harness_backend == "local":
|
|
512
|
+
concurrency = eval_concurrency if eval_concurrency is not None else 1
|
|
513
|
+
if concurrency != 1 and doc.runtime_kind() == "pi-node":
|
|
514
|
+
# Local pi runtimes are single-episode resources (one runner port/workdir, or
|
|
515
|
+
# one RunnerLink channel): parallel cells would collide. Checked per-doc because
|
|
516
|
+
# a delta can flip param:runtime-kind mid-search.
|
|
517
|
+
raise ValueError(
|
|
518
|
+
"pi-node harnesses run one episode at a time under harness_backend='local' "
|
|
519
|
+
"(single runner port/channel); use eval_concurrency=1 or "
|
|
520
|
+
"harness_backend='e2b'"
|
|
521
|
+
)
|
|
522
|
+
runtime = doc.runtime(agent_provider)
|
|
523
|
+
else:
|
|
524
|
+
# The pi process runs in pooled sandboxes; every cell at once by default. Tool calls
|
|
525
|
+
# still route to the world model — the environment is sim regardless of backend.
|
|
526
|
+
concurrency = eval_concurrency if eval_concurrency is not None else 0
|
|
527
|
+
runtime = doc.runtime(
|
|
528
|
+
agent_provider,
|
|
529
|
+
backend="e2b",
|
|
530
|
+
e2b_pool=sandbox_pool,
|
|
531
|
+
should_cancel=should_cancel,
|
|
532
|
+
)
|
|
533
|
+
try:
|
|
534
|
+
report = evaluate_closed_loop(
|
|
535
|
+
split,
|
|
536
|
+
world_model,
|
|
537
|
+
agent_provider,
|
|
538
|
+
judge,
|
|
539
|
+
label=doc.name,
|
|
540
|
+
k=k_eff,
|
|
541
|
+
concurrency=concurrency,
|
|
542
|
+
runtime=runtime,
|
|
543
|
+
should_cancel=should_cancel,
|
|
544
|
+
)
|
|
545
|
+
except RuntimeCancelled as exc:
|
|
546
|
+
# Cancelled cells are not scoreable outcomes: do not judge them. Converting at the
|
|
547
|
+
# search boundary preserves the public cancellation contract while the surrounding
|
|
548
|
+
# finally closes the shared pool and retires every active evaluator sandbox.
|
|
549
|
+
raise HarnessSearchCancelled(
|
|
550
|
+
"harness search cancelled", worker_usage=exc.worker_usage
|
|
551
|
+
) from exc
|
|
552
|
+
# Tally the pi worker's self-metered tokens across every score wave (seed, screens,
|
|
553
|
+
# full splits, holdout, confirmations): its LLM calls bypass the Provider, so this is
|
|
554
|
+
# the only record. None on backends whose runtimes don't self-meter (local).
|
|
555
|
+
worker_usages.append(report.worker_usage)
|
|
556
|
+
# Append first so a cancellation that lands after evaluation but before
|
|
557
|
+
# the report is consumed still carries the completed wave's spend.
|
|
558
|
+
_check_cancelled()
|
|
559
|
+
return report
|
|
560
|
+
|
|
561
|
+
seed_report = _score(seed_doc, tasks)
|
|
562
|
+
reports[seed_doc.doc_hash] = seed_report
|
|
563
|
+
if holdout:
|
|
564
|
+
holdout_reports[seed_doc.doc_hash] = _score(seed_doc, holdout)
|
|
565
|
+
if on_progress is not None:
|
|
566
|
+
on_progress(0, seed_doc.name, seed_report.success_rate, True)
|
|
567
|
+
|
|
568
|
+
champion_hash = seed_doc.doc_hash
|
|
569
|
+
best_full = seed_report.success_rate
|
|
570
|
+
# The regression suite: tasks the champion lineage has fully passed. Wins promote in
|
|
571
|
+
# on accept.
|
|
572
|
+
suite = sorted(
|
|
573
|
+
task.task_id
|
|
574
|
+
for task in tasks
|
|
575
|
+
if seed_report.per_task[task.task_id].success_rate >= 1.0 - _TIE_EPS
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
proposal_records: list[ProposalRecord] = []
|
|
579
|
+
|
|
580
|
+
def _stage_dead(records: list[ProposalRecord], record: ProposalRecord) -> None:
|
|
581
|
+
"""Stage one dead proposal for ordered publication after batch selection."""
|
|
582
|
+
records.append(record)
|
|
583
|
+
_note(
|
|
584
|
+
_dead_proposal_note(
|
|
585
|
+
record,
|
|
586
|
+
iterations=iterations,
|
|
587
|
+
batch_size=proposal_batch_size,
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
for iteration_index in range(1, iterations + 1):
|
|
592
|
+
_check_cancelled()
|
|
593
|
+
# Eval runners from the previous iteration would otherwise sit idle through the
|
|
594
|
+
# potentially long proposer call. Sibling score waves still share the newly warmed
|
|
595
|
+
# pool for this whole iteration.
|
|
596
|
+
if sandbox_pool is not None:
|
|
597
|
+
sandbox_pool.retire_idle()
|
|
598
|
+
|
|
599
|
+
frozen_champion_hash = champion_hash
|
|
600
|
+
parent = docs[frozen_champion_hash]
|
|
601
|
+
parent_report = reports[frozen_champion_hash]
|
|
602
|
+
frozen_champion_score = parent_report.success_rate
|
|
603
|
+
frozen_best_full = best_full
|
|
604
|
+
frozen_suite = list(suite)
|
|
605
|
+
frozen_champion_holdout = holdout_reports.get(frozen_champion_hash)
|
|
606
|
+
clusters = cluster_failures(parent_report, tasks)
|
|
607
|
+
batch_cluster_key: FailureClusterKey | None = None
|
|
608
|
+
batch_cluster_expansion_recorded = False
|
|
609
|
+
if clusters:
|
|
610
|
+
trigger = select_failure_cluster(
|
|
611
|
+
clusters,
|
|
612
|
+
failure_cluster_expansions,
|
|
613
|
+
parent_doc_hash=parent.doc_hash,
|
|
614
|
+
)
|
|
615
|
+
batch_cluster_key = _failure_cluster_key(parent.doc_hash, trigger)
|
|
616
|
+
else:
|
|
617
|
+
trigger = FailureSignature(mechanism=ALL_PASS_MECHANISM)
|
|
618
|
+
evidence = render_evidence(trigger, parent_report, tasks)
|
|
619
|
+
try:
|
|
620
|
+
batch = proposer.propose_batch(
|
|
621
|
+
parent,
|
|
622
|
+
trigger,
|
|
623
|
+
evidence,
|
|
624
|
+
history=archive.deltas,
|
|
625
|
+
count=proposal_batch_size,
|
|
626
|
+
should_cancel=should_cancel,
|
|
627
|
+
)
|
|
628
|
+
if len(batch) != proposal_batch_size:
|
|
629
|
+
raise ValueError(
|
|
630
|
+
f"proposer returned {len(batch)} proposals; expected {proposal_batch_size}"
|
|
631
|
+
)
|
|
632
|
+
except HarnessSearchCancelled:
|
|
633
|
+
raise
|
|
634
|
+
except Exception as exc: # noqa: BLE001 - provider/agent/transport failure
|
|
635
|
+
batch = [ProposalFailure(reason=str(exc))] * proposal_batch_size
|
|
636
|
+
_check_cancelled()
|
|
637
|
+
|
|
638
|
+
batch_records: list[ProposalRecord] = []
|
|
639
|
+
batch_deltas: list[HarnessDelta] = []
|
|
640
|
+
scored_proposals: list[_ScoredProposal] = []
|
|
641
|
+
seen_delta_ids = {delta.delta_id for delta in archive.deltas}
|
|
642
|
+
seen_child_hashes = {
|
|
643
|
+
delta.child_doc_hash for delta in archive.deltas if delta.child_doc_hash is not None
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
for proposal_index, delta in enumerate(batch, 1):
|
|
647
|
+
_check_cancelled()
|
|
648
|
+
label = _proposal_label(
|
|
649
|
+
iteration_index,
|
|
650
|
+
proposal_index,
|
|
651
|
+
iterations=iterations,
|
|
652
|
+
batch_size=proposal_batch_size,
|
|
653
|
+
)
|
|
654
|
+
if isinstance(delta, ProposalFailure):
|
|
655
|
+
skipped += 1
|
|
656
|
+
_stage_dead(
|
|
657
|
+
batch_records,
|
|
658
|
+
ProposalRecord(
|
|
659
|
+
iteration=iteration_index,
|
|
660
|
+
proposal_index=proposal_index,
|
|
661
|
+
outcome="proposer_error",
|
|
662
|
+
trigger=trigger,
|
|
663
|
+
reason=delta.reason,
|
|
664
|
+
champion_score=frozen_champion_score,
|
|
665
|
+
),
|
|
666
|
+
)
|
|
667
|
+
continue
|
|
668
|
+
if delta is None:
|
|
669
|
+
skipped += 1
|
|
670
|
+
_stage_dead(
|
|
671
|
+
batch_records,
|
|
672
|
+
ProposalRecord(
|
|
673
|
+
iteration=iteration_index,
|
|
674
|
+
proposal_index=proposal_index,
|
|
675
|
+
outcome="unusable",
|
|
676
|
+
trigger=trigger,
|
|
677
|
+
reason="unparseable or truncated meta reply",
|
|
678
|
+
champion_score=frozen_champion_score,
|
|
679
|
+
),
|
|
680
|
+
)
|
|
681
|
+
continue
|
|
682
|
+
ops_summary = [f"{op.op} {op.surface_id}" for op in delta.ops]
|
|
683
|
+
rationales = [op.rationale[:1_000] for op in delta.ops]
|
|
684
|
+
expected_effect = delta.expected_effect[:1_000]
|
|
685
|
+
if delta.delta_id in seen_delta_ids:
|
|
686
|
+
# The proposer re-proposed a delta this run already judged. Re-evaluating it
|
|
687
|
+
# would spend a screen (or worse) to learn a known verdict; skip without spend.
|
|
688
|
+
# Preserve this proposal as a distinct rejected archive entry so history remains
|
|
689
|
+
# complete, including duplicates inside the current sibling batch.
|
|
690
|
+
delta.verdict = GateRecord(
|
|
691
|
+
accepted=False,
|
|
692
|
+
reason="invalid before eval: duplicate of an already-proposed delta",
|
|
693
|
+
)
|
|
694
|
+
batch_deltas.append(delta)
|
|
695
|
+
skipped += 1
|
|
696
|
+
_stage_dead(
|
|
697
|
+
batch_records,
|
|
698
|
+
ProposalRecord(
|
|
699
|
+
iteration=iteration_index,
|
|
700
|
+
proposal_index=proposal_index,
|
|
701
|
+
outcome="invalid",
|
|
702
|
+
delta_id=delta.delta_id,
|
|
703
|
+
trigger=delta.trigger,
|
|
704
|
+
expected_effect=expected_effect,
|
|
705
|
+
ops=ops_summary,
|
|
706
|
+
rationales=rationales,
|
|
707
|
+
reason="duplicate of an already-proposed delta",
|
|
708
|
+
champion_score=frozen_champion_score,
|
|
709
|
+
),
|
|
710
|
+
)
|
|
711
|
+
continue
|
|
712
|
+
seen_delta_ids.add(delta.delta_id)
|
|
713
|
+
batch_deltas.append(delta)
|
|
714
|
+
try:
|
|
715
|
+
child_name = f"{name}-i{iteration_index}-p{proposal_index}"
|
|
716
|
+
child = apply_delta(parent, delta, child_name)
|
|
717
|
+
except ValueError as exc:
|
|
718
|
+
delta.verdict = GateRecord(accepted=False, reason=f"invalid before eval: {exc}")
|
|
719
|
+
skipped += 1
|
|
720
|
+
_stage_dead(
|
|
721
|
+
batch_records,
|
|
722
|
+
ProposalRecord(
|
|
723
|
+
iteration=iteration_index,
|
|
724
|
+
proposal_index=proposal_index,
|
|
725
|
+
outcome="invalid",
|
|
726
|
+
delta_id=delta.delta_id,
|
|
727
|
+
trigger=delta.trigger,
|
|
728
|
+
expected_effect=expected_effect,
|
|
729
|
+
ops=ops_summary,
|
|
730
|
+
rationales=rationales,
|
|
731
|
+
reason=f"invalid before eval: {exc}",
|
|
732
|
+
champion_score=frozen_champion_score,
|
|
733
|
+
),
|
|
734
|
+
)
|
|
735
|
+
continue
|
|
736
|
+
if child.doc_hash in seen_child_hashes:
|
|
737
|
+
delta.verdict = GateRecord(
|
|
738
|
+
accepted=False,
|
|
739
|
+
reason="invalid before eval: duplicate of an already-proposed child",
|
|
740
|
+
)
|
|
741
|
+
skipped += 1
|
|
742
|
+
_stage_dead(
|
|
743
|
+
batch_records,
|
|
744
|
+
ProposalRecord(
|
|
745
|
+
iteration=iteration_index,
|
|
746
|
+
proposal_index=proposal_index,
|
|
747
|
+
outcome="invalid",
|
|
748
|
+
candidate=child.name,
|
|
749
|
+
candidate_doc_hash=child.doc_hash,
|
|
750
|
+
delta_id=delta.delta_id,
|
|
751
|
+
trigger=delta.trigger,
|
|
752
|
+
expected_effect=expected_effect,
|
|
753
|
+
ops=ops_summary,
|
|
754
|
+
rationales=rationales,
|
|
755
|
+
reason="duplicate of an already-proposed child",
|
|
756
|
+
champion_score=frozen_champion_score,
|
|
757
|
+
),
|
|
758
|
+
)
|
|
759
|
+
continue
|
|
760
|
+
seen_child_hashes.add(child.doc_hash)
|
|
761
|
+
if harness_backend == "e2b" and child.runtime_kind() != "pi-node":
|
|
762
|
+
# A delta that abandons the pi-node runtime cannot execute on this backend:
|
|
763
|
+
# `doc.runtime(backend="e2b")` would raise mid-score and abort the whole search.
|
|
764
|
+
# Reject-and-archive it like any other invalid-before-eval proposal.
|
|
765
|
+
delta.verdict = GateRecord(
|
|
766
|
+
accepted=False,
|
|
767
|
+
reason=(
|
|
768
|
+
f"invalid before eval: runtime kind {child.runtime_kind()!r} cannot "
|
|
769
|
+
"run on harness_backend='e2b' (pi-node only)"
|
|
770
|
+
),
|
|
771
|
+
)
|
|
772
|
+
skipped += 1
|
|
773
|
+
_stage_dead(
|
|
774
|
+
batch_records,
|
|
775
|
+
ProposalRecord(
|
|
776
|
+
iteration=iteration_index,
|
|
777
|
+
proposal_index=proposal_index,
|
|
778
|
+
outcome="invalid",
|
|
779
|
+
candidate=child.name,
|
|
780
|
+
candidate_doc_hash=child.doc_hash,
|
|
781
|
+
delta_id=delta.delta_id,
|
|
782
|
+
trigger=delta.trigger,
|
|
783
|
+
expected_effect=expected_effect,
|
|
784
|
+
ops=ops_summary,
|
|
785
|
+
rationales=rationales,
|
|
786
|
+
reason=str(delta.verdict.reason),
|
|
787
|
+
champion_score=frozen_champion_score,
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
continue
|
|
791
|
+
|
|
792
|
+
# Discount the selected cluster only when this batch produces its first child
|
|
793
|
+
# that can enter evaluation. Parsed-but-duplicate, inapplicable, or
|
|
794
|
+
# backend-invalid deltas teach the search nothing about that failure mechanism.
|
|
795
|
+
# Siblings share one expansion, so later children must not discount it again.
|
|
796
|
+
if batch_cluster_key is not None and not batch_cluster_expansion_recorded:
|
|
797
|
+
failure_cluster_expansions[batch_cluster_key] = (
|
|
798
|
+
failure_cluster_expansions.get(batch_cluster_key, 0) + 1
|
|
799
|
+
)
|
|
800
|
+
batch_cluster_expansion_recorded = True
|
|
801
|
+
|
|
802
|
+
# Before a full-split eval, the delta must improve its target cluster. Compare
|
|
803
|
+
# task success first, then assertion-level partial credit, so a 0%-to-75%
|
|
804
|
+
# assertion lift is not flattened into the same "0 vs 0" as no effect.
|
|
805
|
+
screen_child_value: float | None = None
|
|
806
|
+
screen_parent_value: float | None = None
|
|
807
|
+
screen_child_fraction_value: float | None = None
|
|
808
|
+
screen_parent_fraction_value: float | None = None
|
|
809
|
+
screen_tasks = [t for t in tasks if t.task_id in set(trigger.task_ids)]
|
|
810
|
+
if screen_tasks:
|
|
811
|
+
screen_report = _score(child, screen_tasks)
|
|
812
|
+
parent_mean = _suite_rate(parent_report, sorted(trigger.task_ids))
|
|
813
|
+
child_mean = _suite_rate(screen_report, sorted(trigger.task_ids))
|
|
814
|
+
parent_fraction = _suite_fraction(parent_report, sorted(trigger.task_ids))
|
|
815
|
+
child_fraction = _suite_fraction(screen_report, sorted(trigger.task_ids))
|
|
816
|
+
screen_child_value = child_mean
|
|
817
|
+
screen_parent_value = parent_mean
|
|
818
|
+
screen_child_fraction_value = child_fraction
|
|
819
|
+
screen_parent_fraction_value = parent_fraction
|
|
820
|
+
success_regressed = child_mean < parent_mean - _TIE_EPS
|
|
821
|
+
success_tied = abs(child_mean - parent_mean) <= _TIE_EPS
|
|
822
|
+
fraction_did_not_improve = child_fraction <= parent_fraction + _TIE_EPS
|
|
823
|
+
feedback_error = _record_proposer_evaluation(
|
|
824
|
+
proposer,
|
|
825
|
+
delta,
|
|
826
|
+
stage="screen",
|
|
827
|
+
report=screen_report,
|
|
828
|
+
tasks=screen_tasks,
|
|
829
|
+
summary=(
|
|
830
|
+
f"trigger success {child_mean:.3f} vs parent {parent_mean:.3f}; "
|
|
831
|
+
f"assertion fraction {child_fraction:.3f} vs parent "
|
|
832
|
+
f"{parent_fraction:.3f}"
|
|
833
|
+
),
|
|
834
|
+
)
|
|
835
|
+
if feedback_error is not None:
|
|
836
|
+
_note(
|
|
837
|
+
f"{label}: screen feedback could not be persisted "
|
|
838
|
+
f"({feedback_error}); continuing"
|
|
839
|
+
)
|
|
840
|
+
if success_regressed or (success_tied and fraction_did_not_improve):
|
|
841
|
+
delta.verdict = GateRecord(
|
|
842
|
+
accepted=False,
|
|
843
|
+
reason=(
|
|
844
|
+
f"screened out: trigger success {child_mean:.2f} vs parent "
|
|
845
|
+
f"{parent_mean:.2f}; assertion fraction {child_fraction:.2f} vs "
|
|
846
|
+
f"parent {parent_fraction:.2f} over {len(screen_tasks)} task(s), "
|
|
847
|
+
f"k={k}; "
|
|
848
|
+
"the delta did not improve its own target"
|
|
849
|
+
),
|
|
850
|
+
)
|
|
851
|
+
screened += 1
|
|
852
|
+
_stage_dead(
|
|
853
|
+
batch_records,
|
|
854
|
+
ProposalRecord(
|
|
855
|
+
iteration=iteration_index,
|
|
856
|
+
proposal_index=proposal_index,
|
|
857
|
+
outcome="screened",
|
|
858
|
+
candidate=child.name,
|
|
859
|
+
candidate_doc_hash=child.doc_hash,
|
|
860
|
+
delta_id=delta.delta_id,
|
|
861
|
+
trigger=delta.trigger,
|
|
862
|
+
expected_effect=expected_effect,
|
|
863
|
+
ops=ops_summary,
|
|
864
|
+
rationales=rationales,
|
|
865
|
+
reason=str(delta.verdict.reason),
|
|
866
|
+
screen_child=child_mean,
|
|
867
|
+
screen_parent=parent_mean,
|
|
868
|
+
screen_child_fraction=child_fraction,
|
|
869
|
+
screen_parent_fraction=parent_fraction,
|
|
870
|
+
champion_score=frozen_champion_score,
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
continue
|
|
874
|
+
|
|
875
|
+
child_report = _score(child, tasks)
|
|
876
|
+
pre_verdict = gate_delta(
|
|
877
|
+
delta,
|
|
878
|
+
child=child_report,
|
|
879
|
+
champion=parent_report,
|
|
880
|
+
best_full=frozen_best_full,
|
|
881
|
+
suite=frozen_suite,
|
|
882
|
+
)
|
|
883
|
+
# The held-out tier is measured for every candidate that could still be accepted,
|
|
884
|
+
# including candidates whose suite/full veto is narrow enough for a confirmation
|
|
885
|
+
# re-run. A confirmation may overturn a veto, but never bypass the held-out tier.
|
|
886
|
+
could_accept = pre_verdict.accepted or (
|
|
887
|
+
confirm_narrow_vetoes
|
|
888
|
+
and narrow_failing_tiers(
|
|
889
|
+
pre_verdict, k=k, n_suite=len(frozen_suite), n_holdout=0
|
|
890
|
+
)
|
|
891
|
+
is not None
|
|
892
|
+
)
|
|
893
|
+
if holdout and could_accept:
|
|
894
|
+
child_holdout = _score(child, holdout)
|
|
895
|
+
holdout_reports[child.doc_hash] = child_holdout
|
|
896
|
+
if frozen_champion_holdout is None:
|
|
897
|
+
frozen_champion_holdout = _score(parent, holdout)
|
|
898
|
+
holdout_reports[frozen_champion_hash] = frozen_champion_holdout
|
|
899
|
+
verdict = gate_delta(
|
|
900
|
+
delta,
|
|
901
|
+
child=child_report,
|
|
902
|
+
champion=parent_report,
|
|
903
|
+
best_full=frozen_best_full,
|
|
904
|
+
suite=frozen_suite,
|
|
905
|
+
child_holdout=child_holdout,
|
|
906
|
+
champion_holdout=frozen_champion_holdout,
|
|
907
|
+
)
|
|
908
|
+
else:
|
|
909
|
+
verdict = pre_verdict
|
|
910
|
+
tiers = (
|
|
911
|
+
narrow_failing_tiers(
|
|
912
|
+
verdict,
|
|
913
|
+
k=k,
|
|
914
|
+
n_suite=len(frozen_suite),
|
|
915
|
+
n_holdout=len(holdout or []),
|
|
916
|
+
)
|
|
917
|
+
if confirm_narrow_vetoes
|
|
918
|
+
else None
|
|
919
|
+
)
|
|
920
|
+
if tiers:
|
|
921
|
+
confirmations += 1
|
|
922
|
+
confirmed_ok = True
|
|
923
|
+
notes: list[str] = []
|
|
924
|
+
for tier in tiers:
|
|
925
|
+
tier_tasks = (
|
|
926
|
+
[t for t in tasks if t.task_id in set(frozen_suite)]
|
|
927
|
+
if tier == "suite"
|
|
928
|
+
else list(holdout or [])
|
|
929
|
+
)
|
|
930
|
+
child_re = _score(child, tier_tasks, k_override=2 * k)
|
|
931
|
+
champ_re = _score(parent, tier_tasks, k_override=2 * k)
|
|
932
|
+
re_delta = child_re.success_rate - champ_re.success_rate
|
|
933
|
+
re_fraction_delta = child_re.mean_fraction - champ_re.mean_fraction
|
|
934
|
+
notes.append(
|
|
935
|
+
f"{tier} re-measured at k={2 * k}: success {re_delta:+.3f}, "
|
|
936
|
+
f"assertion fraction {re_fraction_delta:+.3f}"
|
|
937
|
+
)
|
|
938
|
+
if re_delta < -_TIE_EPS or (
|
|
939
|
+
abs(re_delta) <= _TIE_EPS and re_fraction_delta < -_TIE_EPS
|
|
940
|
+
):
|
|
941
|
+
confirmed_ok = False
|
|
942
|
+
outcome = "veto overturned" if confirmed_ok else "regression confirmed"
|
|
943
|
+
verdict = GateRecord(
|
|
944
|
+
suite_delta=verdict.suite_delta,
|
|
945
|
+
suite_fraction_delta=verdict.suite_fraction_delta,
|
|
946
|
+
full_delta=verdict.full_delta,
|
|
947
|
+
full_fraction_delta=verdict.full_fraction_delta,
|
|
948
|
+
holdout_delta=verdict.holdout_delta,
|
|
949
|
+
holdout_fraction_delta=verdict.holdout_fraction_delta,
|
|
950
|
+
accepted=confirmed_ok,
|
|
951
|
+
reason=f"confirmation re-run ({outcome}): {'; '.join(notes)} | initially: "
|
|
952
|
+
+ verdict.reason,
|
|
953
|
+
)
|
|
954
|
+
docs[child.doc_hash] = child
|
|
955
|
+
reports[child.doc_hash] = child_report
|
|
956
|
+
record = ProposalRecord(
|
|
957
|
+
iteration=iteration_index,
|
|
958
|
+
proposal_index=proposal_index,
|
|
959
|
+
outcome="scored",
|
|
960
|
+
candidate=child.name,
|
|
961
|
+
candidate_doc_hash=child.doc_hash,
|
|
962
|
+
delta_id=delta.delta_id,
|
|
963
|
+
trigger=delta.trigger,
|
|
964
|
+
expected_effect=expected_effect,
|
|
965
|
+
ops=ops_summary,
|
|
966
|
+
rationales=rationales,
|
|
967
|
+
reason=str(verdict.reason),
|
|
968
|
+
score=child_report.success_rate,
|
|
969
|
+
gate_eligible=verdict.accepted,
|
|
970
|
+
screen_child=screen_child_value,
|
|
971
|
+
screen_parent=screen_parent_value,
|
|
972
|
+
screen_child_fraction=screen_child_fraction_value,
|
|
973
|
+
screen_parent_fraction=screen_parent_fraction_value,
|
|
974
|
+
champion_score=frozen_champion_score,
|
|
975
|
+
)
|
|
976
|
+
batch_records.append(record)
|
|
977
|
+
scored_proposals.append(
|
|
978
|
+
_ScoredProposal(
|
|
979
|
+
proposal_index=proposal_index,
|
|
980
|
+
child=child,
|
|
981
|
+
delta=delta,
|
|
982
|
+
report=child_report,
|
|
983
|
+
gate=verdict,
|
|
984
|
+
record=record,
|
|
985
|
+
)
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
_check_cancelled()
|
|
989
|
+
eligible = [candidate for candidate in scored_proposals if candidate.gate.accepted]
|
|
990
|
+
winner = (
|
|
991
|
+
max(
|
|
992
|
+
eligible,
|
|
993
|
+
key=lambda candidate: (
|
|
994
|
+
candidate.report.success_rate,
|
|
995
|
+
candidate.report.mean_fraction,
|
|
996
|
+
-candidate.proposal_index,
|
|
997
|
+
),
|
|
998
|
+
)
|
|
999
|
+
if eligible
|
|
1000
|
+
else None
|
|
1001
|
+
)
|
|
1002
|
+
for candidate in scored_proposals:
|
|
1003
|
+
gate_eligible = candidate.gate.accepted
|
|
1004
|
+
if winner is candidate:
|
|
1005
|
+
final_gate = candidate.gate
|
|
1006
|
+
elif gate_eligible:
|
|
1007
|
+
assert winner is not None
|
|
1008
|
+
final_gate = candidate.gate.model_copy(
|
|
1009
|
+
update={
|
|
1010
|
+
"accepted": False,
|
|
1011
|
+
"reason": (
|
|
1012
|
+
"gate eligible but not selected: "
|
|
1013
|
+
f"proposal {winner.proposal_index} ranked higher by full success, "
|
|
1014
|
+
"assertion fraction, then proposal order | " + candidate.gate.reason
|
|
1015
|
+
),
|
|
1016
|
+
}
|
|
1017
|
+
)
|
|
1018
|
+
else:
|
|
1019
|
+
final_gate = candidate.gate
|
|
1020
|
+
candidate.delta.verdict = final_gate
|
|
1021
|
+
candidate.record.gate_eligible = gate_eligible
|
|
1022
|
+
candidate.record.selected = winner is candidate
|
|
1023
|
+
candidate.record.reason = final_gate.reason
|
|
1024
|
+
|
|
1025
|
+
# Full-stage history must describe final selection, not preliminary gate eligibility.
|
|
1026
|
+
# Persist it before the atomic commit so cancellation cannot publish a partial winner.
|
|
1027
|
+
for candidate in scored_proposals:
|
|
1028
|
+
assert candidate.delta.verdict is not None
|
|
1029
|
+
feedback_error = _record_proposer_evaluation(
|
|
1030
|
+
proposer,
|
|
1031
|
+
candidate.delta,
|
|
1032
|
+
stage="full",
|
|
1033
|
+
report=candidate.report,
|
|
1034
|
+
tasks=tasks,
|
|
1035
|
+
summary=candidate.delta.verdict.reason,
|
|
1036
|
+
)
|
|
1037
|
+
if feedback_error is not None:
|
|
1038
|
+
_note(
|
|
1039
|
+
f"iteration {iteration_index}/{iterations} proposal "
|
|
1040
|
+
f"{candidate.proposal_index}/{proposal_batch_size}: full feedback could "
|
|
1041
|
+
f"not be persisted ({feedback_error}); continuing"
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
# One commit boundary for the whole iteration. Diagnostic notes may already have
|
|
1045
|
+
# streamed, but cancellation before this point publishes no archive lineage,
|
|
1046
|
+
# champion, suite, acceptance/proposal callback, or champion checkpoint.
|
|
1047
|
+
_check_cancelled()
|
|
1048
|
+
archive.deltas.extend(batch_deltas)
|
|
1049
|
+
if winner is not None:
|
|
1050
|
+
champion_hash = winner.child.doc_hash
|
|
1051
|
+
best_full = max(best_full, winner.report.success_rate)
|
|
1052
|
+
promoted = {
|
|
1053
|
+
task_id
|
|
1054
|
+
for task_id, outcome in winner.report.per_task.items()
|
|
1055
|
+
if outcome.success_rate >= 1.0 - _TIE_EPS
|
|
1056
|
+
}
|
|
1057
|
+
suite = sorted(set(suite) | promoted)
|
|
1058
|
+
if on_accept is not None:
|
|
1059
|
+
on_accept(winner.child, winner.delta, winner.report.success_rate)
|
|
1060
|
+
|
|
1061
|
+
proposal_records.extend(batch_records)
|
|
1062
|
+
if on_proposal is not None:
|
|
1063
|
+
for record in batch_records:
|
|
1064
|
+
on_proposal(record)
|
|
1065
|
+
if on_progress is not None:
|
|
1066
|
+
champion = docs[champion_hash]
|
|
1067
|
+
on_progress(
|
|
1068
|
+
iteration_index,
|
|
1069
|
+
champion.name,
|
|
1070
|
+
reports[champion_hash].success_rate,
|
|
1071
|
+
winner is not None,
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
_check_cancelled()
|
|
1075
|
+
best = docs[champion_hash].model_copy(update={"name": name, "version": 0})
|
|
1076
|
+
result = CreateResult(
|
|
1077
|
+
best=best,
|
|
1078
|
+
best_score=reports[champion_hash].success_rate,
|
|
1079
|
+
archive=archive,
|
|
1080
|
+
reports=reports,
|
|
1081
|
+
holdout_reports=holdout_reports,
|
|
1082
|
+
suite=suite,
|
|
1083
|
+
skipped=skipped,
|
|
1084
|
+
proposal_records=proposal_records,
|
|
1085
|
+
screened=screened,
|
|
1086
|
+
confirmations=confirmations,
|
|
1087
|
+
iterations=iterations,
|
|
1088
|
+
proposal_batch_size=proposal_batch_size,
|
|
1089
|
+
worker_usage=combine_usage(worker_usages),
|
|
1090
|
+
)
|
|
1091
|
+
return result
|
|
1092
|
+
except HarnessSearchCancelled as error:
|
|
1093
|
+
# A cancellation inside evaluation carries that wave's completed and
|
|
1094
|
+
# partial cells. Waves that returned normally were appended above. The
|
|
1095
|
+
# search exception is the authoritative aggregate for callers because
|
|
1096
|
+
# cancellation intentionally has no partial CreateResult.
|
|
1097
|
+
error.worker_usage = combine_usage([*worker_usages, error.worker_usage])
|
|
1098
|
+
cancelled = error
|
|
1099
|
+
raise
|
|
1100
|
+
finally:
|
|
1101
|
+
if sandbox_pool is not None:
|
|
1102
|
+
sandbox_pool.close()
|
|
1103
|
+
usage = sandbox_pool.usage()
|
|
1104
|
+
if result is not None:
|
|
1105
|
+
result.sandbox_usage = usage
|
|
1106
|
+
if cancelled is not None:
|
|
1107
|
+
cancelled.sandbox_usage = usage
|
|
1108
|
+
if on_sandbox_usage is not None:
|
|
1109
|
+
on_sandbox_usage(usage)
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _suite_rate(report: ClosedLoopReport, suite: list[str]) -> float:
|
|
1113
|
+
"""Mean per-task success over the regression suite; an empty suite constrains nothing."""
|
|
1114
|
+
if not suite:
|
|
1115
|
+
return 1.0
|
|
1116
|
+
rates = [
|
|
1117
|
+
outcome.success_rate
|
|
1118
|
+
for task_id in suite
|
|
1119
|
+
if (outcome := report.per_task.get(task_id)) is not None
|
|
1120
|
+
]
|
|
1121
|
+
# Dividing by len(suite), not len(rates): a suite task missing from the report counts as 0
|
|
1122
|
+
# (fail-closed), though suite tasks are always a subset of the scored split in practice.
|
|
1123
|
+
return sum(rates) / len(suite)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _suite_fraction(report: ClosedLoopReport, suite: list[str]) -> float:
|
|
1127
|
+
"""Mean assertion completion over a task subset; missing tasks fail closed."""
|
|
1128
|
+
if not suite:
|
|
1129
|
+
return 1.0
|
|
1130
|
+
fractions = [
|
|
1131
|
+
outcome.mean_fraction
|
|
1132
|
+
for task_id in suite
|
|
1133
|
+
if (outcome := report.per_task.get(task_id)) is not None
|
|
1134
|
+
]
|
|
1135
|
+
return sum(fractions) / len(suite)
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _record_proposer_evaluation(
|
|
1139
|
+
proposer: DeltaProposer,
|
|
1140
|
+
delta: HarnessDelta,
|
|
1141
|
+
*,
|
|
1142
|
+
stage: str,
|
|
1143
|
+
report: ClosedLoopReport,
|
|
1144
|
+
tasks: list[TaskSpec],
|
|
1145
|
+
summary: str,
|
|
1146
|
+
) -> str | None:
|
|
1147
|
+
"""Best-effort durable trace feedback; evaluation correctness never depends on telemetry."""
|
|
1148
|
+
recorder = getattr(proposer, "record_evaluation", None)
|
|
1149
|
+
if not callable(recorder):
|
|
1150
|
+
return None
|
|
1151
|
+
content = (
|
|
1152
|
+
f"# Candidate evaluation: {stage}\n\n"
|
|
1153
|
+
f"Delta: {delta.delta_id}\n\n"
|
|
1154
|
+
f"Expected effect: {delta.expected_effect}\n\n"
|
|
1155
|
+
f"Outcome: {summary}\n\n"
|
|
1156
|
+
f"{render_evidence(delta.trigger, report, tasks)}"
|
|
1157
|
+
)
|
|
1158
|
+
try:
|
|
1159
|
+
recorder(delta, stage=stage, content=content)
|
|
1160
|
+
except HarnessSearchCancelled:
|
|
1161
|
+
raise
|
|
1162
|
+
except Exception as error: # noqa: BLE001 - optional E2B feedback must not abort scored work
|
|
1163
|
+
return str(error)
|
|
1164
|
+
return None
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _proposal_label(
|
|
1168
|
+
iteration_index: int, proposal_index: int, *, iterations: int, batch_size: int
|
|
1169
|
+
) -> str:
|
|
1170
|
+
"""Human-readable proposal identity that stays concise for singleton batches."""
|
|
1171
|
+
if batch_size == 1:
|
|
1172
|
+
return f"iteration {iteration_index}/{iterations}"
|
|
1173
|
+
return f"iteration {iteration_index}/{iterations} proposal {proposal_index}/{batch_size}"
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def _dead_proposal_note(record: ProposalRecord, *, iterations: int, batch_size: int) -> str:
|
|
1177
|
+
"""Render one dead proposal for the lightweight narration callback."""
|
|
1178
|
+
label = _proposal_label(
|
|
1179
|
+
record.iteration,
|
|
1180
|
+
record.proposal_index,
|
|
1181
|
+
iterations=iterations,
|
|
1182
|
+
batch_size=batch_size,
|
|
1183
|
+
)
|
|
1184
|
+
reason = record.reason or "no reason reported"
|
|
1185
|
+
if record.outcome == "proposer_error":
|
|
1186
|
+
return f"{label}: proposer call failed ({reason}); skipped"
|
|
1187
|
+
if record.outcome == "unusable":
|
|
1188
|
+
return f"{label}: proposal unusable ({reason}); skipped"
|
|
1189
|
+
if record.outcome == "invalid":
|
|
1190
|
+
return f"{label}: {reason}; skipped"
|
|
1191
|
+
return f"{label}: {reason}"
|