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