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
@@ -0,0 +1,569 @@
1
+ """One-turn agentic proposal of complete harness source trees from a scored population.
2
+
3
+ `ProjectCandidateProposer` implements the population loop's `CandidateProposer` seam with the
4
+ paper's protocol: a FRESH agent project (E2B sandbox) per proposal slot, a navigable filesystem
5
+ history of every evaluated candidate (complete source, score report, and the raw per-trial
6
+ `wmo-run.json` transcripts plus verifier outputs), a fixed-seed stage in `candidate/`, exactly
7
+ one agent turn with `retry_recoverable=False`, then a bounded snapshot, parse, and `node`
8
+ interface validation. Every failure path (agent error, missing submit, snapshot failure,
9
+ invalid tree, syntax error, or transport death) consumes the slot as a
10
+ :class:`CandidateProposalError` with raw evidence persisted under `proposals/slot-NNNN/` in the
11
+ run directory, so later slots learn from it.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import shlex
19
+ from collections.abc import Callable, Collection, Sequence
20
+ from pathlib import Path
21
+ from typing import Protocol
22
+
23
+ from pydantic import JsonValue
24
+
25
+ from wmo.agents.project import (
26
+ DEFAULT_SOURCE_TREE_MAX_BYTES,
27
+ DEFAULT_SOURCE_TREE_MAX_FILES,
28
+ AgentProjectRun,
29
+ ProjectBashResult,
30
+ )
31
+ from wmo.harness.doc import HarnessDoc
32
+ from wmo.harness.live_session import SessionEvent
33
+ from wmo.harness.population import (
34
+ CandidateProposal,
35
+ CandidateProposalError,
36
+ EvaluatedCandidate,
37
+ candidate_slot_id,
38
+ )
39
+ from wmo.harness.runtime import HarnessSearchCancelled
40
+ from wmo.harness.scoring import ScoreCell
41
+ from wmo.harness.source_tree import HarnessSourceTree
42
+ from wmo.providers.base import ToolCallingProvider
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ CANDIDATE_STAGE_DIR = "candidate"
47
+ PROPOSALS_DIR = "proposals"
48
+ # Per-candidate budget for materialized raw trial evidence; breaches truncate, never halt.
49
+ DEFAULT_CANDIDATE_HISTORY_BYTES = 256 * 1024 * 1024
50
+ # Per-file cap before head/tail truncation (transcripts keep both ends plus a marker).
51
+ DEFAULT_HISTORY_FILE_BYTES = 2 * 1024 * 1024
52
+ _WMO_RUN_FILENAME = "wmo-run.json"
53
+ _TRIAL_AGENT_DIR = "agent"
54
+ _TRIAL_VERIFIER_DIR = "verifier"
55
+
56
+
57
+ class CandidateProject(Protocol):
58
+ """The project-side operations one proposal slot needs (a fresh instance per slot)."""
59
+
60
+ workspace: str
61
+
62
+ def write_text(self, path: str, content: str) -> None: ...
63
+
64
+ def run_bash(self, command: str) -> ProjectBashResult: ...
65
+
66
+ def stage_source_tree(self, tree: HarnessSourceTree, dest: str) -> None: ...
67
+
68
+ def snapshot_source_tree(
69
+ self,
70
+ directory: str,
71
+ *,
72
+ max_files: int,
73
+ max_bytes: int,
74
+ ) -> HarnessSourceTree: ...
75
+
76
+ def run(
77
+ self,
78
+ agent: HarnessDoc,
79
+ provider: ToolCallingProvider,
80
+ instruction: str,
81
+ *,
82
+ on_event: Callable[[SessionEvent], None] | None = None,
83
+ should_cancel: Callable[[], bool] | None = None,
84
+ writable_files: Collection[str] | None = None,
85
+ retry_recoverable: bool = True,
86
+ ) -> AgentProjectRun: ...
87
+
88
+ def close(self) -> None: ...
89
+
90
+
91
+ class ProjectCandidateProposer:
92
+ """Run one contained coding turn per slot against the complete scored population."""
93
+
94
+ def __init__(
95
+ self,
96
+ agent: HarnessDoc,
97
+ provider: ToolCallingProvider,
98
+ *,
99
+ project_factory: Callable[[], CandidateProject],
100
+ run_dir: str | Path,
101
+ max_source_files: int = DEFAULT_SOURCE_TREE_MAX_FILES,
102
+ max_source_bytes: int = DEFAULT_SOURCE_TREE_MAX_BYTES,
103
+ max_candidate_history_bytes: int = DEFAULT_CANDIDATE_HISTORY_BYTES,
104
+ max_history_file_bytes: int = DEFAULT_HISTORY_FILE_BYTES,
105
+ ) -> None:
106
+ for field, value in (
107
+ ("max_source_files", max_source_files),
108
+ ("max_source_bytes", max_source_bytes),
109
+ ("max_candidate_history_bytes", max_candidate_history_bytes),
110
+ ("max_history_file_bytes", max_history_file_bytes),
111
+ ):
112
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
113
+ raise ValueError(f"{field} must be a positive integer")
114
+ self._agent = agent
115
+ self._provider = provider
116
+ self._project_factory = project_factory
117
+ self._run_dir = Path(run_dir)
118
+ self._max_source_files = max_source_files
119
+ self._max_source_bytes = max_source_bytes
120
+ self._max_candidate_history_bytes = max_candidate_history_bytes
121
+ self._max_history_file_bytes = max_history_file_bytes
122
+
123
+ def propose(
124
+ self,
125
+ population: Sequence[EvaluatedCandidate],
126
+ *,
127
+ slot: int,
128
+ should_cancel: Callable[[], bool] | None = None,
129
+ ) -> CandidateProposal:
130
+ """Produce one new candidate from the fixed seed stage, never a selected parent."""
131
+ if not population:
132
+ raise ValueError("candidate proposal requires an evaluated seed in the population")
133
+ if isinstance(slot, bool) or not isinstance(slot, int) or slot < 1:
134
+ raise ValueError("slot must be a positive integer")
135
+ _check_cancelled(should_cancel)
136
+ candidate_id = candidate_slot_id(slot)
137
+ slot_dir = self._run_dir / PROPOSALS_DIR / f"slot-{slot:04d}"
138
+ # A crashed earlier attempt at this same slot is still evidence: move it aside into
139
+ # attempt-K/ (never delete it) so the redone turn starts clean but later slots and
140
+ # humans can still read what happened.
141
+ _set_aside_prior_attempt(slot_dir)
142
+ slot_dir.mkdir(parents=True, exist_ok=True)
143
+ try:
144
+ return self._propose(
145
+ tuple(population),
146
+ candidate_id=candidate_id,
147
+ slot=slot,
148
+ slot_dir=slot_dir,
149
+ should_cancel=should_cancel,
150
+ )
151
+ except (HarnessSearchCancelled, CandidateProposalError):
152
+ raise
153
+ except Exception as error:
154
+ # Transport and materialization failures consume the paid slot (never a silent
155
+ # replay of a proposal turn); the population loop records the evidence path.
156
+ reason = f"proposal infrastructure failed: {error}"
157
+ _write_json(slot_dir / "error.json", {"candidate_id": candidate_id, "reason": reason})
158
+ raise CandidateProposalError(
159
+ candidate_id, reason, evidence_dir=str(slot_dir)
160
+ ) from error
161
+
162
+ def _propose(
163
+ self,
164
+ population: tuple[EvaluatedCandidate, ...],
165
+ *,
166
+ candidate_id: str,
167
+ slot: int,
168
+ slot_dir: Path,
169
+ should_cancel: Callable[[], bool] | None,
170
+ ) -> CandidateProposal:
171
+ project = self._project_factory()
172
+ try:
173
+ self._materialize_history(project, population)
174
+ self._materialize_prior_proposals(project, slot)
175
+ project.stage_source_tree(population[0].source, CANDIDATE_STAGE_DIR)
176
+ request = _proposal_request(
177
+ candidate_id=candidate_id,
178
+ stage_dir=f"{project.workspace}/{CANDIDATE_STAGE_DIR}",
179
+ slot=slot,
180
+ population_count=len(population),
181
+ )
182
+ (slot_dir / "REQUEST.md").write_text(request, encoding="utf-8")
183
+ project.write_text(f"{PROPOSALS_DIR}/slot-{slot:04d}/REQUEST.md", request)
184
+ _check_cancelled(should_cancel)
185
+
186
+ events: list[SessionEvent] = []
187
+ run_error: str | None = None
188
+ try:
189
+ project.run(
190
+ self._agent,
191
+ self._provider,
192
+ request,
193
+ on_event=events.append,
194
+ should_cancel=should_cancel,
195
+ writable_files=(),
196
+ retry_recoverable=False,
197
+ )
198
+ except HarnessSearchCancelled:
199
+ self._write_events(slot_dir, events)
200
+ raise
201
+ except Exception as error: # noqa: BLE001 - the failed turn is slot evidence
202
+ run_error = str(error)
203
+ self._write_events(slot_dir, events)
204
+ _check_cancelled(should_cancel)
205
+
206
+ source: HarnessSourceTree | None = None
207
+ snapshot_error: str | None = None
208
+ try:
209
+ source = project.snapshot_source_tree(
210
+ CANDIDATE_STAGE_DIR,
211
+ max_files=self._max_source_files,
212
+ max_bytes=self._max_source_bytes,
213
+ )
214
+ except Exception as error: # noqa: BLE001 - a bad snapshot is slot evidence
215
+ snapshot_error = str(error)
216
+ if source is not None:
217
+ for item in source.files:
218
+ target = slot_dir / "source" / item.path
219
+ target.parent.mkdir(parents=True, exist_ok=True)
220
+ # Exact bytes: newline translation would silently change content hashes.
221
+ target.write_bytes(item.content.encode("utf-8"))
222
+
223
+ failures: list[str] = []
224
+ if run_error is not None:
225
+ failures.append(f"agent turn failed: {run_error}")
226
+ if not any(event.kind == "submit" for event in events):
227
+ failures.append("agent turn did not submit a completed candidate")
228
+ if snapshot_error is not None:
229
+ failures.append(f"candidate snapshot failed: {snapshot_error}")
230
+ candidate: HarnessDoc | None = None
231
+ if source is not None:
232
+ try:
233
+ candidate = source.to_doc(candidate_id)
234
+ except (TypeError, ValueError) as error:
235
+ failures.append(f"candidate source is not a valid harness: {error}")
236
+ if source is not None and candidate is not None and not failures:
237
+ failures.extend(_interface_errors(project, source))
238
+
239
+ _write_json(
240
+ slot_dir / "status.json",
241
+ {
242
+ "candidate_id": candidate_id,
243
+ "valid": candidate is not None and not failures,
244
+ "candidate_doc_hash": None if candidate is None else candidate.doc_hash,
245
+ "source_tree_hash": None if source is None else source.tree_hash,
246
+ "errors": failures,
247
+ },
248
+ )
249
+ if failures or source is None or candidate is None:
250
+ reason = "; ".join(failures) or "candidate source was not captured"
251
+ raise CandidateProposalError(candidate_id, reason, evidence_dir=str(slot_dir))
252
+ return CandidateProposal(candidate_id=candidate_id, source=source, candidate=candidate)
253
+ finally:
254
+ try:
255
+ project.close()
256
+ except Exception: # noqa: BLE001 - closing must not mask the slot outcome
257
+ logger.warning(
258
+ "proposer project close failed; the sandbox may stay billable until its "
259
+ "lifetime timeout",
260
+ exc_info=True,
261
+ )
262
+
263
+ def _materialize_history(
264
+ self,
265
+ project: CandidateProject,
266
+ population: tuple[EvaluatedCandidate, ...],
267
+ ) -> None:
268
+ """Write every evaluated candidate's source, report, and raw trial evidence."""
269
+ manifest: list[dict[str, JsonValue]] = []
270
+ for evaluated in population:
271
+ directory = f"history/{evaluated.candidate_id}"
272
+ for item in evaluated.source.files:
273
+ project.write_text(f"{directory}/source/{item.path}", item.content)
274
+ report = evaluated.report
275
+ cells: list[dict[str, JsonValue]] = []
276
+ budget = self._max_candidate_history_bytes
277
+ for cell in report.cells:
278
+ trial_dir = f"{directory}/trials/{cell.task_id}/attempt-{cell.attempt}"
279
+ cells.append(
280
+ {
281
+ "task_id": cell.task_id,
282
+ "attempt": cell.attempt,
283
+ "reward": cell.reward,
284
+ "passed": cell.passed,
285
+ "note": cell.note,
286
+ "trial_dir": trial_dir,
287
+ }
288
+ )
289
+ budget = self._copy_trial_evidence(project, trial_dir, cell, budget)
290
+ project.write_text(
291
+ f"{directory}/report.json",
292
+ _json(
293
+ {
294
+ "candidate_id": evaluated.candidate_id,
295
+ "score": report.score,
296
+ "pass_rate": report.pass_rate,
297
+ "reward_mode": report.reward_mode,
298
+ "attempts": report.request.attempts,
299
+ "cells": cells,
300
+ }
301
+ ),
302
+ )
303
+ manifest.append(
304
+ {
305
+ "candidate_id": evaluated.candidate_id,
306
+ "score": report.score,
307
+ "pass_rate": report.pass_rate,
308
+ "by_task": {
309
+ task_id: sum(1 for cell in cells_ if cell.passed) / len(cells_)
310
+ for task_id, cells_ in report.by_task().items()
311
+ },
312
+ "source_dir": f"{directory}/source",
313
+ "report": f"{directory}/report.json",
314
+ "trials_dir": f"{directory}/trials",
315
+ }
316
+ )
317
+ project.write_text(
318
+ "history/manifest.json",
319
+ _json(
320
+ {
321
+ "candidate_count": len(manifest),
322
+ "candidates": manifest,
323
+ "proposals_dir": PROPOSALS_DIR,
324
+ }
325
+ ),
326
+ )
327
+
328
+ def _copy_trial_evidence(
329
+ self,
330
+ project: CandidateProject,
331
+ trial_dir: str,
332
+ cell: ScoreCell,
333
+ budget: int,
334
+ ) -> int:
335
+ """Copy one trial's transcript and verifier output (never harbor's ceremony files).
336
+
337
+ Every gap is marked where the proposer will look: a trial with no recorded artifact
338
+ directory gets `NO-EVIDENCE.md`, and a trial whose files were truncated or omitted by
339
+ the byte budget gets `EVIDENCE-TRUNCATED.md` naming exactly what is incomplete.
340
+ """
341
+ if not cell.artifact_dir:
342
+ project.write_text(
343
+ f"{trial_dir}/NO-EVIDENCE.md",
344
+ "This trial recorded no artifact directory; no raw evidence was captured.",
345
+ )
346
+ return budget
347
+ artifact_dir = Path(cell.artifact_dir)
348
+ sources: list[tuple[Path, str]] = []
349
+ transcript = artifact_dir / _TRIAL_AGENT_DIR / _WMO_RUN_FILENAME
350
+ if transcript.is_file():
351
+ sources.append((transcript, f"{trial_dir}/{_WMO_RUN_FILENAME}"))
352
+ verifier_dir = artifact_dir / _TRIAL_VERIFIER_DIR
353
+ if verifier_dir.is_dir():
354
+ sources.extend(
355
+ (path, f"{trial_dir}/{_TRIAL_VERIFIER_DIR}/{path.relative_to(verifier_dir)}")
356
+ for path in sorted(verifier_dir.rglob("*"))
357
+ if path.is_file()
358
+ )
359
+ incomplete: list[str] = []
360
+ for host_path, target in sources:
361
+ if budget <= 0:
362
+ incomplete.append(f"omitted (byte budget reached): {target}")
363
+ continue
364
+ try:
365
+ content, was_truncated = _read_evidence(host_path, self._max_history_file_bytes)
366
+ except OSError as error:
367
+ content, was_truncated = f"[unreadable evidence file: {error}]", False
368
+ project.write_text(target, content)
369
+ budget -= len(content.encode("utf-8"))
370
+ if was_truncated:
371
+ incomplete.append(f"head/tail truncated: {target}")
372
+ if incomplete:
373
+ project.write_text(
374
+ f"{trial_dir}/EVIDENCE-TRUNCATED.md",
375
+ "This trial's raw evidence is incomplete:\n" + "\n".join(incomplete),
376
+ )
377
+ return budget
378
+
379
+ def _materialize_prior_proposals(self, project: CandidateProject, slot: int) -> None:
380
+ """Upload every earlier slot's proposal trace so failures teach later turns."""
381
+ proposals_root = self._run_dir / PROPOSALS_DIR
382
+ if not proposals_root.is_dir():
383
+ return
384
+ budget = self._max_candidate_history_bytes
385
+ omitted: list[str] = []
386
+ for slot_dir in sorted(proposals_root.iterdir()):
387
+ if not slot_dir.is_dir() or slot_dir.name == f"slot-{slot:04d}":
388
+ continue
389
+ for path in sorted(slot_dir.rglob("*")):
390
+ if not path.is_file():
391
+ continue
392
+ relative = path.relative_to(proposals_root).as_posix()
393
+ if budget <= 0:
394
+ omitted.append(relative)
395
+ continue
396
+ try:
397
+ content, _was_truncated = _read_evidence(path, self._max_history_file_bytes)
398
+ except OSError as error:
399
+ content = f"[unreadable evidence file: {error}]"
400
+ project.write_text(f"{PROPOSALS_DIR}/{relative}", content)
401
+ budget -= len(content.encode("utf-8"))
402
+ if omitted:
403
+ project.write_text(
404
+ f"{PROPOSALS_DIR}/TRUNCATED.md",
405
+ "Prior proposal evidence beyond the byte budget was omitted:\n"
406
+ + "\n".join(omitted),
407
+ )
408
+
409
+ def _write_events(self, slot_dir: Path, events: Sequence[SessionEvent]) -> None:
410
+ _write_json(
411
+ slot_dir / "events.json",
412
+ [{"kind": event.kind, "payload": event.payload} for event in events],
413
+ )
414
+
415
+
416
+ # In-sandbox syntax validation for candidate .ts/.js files. `node --check` cannot be used for
417
+ # TypeScript: it does NOT strip types under --check (verified on node 22.23), so it falsely
418
+ # rejects valid TS including the seed's own vendored files. `stripTypeScriptTypes` (node >=
419
+ # 22.13) fully parses TS module grammar and throws on syntax errors, and plain JS is a subset
420
+ # of that grammar, so one code path validates both. When the sandbox's node predates it, the
421
+ # script reports the skip marker and validation is SKIPPED rather than false-rejecting every
422
+ # candidate.
423
+ _TS_VALIDATION_SKIP_MARKER = "typescript-validation-skipped"
424
+ _TS_CHECK_SCRIPT = f"""\
425
+ const {{ readFileSync }} = require('node:fs');
426
+ let strip;
427
+ try {{
428
+ ({{ stripTypeScriptTypes: strip }} = require('node:module'));
429
+ }} catch {{}}
430
+ if (typeof strip !== 'function') {{
431
+ console.log('{_TS_VALIDATION_SKIP_MARKER}: node:module.stripTypeScriptTypes unavailable');
432
+ process.exit(0);
433
+ }}
434
+ let failed = false;
435
+ for (const path of process.argv.slice(1)) {{
436
+ try {{
437
+ strip(readFileSync(path, 'utf8'));
438
+ }} catch (error) {{
439
+ failed = true;
440
+ console.error(path + ': ' + (error && error.message ? error.message : String(error)));
441
+ }}
442
+ }}
443
+ process.exit(failed ? 1 : 0);
444
+ """
445
+
446
+
447
+ def _interface_errors(project: CandidateProject, source: HarnessSourceTree) -> list[str]:
448
+ """The paper's interface-validation gate: parse-check every candidate code file.
449
+
450
+ A candidate that cannot parse must never burn a paid evaluation. One in-sandbox node
451
+ invocation strips/parses every `.ts`/`.js` file via `node:module.stripTypeScriptTypes`
452
+ (which throws on bad syntax); failures preserve node's per-file error as slot evidence.
453
+ """
454
+ staged = [
455
+ f"{CANDIDATE_STAGE_DIR}/{item.path}"
456
+ for item in source.files
457
+ if item.path.endswith((".ts", ".js"))
458
+ ]
459
+ if not staged:
460
+ return []
461
+ quoted_paths = " ".join(shlex.quote(path) for path in staged)
462
+ result = project.run_bash(f"node -e {shlex.quote(_TS_CHECK_SCRIPT)} {quoted_paths}")
463
+ if result.exit_code == 0:
464
+ if _TS_VALIDATION_SKIP_MARKER in result.stdout:
465
+ logger.warning("candidate interface validation skipped: %s", result.stdout.strip())
466
+ return []
467
+ detail = result.stderr or result.stdout or f"exit {result.exit_code}"
468
+ return [f"interface validation failed: {detail}"]
469
+
470
+
471
+ def _set_aside_prior_attempt(slot_dir: Path) -> None:
472
+ """Move a crashed prior attempt's evidence into `attempt-K/` instead of deleting it."""
473
+ if not slot_dir.is_dir():
474
+ return
475
+ prior = [path for path in slot_dir.iterdir() if not path.name.startswith("attempt-")]
476
+ if not prior:
477
+ return
478
+ attempt_count = sum(
479
+ 1 for path in slot_dir.iterdir() if path.is_dir() and path.name.startswith("attempt-")
480
+ )
481
+ attempt_dir = slot_dir / f"attempt-{attempt_count + 1}"
482
+ attempt_dir.mkdir()
483
+ for path in prior:
484
+ path.rename(attempt_dir / path.name)
485
+
486
+
487
+ def _read_evidence(path: Path, max_bytes: int) -> tuple[str, bool]:
488
+ """Read one evidence file, head/tail-truncating oversized content with a marker.
489
+
490
+ Returns the content and whether it was truncated.
491
+ """
492
+ size = path.stat().st_size
493
+ if size <= max_bytes:
494
+ return path.read_bytes().decode("utf-8", errors="replace"), False
495
+ half = max_bytes // 2
496
+ with path.open("rb") as handle:
497
+ head = handle.read(half)
498
+ handle.seek(size - half)
499
+ tail = handle.read(half)
500
+ return (
501
+ head.decode("utf-8", errors="replace")
502
+ + f"\n... {size - max_bytes} bytes truncated ...\n"
503
+ + tail.decode("utf-8", errors="replace")
504
+ ), True
505
+
506
+
507
+ def _proposal_request(
508
+ *,
509
+ candidate_id: str,
510
+ stage_dir: str,
511
+ slot: int,
512
+ population_count: int,
513
+ ) -> str:
514
+ previous_trace = (
515
+ f"The most recent proposal-turn trace is `{PROPOSALS_DIR}/slot-{slot - 1:04d}/`; "
516
+ "failed turns keep their captured source and errors in the same layout."
517
+ if slot > 1
518
+ else "There is no earlier proposal turn in this project."
519
+ )
520
+ return f"""Produce exactly one complete harness candidate: {candidate_id}.
521
+
522
+ Read `history/manifest.json`. It indexes all {population_count} evaluated candidates: complete
523
+ source directories, full score reports, and raw per-trial evidence under
524
+ `history/<candidate>/trials/<task>/attempt-N/` (`{_WMO_RUN_FILENAME}` is the complete agent
525
+ transcript for that trial; `verifier/` holds the verifier's own output). Earlier proposal-turn
526
+ traces, including failed ones, remain under `{PROPOSALS_DIR}/`. {previous_trace} Use the full
527
+ population as evidence.
528
+
529
+ Your only candidate output is this directory:
530
+ `{stage_dir}`
531
+
532
+ The host initialized it with a complete, freely editable copy of the fixed first evaluated seed at
533
+ `history/candidate-0000/source`. Every proposal slot receives that same fixed scaffold regardless
534
+ of later candidates or scores. Use bash to inspect the immutable project evidence and to create,
535
+ edit, delete, replace, and test files inside the candidate directory. Leave one complete
536
+ standalone harness source tree there; it must not import from or otherwise depend on `history/`,
537
+ `{PROPOSALS_DIR}/`, or any other project path. The final portable source tree must contain a
538
+ UTF-8 `SYSTEM.md`; optional `config.toml`, `runtime.py`, `skills/*.md`, and code files must
539
+ remain valid in the portable source format and parse together as one complete harness.
540
+
541
+ Filename grammar (strict): outside the reserved names above, every file path must be lowercase
542
+ kebab-case, meaning runs of [a-z0-9] separated by single '/', '.', or '-' characters (for example
543
+ `src/agent-loop.ts`). Uppercase letters and underscores are rejected (`src/a_b.ts` is invalid).
544
+ Because '/' and '.' both canonicalize to '-', paths like `a-b.ts` and `a/b.ts` collide; paths
545
+ differing only by letter case collide too, and no file path may also be a directory prefix of
546
+ another (`a` next to `a/b.ts`). One bad filename invalidates the whole candidate, so name files
547
+ carefully instead of spending this slot on cosmetics.
548
+
549
+ Do not modify immutable project paths. When the candidate is complete, call submit. The host
550
+ snapshots the directory once after this turn, parse-checks every `.ts`/`.js` file with node's
551
+ TypeScript syntax stripper (a syntax error anywhere invalidates the candidate), and will not ask
552
+ for a repair.
553
+
554
+ This turn's immutable request and trace are stored under `{PROPOSALS_DIR}/slot-{slot:04d}/`.
555
+ """
556
+
557
+
558
+ def _json(value: JsonValue) -> str:
559
+ return json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)
560
+
561
+
562
+ def _write_json(path: Path, value: JsonValue) -> None:
563
+ path.parent.mkdir(parents=True, exist_ok=True)
564
+ path.write_text(_json(value) + "\n", encoding="utf-8")
565
+
566
+
567
+ def _check_cancelled(should_cancel: Callable[[], bool] | None) -> None:
568
+ if should_cancel is not None and should_cancel():
569
+ raise HarnessSearchCancelled("harness search cancelled")