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.
Files changed (308) hide show
  1. llm_waterfall/LICENSE +21 -0
  2. llm_waterfall/__init__.py +53 -0
  3. llm_waterfall/adapters/__init__.py +36 -0
  4. llm_waterfall/adapters/anthropic.py +105 -0
  5. llm_waterfall/adapters/aws_mantle.py +47 -0
  6. llm_waterfall/adapters/azure_openai.py +71 -0
  7. llm_waterfall/adapters/base.py +51 -0
  8. llm_waterfall/adapters/bedrock.py +309 -0
  9. llm_waterfall/adapters/openai.py +130 -0
  10. llm_waterfall/classify.py +184 -0
  11. llm_waterfall/pricing.py +110 -0
  12. llm_waterfall/py.typed +0 -0
  13. llm_waterfall/types.py +295 -0
  14. llm_waterfall/waterfall.py +255 -0
  15. wmo/__init__.py +38 -0
  16. wmo/agents/__init__.py +7 -0
  17. wmo/agents/default.py +29 -0
  18. wmo/agents/meta.py +55 -0
  19. wmo/agents/optimizer.py +55 -0
  20. wmo/agents/project.py +928 -0
  21. wmo/cli/__init__.py +5 -0
  22. wmo/cli/agent_session.py +1123 -0
  23. wmo/cli/app.py +2489 -0
  24. wmo/cli/e2b_cmds.py +212 -0
  25. wmo/cli/eval_closed_loop.py +207 -0
  26. wmo/cli/harness_app.py +1147 -0
  27. wmo/cli/harness_distill.py +659 -0
  28. wmo/cli/hosted_session.py +880 -0
  29. wmo/cli/ingest_cmd.py +165 -0
  30. wmo/cli/model_roles.py +82 -0
  31. wmo/cli/platform_cmds.py +372 -0
  32. wmo/cli/route_app.py +274 -0
  33. wmo/cli/session_state.py +243 -0
  34. wmo/cli/ui.py +1107 -0
  35. wmo/cli/workspace_sync.py +504 -0
  36. wmo/config/__init__.py +60 -0
  37. wmo/config/card.py +129 -0
  38. wmo/config/config.py +367 -0
  39. wmo/config/dotenv.py +67 -0
  40. wmo/config/settings.py +128 -0
  41. wmo/config/store.py +177 -0
  42. wmo/conftest.py +19 -0
  43. wmo/connect/__init__.py +88 -0
  44. wmo/connect/apps.py +78 -0
  45. wmo/connect/brave.py +284 -0
  46. wmo/connect/connector.py +79 -0
  47. wmo/connect/credentials.py +164 -0
  48. wmo/connect/github.py +321 -0
  49. wmo/connect/google.py +627 -0
  50. wmo/connect/notion.py +790 -0
  51. wmo/connect/oauth.py +461 -0
  52. wmo/connect/slack.py +555 -0
  53. wmo/connect/store.py +199 -0
  54. wmo/connect/types.py +156 -0
  55. wmo/core/__init__.py +21 -0
  56. wmo/core/parsing.py +281 -0
  57. wmo/core/render.py +271 -0
  58. wmo/core/text.py +40 -0
  59. wmo/core/types.py +116 -0
  60. wmo/distill/__init__.py +14 -0
  61. wmo/distill/agents.py +140 -0
  62. wmo/distill/config.py +1006 -0
  63. wmo/distill/cost.py +437 -0
  64. wmo/distill/data.py +921 -0
  65. wmo/distill/deadlines.py +254 -0
  66. wmo/distill/fake_tinker.py +734 -0
  67. wmo/distill/gate.py +122 -0
  68. wmo/distill/loop.py +3499 -0
  69. wmo/distill/renderers.py +399 -0
  70. wmo/distill/rendering.py +620 -0
  71. wmo/distill/rollouts.py +726 -0
  72. wmo/distill/samples.py +195 -0
  73. wmo/distill/store.py +829 -0
  74. wmo/distill/teacher.py +714 -0
  75. wmo/distill/tokens.py +535 -0
  76. wmo/distill/tracking.py +552 -0
  77. wmo/distill/tripwire.py +411 -0
  78. wmo/distill/xtoken/byte_offsets.py +152 -0
  79. wmo/distill/xtoken/chunks.py +457 -0
  80. wmo/distill/xtoken/prompt_logprobs.py +475 -0
  81. wmo/distill/xtoken/teacher_render.py +346 -0
  82. wmo/engine/__init__.py +28 -0
  83. wmo/engine/autoconfig.py +367 -0
  84. wmo/engine/build.py +346 -0
  85. wmo/engine/demo.py +77 -0
  86. wmo/engine/eval_suites.py +245 -0
  87. wmo/engine/grounding.py +491 -0
  88. wmo/engine/knowledge.py +291 -0
  89. wmo/engine/loader.py +36 -0
  90. wmo/engine/play.py +92 -0
  91. wmo/engine/prompts.py +99 -0
  92. wmo/engine/replay.py +443 -0
  93. wmo/engine/reporting.py +58 -0
  94. wmo/engine/workspace.py +468 -0
  95. wmo/engine/world_model.py +568 -0
  96. wmo/env/__init__.py +22 -0
  97. wmo/env/base.py +121 -0
  98. wmo/env/closed_loop.py +229 -0
  99. wmo/env/episode.py +107 -0
  100. wmo/env/llm_agent.py +93 -0
  101. wmo/env/scenarios.py +73 -0
  102. wmo/evals/__init__.py +52 -0
  103. wmo/evals/agreement.py +110 -0
  104. wmo/evals/base.py +45 -0
  105. wmo/evals/closed_loop.py +480 -0
  106. wmo/evals/failover.py +96 -0
  107. wmo/evals/gold.py +127 -0
  108. wmo/evals/grid.py +394 -0
  109. wmo/evals/grid_plot.py +205 -0
  110. wmo/evals/harbor/__init__.py +27 -0
  111. wmo/evals/harbor/agent.py +573 -0
  112. wmo/evals/harbor/ctrf.py +171 -0
  113. wmo/evals/harbor/e2b_environment.py +587 -0
  114. wmo/evals/harbor/e2b_template_policy.py +144 -0
  115. wmo/evals/harbor/scorer.py +875 -0
  116. wmo/evals/harbor/tasks.py +140 -0
  117. wmo/evals/open_loop.py +194 -0
  118. wmo/evals/tasks.py +53 -0
  119. wmo/harness/__init__.py +51 -0
  120. wmo/harness/code_runtime.py +288 -0
  121. wmo/harness/create.py +1191 -0
  122. wmo/harness/delta.py +220 -0
  123. wmo/harness/doc.py +556 -0
  124. wmo/harness/e2b_ledger.py +342 -0
  125. wmo/harness/e2b_reap.py +476 -0
  126. wmo/harness/e2b_sandbox.py +350 -0
  127. wmo/harness/environment.py +35 -0
  128. wmo/harness/live_session.py +543 -0
  129. wmo/harness/mutate.py +343 -0
  130. wmo/harness/pi_e2b.py +1710 -0
  131. wmo/harness/pi_entry/entry.ts +268 -0
  132. wmo/harness/pi_entry/runner_frames.ts +92 -0
  133. wmo/harness/pi_entry/runner_live.ts +587 -0
  134. wmo/harness/pi_entry/runner_service.ts +270 -0
  135. wmo/harness/pi_entry/runner_stdio.ts +374 -0
  136. wmo/harness/pi_entry/runner_termination.ts +142 -0
  137. wmo/harness/pi_local.py +262 -0
  138. wmo/harness/pi_runtime.py +495 -0
  139. wmo/harness/pi_vendor.py +65 -0
  140. wmo/harness/population.py +509 -0
  141. wmo/harness/project_proposer.py +569 -0
  142. wmo/harness/proposer.py +977 -0
  143. wmo/harness/runner_link.py +619 -0
  144. wmo/harness/runtime.py +389 -0
  145. wmo/harness/scoring.py +247 -0
  146. wmo/harness/skills.py +116 -0
  147. wmo/harness/source_tree.py +319 -0
  148. wmo/harness/store.py +176 -0
  149. wmo/harness/tools.py +105 -0
  150. wmo/harness/vendor/manifest.sha256 +58 -0
  151. wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
  152. wmo/harness/vendor/pi-agent/LICENSE +21 -0
  153. wmo/harness/vendor/pi-agent/README.md +488 -0
  154. wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
  155. wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
  156. wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
  157. wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
  158. wmo/harness/vendor/pi-agent/docs/models.md +966 -0
  159. wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
  160. wmo/harness/vendor/pi-agent/package.json +60 -0
  161. wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
  162. wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
  163. wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
  164. wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
  165. wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
  166. wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
  167. wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
  168. wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
  169. wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
  170. wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
  171. wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
  172. wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
  173. wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
  174. wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
  175. wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
  176. wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
  177. wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
  178. wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
  179. wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
  180. wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
  181. wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
  182. wmo/harness/vendor/pi-agent/src/index.ts +44 -0
  183. wmo/harness/vendor/pi-agent/src/node.ts +2 -0
  184. wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
  185. wmo/harness/vendor/pi-agent/src/types.ts +428 -0
  186. wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
  187. wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
  188. wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
  189. wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
  190. wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
  191. wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
  192. wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
  193. wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
  194. wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
  195. wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
  196. wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
  197. wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
  198. wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
  199. wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
  200. wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
  201. wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
  202. wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
  203. wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
  204. wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
  205. wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
  206. wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
  207. wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
  208. wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
  209. wmo/harness/vendor/vendor_pi.sh +59 -0
  210. wmo/harness/workspace_patch.py +270 -0
  211. wmo/ingest/__init__.py +47 -0
  212. wmo/ingest/adapter.py +72 -0
  213. wmo/ingest/base.py +114 -0
  214. wmo/ingest/braintrust.py +339 -0
  215. wmo/ingest/detect.py +126 -0
  216. wmo/ingest/langfuse.py +291 -0
  217. wmo/ingest/langsmith.py +444 -0
  218. wmo/ingest/mastra.py +330 -0
  219. wmo/ingest/messages.py +170 -0
  220. wmo/ingest/normalize.py +679 -0
  221. wmo/ingest/otel_genai.py +69 -0
  222. wmo/ingest/otel_writer.py +100 -0
  223. wmo/ingest/phoenix.py +150 -0
  224. wmo/ingest/postgres.py +246 -0
  225. wmo/ingest/posthog.py +320 -0
  226. wmo/ingest/quality.py +28 -0
  227. wmo/ingest/stream.py +209 -0
  228. wmo/ingest/testdata/sample_otlp.json +60 -0
  229. wmo/ingest/testdata/sample_spans.jsonl +3 -0
  230. wmo/optimize/__init__.py +25 -0
  231. wmo/optimize/base.py +143 -0
  232. wmo/optimize/gepa.py +806 -0
  233. wmo/optimize/judge.py +262 -0
  234. wmo/optimize/judge_quality.py +359 -0
  235. wmo/optimize/knn.py +468 -0
  236. wmo/optimize/numeric.py +152 -0
  237. wmo/optimize/outcomes.py +103 -0
  238. wmo/optimize/policy.py +669 -0
  239. wmo/optimize/report.py +231 -0
  240. wmo/optimize/reward.py +129 -0
  241. wmo/optimize/routing.py +373 -0
  242. wmo/platform/__init__.py +6 -0
  243. wmo/platform/auth.py +115 -0
  244. wmo/platform/client.py +551 -0
  245. wmo/platform/credentials.py +126 -0
  246. wmo/platform/transfer.py +158 -0
  247. wmo/providers/__init__.py +40 -0
  248. wmo/providers/_bedrock_chat.py +155 -0
  249. wmo/providers/_openai_common.py +182 -0
  250. wmo/providers/_responses_common.py +472 -0
  251. wmo/providers/anthropic.py +134 -0
  252. wmo/providers/azure_openai.py +296 -0
  253. wmo/providers/base.py +300 -0
  254. wmo/providers/bedrock.py +312 -0
  255. wmo/providers/models.py +205 -0
  256. wmo/providers/openai.py +143 -0
  257. wmo/providers/openai_responses.py +240 -0
  258. wmo/providers/pool.py +170 -0
  259. wmo/providers/registry.py +73 -0
  260. wmo/providers/retry.py +151 -0
  261. wmo/providers/tinker.py +936 -0
  262. wmo/providers/waterfall.py +336 -0
  263. wmo/research/__init__.py +81 -0
  264. wmo/research/ablation.py +133 -0
  265. wmo/research/concurrency_plot.py +523 -0
  266. wmo/research/concurrency_run.py +240 -0
  267. wmo/research/concurrency_scaling.py +270 -0
  268. wmo/research/gepa_scaling.py +274 -0
  269. wmo/research/pipeline.py +198 -0
  270. wmo/research/scaling_split.py +82 -0
  271. wmo/research/scenario_fidelity.py +198 -0
  272. wmo/research/scenario_recovery.py +92 -0
  273. wmo/research/seed_stability.py +90 -0
  274. wmo/research/trace_scaling.py +348 -0
  275. wmo/retrieval/__init__.py +6 -0
  276. wmo/retrieval/embedders.py +105 -0
  277. wmo/retrieval/leakfree.py +52 -0
  278. wmo/retrieval/retriever.py +173 -0
  279. wmo/scenarios/__init__.py +58 -0
  280. wmo/scenarios/builder.py +152 -0
  281. wmo/scenarios/mining/__init__.py +27 -0
  282. wmo/scenarios/mining/clustering.py +171 -0
  283. wmo/scenarios/mining/facets.py +226 -0
  284. wmo/scenarios/mining/selection.py +220 -0
  285. wmo/scenarios/synthesis/__init__.py +6 -0
  286. wmo/scenarios/synthesis/scenario_set.py +63 -0
  287. wmo/scenarios/synthesis/synthesizer.py +85 -0
  288. wmo/scenarios/verification/__init__.py +17 -0
  289. wmo/scenarios/verification/judge.py +97 -0
  290. wmo/scenarios/verification/verify.py +135 -0
  291. wmo/serving/__init__.py +5 -0
  292. wmo/serving/builds.py +451 -0
  293. wmo/serving/chat.py +878 -0
  294. wmo/serving/endpoint_config.py +64 -0
  295. wmo/serving/savings.py +250 -0
  296. wmo/serving/server.py +553 -0
  297. wmo/serving/traces_source.py +206 -0
  298. wmo/telemetry.py +213 -0
  299. wmo/tracking/__init__.py +36 -0
  300. wmo/tracking/clock.py +24 -0
  301. wmo/tracking/metered.py +125 -0
  302. wmo/tracking/pricing.py +99 -0
  303. wmo/tracking/store.py +31 -0
  304. wmo/tracking/tracker.py +149 -0
  305. world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
  306. world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
  307. world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
  308. 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}"