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/cli/harness_app.py ADDED
@@ -0,0 +1,1147 @@
1
+ """Agent-harness optimization and inspection under `.wmo/harnesses/<name>/`.
2
+
3
+ A harness is the scaffold an agent runs with: prompt surfaces, a tool policy, loop parameters, and
4
+ skills, stored as immutable numbered versions with movable aliases (`champion` is what runs by
5
+ default). `init` writes the baseline as v1; `list`/`show` inspect what exists;
6
+ `wmo optimize harness`
7
+ searches for a better harness by **inverting the world model**. Delta variants are scored
8
+ closed-loop against it and gated on non-regression, so the environment model the traces built now
9
+ steers what the agent's scaffold should be. Run one closed-loop with
10
+ `wmo eval <tasks> --mode closed-loop --harness <name>[@ref]`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ import shlex
18
+ from pathlib import Path
19
+ from typing import Literal
20
+
21
+ import typer
22
+ from pydantic import BaseModel, ConfigDict, ValidationError
23
+ from rich.console import Console
24
+ from rich.markup import escape
25
+ from rich.prompt import Confirm, IntPrompt, Prompt
26
+ from rich.table import Table
27
+
28
+ from wmo.agents.default import default_agent
29
+ from wmo.agents.optimizer import optimizer_agent
30
+ from wmo.agents.project import AgentProject
31
+ from wmo.cli.harness_distill import run_distill
32
+ from wmo.cli.model_roles import resolve_opt_in_model_provider, resolve_required_model_config
33
+ from wmo.config import ARTIFACT_DIR, WorldModelStore
34
+ from wmo.config.store import validate_name
35
+ from wmo.core.types import JsonObject
36
+ from wmo.engine import load_world_model
37
+ from wmo.engine.world_model import WorldModel
38
+ from wmo.evals.gold import GoldJudge
39
+ from wmo.evals.tasks import TaskSpec, load_tasks
40
+ from wmo.harness.create import ProposalRecord, create_harness
41
+ from wmo.harness.doc import HarnessDoc
42
+ from wmo.harness.e2b_sandbox import E2B_TEMPLATE_ENV, resolve_e2b_template
43
+ from wmo.harness.population import (
44
+ CandidateProposer,
45
+ PopulationResult,
46
+ PopulationRunState,
47
+ SlotOutcome,
48
+ write_json_atomic,
49
+ )
50
+ from wmo.harness.population import (
51
+ optimize as optimize_population,
52
+ )
53
+ from wmo.harness.project_proposer import CandidateProject, ProjectCandidateProposer
54
+ from wmo.harness.proposer import ProviderDeltaProposer
55
+ from wmo.harness.runtime import DEFAULT_EVAL_EPISODE_TIMEOUT_S
56
+ from wmo.harness.scoring import RewardMode, Scorer, ScoreRequest
57
+ from wmo.harness.source_tree import HarnessSourceTree
58
+ from wmo.harness.store import CHAMPION_ALIAS, HarnessStore
59
+ from wmo.providers.base import Provider, ProviderConfig, ToolCallingProvider
60
+ from wmo.providers.registry import get_provider
61
+
62
+ # The default agent seed's literal CLI name: `wmo optimize harness pi harbor ...` starts from the
63
+ # built-in pi agent and publishes new versions under the store name "pi".
64
+ DEFAULT_SEED_AGENT = "pi"
65
+ _DEFAULT_HARBOR_ITERATIONS = 10
66
+ _HARBOR_ENVIRONMENT = "harbor"
67
+ _HARBOR_EXTRA_HINT = (
68
+ "the harbor environment needs the harbor extra; run `uv sync --extra harbor` "
69
+ "(or `pip install 'world-model-optimizer[harbor]'`)"
70
+ )
71
+
72
+ harness_app = typer.Typer(
73
+ help="Inspect and initialize named, versioned agent harnesses.",
74
+ no_args_is_help=True,
75
+ )
76
+ _console = Console()
77
+
78
+
79
+ @harness_app.command("list")
80
+ def list_harnesses(root: str = typer.Option(ARTIFACT_DIR, help="Project dir.")) -> None:
81
+ """List every harness with its versions and aliases."""
82
+ store = HarnessStore(root)
83
+ names = store.list_names()
84
+ if not names:
85
+ _console.print(
86
+ "[yellow]no harnesses yet[/yellow]; `wmo harness init <name>` creates the baseline"
87
+ )
88
+ return
89
+ table = Table(title="Harnesses")
90
+ table.add_column("Name", no_wrap=True)
91
+ table.add_column("Versions", justify="right")
92
+ table.add_column("Aliases")
93
+ table.add_column("Doc hash (champion)")
94
+ broken: list[tuple[str, str]] = []
95
+ for name in names:
96
+ try:
97
+ doc = store.load(name)
98
+ aliases = ", ".join(f"{a}=v{v}" for a, v in sorted(store.aliases(name).items()))
99
+ table.add_row(
100
+ name,
101
+ f"{len(store.versions(name))}",
102
+ aliases or "—",
103
+ doc.doc_hash[:12],
104
+ )
105
+ except (ValueError, FileNotFoundError) as exc: # one broken dir must not hide the rest
106
+ broken.append((name, str(exc)))
107
+ _console.print(table)
108
+ for name, reason in broken:
109
+ _console.print(f"[red]broken[/red] {name}: {reason}")
110
+
111
+
112
+ @harness_app.command("show")
113
+ def show_harness(
114
+ name: str = typer.Argument(..., help="Harness name, optionally name@ref (version or alias)."),
115
+ root: str = typer.Option(ARTIFACT_DIR, help="Project dir."),
116
+ ) -> None:
117
+ """Print one harness version's surfaces."""
118
+ base, _, ref = name.partition("@")
119
+ try:
120
+ doc = HarnessStore(root).load(base, ref or None)
121
+ except (FileNotFoundError, ValueError) as exc:
122
+ raise typer.BadParameter(str(exc)) from exc
123
+ _console.print(f"[bold]{doc.name}[/bold] v{doc.version} doc_hash={doc.doc_hash[:12]}")
124
+ for surface in doc.surfaces:
125
+ budget = f" budget={surface.budget}" if surface.budget is not None else ""
126
+ _console.print(
127
+ f"\n[bold]{surface.id}[/bold] ({surface.kind.value}, "
128
+ f"hash={surface.content_hash[:12]}{budget})"
129
+ )
130
+ _console.print(surface.content)
131
+
132
+
133
+ optimize_app = typer.Typer(
134
+ help="Optimizers behind one switch: harness (agent-scaffold search) today; route "
135
+ "(learned inference policy) and training-type optimizers join as subcommands.",
136
+ no_args_is_help=True,
137
+ )
138
+
139
+ # Local import placement: route_app imports the optimize package; registering here keeps the
140
+ # whole optimizer family visible in one place.
141
+ from wmo.cli.route_app import route_app # noqa: E402
142
+
143
+ optimize_app.add_typer(route_app, name="route")
144
+
145
+
146
+ @optimize_app.command("harness")
147
+ def optimize(
148
+ ctx: typer.Context,
149
+ name: str = typer.Argument(
150
+ None,
151
+ help="Agent name for the optimized harness. For the harbor environment this is the "
152
+ "seed and the publication target: the bare literal 'pi' is ALWAYS the built-in "
153
+ "default agent (fixed-seed protocol), even after a stored 'pi' champion exists; "
154
+ "seed from a stored version explicitly with 'pi@champion' or 'pi@vN'.",
155
+ ),
156
+ model: str = typer.Argument(
157
+ None,
158
+ help="World model to optimize against (default: the only built one), or the literal "
159
+ "'harbor' to optimize on real harbor benchmark tasks.",
160
+ ),
161
+ tasks_file: str = typer.Option(None, "--tasks", help="JSONL task file to optimize against."),
162
+ holdout_file: str = typer.Option(
163
+ None,
164
+ "--holdout",
165
+ help="Optional JSONL held-out task file: accepted deltas must also be no worse here.",
166
+ ),
167
+ backend: str = typer.Option(
168
+ "local",
169
+ "--backend",
170
+ help="Where the harness PROCESS runs: local (in/from this process) or e2b (the real "
171
+ "pi agent inside pooled E2B sandboxes). The environment is always the world model.",
172
+ ),
173
+ eval_concurrency: int | None = typer.Option(
174
+ None,
175
+ "--eval-concurrency",
176
+ min=0,
177
+ help="(task, attempt) cells run at once per eval. Default: 1 for local; "
178
+ "0 (= all cells at once) for e2b.",
179
+ ),
180
+ e2b_template: str | None = typer.Option(
181
+ None,
182
+ "--e2b-template",
183
+ envvar=E2B_TEMPLATE_ENV,
184
+ help="Prebaked E2B sandbox template for --backend e2b (default: "
185
+ "$WMO_E2B_TEMPLATE; without one, every sandbox bootstraps node + the pi runner deps).",
186
+ ),
187
+ seed: str = typer.Option(
188
+ None,
189
+ "--seed",
190
+ help="Harness to start from, as a name or name@version (default: built-in baseline).",
191
+ ),
192
+ iterations: int = typer.Option(
193
+ None,
194
+ min=0,
195
+ help="Propose-and-gate steps (the search budget). 0 scores the seed only (harbor env), "
196
+ "the way a baseline or a frozen champion is scored on a task set.",
197
+ ),
198
+ proposal_batch_size: int = typer.Option(
199
+ 1,
200
+ "--proposal-batch-size",
201
+ min=1,
202
+ help="Sibling proposals generated against each selected parent.",
203
+ ),
204
+ k: int = typer.Option(3, min=1, help="Closed-loop passes per task per variant."),
205
+ root: str = typer.Option(ARTIFACT_DIR, help="Project dir."),
206
+ archive_out: str = typer.Option(
207
+ None, "--archive", help="Also write the full delta archive JSON here."
208
+ ),
209
+ yes: bool = typer.Option(False, "--yes", help="Skip the cost confirmation prompt."),
210
+ harbor_config: str = typer.Option(
211
+ None,
212
+ "--harbor-config",
213
+ help="(harbor) Harbor JobConfig template, YAML or JSON: the task-environment config "
214
+ "and harbor tuning, with exactly one dataset and no direct tasks.",
215
+ ),
216
+ task_ids_file: str = typer.Option(
217
+ None,
218
+ "--task-ids",
219
+ help="(harbor) JSON file containing the exact task-id string list to optimize on.",
220
+ ),
221
+ attempts: int = typer.Option(
222
+ None, "--attempts", min=1, help="(harbor) Attempts per task per candidate (default 1)."
223
+ ),
224
+ reward_key: str = typer.Option(
225
+ None,
226
+ "--reward-key",
227
+ help="(harbor) Verifier reward key to optimize (default 'reward').",
228
+ ),
229
+ reward_mode: str = typer.Option(
230
+ None,
231
+ "--reward-mode",
232
+ help="(harbor) raw | positive-binary (default positive-binary: reward > 0 passes).",
233
+ ),
234
+ harbor_retries: int = typer.Option(
235
+ None,
236
+ "--harbor-retries",
237
+ min=0,
238
+ help="(harbor) Harbor-level retries per failed trial (default 0).",
239
+ ),
240
+ episode_timeout: float = typer.Option(
241
+ None,
242
+ "--episode-timeout",
243
+ min=0.001,
244
+ help="(harbor) Wall seconds per evaluated episode (default 300; needs --backend e2b).",
245
+ ),
246
+ run_dir: str = typer.Option(
247
+ None,
248
+ "--run-dir",
249
+ help="(harbor) Directory holding ALL durable run state (required for harbor).",
250
+ ),
251
+ resume: bool = typer.Option(
252
+ False,
253
+ "--resume",
254
+ help="(harbor) Continue the interrupted run recorded in --run-dir.",
255
+ ),
256
+ max_iterations_this_run: int = typer.Option(
257
+ None,
258
+ "--max-iterations-this-run",
259
+ min=1,
260
+ help="(harbor) Stop after this many new boundaries this invocation; continue later "
261
+ "with --resume.",
262
+ ),
263
+ mode: str = typer.Option(
264
+ "search",
265
+ "--mode",
266
+ help="search (the default propose-and-gate optimizer) or distill (harbor only: "
267
+ "on-policy distillation of the agent MODEL itself; the harness stays fixed and "
268
+ "an accepted run produces a trained adapter).",
269
+ ),
270
+ distill_config: str = typer.Option(
271
+ None,
272
+ "--distill-config",
273
+ help="(distill) Per-run distillation TOML (student, teacher, harbor, train, gate, "
274
+ "pricing, budget sections). Required to start; a resume reuses the run dir's "
275
+ "config.toml snapshot.",
276
+ ),
277
+ holdout_task_ids_file: str = typer.Option(
278
+ None,
279
+ "--holdout-task-ids",
280
+ help="(distill) JSON file with the exact holdout task-id list; baselines and the "
281
+ "promotion gate are measured here, disjoint from --task-ids.",
282
+ ),
283
+ promote: bool = typer.Option(
284
+ False,
285
+ "--promote",
286
+ help="(distill) After an accepted gate, offer to write [models.agent] in "
287
+ "settings.toml pointing at the distilled adapter (always asks for confirmation).",
288
+ ),
289
+ ) -> None:
290
+ """Optimize an agent harness by searching against a world model or on harbor tasks.
291
+
292
+ The optimizer proposes harness changes, scores them against the environment, and saves the
293
+ best result as the new champion. Configure distinct agent and optimizer models with
294
+ `models.agent` and `models.meta` in `.wmo/settings.toml`.
295
+
296
+ With a world-model ENVIRONMENT, the backend controls where the worker process runs and the
297
+ environment remains the world model. With the literal `harbor` ENVIRONMENT, complete-source
298
+ candidates are scored on real benchmark tasks: --backend controls the task environment and
299
+ worker placement (local = docker tasks + local pi; e2b = E2B tasks + sandboxed pi), while
300
+ the PROPOSER project always runs in E2B in this version.
301
+
302
+ `--mode distill` (harbor environment only) trains the agent model instead of editing
303
+ the harness: on-policy distillation of a Tinker LoRA student from pi-agent rollouts on
304
+ the harbor tasks, gated on holdout solve rates against the teacher.
305
+ """
306
+ if mode not in ("search", "distill"):
307
+ raise typer.BadParameter(f"unknown --mode {mode!r}; choose search or distill")
308
+ if mode == "search":
309
+ distill_only = [
310
+ flag
311
+ for flag, provided in (
312
+ ("--distill-config", distill_config is not None),
313
+ ("--holdout-task-ids", holdout_task_ids_file is not None),
314
+ ("--promote", promote),
315
+ )
316
+ if provided
317
+ ]
318
+ if distill_only:
319
+ raise typer.BadParameter(
320
+ f"{', '.join(distill_only)} apply only to --mode distill; add "
321
+ "--mode distill (harbor environment) or drop them"
322
+ )
323
+ elif model != _HARBOR_ENVIRONMENT:
324
+ raise typer.BadParameter(
325
+ "--mode distill requires the harbor environment: "
326
+ "`wmo optimize harness <agent> harbor --mode distill ...`; a world-model distill "
327
+ "backend is a documented follow-on and is not available yet"
328
+ )
329
+ if model == _HARBOR_ENVIRONMENT:
330
+ world_model_only = [
331
+ flag
332
+ for param, flag in (
333
+ ("tasks_file", "--tasks"),
334
+ ("holdout_file", "--holdout"),
335
+ ("seed", "--seed"),
336
+ ("k", "--k"),
337
+ ("eval_concurrency", "--eval-concurrency"),
338
+ ("proposal_batch_size", "--proposal-batch-size"),
339
+ ("archive_out", "--archive"),
340
+ )
341
+ if _explicit(ctx, param)
342
+ ]
343
+ if world_model_only:
344
+ raise typer.BadParameter(
345
+ f"{', '.join(world_model_only)} apply only to a world-model environment; "
346
+ "drop them for `wmo optimize harness <agent> harbor ...`"
347
+ )
348
+ # Shared by both modes: 'harbor' must unambiguously mean the benchmark
349
+ # environment, never a stored world model that happens to carry the name.
350
+ if WorldModelStore(root).exists(_HARBOR_ENVIRONMENT):
351
+ raise typer.BadParameter(
352
+ "a stored world model is literally named 'harbor', which now selects the "
353
+ "harbor benchmark environment; rename that model directory under "
354
+ "<root>/models/ and retry"
355
+ )
356
+ if mode == "distill":
357
+ search_only = [
358
+ flag
359
+ for param, flag in (
360
+ ("iterations", "--iterations"),
361
+ ("harbor_config", "--harbor-config"),
362
+ ("attempts", "--attempts"),
363
+ ("reward_key", "--reward-key"),
364
+ ("reward_mode", "--reward-mode"),
365
+ ("harbor_retries", "--harbor-retries"),
366
+ ("episode_timeout", "--episode-timeout"),
367
+ ("max_iterations_this_run", "--max-iterations-this-run"),
368
+ ("e2b_template", "--e2b-template"),
369
+ )
370
+ if _explicit(ctx, param)
371
+ ]
372
+ if search_only:
373
+ raise typer.BadParameter(
374
+ f"{', '.join(search_only)} apply only to --mode search; a distill "
375
+ "run's harbor job template, attempts, and schedule come from "
376
+ "--distill-config"
377
+ )
378
+ run_distill(
379
+ _console,
380
+ agent_name=name,
381
+ distill_config_path=distill_config,
382
+ task_ids_path=task_ids_file,
383
+ holdout_task_ids_path=holdout_task_ids_file,
384
+ run_dir=run_dir,
385
+ backend=backend if _explicit(ctx, "backend") else None,
386
+ resume=resume,
387
+ yes=yes,
388
+ promote=promote,
389
+ root=root,
390
+ )
391
+ return
392
+ _optimize_harbor(
393
+ ctx,
394
+ name=name,
395
+ backend=backend,
396
+ e2b_template=e2b_template,
397
+ iterations=iterations,
398
+ harbor_config=harbor_config,
399
+ task_ids_file=task_ids_file,
400
+ attempts=attempts,
401
+ reward_key=reward_key,
402
+ reward_mode=reward_mode,
403
+ harbor_retries=harbor_retries,
404
+ episode_timeout=episode_timeout,
405
+ run_dir_option=run_dir,
406
+ resume=resume,
407
+ max_iterations_this_run=max_iterations_this_run,
408
+ root=root,
409
+ yes=yes,
410
+ )
411
+ return
412
+ harbor_only = [
413
+ flag
414
+ for flag, provided in (
415
+ ("--harbor-config", harbor_config is not None),
416
+ ("--task-ids", task_ids_file is not None),
417
+ ("--attempts", attempts is not None),
418
+ ("--reward-key", reward_key is not None),
419
+ ("--reward-mode", reward_mode is not None),
420
+ ("--harbor-retries", harbor_retries is not None),
421
+ ("--episode-timeout", episode_timeout is not None),
422
+ ("--run-dir", run_dir is not None),
423
+ ("--resume", resume),
424
+ ("--max-iterations-this-run", max_iterations_this_run is not None),
425
+ )
426
+ if provided
427
+ ]
428
+ if harbor_only:
429
+ raise typer.BadParameter(
430
+ f"{', '.join(harbor_only)} apply only to the harbor environment; "
431
+ "use `wmo optimize harness <agent> harbor ...`"
432
+ )
433
+ if iterations == 0:
434
+ raise typer.BadParameter(
435
+ "--iterations 0 (score-only) applies only to the harbor environment; "
436
+ "world-model optimization needs at least one search iteration"
437
+ )
438
+ interactive = _console.is_terminal
439
+ if name is None:
440
+ if not interactive:
441
+ raise typer.BadParameter("provide a harness NAME (or run at a TTY for the wizard)")
442
+ name = Prompt.ask("Name for the created harness", default="evolved")
443
+ if tasks_file is None:
444
+ if not interactive:
445
+ raise typer.BadParameter("provide --tasks (or run at a TTY for the wizard)")
446
+ tasks_file = Prompt.ask("Task file (JSONL of task_id/instruction/gold)")
447
+ if iterations is None:
448
+ iterations = (
449
+ IntPrompt.ask("Search iterations (each = 1 delta + 1 gated eval)", default=5)
450
+ if interactive
451
+ else 5
452
+ )
453
+
454
+ if backend not in ("local", "e2b"):
455
+ raise typer.BadParameter(f"unknown --backend {backend!r}; choose local or e2b")
456
+ # Fail on a bad name NOW, not after the search has spent its eval budget on the save.
457
+ try:
458
+ validate_name(name)
459
+ except ValueError as exc:
460
+ raise typer.BadParameter(str(exc)) from exc
461
+ tasks = _load_task_file(tasks_file)
462
+ holdout = _load_task_file(holdout_file) if holdout_file else None
463
+ store = HarnessStore(root)
464
+ seed_doc = _resolve_seed(store, seed)
465
+ # The world model IS the environment on every backend, so it is always required.
466
+ world_model, provider, model_name = _load_world_model(model, root)
467
+ meta_provider, meta_model = resolve_opt_in_model_provider(root, "meta", provider)
468
+ agent_provider, agent_model = resolve_opt_in_model_provider(root, "agent", provider)
469
+
470
+ candidate_count = iterations * proposal_batch_size
471
+ rollouts = (candidate_count + 1) * k * len(tasks)
472
+ holdout_note = (
473
+ f" (+ up to {(candidate_count + 1) * k * len(holdout)} held-out)" if holdout else ""
474
+ )
475
+ backend_note = (
476
+ " (pi harness in pooled E2B sandboxes; env stays the world model)"
477
+ if backend == "e2b"
478
+ else ""
479
+ )
480
+ meta_note = (
481
+ f" (proposer: {meta_model} from settings models.meta)" if meta_model is not None else ""
482
+ )
483
+ agent_note = (
484
+ f" (agent-under-test: {agent_model} from settings models.agent)"
485
+ if agent_model is not None
486
+ else ""
487
+ )
488
+ _console.print(
489
+ f"searching from [bold]{seed_doc.name}[/bold] against world model "
490
+ f"[bold]{model_name}[/bold]: {iterations} iteration(s), "
491
+ f"{proposal_batch_size} proposal(s)/iteration, k={k}, {len(tasks)} task(s) "
492
+ f"-> up to ~{rollouts} rollouts{holdout_note} + {candidate_count} proposals"
493
+ f"{meta_note}{agent_note}{backend_note}"
494
+ )
495
+ if interactive and not yes and not Confirm.ask("Proceed?", default=True):
496
+ raise typer.Exit(0)
497
+
498
+ def _progress(iteration: int, variant: str, score: float, changed: bool) -> None:
499
+ tag = "seed" if iteration == 0 else f"iter {iteration}"
500
+ state = (
501
+ "seed"
502
+ if iteration == 0
503
+ else "[green]selected[/green]"
504
+ if changed
505
+ else "[yellow]unchanged[/yellow]"
506
+ )
507
+ _console.print(f" [{tag}] {variant}: success_rate={score:.3f} {state}")
508
+
509
+ def _note(message: str) -> None:
510
+ # Dead proposals narrate here; scored proposals use the structured callback below.
511
+ _console.print(f" [dim]{message}[/dim]")
512
+
513
+ def _proposal(record: ProposalRecord) -> None:
514
+ if record.outcome != "scored":
515
+ return
516
+ assert record.candidate is not None and record.score is not None
517
+ state = (
518
+ "[green]selected[/green]"
519
+ if record.selected
520
+ else "[cyan]eligible, not selected[/cyan]"
521
+ if record.gate_eligible
522
+ else "[yellow]rejected by gate[/yellow]"
523
+ )
524
+ _console.print(
525
+ f" [iteration {record.iteration} proposal {record.proposal_index}] "
526
+ f"{record.candidate}: success_rate={record.score:.3f} {state}"
527
+ )
528
+
529
+ result = create_harness(
530
+ name,
531
+ seed_doc,
532
+ tasks,
533
+ world_model,
534
+ agent_provider,
535
+ ProviderDeltaProposer(meta_provider),
536
+ GoldJudge(provider),
537
+ iterations=iterations,
538
+ proposal_batch_size=proposal_batch_size,
539
+ k=k,
540
+ holdout=holdout,
541
+ harness_backend="e2b" if backend == "e2b" else "local",
542
+ eval_concurrency=eval_concurrency,
543
+ e2b_template=e2b_template,
544
+ on_progress=_progress,
545
+ on_note=_note,
546
+ on_proposal=_proposal,
547
+ )
548
+ saved = store.save_version(result.best, alias=CHAMPION_ALIAS)
549
+ selected = len(result.archive.accepted())
550
+ _console.print(
551
+ f"[green]created[/green] [bold]{name}[/bold] v{saved.version} (champion) "
552
+ f"success_rate={result.best_score:.3f}: {len(result.archive.deltas)} delta(s) audited, "
553
+ f"{selected} selected, {result.skipped} skipped -> {store.dir_for(name)}"
554
+ )
555
+ run_argv = [
556
+ "wmo",
557
+ "eval",
558
+ tasks_file,
559
+ "--mode",
560
+ "closed-loop",
561
+ "--name",
562
+ model_name,
563
+ "--root",
564
+ root,
565
+ "--k",
566
+ str(k),
567
+ "--harness",
568
+ f"{name}@{saved.version}",
569
+ ]
570
+ if backend == "e2b":
571
+ run_argv.extend(("--harness-backend", "e2b"))
572
+ if eval_concurrency is not None:
573
+ run_argv.extend(("--eval-concurrency", str(eval_concurrency)))
574
+ if backend == "e2b" and e2b_template is not None:
575
+ run_argv.extend(("--e2b-template", e2b_template))
576
+ _console.print(
577
+ f" run it: [bold]{escape(shlex.join(run_argv))}[/bold]",
578
+ soft_wrap=True,
579
+ )
580
+ if archive_out:
581
+ Path(archive_out).write_text(result.archive.model_dump_json(indent=2), encoding="utf-8")
582
+ _console.print(f" wrote archive -> {archive_out}")
583
+
584
+
585
+ def _load_task_file(path: str) -> list[TaskSpec]:
586
+ try:
587
+ return load_tasks(path)
588
+ except (OSError, ValueError) as exc:
589
+ raise typer.BadParameter(f"cannot load tasks from {path!r}: {exc}") from exc
590
+
591
+
592
+ def _resolve_seed(store: HarnessStore, seed: str | None) -> HarnessDoc:
593
+ if seed is None:
594
+ return HarnessDoc.baseline()
595
+ base, _, ref = seed.partition("@")
596
+ try:
597
+ return store.load(base, ref or None)
598
+ except (FileNotFoundError, ValueError) as exc:
599
+ raise typer.BadParameter(str(exc)) from exc
600
+
601
+
602
+ def _load_world_model(name: str | None, root: str) -> tuple[WorldModel, Provider, str]:
603
+ """Resolve a world model by name (or the sole built one) and load it with its provider."""
604
+ store = WorldModelStore(root)
605
+ try:
606
+ model_dir = store.resolve(name)
607
+ except (FileNotFoundError, ValueError) as exc:
608
+ raise typer.BadParameter(str(exc)) from exc
609
+ world_model, provider = load_world_model(model_dir)
610
+ return world_model, provider, model_dir.name
611
+
612
+
613
+ # -- the harbor environment: complete-source population optimization on real tasks ------------
614
+
615
+
616
+ class _HarborRunConfig(BaseModel):
617
+ """The resolved harbor optimize configuration persisted as `run-config.json`."""
618
+
619
+ model_config = ConfigDict(frozen=True)
620
+
621
+ agent: str
622
+ attempts: int
623
+ backend: Literal["local", "e2b"]
624
+ e2b_template: str | None
625
+ episode_timeout_s: float
626
+ # The PARSED RAW job template mapping, never a harbor model_dump (which would redact
627
+ # sensitive-named env values that differ from this process's environment).
628
+ harbor_job_template: JsonObject
629
+ harbor_retries: int
630
+ iterations: int
631
+ # The worker and proposer model identities resolved at run start; a resume re-resolves the
632
+ # roles and rejects a mismatch so a mid-run settings.toml edit cannot silently change what
633
+ # is being scored or who is proposing.
634
+ proposer_model: JsonObject
635
+ worker_model: JsonObject
636
+ reward_key: str
637
+ reward_mode: RewardMode
638
+ # The stored-seed version this run started from; None means the built-in default agent.
639
+ # Resumes load the seed from the run dir itself, so champion movement (including this
640
+ # run's own publication) can never re-resolve the seed to a different tree.
641
+ seed_version: int | None
642
+ task_ids: tuple[str, ...]
643
+ # Per-task provenance pins (git commit / package ref / local path) resolved on the first
644
+ # run; None until the dataset has resolved once. A resume that resolves differently is
645
+ # rejected instead of silently scoring different task bytes.
646
+ task_pins: JsonObject | None = None
647
+
648
+
649
+ def _model_identity(config: ProviderConfig) -> JsonObject:
650
+ """The provider identity fields a run pins (and a resume must re-resolve identically).
651
+
652
+ `model_type` and `endpoint` are part of the identity: for a tinker-provider
653
+ role model_type selects the renderer/tokenizer, and endpoint selects the
654
+ serving host, so a mid-run settings edit to either would silently change
655
+ the worker under test if they were not pinned.
656
+ """
657
+ return {
658
+ "provider": config.kind.value,
659
+ "model": config.model,
660
+ "model_type": config.model_type,
661
+ "deployment": config.deployment,
662
+ "region": config.region,
663
+ "endpoint": config.endpoint,
664
+ "reasoning_effort": config.reasoning_effort,
665
+ }
666
+
667
+
668
+ def _explicit(ctx: typer.Context, param: str) -> bool:
669
+ """Whether `param` was explicitly passed on the command line.
670
+
671
+ Compared by enum NAME: typer vendors click, so its ParameterSource enum is not
672
+ click.core's class and an identity check would silently never match.
673
+ """
674
+ source = ctx.get_parameter_source(param)
675
+ return source is not None and source.name == "COMMANDLINE"
676
+
677
+
678
+ def _optimize_harbor(
679
+ ctx: typer.Context,
680
+ *,
681
+ name: str | None,
682
+ backend: str,
683
+ e2b_template: str | None,
684
+ iterations: int | None,
685
+ harbor_config: str | None,
686
+ task_ids_file: str | None,
687
+ attempts: int | None,
688
+ reward_key: str | None,
689
+ reward_mode: str | None,
690
+ harbor_retries: int | None,
691
+ episode_timeout: float | None,
692
+ run_dir_option: str | None,
693
+ resume: bool,
694
+ max_iterations_this_run: int | None,
695
+ root: str,
696
+ yes: bool,
697
+ ) -> None:
698
+ """Run the harbor population optimizer: fixed seed, sequential complete-source proposals."""
699
+ if name is None:
700
+ raise typer.BadParameter(
701
+ "provide the seed agent NAME (the literal 'pi' is the built-in default agent): "
702
+ "`wmo optimize harness pi harbor --harbor-config ... --task-ids ... --run-dir ...`"
703
+ )
704
+ if backend not in ("local", "e2b"):
705
+ raise typer.BadParameter(f"unknown --backend {backend!r}; choose local or e2b")
706
+ if reward_mode is not None and reward_mode not in ("raw", "positive-binary"):
707
+ raise typer.BadParameter(
708
+ f"unknown --reward-mode {reward_mode!r}; choose raw or positive-binary"
709
+ )
710
+ if run_dir_option is None:
711
+ raise typer.BadParameter(
712
+ "--run-dir is required for the harbor environment: it holds all durable run state"
713
+ )
714
+ # The 'harbor'-named world model ambiguity is rejected by optimize() before
715
+ # either mode dispatches here.
716
+
717
+ run_dir = Path(run_dir_option)
718
+ config_path = run_dir / "run-config.json"
719
+ meta_config = resolve_required_model_config(root, "meta")
720
+ agent_config = resolve_required_model_config(root, "agent")
721
+ if resume:
722
+ config = _resumed_harbor_config(
723
+ ctx,
724
+ config_path,
725
+ name=name,
726
+ backend=backend,
727
+ e2b_template=e2b_template,
728
+ iterations=iterations,
729
+ harbor_config=harbor_config,
730
+ task_ids_file=task_ids_file,
731
+ attempts=attempts,
732
+ reward_key=reward_key,
733
+ reward_mode=reward_mode,
734
+ harbor_retries=harbor_retries,
735
+ episode_timeout=episode_timeout,
736
+ meta_config=meta_config,
737
+ agent_config=agent_config,
738
+ )
739
+ seed_name, seed_tree = _resumed_harbor_seed(run_dir, root, config)
740
+ else:
741
+ if config_path.exists():
742
+ raise typer.BadParameter(
743
+ f"{run_dir} already holds a run; pass --resume to continue it or choose a "
744
+ "fresh --run-dir"
745
+ )
746
+ if harbor_config is None or task_ids_file is None:
747
+ raise typer.BadParameter(
748
+ "--harbor-config and --task-ids are required to start a harbor optimization"
749
+ )
750
+ seed_name, seed_tree, seed_version = _resolve_harbor_seed(root, name)
751
+ config = _HarborRunConfig(
752
+ agent=name,
753
+ attempts=attempts if attempts is not None else 1,
754
+ backend="e2b" if backend == "e2b" else "local",
755
+ e2b_template=resolve_e2b_template(e2b_template),
756
+ episode_timeout_s=(
757
+ episode_timeout if episode_timeout is not None else DEFAULT_EVAL_EPISODE_TIMEOUT_S
758
+ ),
759
+ harbor_job_template=_load_harbor_job_template(Path(harbor_config)),
760
+ harbor_retries=harbor_retries if harbor_retries is not None else 0,
761
+ iterations=iterations if iterations is not None else _DEFAULT_HARBOR_ITERATIONS,
762
+ proposer_model=_model_identity(meta_config),
763
+ worker_model=_model_identity(agent_config),
764
+ reward_key=reward_key if reward_key is not None else "reward",
765
+ reward_mode="raw" if reward_mode == "raw" else "positive-binary",
766
+ seed_version=seed_version,
767
+ task_ids=_load_harbor_task_ids(Path(task_ids_file)),
768
+ )
769
+ # Validated on the EFFECTIVE (stored-or-CLI) config: a consistent resume of an e2b run may
770
+ # restate --episode-timeout even though this invocation's --backend default is local.
771
+ if config.backend == "local" and config.episode_timeout_s != DEFAULT_EVAL_EPISODE_TIMEOUT_S:
772
+ raise typer.BadParameter("--episode-timeout requires --backend e2b")
773
+
774
+ _console.print(
775
+ f"harbor population search from [bold]{config.agent}[/bold]: 1 seed + "
776
+ f"{config.iterations} proposal slot(s), {len(config.task_ids)} task(s), "
777
+ f"{config.attempts} attempt(s), reward mode {config.reward_mode}, "
778
+ f"worker backend {config.backend} (proposer project: E2B) -> {run_dir}"
779
+ )
780
+ if _console.is_terminal and not yes and not Confirm.ask("Proceed?", default=True):
781
+ raise typer.Exit(0)
782
+
783
+ scorer, task_pins = _build_harbor_scorer(config, run_dir=run_dir, provider_config=agent_config)
784
+ if resume:
785
+ if config.task_pins is not None and task_pins != config.task_pins:
786
+ raise typer.BadParameter(
787
+ "the dataset resolved differently than this run recorded: "
788
+ f"recorded pins {config.task_pins}, resolved pins {task_pins}; restore the "
789
+ "recorded dataset revision or start a fresh --run-dir"
790
+ )
791
+ else:
792
+ # Recorded only now: the template validated, the user confirmed, and the dataset pins
793
+ # are known, so a declined or failed start never poisons the run dir.
794
+ config = config.model_copy(update={"task_pins": task_pins})
795
+ run_dir.mkdir(parents=True, exist_ok=True)
796
+ write_json_atomic(config_path, config.model_dump(mode="json"))
797
+ proposer = _build_harbor_proposer(
798
+ run_dir=run_dir, meta_config=meta_config, e2b_template=config.e2b_template
799
+ )
800
+
801
+ def _on_boundary(outcome: SlotOutcome) -> None:
802
+ if outcome.evaluated is None:
803
+ _console.print(
804
+ f" [slot {outcome.slot}] {outcome.candidate_id}: "
805
+ f"[yellow]invalid[/yellow] ({escape(outcome.reason)})"
806
+ )
807
+ else:
808
+ tag = "seed" if outcome.slot == 0 else f"slot {outcome.slot}"
809
+ _console.print(f" [{tag}] {outcome.candidate_id}: score={outcome.evaluated.score:.3f}")
810
+
811
+ result = optimize_population(
812
+ seed_tree,
813
+ scorer,
814
+ proposer,
815
+ config.iterations,
816
+ run_dir=run_dir,
817
+ max_new_boundaries=max_iterations_this_run,
818
+ on_boundary=_on_boundary,
819
+ )
820
+ if not result.completed:
821
+ _console.print(
822
+ f"[yellow]checkpointed[/yellow] {len(result.outcomes)}/{config.iterations + 1} "
823
+ f"boundaries; continue with: [bold]wmo optimize harness {config.agent} harbor --resume "
824
+ f"--run-dir {run_dir}[/bold]"
825
+ )
826
+ return
827
+ _publish_harbor_winner(result, root=root, seed_name=seed_name, run_dir=run_dir)
828
+
829
+
830
+ def _publish_harbor_winner(
831
+ result: PopulationResult, *, root: str, seed_name: str, run_dir: Path
832
+ ) -> None:
833
+ """Save the score winner as the seed agent's next version, exactly once per run.
834
+
835
+ `published.json` is the run's publication record and is written LAST: a completed run
836
+ resumed again re-prints that record instead of appending a duplicate store version and
837
+ moving the champion alias a second time.
838
+ """
839
+ published_path = run_dir / "published.json"
840
+ if published_path.exists():
841
+ record = json.loads(published_path.read_text(encoding="utf-8"))
842
+ _console.print(
843
+ f"[green]already published[/green] [bold]{record.get('name')}[/bold] "
844
+ f"v{record.get('version')} (doc_hash={str(record.get('doc_hash'))[:12]}) "
845
+ f"from {record.get('best_candidate_id')}; evidence -> {run_dir}"
846
+ )
847
+ return
848
+ store = HarnessStore(root)
849
+ winner = result.best.source.to_doc(seed_name)
850
+ saved = store.save_version(winner, alias=CHAMPION_ALIAS)
851
+ write_json_atomic(
852
+ published_path,
853
+ {
854
+ "name": saved.name,
855
+ "version": saved.version,
856
+ "doc_hash": saved.doc_hash,
857
+ "best_candidate_id": result.best.candidate_id,
858
+ "best_score": result.best_score,
859
+ },
860
+ )
861
+ scored = sum(1 for outcome in result.outcomes if outcome.evaluated is not None)
862
+ _console.print(
863
+ f"[green]optimized[/green] [bold]{seed_name}[/bold] v{saved.version} (champion) "
864
+ f"score={result.best_score:.3f} from {result.best.candidate_id} "
865
+ f"({scored} scored, {len(result.outcomes) - scored} invalid slot(s)) "
866
+ f"-> {store.dir_for(seed_name)}; evidence -> {run_dir}"
867
+ )
868
+
869
+
870
+ def _resumed_harbor_seed(
871
+ run_dir: Path, root: str, config: _HarborRunConfig
872
+ ) -> tuple[str, HarnessSourceTree]:
873
+ """The seed for a resumed run: the run dir's own record, never a live movable ref.
874
+
875
+ `candidates/candidate-0000/source` is authoritative once the seed boundary committed;
876
+ re-resolving the NAME live would let champion movement (including this run's own
877
+ publication) resolve a different tree and brick a legitimate resume. Before the first
878
+ boundary the seed is re-resolved from the PINNED version recorded at start.
879
+ """
880
+ seed_name = config.agent.partition("@")[0]
881
+ # load() only returns COMMITTED boundaries (state.json) and re-verifies each candidate's
882
+ # doc hash, so a crash that left a partial candidate-0000/source dir cannot leak in.
883
+ outcomes = PopulationRunState(run_dir).load()
884
+ if outcomes and outcomes[0].evaluated is not None:
885
+ return seed_name, outcomes[0].evaluated.source
886
+ if config.seed_version is None:
887
+ return seed_name, HarnessSourceTree.from_doc(default_agent(seed_name))
888
+ try:
889
+ doc = HarnessStore(root).load(seed_name, str(config.seed_version))
890
+ except (FileNotFoundError, ValueError) as error:
891
+ raise typer.BadParameter(
892
+ f"cannot reload the recorded seed {seed_name}@v{config.seed_version}: {error}"
893
+ ) from error
894
+ return seed_name, HarnessSourceTree.from_doc(doc)
895
+
896
+
897
+ def _resumed_harbor_config(
898
+ ctx: typer.Context,
899
+ config_path: Path,
900
+ *,
901
+ name: str,
902
+ backend: str,
903
+ e2b_template: str | None,
904
+ iterations: int | None,
905
+ harbor_config: str | None,
906
+ task_ids_file: str | None,
907
+ attempts: int | None,
908
+ reward_key: str | None,
909
+ reward_mode: str | None,
910
+ harbor_retries: int | None,
911
+ episode_timeout: float | None,
912
+ meta_config: ProviderConfig,
913
+ agent_config: ProviderConfig,
914
+ ) -> _HarborRunConfig:
915
+ """Load the recorded run config, rejecting explicit CLI flags that conflict with it."""
916
+ if not config_path.is_file():
917
+ raise typer.BadParameter(
918
+ f"--resume found no run-config.json under {config_path.parent}; "
919
+ "start the run once without --resume"
920
+ )
921
+ try:
922
+ stored = _HarborRunConfig.model_validate_json(config_path.read_text(encoding="utf-8"))
923
+ except ValidationError as error:
924
+ raise typer.BadParameter(f"cannot load {config_path}: {error}") from error
925
+
926
+ conflicts: list[str] = []
927
+ if name != stored.agent:
928
+ conflicts.append(f"NAME {name!r} != recorded {stored.agent!r}")
929
+ checks: list[tuple[str, str, object, object]] = [
930
+ ("backend", "--backend", backend, stored.backend),
931
+ ("iterations", "--iterations", iterations, stored.iterations),
932
+ ("attempts", "--attempts", attempts, stored.attempts),
933
+ ("reward_key", "--reward-key", reward_key, stored.reward_key),
934
+ ("reward_mode", "--reward-mode", reward_mode, stored.reward_mode),
935
+ ("harbor_retries", "--harbor-retries", harbor_retries, stored.harbor_retries),
936
+ ("episode_timeout", "--episode-timeout", episode_timeout, stored.episode_timeout_s),
937
+ (
938
+ "e2b_template",
939
+ "--e2b-template",
940
+ resolve_e2b_template(e2b_template),
941
+ stored.e2b_template,
942
+ ),
943
+ ]
944
+ conflicts.extend(
945
+ f"{flag} {value!r} != recorded {recorded!r}"
946
+ for param, flag, value, recorded in checks
947
+ if _explicit(ctx, param) and value != recorded
948
+ )
949
+ if _explicit(ctx, "harbor_config") and harbor_config is not None:
950
+ template = _load_harbor_job_template(Path(harbor_config))
951
+ if template != stored.harbor_job_template:
952
+ conflicts.append("--harbor-config differs from the recorded job template")
953
+ if _explicit(ctx, "task_ids_file") and task_ids_file is not None:
954
+ task_ids = _load_harbor_task_ids(Path(task_ids_file))
955
+ if task_ids != stored.task_ids:
956
+ conflicts.append("--task-ids differs from the recorded task list")
957
+ if conflicts:
958
+ raise typer.BadParameter(
959
+ "--resume uses the recorded run-config.json; conflicting flag(s): "
960
+ + "; ".join(conflicts)
961
+ + ". Drop them to continue this run, or start a fresh --run-dir"
962
+ )
963
+ # Provider roles come from settings.toml, not flags, so they are ALWAYS re-checked: a
964
+ # mid-run settings edit that changes the worker or proposer identity would silently break
965
+ # cross-slot score comparability.
966
+ role_changes = [
967
+ f"settings [models.{role}] resolved to {resolved} but this run recorded {recorded}"
968
+ for role, resolved, recorded in (
969
+ ("agent", _model_identity(agent_config), stored.worker_model),
970
+ ("meta", _model_identity(meta_config), stored.proposer_model),
971
+ )
972
+ if resolved != recorded
973
+ ]
974
+ if role_changes:
975
+ raise typer.BadParameter(
976
+ "; ".join(role_changes) + "; restore the recorded models or start a fresh --run-dir"
977
+ )
978
+ return stored
979
+
980
+
981
+ def _resolve_harbor_seed(root: str, agent_ref: str) -> tuple[str, HarnessSourceTree, int | None]:
982
+ """Resolve the seed AGENT positional: literal 'pi' is built-in, otherwise store name@ref.
983
+
984
+ The bare literal 'pi' ALWAYS means the built-in default agent (the fixed-seed protocol),
985
+ even after this command publishes a stored 'pi' champion; compounding runs seed from a
986
+ stored version explicitly via 'pi@champion' or 'pi@vN'. Returns the publication base name,
987
+ the seed tree, and the resolved store version (None for the built-in seed) so a resume can
988
+ pin exactly what this run started from.
989
+ """
990
+ base, _, ref = agent_ref.partition("@")
991
+ try:
992
+ validate_name(base)
993
+ except ValueError as error:
994
+ raise typer.BadParameter(str(error)) from error
995
+ if base == DEFAULT_SEED_AGENT and not ref:
996
+ return base, HarnessSourceTree.from_doc(default_agent(base)), None
997
+ try:
998
+ doc = HarnessStore(root).load(base, ref or None)
999
+ except (FileNotFoundError, ValueError) as error:
1000
+ raise typer.BadParameter(
1001
+ f"{error}; the built-in default agent seed is the literal {DEFAULT_SEED_AGENT!r}"
1002
+ ) from error
1003
+ return base, HarnessSourceTree.from_doc(doc), doc.version
1004
+
1005
+
1006
+ def _load_harbor_job_template(path: Path) -> JsonObject:
1007
+ """Load one harbor JobConfig template (YAML or JSON) as the PARSED RAW mapping.
1008
+
1009
+ The raw mapping is validated against `JobConfig` for checking only and returned untouched:
1010
+ serializing harbor models redacts sensitive-named env values that differ from this
1011
+ process's environment, so a `model_dump` here would corrupt the recorded run config and
1012
+ every trial's environment. The harbor SDK is an optional extra imported lazily, exactly
1013
+ like the e2b extra: only the harbor environment needs it, and `import wmo` must succeed
1014
+ without it.
1015
+ """
1016
+ try:
1017
+ import yaml
1018
+ from harbor.models.job.config import JobConfig
1019
+ except ImportError as error:
1020
+ raise typer.BadParameter(_HARBOR_EXTRA_HINT) from error
1021
+ try:
1022
+ raw = yaml.safe_load(path.read_text(encoding="utf-8"))
1023
+ JobConfig.model_validate(raw)
1024
+ except (OSError, yaml.YAMLError, ValueError, TypeError) as error:
1025
+ raise typer.BadParameter(f"cannot load the harbor config from {path}: {error}") from error
1026
+ if not isinstance(raw, dict):
1027
+ raise typer.BadParameter(f"the harbor config in {path} must be a mapping")
1028
+ try:
1029
+ normalized = json.loads(json.dumps(raw))
1030
+ except (TypeError, ValueError) as error:
1031
+ raise typer.BadParameter(
1032
+ f"the harbor config in {path} contains values that cannot be recorded as JSON: {error}"
1033
+ ) from error
1034
+ return normalized
1035
+
1036
+
1037
+ def _load_harbor_task_ids(path: Path) -> tuple[str, ...]:
1038
+ """Load the exact ordered task-id list, validated by the canonical score request rules."""
1039
+ try:
1040
+ raw = json.loads(path.read_text(encoding="utf-8"))
1041
+ except (OSError, json.JSONDecodeError) as error:
1042
+ raise typer.BadParameter(f"cannot load task ids from {path}: {error}") from error
1043
+ if not isinstance(raw, list) or not all(isinstance(item, str) for item in raw):
1044
+ raise typer.BadParameter(f"{path} must contain one JSON array of task-id strings")
1045
+ try:
1046
+ request = ScoreRequest(task_ids=tuple(raw), attempts=1)
1047
+ except ValidationError as error:
1048
+ raise typer.BadParameter(f"invalid task ids in {path}: {error}") from error
1049
+ return request.task_ids
1050
+
1051
+
1052
+ def _build_harbor_scorer(
1053
+ config: _HarborRunConfig, *, run_dir: Path, provider_config: ProviderConfig
1054
+ ) -> tuple[Scorer, JsonObject]:
1055
+ """Construct the harbor evaluator and its resolved per-task provenance pins.
1056
+
1057
+ The harbor import is lazy (optional extra). The pins (git commit / package ref / local
1058
+ path per task id) are recorded in the run config on the first run and re-checked on
1059
+ resume, so a dataset that re-resolves differently is rejected instead of silently scored.
1060
+ """
1061
+ try:
1062
+ from harbor.models.job.config import JobConfig
1063
+
1064
+ from wmo.evals.harbor.scorer import HarborScorer
1065
+ except ImportError as error:
1066
+ raise typer.BadParameter(_HARBOR_EXTRA_HINT) from error
1067
+ template = JobConfig.model_validate(
1068
+ {**config.harbor_job_template, "jobs_dir": str(run_dir / "harbor")}
1069
+ )
1070
+ try:
1071
+ scorer = asyncio.run(
1072
+ HarborScorer.create(
1073
+ template,
1074
+ list(config.task_ids),
1075
+ provider_config=provider_config,
1076
+ reward_key=config.reward_key,
1077
+ reward_mode=config.reward_mode,
1078
+ attempts=config.attempts,
1079
+ task_environment="e2b" if config.backend == "e2b" else "docker",
1080
+ harness_backend=config.backend,
1081
+ # "" pins template absence so the scorer cannot re-read a changed environment.
1082
+ e2b_template=(config.e2b_template or "") if config.backend == "e2b" else None,
1083
+ episode_timeout_s=(
1084
+ config.episode_timeout_s
1085
+ if config.backend == "e2b"
1086
+ else DEFAULT_EVAL_EPISODE_TIMEOUT_S
1087
+ ),
1088
+ harbor_retries=config.harbor_retries,
1089
+ )
1090
+ )
1091
+ except ValueError as error:
1092
+ raise typer.BadParameter(f"cannot build the harbor scorer: {error}") from error
1093
+ return scorer, dict(scorer.task_pins)
1094
+
1095
+
1096
+ def _build_harbor_proposer(
1097
+ *, run_dir: Path, meta_config: ProviderConfig, e2b_template: str | None
1098
+ ) -> CandidateProposer:
1099
+ """Wire the proposer: a fresh E2B project per slot driving the optimizer persona.
1100
+
1101
+ The proposer project requires E2B even under `--backend local` in this version: the
1102
+ optimizer persona needs a contained filesystem plus node for interface validation, and the
1103
+ pi worker template already ships both.
1104
+ """
1105
+ provider = get_provider(meta_config)
1106
+ if not isinstance(provider, ToolCallingProvider):
1107
+ raise typer.BadParameter(
1108
+ "settings [models.meta] provider lacks structured tool calling, which the "
1109
+ "harbor proposer requires"
1110
+ )
1111
+
1112
+ def project_factory() -> CandidateProject:
1113
+ return AgentProject.create(
1114
+ template=e2b_template,
1115
+ metadata={"wmo_component": "optimize-harbor-proposer"},
1116
+ )
1117
+
1118
+ return ProjectCandidateProposer(
1119
+ optimizer_agent(),
1120
+ provider,
1121
+ project_factory=project_factory,
1122
+ run_dir=run_dir,
1123
+ )
1124
+
1125
+
1126
+ @harness_app.command("init")
1127
+ def init_harness(
1128
+ name: str = typer.Argument("baseline", help="Name for the new harness."),
1129
+ root: str = typer.Option(ARTIFACT_DIR, help="Project dir."),
1130
+ ) -> None:
1131
+ """Write the baseline harness as v1 and point `champion` at it."""
1132
+ store = HarnessStore(root)
1133
+ try:
1134
+ if store.exists(name):
1135
+ raise typer.BadParameter(
1136
+ f"harness {name!r} already exists; new versions are appended by "
1137
+ "`wmo optimize harness`, and aliases move with `set_alias`"
1138
+ )
1139
+ doc = store.save_version(HarnessDoc.baseline(name), alias=CHAMPION_ALIAS)
1140
+ except ValueError as exc: # invalid name -> usage error, not a traceback
1141
+ raise typer.BadParameter(str(exc)) from exc
1142
+ _console.print(
1143
+ f"[green]wrote[/green] {name} v{doc.version} (champion) -> {store.dir_for(name)}"
1144
+ )
1145
+ _console.print(
1146
+ f"run it: [bold]wmo eval <tasks.jsonl> --mode closed-loop --harness {name}[/bold]"
1147
+ )