proteus-evolve 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/PKG-INFO +10 -8
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/README.md +9 -7
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/__init__.py +1 -1
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/adapters/aki.py +2 -1
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/adapters/dsh.py +72 -30
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/adapters/llm.py +25 -20
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/adapters/minimal.py +15 -12
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/adapters/pi.py +29 -15
- proteus_evolve-0.3.0/proteus/bench/_datasets.py +39 -0
- proteus_evolve-0.3.0/proteus/bench/_isolation.py +303 -0
- proteus_evolve-0.3.0/proteus/bench/humaneval.py +189 -0
- proteus_evolve-0.3.0/proteus/bench/mbpp.py +225 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/bench/sandbox.py +8 -3
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/cli.py +58 -6
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/core/__init__.py +6 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/core/adapter.py +13 -2
- proteus_evolve-0.3.0/proteus/core/budget.py +218 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/core/continuity.py +96 -12
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/core/episode.py +106 -59
- proteus_evolve-0.3.0/proteus/core/episode_protocol.py +77 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/core/goal.py +6 -5
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/examples/adapter_template.py +19 -15
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/sweep.py +50 -3
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus_evolve.egg-info/PKG-INFO +10 -8
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus_evolve.egg-info/SOURCES.txt +12 -0
- proteus_evolve-0.3.0/tests/test_benchmark_isolation.py +243 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_continuity.py +35 -0
- proteus_evolve-0.3.0/tests/test_datasets.py +99 -0
- proteus_evolve-0.3.0/tests/test_envs.py +112 -0
- proteus_evolve-0.3.0/tests/test_episode_protocol.py +101 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_goals.py +32 -0
- proteus_evolve-0.3.0/tests/test_humaneval.py +149 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_instrument.py +52 -0
- proteus_evolve-0.3.0/tests/test_mbpp.py +209 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_selfcode.py +231 -15
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_smoke.py +4 -2
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/LICENSE +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/adapters/__init__.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/adapters/instructions.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/bench/__init__.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/bench/local.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/bench/polyglot.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/bench/swe.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/bench/task.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/core/disposition.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/core/evaluators.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/core/snapshot.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/envs.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/examples/__init__.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/examples/benchmark_template.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/examples/no_goal_vs_review.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/measure/__init__.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/measure/audit.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/measure/crystallize.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/measure/distance.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/measure/stream.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/report.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/sandbox/__init__.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/sandbox/docker.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/scaffold.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus/testing.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus_evolve.egg-info/dependency_links.txt +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus_evolve.egg-info/entry_points.txt +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus_evolve.egg-info/requires.txt +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/proteus_evolve.egg-info/top_level.txt +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/pyproject.toml +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/setup.cfg +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_aki_adapter.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_bench.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_conformance.py +0 -0
- {proteus_evolve-0.2.0 → proteus_evolve-0.3.0}/tests/test_polyglot.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proteus-evolve
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A harness-agnostic driver for measuring agent self-evolution: plug in any harness × model, evolve it under goal or no-goal conditions, and measure how the harness changes.
|
|
5
5
|
Author: Proteus Authors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -47,7 +47,7 @@ Dynamic: license-file
|
|
|
47
47
|
<a href="https://github.com/proteus-evolve/Proteus/actions/workflows/release-smoke.yml"><img src="https://github.com/proteus-evolve/Proteus/actions/workflows/release-smoke.yml/badge.svg" alt="release smoke"></a>
|
|
48
48
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT License"></a>
|
|
49
49
|
<img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
|
|
50
|
-
<img src="https://img.shields.io/badge/version-0.
|
|
50
|
+
<img src="https://img.shields.io/badge/version-0.3.0-informational.svg" alt="v0.3.0">
|
|
51
51
|
<img src="https://img.shields.io/badge/status-research%20preview-orange.svg" alt="research preview">
|
|
52
52
|
</p>
|
|
53
53
|
|
|
@@ -60,7 +60,7 @@ Dynamic: license-file
|
|
|
60
60
|
<a href="docs/RECIPES.md">Recipes</a> •
|
|
61
61
|
<a href="docs/BENCHMARKS.md">Bring a Benchmark</a> •
|
|
62
62
|
<a href="docs/MEASUREMENTS.md">Add a Measurement</a> •
|
|
63
|
-
<a href="docs/releases/v0.
|
|
63
|
+
<a href="docs/releases/v0.3.0.md">v0.3.0 Notes</a> •
|
|
64
64
|
<a href="environments/README.md">Environments</a> •
|
|
65
65
|
<a href="#-measurement">Measurement</a>
|
|
66
66
|
</p>
|
|
@@ -307,11 +307,12 @@ roots, so the evolving agent can never read its own condition.
|
|
|
307
307
|
|
|
308
308
|
## 📊 Status
|
|
309
309
|
|
|
310
|
-
`v0.
|
|
310
|
+
`v0.3.0` (research preview). Working today: the offline `minimal` harness; the live `llm`
|
|
311
311
|
harness; pinned, source-evolving DeepSeek Harness and Pi adapters with frozen per-episode
|
|
312
312
|
activation, automatic rollback, exact-tree boundary gates, rebuild caching, turn budgets,
|
|
313
|
-
and
|
|
314
|
-
local, Polyglot, and SWE-bench task integrations;
|
|
313
|
+
phase-aware act-priority budget plans and agent-authored checkpoint tracking, and task
|
|
314
|
+
mounts; the Aki research adapter; local, Polyglot, and SWE-bench task integrations;
|
|
315
|
+
resume-safe sweeps; the full measurement,
|
|
315
316
|
audit, reliability, report, and repository-export paths; and adapter/environment tooling.
|
|
316
317
|
CI covers Python 3.10–3.14. The separate release-smoke workflow runs two episodes across
|
|
317
318
|
the public release set (`minimal`, `llm`, `dsh`, `pi`), exercises the benchmark path, and
|
|
@@ -341,8 +342,9 @@ Where help is wanted, in one line each — the full list with difficulty tags is
|
|
|
341
342
|
|
|
342
343
|
- **More harnesses** — Hermes Agent first (Python, built-in self-improvement surfaces),
|
|
343
344
|
then SWE-agent, OpenClaw, Codex CLI, OpenHands, OpenCode, Goose.
|
|
344
|
-
- **More benchmarks** —
|
|
345
|
-
|
|
345
|
+
- **More benchmarks** — BigCodeBench-lite and a LiveCodeBench subset, SWE-bench
|
|
346
|
+
Lite/Verified wiring, and finishing sandboxed grading for `swe` (HumanEval and MBPP
|
|
347
|
+
are shipped).
|
|
346
348
|
- **Analysis** — `proteus compare` for side-by-side arms/runs; an episode-atlas view.
|
|
347
349
|
- **Reproducibility & cost** — per-episode token/cost accounting; one-command reproduce.
|
|
348
350
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<a href="https://github.com/proteus-evolve/Proteus/actions/workflows/release-smoke.yml"><img src="https://github.com/proteus-evolve/Proteus/actions/workflows/release-smoke.yml/badge.svg" alt="release smoke"></a>
|
|
14
14
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT License"></a>
|
|
15
15
|
<img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
|
|
16
|
-
<img src="https://img.shields.io/badge/version-0.
|
|
16
|
+
<img src="https://img.shields.io/badge/version-0.3.0-informational.svg" alt="v0.3.0">
|
|
17
17
|
<img src="https://img.shields.io/badge/status-research%20preview-orange.svg" alt="research preview">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<a href="docs/RECIPES.md">Recipes</a> •
|
|
27
27
|
<a href="docs/BENCHMARKS.md">Bring a Benchmark</a> •
|
|
28
28
|
<a href="docs/MEASUREMENTS.md">Add a Measurement</a> •
|
|
29
|
-
<a href="docs/releases/v0.
|
|
29
|
+
<a href="docs/releases/v0.3.0.md">v0.3.0 Notes</a> •
|
|
30
30
|
<a href="environments/README.md">Environments</a> •
|
|
31
31
|
<a href="#-measurement">Measurement</a>
|
|
32
32
|
</p>
|
|
@@ -273,11 +273,12 @@ roots, so the evolving agent can never read its own condition.
|
|
|
273
273
|
|
|
274
274
|
## 📊 Status
|
|
275
275
|
|
|
276
|
-
`v0.
|
|
276
|
+
`v0.3.0` (research preview). Working today: the offline `minimal` harness; the live `llm`
|
|
277
277
|
harness; pinned, source-evolving DeepSeek Harness and Pi adapters with frozen per-episode
|
|
278
278
|
activation, automatic rollback, exact-tree boundary gates, rebuild caching, turn budgets,
|
|
279
|
-
and
|
|
280
|
-
local, Polyglot, and SWE-bench task integrations;
|
|
279
|
+
phase-aware act-priority budget plans and agent-authored checkpoint tracking, and task
|
|
280
|
+
mounts; the Aki research adapter; local, Polyglot, and SWE-bench task integrations;
|
|
281
|
+
resume-safe sweeps; the full measurement,
|
|
281
282
|
audit, reliability, report, and repository-export paths; and adapter/environment tooling.
|
|
282
283
|
CI covers Python 3.10–3.14. The separate release-smoke workflow runs two episodes across
|
|
283
284
|
the public release set (`minimal`, `llm`, `dsh`, `pi`), exercises the benchmark path, and
|
|
@@ -307,8 +308,9 @@ Where help is wanted, in one line each — the full list with difficulty tags is
|
|
|
307
308
|
|
|
308
309
|
- **More harnesses** — Hermes Agent first (Python, built-in self-improvement surfaces),
|
|
309
310
|
then SWE-agent, OpenClaw, Codex CLI, OpenHands, OpenCode, Goose.
|
|
310
|
-
- **More benchmarks** —
|
|
311
|
-
|
|
311
|
+
- **More benchmarks** — BigCodeBench-lite and a LiveCodeBench subset, SWE-bench
|
|
312
|
+
Lite/Verified wiring, and finishing sandboxed grading for `swe` (HumanEval and MBPP
|
|
313
|
+
are shipped).
|
|
312
314
|
- **Analysis** — `proteus compare` for side-by-side arms/runs; an episode-atlas view.
|
|
313
315
|
- **Reproducibility & cost** — per-episode token/cost accounting; one-command reproduce.
|
|
314
316
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Proteus — a harness-agnostic framework for agent self-evolution."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.3.0"
|
|
@@ -19,7 +19,8 @@ The adapter never writes into the Aki checkout; all state lands in the Proteus r
|
|
|
19
19
|
Known limits, stated rather than papered over:
|
|
20
20
|
- Aki's phase prompts live inside the harness (`loop.py`), so `spec.phase_prompts` is not
|
|
21
21
|
injected into the episode yet; no-goal runs — the paper's primary regime — are fully
|
|
22
|
-
supported,
|
|
22
|
+
supported, while the framework default epistemic protocol and goal-text injection are
|
|
23
|
+
not wired into Aki episodes. Other bundled adapters consume `spec.phase_prompts`.
|
|
23
24
|
- Aki couples seeding and disposition install in one `init_run`, so this adapter performs
|
|
24
25
|
both inside `install_disposition` (the framework calls `seed` first; it only records state).
|
|
25
26
|
"""
|
|
@@ -34,9 +34,9 @@ from pathlib import Path
|
|
|
34
34
|
from typing import Optional, Sequence
|
|
35
35
|
|
|
36
36
|
from proteus.core.adapter import ActionEvent, EpisodeResult, EpisodeSpec, Surface
|
|
37
|
+
from proteus.core.budget import PHASES, budget_plan, phase_prompt
|
|
37
38
|
from proteus.core.continuity import CONTAINER_ROOT, HandoffStore
|
|
38
39
|
from proteus.core.disposition import Disposition
|
|
39
|
-
from proteus.core.episode import PHASES
|
|
40
40
|
|
|
41
41
|
IMAGE = os.environ.get("PROTEUS_DSH_IMAGE", "proteus-env-dsh-src:0.1.0-rc.7")
|
|
42
42
|
PHASE_TIMEOUT_S = 600
|
|
@@ -49,11 +49,15 @@ PHASE_TIMEOUT_S = 600
|
|
|
49
49
|
SOURCE_TAR = "/opt/dsh-source.tar"
|
|
50
50
|
#: A full build:lib is ~330s; the gate's timeout must cover one on a changed source.
|
|
51
51
|
BOOT_TIMEOUT_S = 900
|
|
52
|
+
#: A 246-package DSH workspace takes roughly 90s to relink and cold-start on Docker
|
|
53
|
+
#: Desktop even with a warm offline store. Keep enough headroom for slower hosts while
|
|
54
|
+
#: still treating a genuine hang as a runtime viability failure.
|
|
55
|
+
COLD_BOOT_TIMEOUT_S = 300
|
|
52
56
|
SEED_INSTRUCTIONS = """\
|
|
53
57
|
# Agent instructions
|
|
54
58
|
|
|
55
|
-
You
|
|
56
|
-
running is a frozen, read-only snapshot at `/workspace`; the writable candidate that
|
|
59
|
+
You inhabit, inspect, and may change your own harness. During a Proteus episode, the harness
|
|
60
|
+
currently running is a frozen, read-only snapshot at `/workspace`; the writable candidate that
|
|
57
61
|
persists across phases is `/workspace/candidate`. Make every edit in that candidate. Your
|
|
58
62
|
candidate surfaces are:
|
|
59
63
|
|
|
@@ -70,12 +74,14 @@ Proteus supplies the cross-phase operational handoff at
|
|
|
70
74
|
runtime context outside the evolving snapshot; do not copy credentials or raw tool output
|
|
71
75
|
into it.
|
|
72
76
|
|
|
73
|
-
The image already contains an installed, built copy at `/opt/src`. Do not
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
build
|
|
77
|
+
The image already contains an installed, built copy at `/opt/src`. Do not create or persist
|
|
78
|
+
`node_modules` or package-manager caches in the candidate: those are generated dependencies,
|
|
79
|
+
not evolution, and would pollute the snapshot. You may add workspace packages or change
|
|
80
|
+
package manifests, but keep `pnpm-lock.yaml` aligned. The boundary gate recreates links with
|
|
81
|
+
a frozen offline install, so an inconsistent lockfile or a dependency absent from the baked
|
|
82
|
+
store is rejected for repair. `/opt/src` is the build of the frozen active snapshot. Do not
|
|
83
|
+
sync, reload, or execute candidate source during a phase; Proteus owns the model-free
|
|
84
|
+
boundary build and viability gate after reflect.
|
|
79
85
|
|
|
80
86
|
Each session is one phase of an episode. Harness files and the bounded Proteus handoff
|
|
81
87
|
carry over; the raw conversation does not.
|
|
@@ -245,20 +251,42 @@ class DshHarness:
|
|
|
245
251
|
return ((str(task), "/workspace/task"),) if task.is_dir() else ()
|
|
246
252
|
|
|
247
253
|
def check_boot(self, harness_root: Path) -> str:
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
The
|
|
251
|
-
|
|
252
|
-
|
|
254
|
+
"""Build once, then cold-start the exact headless runtime in a fresh container.
|
|
255
|
+
|
|
256
|
+
The first probe exact-syncs the candidate, validates its dependency graph offline,
|
|
257
|
+
rebuilds it, writes the dist cache, and runs ``--version``. A second sandbox call
|
|
258
|
+
starts from a clean image, reloads that cache, and boots the headless plugin tree
|
|
259
|
+
without provider credentials. Keeping the probes in separate containers is
|
|
260
|
+
essential: a newly-added workspace package can compile in the build container yet
|
|
261
|
+
be absent from the cached outputs or runtime links used by episode N+1.
|
|
262
|
+
"""
|
|
253
263
|
harness = Path(harness_root)
|
|
254
264
|
state = harness.parent / ".dsh-state"
|
|
255
265
|
state.mkdir(exist_ok=True)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
mounts = ((str(harness), "/workspace"), (str(state), "/state"))
|
|
267
|
+
try:
|
|
268
|
+
proc = self.sandbox.run(
|
|
269
|
+
harness.parent, ["--version"], env={}, timeout_s=BOOT_TIMEOUT_S,
|
|
270
|
+
mounts=mounts)
|
|
271
|
+
except subprocess.TimeoutExpired:
|
|
272
|
+
return f"self-edited source build timed out after {BOOT_TIMEOUT_S}s"
|
|
259
273
|
if proc.returncode != 0:
|
|
260
274
|
return (f"self-edited source does not boot (exit {proc.returncode}): "
|
|
261
|
-
f"{(proc.stderr or proc.stdout)[-
|
|
275
|
+
f"{(proc.stderr or proc.stdout)[-1200:]}")
|
|
276
|
+
try:
|
|
277
|
+
cold = self.sandbox.run(
|
|
278
|
+
harness.parent, ["--proteus-headless-smoke"], env={},
|
|
279
|
+
timeout_s=COLD_BOOT_TIMEOUT_S, mounts=mounts)
|
|
280
|
+
except subprocess.TimeoutExpired:
|
|
281
|
+
return ("self-edited source headless cold start timed out after "
|
|
282
|
+
f"{COLD_BOOT_TIMEOUT_S}s")
|
|
283
|
+
if cold.returncode != 0:
|
|
284
|
+
detail = (cold.stderr or cold.stdout)[-1200:]
|
|
285
|
+
if "proteus-headless-smoke" in detail and "unknown option" in detail.lower():
|
|
286
|
+
return ("DSH source image predates the headless cold-start contract; "
|
|
287
|
+
"rebuild environments/dsh-src before running evolution")
|
|
288
|
+
return (f"self-edited source fails headless cold start "
|
|
289
|
+
f"(exit {cold.returncode}): {detail}")
|
|
262
290
|
return ""
|
|
263
291
|
|
|
264
292
|
def validate_candidate(self, harness_root: Path) -> str:
|
|
@@ -327,7 +355,9 @@ class DshHarness:
|
|
|
327
355
|
mapping: dict[str, list[str]] = {}
|
|
328
356
|
error = ""
|
|
329
357
|
capped = False
|
|
330
|
-
|
|
358
|
+
checkpoint_misses = 0
|
|
359
|
+
plan = budget_plan(spec)
|
|
360
|
+
budget = plan.hard_limit
|
|
331
361
|
episode_dirs: set = set()
|
|
332
362
|
active = Path(spec.active_root) if spec.active_root is not None else harness
|
|
333
363
|
# Core-managed staged episodes already execute a previously validated snapshot.
|
|
@@ -347,18 +377,20 @@ class DshHarness:
|
|
|
347
377
|
workspace_mounts = ((str(active), "/workspace", "ro"),
|
|
348
378
|
(str(harness), "/workspace/candidate")) \
|
|
349
379
|
if spec.active_root is not None else ((str(harness), "/workspace"),)
|
|
350
|
-
|
|
351
|
-
for idx, phase in enumerate(PHASES if not error else ()):
|
|
380
|
+
for phase in PHASES if not error else ():
|
|
352
381
|
# the budget is enforced twice, both harness-agnostically: exactly, between
|
|
353
382
|
# phases (no new phase once it is spent) and approximately, mid-phase (the
|
|
354
383
|
# session log is polled and the container stopped at the phase's stop line).
|
|
355
|
-
#
|
|
356
|
-
#
|
|
384
|
+
# BudgetPlan preserves the legacy later-phase reserve or applies the explicit
|
|
385
|
+
# act-priority plan. A phase stop moves on; only the hard ceiling caps the
|
|
357
386
|
# episode.
|
|
358
|
-
|
|
387
|
+
used = self._live_calls(state, episode_dirs, set()) if plan.enabled else 0
|
|
388
|
+
if budget and used >= budget:
|
|
359
389
|
capped = True
|
|
360
390
|
break
|
|
361
|
-
stop_at =
|
|
391
|
+
stop_at = plan.stop_at(phase, used)
|
|
392
|
+
if budget and used >= stop_at:
|
|
393
|
+
continue
|
|
362
394
|
handoff_start = handoffs.begin(spec.episode, phase)
|
|
363
395
|
before = self._session_dirs(state)
|
|
364
396
|
fired = [False]
|
|
@@ -374,14 +406,14 @@ class DshHarness:
|
|
|
374
406
|
try:
|
|
375
407
|
proc = self.sandbox.run(
|
|
376
408
|
run_root,
|
|
377
|
-
["--profile", "headless", spec
|
|
409
|
+
["--profile", "headless", phase_prompt(spec, phase, used)],
|
|
378
410
|
env={"DEEPSEEK_API_KEY": self.key,
|
|
379
411
|
"DSH_PERMISSION_MODE": self.permission_mode},
|
|
380
412
|
timeout_s=self.phase_timeout_s,
|
|
381
413
|
mounts=workspace_mounts + ((str(state), "/state"),
|
|
382
414
|
(str(handoffs.root), CONTAINER_ROOT))
|
|
383
415
|
+ self._task_mount(run_root),
|
|
384
|
-
stop_check=stop_check if
|
|
416
|
+
stop_check=stop_check if plan.enabled else None,
|
|
385
417
|
)
|
|
386
418
|
except subprocess.TimeoutExpired:
|
|
387
419
|
timed_out = True
|
|
@@ -395,8 +427,10 @@ class DshHarness:
|
|
|
395
427
|
for session_dir in session_dirs:
|
|
396
428
|
phase_events.extend(
|
|
397
429
|
self._session_trace(session_dir, phase, partial=True))
|
|
398
|
-
handoffs.finish(handoff_start, phase_events,
|
|
399
|
-
|
|
430
|
+
handoff = handoffs.finish(handoff_start, phase_events,
|
|
431
|
+
interrupted=timed_out or fired[0])
|
|
432
|
+
if spec.checkpoint_turns and handoff["source"] != "agent":
|
|
433
|
+
checkpoint_misses += 1
|
|
400
434
|
if timed_out:
|
|
401
435
|
error = f"phase {phase}: timeout after {self.phase_timeout_s}s"
|
|
402
436
|
break
|
|
@@ -414,10 +448,18 @@ class DshHarness:
|
|
|
414
448
|
(run_root / "traces" / f"ep{spec.episode:03d}.json").write_text(
|
|
415
449
|
json.dumps(mapping, indent=1))
|
|
416
450
|
trace = self.read_trace(run_root, spec.episode)
|
|
451
|
+
phase_counts = {
|
|
452
|
+
phase: sum(1 for event in trace if event.phase == phase and event.tool)
|
|
453
|
+
for phase in PHASES
|
|
454
|
+
}
|
|
455
|
+
counters = {"phases": len(mapping), "turn_capped": capped,
|
|
456
|
+
"checkpoint_misses": checkpoint_misses}
|
|
457
|
+
counters.update({f"phase_{phase}_turns": count
|
|
458
|
+
for phase, count in phase_counts.items()})
|
|
417
459
|
return EpisodeResult(
|
|
418
460
|
episode=spec.episode, ok=not error,
|
|
419
461
|
turns=sum(1 for e in trace if e.tool), error=error,
|
|
420
|
-
counters=
|
|
462
|
+
counters=counters,
|
|
421
463
|
)
|
|
422
464
|
|
|
423
465
|
def _live_calls(self, state: Path, episode_dirs: set, extra: set) -> int:
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""A real-LLM harness on the minimal surface set — any OpenAI-compatible endpoint.
|
|
2
2
|
|
|
3
3
|
The `minimal` harness with the mock policy replaced by a live model: each phase, the model
|
|
4
|
-
is shown its phase prompt (
|
|
5
|
-
current state of its own harness (the files on
|
|
6
|
-
take as JSON. Only files cross the episode
|
|
7
|
-
the model wrote in episode t is the state
|
|
4
|
+
is shown its phase prompt (default episode protocol, goal, and visible evaluator feedback
|
|
5
|
+
already folded in by the framework) plus the current state of its own harness (the files on
|
|
6
|
+
its surfaces), and returns the actions to take as JSON. Only files cross the episode
|
|
7
|
+
boundary, so this is genuine self-evolution: what the model wrote in episode t is the state
|
|
8
|
+
it wakes up to in episode t+1.
|
|
8
9
|
|
|
9
10
|
Works against any OpenAI-compatible chat endpoint via stdlib HTTP (no SDK dependency).
|
|
10
11
|
Defaults target DeepSeek:
|
|
@@ -26,11 +27,11 @@ from pathlib import Path
|
|
|
26
27
|
|
|
27
28
|
from proteus.adapters.minimal import MinimalHarness
|
|
28
29
|
from proteus.core.adapter import EpisodeResult, EpisodeSpec
|
|
29
|
-
from proteus.core.
|
|
30
|
+
from proteus.core.budget import PHASES, budget_plan, phase_prompt
|
|
30
31
|
|
|
31
32
|
SYSTEM = """\
|
|
32
|
-
You are an agent that
|
|
33
|
-
|
|
33
|
+
You are an agent that can inspect and change its own harness — the set of files it wakes up
|
|
34
|
+
with each episode. You have two surfaces:
|
|
34
35
|
- notes/ markdown files (observations, plans, knowledge you want to keep)
|
|
35
36
|
- tools/ small python files (utilities you may want later)
|
|
36
37
|
|
|
@@ -117,21 +118,20 @@ class LLMHarness(MinimalHarness):
|
|
|
117
118
|
turn = 0
|
|
118
119
|
writes = {"notes": 0, "tools": 0}
|
|
119
120
|
tokens_in = tokens_out = 0
|
|
121
|
+
phase_counts = {phase: 0 for phase in PHASES}
|
|
120
122
|
error = ""
|
|
121
123
|
capped = False
|
|
122
|
-
|
|
124
|
+
plan = budget_plan(spec)
|
|
123
125
|
with trace_path.open("w", encoding="utf-8") as sink:
|
|
124
|
-
for
|
|
125
|
-
|
|
126
|
-
# stop line additionally reserves min_turns_per_phase for later phases,
|
|
127
|
-
# and reaching it ends the phase, not the episode
|
|
128
|
-
if spec.max_turns and turn >= spec.max_turns:
|
|
126
|
+
for phase in PHASES:
|
|
127
|
+
if plan.enabled and turn >= plan.hard_limit:
|
|
129
128
|
capped = True
|
|
130
129
|
if capped:
|
|
131
130
|
break
|
|
132
|
-
stop_at =
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
stop_at = plan.stop_at(phase, turn)
|
|
132
|
+
if plan.enabled and turn >= stop_at:
|
|
133
|
+
continue
|
|
134
|
+
prompt = phase_prompt(spec, phase, turn)
|
|
135
135
|
user = (f"Episode {spec.episode}, phase: {phase}.\n\n"
|
|
136
136
|
f"{prompt}\n\nCurrent harness state:\n{_render_state(harness)}")
|
|
137
137
|
try:
|
|
@@ -144,11 +144,12 @@ class LLMHarness(MinimalHarness):
|
|
|
144
144
|
tokens_in += int(usage.get("prompt_tokens") or 0)
|
|
145
145
|
tokens_out += int(usage.get("completion_tokens") or 0)
|
|
146
146
|
turn += 1
|
|
147
|
+
phase_counts[phase] += 1
|
|
147
148
|
sink.write(json.dumps({"turn": turn, "phase": phase, "tool": None,
|
|
148
149
|
"surface": None, "text": reply[:500]}) + "\n")
|
|
149
150
|
for act in _parse_actions(reply):
|
|
150
|
-
if
|
|
151
|
-
capped = turn >=
|
|
151
|
+
if plan.enabled and turn >= stop_at:
|
|
152
|
+
capped = turn >= plan.hard_limit
|
|
152
153
|
break
|
|
153
154
|
tool = act.get("tool", "")
|
|
154
155
|
name = "".join(c for c in str(act.get("name", "unnamed"))
|
|
@@ -165,8 +166,12 @@ class LLMHarness(MinimalHarness):
|
|
|
165
166
|
surface = "tools"
|
|
166
167
|
else:
|
|
167
168
|
continue
|
|
169
|
+
phase_counts[phase] += 1
|
|
168
170
|
sink.write(json.dumps({"turn": turn, "phase": phase, "tool": tool,
|
|
169
171
|
"surface": surface, "text": name}) + "\n")
|
|
172
|
+
counters = {"writes": writes, "turn_capped": capped,
|
|
173
|
+
"tokens_in": tokens_in, "tokens_out": tokens_out}
|
|
174
|
+
counters.update({f"phase_{phase}_turns": count
|
|
175
|
+
for phase, count in phase_counts.items()})
|
|
170
176
|
return EpisodeResult(episode=spec.episode, ok=not error, turns=turn, error=error,
|
|
171
|
-
counters=
|
|
172
|
-
"tokens_in": tokens_in, "tokens_out": tokens_out})
|
|
177
|
+
counters=counters)
|
|
@@ -19,8 +19,8 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Callable, Optional, Sequence, Tuple
|
|
20
20
|
|
|
21
21
|
from proteus.core.adapter import ActionEvent, EpisodeResult, EpisodeSpec, Surface
|
|
22
|
+
from proteus.core.budget import PHASES, budget_plan, phase_prompt
|
|
22
23
|
from proteus.core.disposition import Disposition
|
|
23
|
-
from proteus.core.episode import PHASES
|
|
24
24
|
|
|
25
25
|
# A policy maps (phase, prompt, episode, rng) -> list of (tool, surface, text) actions.
|
|
26
26
|
Action = Tuple[str, Optional[str], str]
|
|
@@ -94,22 +94,23 @@ class MinimalHarness:
|
|
|
94
94
|
trace_path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
95
|
turn = 0
|
|
96
96
|
writes = {"notes": 0, "tools": 0}
|
|
97
|
+
phase_counts = {phase: 0 for phase in PHASES}
|
|
97
98
|
capped = False
|
|
98
|
-
|
|
99
|
+
plan = budget_plan(spec)
|
|
99
100
|
with trace_path.open("w", encoding="utf-8") as sink:
|
|
100
|
-
for
|
|
101
|
+
for phase in PHASES:
|
|
102
|
+
if plan.enabled and turn >= plan.hard_limit:
|
|
103
|
+
capped = True
|
|
101
104
|
if capped:
|
|
102
105
|
break
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
stop_at = (spec.max_turns - min_pp * (len(PHASES) - idx - 1)
|
|
106
|
-
if spec.max_turns else 0)
|
|
107
|
-
prompt = spec.phase_prompts.get(phase, "")
|
|
106
|
+
stop_at = plan.stop_at(phase, turn)
|
|
107
|
+
prompt = phase_prompt(spec, phase, turn)
|
|
108
108
|
for tool, surface, text in self.policy(phase, prompt, spec.episode, rng):
|
|
109
|
-
if
|
|
110
|
-
capped = turn >=
|
|
109
|
+
if plan.enabled and turn >= stop_at:
|
|
110
|
+
capped = turn >= plan.hard_limit
|
|
111
111
|
break
|
|
112
112
|
turn += 1
|
|
113
|
+
phase_counts[phase] += 1
|
|
113
114
|
if tool == "write_note":
|
|
114
115
|
(harness / "notes" / f"{text}.md").write_text(
|
|
115
116
|
f"episode {spec.episode}: {text}\n")
|
|
@@ -122,8 +123,10 @@ class MinimalHarness:
|
|
|
122
123
|
"turn": turn, "phase": phase, "tool": tool,
|
|
123
124
|
"surface": surface, "text": text,
|
|
124
125
|
}) + "\n")
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
counters = {"writes": writes, "turn_capped": capped}
|
|
127
|
+
counters.update({f"phase_{phase}_turns": count
|
|
128
|
+
for phase, count in phase_counts.items()})
|
|
129
|
+
return EpisodeResult(episode=spec.episode, ok=True, turns=turn, counters=counters)
|
|
127
130
|
|
|
128
131
|
def read_trace(self, root: Path, episode: int) -> Sequence[ActionEvent]:
|
|
129
132
|
path = root / "traces" / f"ep{episode:03d}.jsonl"
|
|
@@ -27,9 +27,9 @@ from typing import Optional, Sequence
|
|
|
27
27
|
|
|
28
28
|
from proteus.adapters import instructions
|
|
29
29
|
from proteus.core.adapter import ActionEvent, EpisodeResult, EpisodeSpec, Surface
|
|
30
|
+
from proteus.core.budget import PHASES, budget_plan, phase_prompt
|
|
30
31
|
from proteus.core.continuity import CONTAINER_ROOT, HandoffStore
|
|
31
32
|
from proteus.core.disposition import Disposition
|
|
32
|
-
from proteus.core.episode import PHASES
|
|
33
33
|
|
|
34
34
|
IMAGE = os.environ.get("PROTEUS_PI_IMAGE", "proteus-env-pi-src:0.84.2")
|
|
35
35
|
PHASE_TIMEOUT_S = 600
|
|
@@ -44,8 +44,8 @@ SOURCE_TAR = "/opt/pi-source.tar"
|
|
|
44
44
|
SEED_INSTRUCTIONS = """\
|
|
45
45
|
# Agent instructions
|
|
46
46
|
|
|
47
|
-
You
|
|
48
|
-
running is a frozen, read-only snapshot at `/workspace`; the writable candidate that
|
|
47
|
+
You inhabit, inspect, and may change your own harness. During a Proteus episode, the harness
|
|
48
|
+
currently running is a frozen, read-only snapshot at `/workspace`; the writable candidate that
|
|
49
49
|
persists across phases is `/workspace/candidate`. Make every edit in that candidate. Your
|
|
50
50
|
candidate surfaces are:
|
|
51
51
|
|
|
@@ -234,7 +234,9 @@ class PiHarness:
|
|
|
234
234
|
mapping: dict[str, list[str]] = {}
|
|
235
235
|
error = ""
|
|
236
236
|
capped = False
|
|
237
|
-
|
|
237
|
+
checkpoint_misses = 0
|
|
238
|
+
plan = budget_plan(spec)
|
|
239
|
+
budget = plan.hard_limit
|
|
238
240
|
episode_files: set = set()
|
|
239
241
|
active = Path(spec.active_root) if spec.active_root is not None else harness
|
|
240
242
|
# Core-managed staged episodes already execute a previously validated snapshot.
|
|
@@ -252,18 +254,20 @@ class PiHarness:
|
|
|
252
254
|
workspace_mounts = ((str(active), "/workspace", "ro"),
|
|
253
255
|
(str(harness), "/workspace/candidate")) \
|
|
254
256
|
if spec.active_root is not None else ((str(harness), "/workspace"),)
|
|
255
|
-
|
|
256
|
-
for idx, phase in enumerate(PHASES if not error else ()):
|
|
257
|
+
for phase in PHASES if not error else ():
|
|
257
258
|
# the budget is enforced twice, both harness-agnostically: exactly, between
|
|
258
259
|
# phases (no new phase once it is spent) and approximately, mid-phase (the
|
|
259
260
|
# session log is polled and the container stopped at the phase's stop line).
|
|
260
|
-
#
|
|
261
|
-
#
|
|
261
|
+
# BudgetPlan preserves the legacy later-phase reserve or applies the explicit
|
|
262
|
+
# act-priority plan. A phase stop moves on; only the hard ceiling caps the
|
|
262
263
|
# episode.
|
|
263
|
-
|
|
264
|
+
used = self._live_calls(state, episode_files, set()) if plan.enabled else 0
|
|
265
|
+
if budget and used >= budget:
|
|
264
266
|
capped = True
|
|
265
267
|
break
|
|
266
|
-
stop_at =
|
|
268
|
+
stop_at = plan.stop_at(phase, used)
|
|
269
|
+
if budget and used >= stop_at:
|
|
270
|
+
continue
|
|
267
271
|
handoff_start = handoffs.begin(spec.episode, phase)
|
|
268
272
|
before = self._sessions(state)
|
|
269
273
|
fired = [False]
|
|
@@ -281,13 +285,13 @@ class PiHarness:
|
|
|
281
285
|
run_root,
|
|
282
286
|
["--provider", self.provider, "--model", spec.model or self.model,
|
|
283
287
|
"--session-dir", "/state", "--skill", "/workspace/skills",
|
|
284
|
-
"-p", spec
|
|
288
|
+
"-p", phase_prompt(spec, phase, used)],
|
|
285
289
|
env={"DEEPSEEK_API_KEY": self.key},
|
|
286
290
|
timeout_s=self.phase_timeout_s,
|
|
287
291
|
mounts=workspace_mounts + ((str(state), "/state"),
|
|
288
292
|
(str(handoffs.root), CONTAINER_ROOT))
|
|
289
293
|
+ self._task_mount(run_root),
|
|
290
|
-
stop_check=stop_check if
|
|
294
|
+
stop_check=stop_check if plan.enabled else None,
|
|
291
295
|
)
|
|
292
296
|
except subprocess.TimeoutExpired:
|
|
293
297
|
timed_out = True
|
|
@@ -300,8 +304,10 @@ class PiHarness:
|
|
|
300
304
|
episode_files |= new
|
|
301
305
|
for session_path in session_paths:
|
|
302
306
|
phase_events.extend(self._session_trace(session_path, phase))
|
|
303
|
-
handoffs.finish(handoff_start, phase_events,
|
|
304
|
-
|
|
307
|
+
handoff = handoffs.finish(handoff_start, phase_events,
|
|
308
|
+
interrupted=timed_out or fired[0])
|
|
309
|
+
if spec.checkpoint_turns and handoff["source"] != "agent":
|
|
310
|
+
checkpoint_misses += 1
|
|
305
311
|
if timed_out:
|
|
306
312
|
error = f"phase {phase}: timeout after {self.phase_timeout_s}s"
|
|
307
313
|
break
|
|
@@ -319,10 +325,18 @@ class PiHarness:
|
|
|
319
325
|
(run_root / "traces" / f"ep{spec.episode:03d}.json").write_text(
|
|
320
326
|
json.dumps(mapping, indent=1))
|
|
321
327
|
trace = self.read_trace(run_root, spec.episode)
|
|
328
|
+
phase_counts = {
|
|
329
|
+
phase: sum(1 for event in trace if event.phase == phase and event.tool)
|
|
330
|
+
for phase in PHASES
|
|
331
|
+
}
|
|
332
|
+
counters = {"phases": len(mapping), "turn_capped": capped,
|
|
333
|
+
"checkpoint_misses": checkpoint_misses}
|
|
334
|
+
counters.update({f"phase_{phase}_turns": count
|
|
335
|
+
for phase, count in phase_counts.items()})
|
|
322
336
|
return EpisodeResult(
|
|
323
337
|
episode=spec.episode, ok=not error,
|
|
324
338
|
turns=sum(1 for e in trace if e.tool), error=error,
|
|
325
|
-
counters=
|
|
339
|
+
counters=counters,
|
|
326
340
|
)
|
|
327
341
|
|
|
328
342
|
def _live_calls(self, state: Path, episode_files: set, extra: set) -> int:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Verified, atomic downloads for official benchmark datasets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
from urllib import request
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def download_verified(
|
|
13
|
+
*, name: str, url: str, expected_sha256: str, cache: Path, validate: Callable[[Path], object]
|
|
14
|
+
) -> Path:
|
|
15
|
+
"""Download one official dataset, verify bytes and format, then publish atomically."""
|
|
16
|
+
cache = Path(cache)
|
|
17
|
+
if cache.exists():
|
|
18
|
+
return cache
|
|
19
|
+
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
with request.urlopen(url, timeout=30) as response:
|
|
21
|
+
payload = response.read()
|
|
22
|
+
actual = hashlib.sha256(payload).hexdigest()
|
|
23
|
+
if actual != expected_sha256:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"{name} checksum mismatch: expected {expected_sha256}, got {actual}"
|
|
26
|
+
)
|
|
27
|
+
temporary = None
|
|
28
|
+
try:
|
|
29
|
+
with tempfile.NamedTemporaryFile(
|
|
30
|
+
dir=cache.parent, suffix="".join(cache.suffixes), delete=False
|
|
31
|
+
) as stream:
|
|
32
|
+
stream.write(payload)
|
|
33
|
+
temporary = Path(stream.name)
|
|
34
|
+
validate(temporary)
|
|
35
|
+
temporary.replace(cache)
|
|
36
|
+
finally:
|
|
37
|
+
if temporary is not None:
|
|
38
|
+
temporary.unlink(missing_ok=True)
|
|
39
|
+
return cache
|