world-model-optimizer 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
wmo/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
|
+
)
|