chore: squash adjacent standalone commits on nano-as-v1 for merge into main - #1825
Merged
Conversation
Squashed: - refactor!: remove the v1 framework (verifiers/v1) and its core re-exports - refactor!: delete v1 example envs + v1 tests; strip v1 flags from v0 envs - feat: vf-nano is verifiers v1 (submodule + verifiers.v1 alias) - chore: bump deps/vf-nano (legacy v0->Trace bridge) - feat(eval): v0-only vf-eval + add vf-eval-v1 (nano); bump vf-nano (reverse-text-v1) - chore: bump deps/vf-nano (plugins drop vf-nano dep) - refactor(types): scrub v1 artifacts from the v0 State - chore: finish v1 hygiene (init.py scaffolding, v1 tests, byo-harness doc) - refactor(v1): vendor vf-nano as verifiers.v1, drop old v1 packages - refactor(v1): eval/serve scripts, v1 deps to base, bundle shipped plugins - fix(ci): green the test/semgrep jobs after the v1 vendor + drop py3.10 - chore: retire the v1 semgrep policy - feat(v1): Task.system_prompt + harness APPENDS_SYSTEM_PROMPT support - fix: render with the base-model tokenizer, not the per-request model
The openai_chat_completions client now best-effort parses the prompt and completion token ids and sampling logprobs that vLLM returns (return_token_ids + logprobs) into Response.tokens, so MITO training (no renderer) can train on real on-policy tokens instead of re-tokenizing the messages downstream. Sampling args still pass straight through; tokens stay None when the provider returns neither token ids nor logprobs (e.g. eval, or non-vLLM providers). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bridge only kept token ids: it dropped the prompt messages, the response message (content / reasoning / tool calls), finish_reason, usage, and the task's system prompt / answer — so a v0-bridged Trace was a near-empty skeleton next to a native v1 Trace. The cause: v0 RolloutOutput nests these as pydantic objects (messages, Response) and records finish_reason on response.message, but the mapping only handled plain dicts and read finish_reason off the response. Coerce v0 objects to dicts before mapping (_as_dict), read finish_reason/usage from their v0 locations, mirror tokens onto the response (as the native client does), and carry the prompt's system_prompt / instruction / answer onto the task. A v0-bridged Trace now matches the native v1 schema (verified by diffing reverse-text rollouts). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) Rename every taskset under examples/tasksets/ to a `-v1` id (package name, module, and directory) so they no longer collide with the v0 environments of the same name (gsm8k, wiki-search, math-env, ...) when both are installed in one env. reverse-text-v1 was already suffixed; harbor (a bundled taskset with no v0 counterpart) is left as-is. - examples/tasksets/<x> -> <x>_v1, module <x>.py -> <x>_v1.py; verify.py / server.py / facts.json keep their names (read via __file__, never imported) - package tasksets: inner package wiki_search/wikispeedia -> *_v1, with their self-imports and `-m <pkg>.server` launch paths updated to match - root pyproject [tool.uv.sources] + examples group, and configs/*.toml taskset ids - refresh uv.lock Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add RetryConfig (attempts / include / exclude) on EnvConfig.retry and retry a whole rollout with tenacity when it ends with a captured error — parity with v0's rollout-level retries. Matching is by exception type name; include/exclude name exception classes (e.g. ModelError, ProgramError). Flags: --retry.attempts / --retry.include / --retry.exclude. EvalConfig inherits EnvConfig and the env server runs through Environment.episode, so both eval and training get retries. Retries are first-class on the Trace: `errors` is the list of per-attempt errors (oldest first), and `error` is now a computed field returning the most recent — so a retried-then-failed trace shows every error that led to a retry. Retry utilities live in verifiers/v1/retries.py. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): per-rollout token limits (EnvConfig.max_{input,output,total}_tokens)
Add framework-enforced token budgets alongside max_turns: max_input_tokens,
max_output_tokens, max_total_tokens on EnvConfig. The interception server checks
them before each turn via a new RolloutLimits bundle (which also subsumes
max_turns), capping the trace's prompt_len / completion_len / total_tokens
computed properties. Reaching any limit refuses the turn and records it as the
stop condition, and is_truncated now treats the token-limit conditions as
truncation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(v1): drop 'like max_turns' from token-limit field docstrings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(v1): trim limit-check comment in interception
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(v1): ruff format interception
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(v1): reclaim orphaned subprocess workspaces
A rollout's /tmp workspace is removed in `stop()`, but a process killed mid-rollout
(SIGKILL, OOM, hard crash, interrupted teardown) never reaches it, so the workspace
leaks with no way to reclaim it — repeated runs eventually fill /tmp ("No space left
on device" at mkdtemp).
Name each workspace `/tmp/v1-<pid>-*` and, once per process on the first `start()`,
sweep `/tmp/v1-<pid>-*` whose pid is no longer alive. PID-keyed, so a concurrent live
process's workspaces are never touched; graceful per-rollout cleanup (`stop()`) is
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): atexit-based runtime teardown; drop the SIGKILL reaper
Make resource cleanup a backend-agnostic property of `Runtime`:
- a sync `cleanup()` is the teardown source of truth; the public async `stop()` runs it
off the event loop on the happy path.
- `make_runtime` registers each runtime in a WeakSet and arms one sync `atexit` hook that
calls `cleanup()` on anything still live — so a Ctrl-C / SIGTERM that cancels the
rollout's `finally` mid-teardown still frees the workspace / container / sandbox, reusing
each backend's own cleanup. The hook must be sync: at interpreter shutdown the event loop
and its thread-pool are gone, so async teardown raises "cannot schedule new futures".
Drop the PID-tagged `reap_orphans` startup sweep. A SIGKILL/OOM runs no in-process code at
all, so reclaiming it needs an external mechanism; prime sandboxes already self-terminate
via their server-side max-lifetime, and the local subprocess/docker cases are out of scope.
Prefix workspaces/containers/scripts with `vf-` (was `v1-`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(v1): delete the prime sandbox in the sync atexit cleanup too
`cleanup()` (the atexit backstop) only stopped the tunnels and left the sandbox — the
costly resource — to its server-side max-lifetime. prime_sandboxes ships a sync
`SandboxClient`, so delete the sandbox synchronously there as well (the async client can't
run once the loop is gone). Idempotent with the async `stop` on the normal path: a second
delete just 404s.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style: move teardown comments off the statement line (ruff format)
The inline comments pushed two lines past the 88-col limit; moving them above the
statement keeps `ruff format` happy without ruff's awkward auto-wrap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): public register/cleanup_at_exit, trim runtime-teardown comments
- rename the module-level helpers to public `register` / `cleanup_at_exit`
- trim the `_LIVE` block comment and drop the inline "no event loop" why-comments
(the `cleanup` docstring already covers why teardown is sync)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1592) * feat(v1): add textarena taskset with a framework-driven user simulator Add `vf.User` — a first-class user simulator that is structurally a tool server (an MCP server with a runtime), registered on a taskset via the new `Taskset.user_server` hook. The interception server drives it: after each model turn with no tool call it injects the simulator's reply as a user turn and re-prompts the model, so a multi-turn exchange plays out within one program request, transparently to the harness and its program (which never see it). Without a user simulator the interception loop runs exactly once, as before. Ship `textarena-v1` (working example: Wordle) in the tasksets package: the game engine itself is the user simulator (`server.py`), seeded per-rollout with the secret word via env; scoring is a pure function of the trace (a win is a guess that equals the answer, parsed the way TextArena parses moves). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): rename ToolServer -> Tools and serve_mcp -> serve_tools Rename the tool-server surface to `vf.Tools` and the serving helper to `serve_tools`, updating the shipped example tasksets. `User` now subclasses `Tools`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(textarena): require game, one task per word, drop answer_state_key Trim the taskset config to just `game` (now required). Generate one task per word in the game's list and let the eval select (num_tasks / shuffle), dropping the taskset-level num_tasks/seed/max_turns. The secret-word game_state key is "secret_word" (hardcoded in the user simulator), so the per-task answer_state_key is gone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): rename Taskset.tool_servers/user_server -> tools/user Rename the taskset hooks to `tools(task)` and `user(task)` (matching `vf.Tools` / `vf.User`), and the `serve_shared`/`serve_tools` `servers` param to `tools`. Drop the `Respond` intermediary at the call site (`serve_user(...) as server.user`) and unquote the `Taskset.user` return annotation (`User` is now imported, no cycle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(textarena): restrict game to the tested Wordle family (Literal) Type `game` as a Literal of the four Wordle variants verified to work with this taskset's assumptions (single secret word seeded via `secret_word`, exact-match `[word]` scoring): Wordle-v0, -hardcore, -long, -long-hardcore. Other TextArena games store the answer under a different key, lack a word_list, use different win mechanics, or need 2 players / an LLM gamemaster. Also drop capitalized words when sampling answers: the hardcore lists include proper nouns, and TextArena lowercases the guess but not the stored secret, so a capitalized answer is unwinnable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(textarena): game-authoritative scoring via info dict + generic seeding Carry game setup in the task's `info` dict (game id + secret answer) instead of typed per-game fields, and score from the game's own outcome: the user simulator writes `env.state.rewards` to a file in the runtime when the episode ends, and a single generic `@reward` reads it back — no per-game guess parsing (drops the Wordle-specific reward). Seed the secret generically by intercepting `random.choice` during `reset`, so the game selects our answer and derives all of its own state (Wordle's `secret_word`, Hangman's board, ...) without us knowing each game's state keys. This makes Hangman a drop-in: `game` now also accepts `Hangman-v0` / `Hangman-v0-hardcore` (all six verified end-to-end). No interception changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(textarena): generic seed-based ta.Env -> tasks; add WordLadder + WordSearch Build tasks from RNG seeds instead of word-list sampling: load_tasks seeds the game to reproduce each episode (building the instruction per-seed for games whose prompt embeds the setup, e.g. WordLadder/WordSearch) and the simulator re-seeds to the same episode — no per-game word-list or state-key knowledge, so any single-player TextArena game fits. `game` is now Wordle-v0 / -long, Hangman-v0, WordLadder-v0, WordSearch-v0 (the -hardcore Wordle/Hangman variants are dropped: their lists include capitalized proper nouns that are unwinnable under random seeding). Add a `num_tasks` config (seeds have no natural count). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(examples): add wordle-v1 and terminal-bench-2-v1 (pinned example tasksets) Thin example wrappers that pin a built-in taskset to one game/dataset: - wordle-v1: textarena_v1 with `game` pinned to "Wordle-v0". - terminal-bench-2-v1: harbor with `dataset` pinned to "terminal-bench/terminal-bench-2". Each is a ~10-line subclass that fixes the field (Literal default) and reuses everything else. Wired into the `examples` group + [tool.uv.sources]; configs added for both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add alphabet-sort-v1 taskset with a colocated user simulator Port the v0 `alphabet-sort` env to a v1 taskset. Task generation, prompts, and scoring are reused verbatim from v0; the multi-turn follow-ups (v0's `MultiTurnEnv.env_response`) are colocated with the agent as a `vf.User` that replays the pre-generated turns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: package layout and minimal taskset for alphabet-sort-v1 - Move to a package (`alphabet_sort_v1/__init__.py` + `user.py`), matching the other example tasksets. - Inline the dataset building and scoring into the taskset (drop the `get_dataset_builder`/`compute_reward` indirection, the HF `Dataset` round-trip, and unused `info` fields). - Trim the config: fix the source dataset + seed as module constants, rename `dataset_split` to a `split` literal, and document every field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: inline the name-split and scoring into the taskset methods Drop the module-level helper functions: fold the first/last-name sort key into a local in `load_tasks` and the per-turn scoring into the `@vf.reward` method. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop redundant enable_bash=false from eval configs `DefaultHarnessConfig.enable_bash` already defaults to False, so setting it in the user-simulator eval configs is redundant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The subprocess workdir, docker `--name`, and prime sandbox name now equal the rollout's trace id (passed via `make_runtime(config, name=trace.id)`), so each provisioned resource is greppable back to the rollout it serves. Previously they were independent: a random `/tmp/vf-*` workdir, a `vf-<uuid>` container, and a static `vf-program` sandbox name. Standalone / tool runtimes (no single owning rollout) fall back to a unique `vf-<uuid>` name on the `Runtime` base. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): add Modal sandbox runtime
A remote runtime (`--harness.runtime.type modal`) alongside prime: the program runs
in a Modal sandbox and reaches the host interception server over the host-side
prime_tunnel (Modal's own forwarding publishes a sandbox port, not a host one).
Resources map to Modal's Sandbox.create: cpu_cores -> cpu, memory_gb -> memory (MB),
gpu_count/gpu_type -> gpu ("A100:2"), timeout minutes -> seconds, region, network_access
-> block_network. disk_gb has no Modal sandbox knob, so it's accepted but advisory.
Cleanup mirrors prime: async stop() terminates the sandbox on the normal path, sync
cleanup() is the atexit backstop (Modal's sync terminate), both idempotent. Adds modal
to deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): adopt Modal's resource convention across runtimes
Unify the runtime resource fields on Modal's cleaner naming + units: `cpu` (cores),
`memory` (MB), `gpu` (spec string, e.g. "A100:2"), `disk` (MB) — on Task.resources and
every runtime config. Modal uses them natively; prime and docker map them to their APIs
(a shared `parse_gpu` splits the GPU spec into type+count; prime converts MB->GB and
seconds->minutes). Timeouts move to seconds. disk stays advisory on docker/modal
(neither has a per-container/sandbox disk knob) and enforced on prime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): memory/disk resources in GB (modal maps GB->MB)
Use GB for memory and disk (human-friendly: memory=2 not 2048). Prime's API is
already GB so it passes through; docker uses --memory <n>g; modal converts to its
native MB. harbor's memory_mb/storage_mb divide back to GB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: resolve taskset / harness / env ids from the Environments Hub A taskset, harness, or v0 environment id may now be one of: - `name` a local package (unchanged) - `org/name` the env hub, latest version - `org/name@version` the env hub, a pinned version `EnvId` (the type of every id field) is a `str` subclass — it serializes as the plain string everywhere (CLI flag, config TOML, wire), and parses into `org` / `name` / `version`. The derived `name` (org and version stripped, normalized) is what logging, the dashboard, and the output path use, so a hub id never injects `/` into a path. A `TasksetConfig.name` / `HarnessConfig.name` property exposes it on the config. `ensure_installed` makes a hub id importable on demand, reusing the same install path as `prime env install` (`install_from_hub`). It is wired into the v1 plugin loader (tasksets + harnesses) and the v0 legacy bridge, so both v1 plugins and v0 envs install from the hub. A local id is a no-op (no network, no subprocess), so existing local runs are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: make EnvId a validated str in types.py (drop the str subclass + ids module) Addresses review feedback on the previous commit: - `EnvId` is now `Annotated[str, AfterValidator(...)]` — a plain validated string, not a `str` subclass with a custom core schema. The validator checks the `org/name[@Version]` shape (a local id is any module name); the value stays a real `str`, so configs and the wire are unchanged. `env_name` / `env_module` parse it; `ensure_installed` returns the importable module name. - Folded the standalone `verifiers/v1/ids.py` into `verifiers/v1/types.py` (no new module). - `TasksetConfig.name` / `HarnessConfig.name` stay plain properties (BaseConfig forbids extra inputs, so a computed field couldn't round-trip through `config.toml`). * refactor: move the env id back into its own ids.py module Keeps the validated-str EnvId, just relocated out of types.py into verifiers/v1/ids.py. * docs: describe the derived id name as the package name * style: sort imports in taskset.py / __init__.py (ruff I001) * build: anchor ruff config in verifiers pyproject (line-length 88) Running ruff from inside the vendored verifiers submodule otherwise walks up and inherits prime-rl's line-length (120); pin 88 here so it matches verifiers' own CI. Also reflows output.py, whose line changed with the id .name edit. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): run v0 environments on the `eval` CLI (--legacy.id)
Backwards-compat: `uv run eval --legacy.id <v0-env-id> [--legacy.args '{...}']` evaluates
a classic `verifiers.load_environment` env through the v1 eval, bridged to v1 Traces. All
v0 glue lives in verifiers/v1/legacy.py (`run_legacy_eval`, reusing the existing
`rollout_output_to_trace`); the env runs in-process via `env.run_rollout` (no env server /
runtime / interception). Minimal v1 wiring: a `LegacyConfig` (id + args) on EvalConfig and
a one-branch dispatch in the eval CLI; the --rich dashboard (v1 Rollout state) is off for
legacy runs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): move the legacy env selector onto EnvConfig (align with prime-rl)
Put the v0/legacy selector on the base `EnvConfig` — `id` + `args` (forwarded to
`load_environment`) plus `is_legacy` / `env_id` properties — matching prime-rl's shape, so
`EvalConfig` and `EnvServerConfig` inherit it (and prime-rl can drop its duplicates). The
eval CLI now branches on `config.is_legacy` and takes the v0 env via `--id <env> [--args
'{...}']` instead of the nested `--legacy.id`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): add extra_env_kwargs to the legacy env config
`extra_env_kwargs` (on EnvConfig, applied to the loaded v0 env via `env.set_kwargs`) carries
post-load knobs like `max_total_completion_tokens` / `max_seq_len` / `timeout_seconds`,
distinct from `args` (construction kwargs) — mirroring prime-rl main's `extra_env_kwargs`
(missing from this branch). Wired into both legacy entry points (`run_legacy_eval` and
`LegacyEnvServer`). The v0/legacy fields on EnvConfig (`id`/`args`/`extra_env_kwargs` +
`is_legacy`/`env_id`) are now grouped under an explicit legacy separator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(v1): mark --id as legacy in the eval usage line
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fixes) (#1600) * docs(v1): add a verifiers.v1 README Adapt the vf-nano README into verifiers/v1/README.md (the vendored v1 framework), keeping its command-driven structure and folding in the features that landed since: the modal runtime, per-rollout limits + native retries, hub-installable ids, the v0 backwards-compat eval (`--id`), rollout-id resource naming + guaranteed teardown, and the user simulator. Every `uv run eval ...` example is dry-run-verified against the current CLI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): frame v1 as the new version; Highlights header; drop rl extra Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump prime-pydantic-config to dev86 for single-dash short flags The vendored vf-nano README advertises -n/-r/-m/...; dev83 only accepted them as --n (single-dash was parsed as a value). dev86 supports single-dash short aliases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): single-dash aliases, advanced configs in quickstart, lifecycle wording Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): restore Limits & retries; drop Installable ids; Backwards compatibility last Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): note v0 is untouched (old entrypoints fully supported) in backwards-compat Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(v1): serve a v0 env via --id (legacy bridge), mirroring eval The serve CLI now branches on config.is_legacy to LegacyEnvServer, so a classic v0 env can be served over ZMQ with `uv run serve --id <env>` (parity with `eval --id`). Verified: `serve --id reverse-text` brings up the env server (1000 tasks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): tasksets/harnesses section (packages vs examples); serve v0; drop TODOs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop inline comment on the prime-pydantic-config pin Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): lead Highlights with composable taskset×harness + swappable runtime; re-add Harbor section Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): say packages, not plugins Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): first-class harnesses/tasksets packages with a registry Nest the built-in plugins under namespace packages (harnesses/{default,rlm}, tasksets/{harbor,textarena_v1}), each with an __init__ exposing a lazy REGISTRY (id -> dotted module). The loader resolves a built-in id through its group registry to the namespaced module, falling back to a flat import for local examples and hub ids. User-facing ids are unchanged (--harness.id rlm, eval harbor, ...); class names keep the <Name>Harness/<Name>Taskset convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(v1): let serve accept @ file.toml serve rejected config-only argv with USAGE before parsing, even though @ file.toml supplies the ids. Mirror eval's references_config_file check so a saved config runs with just @ file.toml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): remove hello-rlm example Drop the hello-rlm-v1 example package and its pyproject source/group + lock entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): fix tools API ref, drop user-sim section, split examples table tool_servers -> the tools() method; remove the User simulation section; split the examples enumeration into tasksets/harnesses (matching examples/{tasksets,harnesses}). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): re-export built-ins (drop registry), rename harbor -> harbor-v1 - harnesses/tasksets __init__ re-export their plugins' classes instead of a REGISTRY dict; the loader resolves the namespaced module directly via find_spec (flat fallback for local/hub ids). textarena_v1 stays lazy (optional textarena dep) so 'import tasksets' doesn't require it. - rename the built-in taskset harbor -> harbor-v1 (module tasksets.harbor_v1), consistent with textarena-v1 and the -v1 examples; update configs + README. - fix flat imports broken by namespacing: terminal-bench-2-v1 and wordle-v1 import from tasksets.{harbor_v1,textarena_v1}; the textarena user sim launches '-m tasksets.textarena_v1.server', which imports from tasksets.textarena_v1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): note the harbor CLI prerequisite for harbor-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(v1): ruff format serve.py Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1603) * feat(v1): gate task-tool tasksets on harness support (SUPPORTS_TASK_TOOLS) A harness without an MCP client (e.g. rlm) silently ignored a task's tool servers instead of refusing the run. Add a `SUPPORTS_TASK_TOOLS` class flag on `Harness` (default True; False on rlm, which drives its own tools), and have `Environment.__init__` raise an informative error when a taskset that declares tools is paired with a harness that can't expose them. So `eval wikispeedia-v1 --harness.id rlm` now fails fast with a clear message ("Harness 'rlm' does not support task tools, but taskset 'wikispeedia-v1' exposes tool servers (MCP). Run it with a harness that supports task tools ...") rather than silently dropping the MCP tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): drop the SUPPORTS_TASK_TOOLS test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(v1): mark compact harness SUPPORTS_TASK_TOOLS; drop self-explanatory rlm comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): label prime sandboxes + tunnels with the eval run uuid Add `PrimeConfig.labels` (settable via `--harness.runtime.labels`), passed to both `CreateSandboxRequest` and every `Tunnel`. `run_eval` defaults the labels to the eval run's uuid when unset — after `save_config`, so re-running `@ config.toml` gets a fresh uuid rather than reusing the saved one. Every sandbox and tunnel a run creates then shares the run's uuid label, so they can be found and cleaned up together. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): drop the run-uuid default for prime labels for now Leave `PrimeConfig.labels` settable (sandbox + tunnels), but don't auto-default them to the eval run uuid in run_eval for now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(v1): rate-limit prime tunnel creation to stay under the per-token cap
Each prime rollout opens a tunnel; prime caps tunnel creation at 100/min per API
token, so at high concurrency tunnel.start() returns 429 (per-token limit), raising
ProgramError and killing the rollout (~61% failures at 256 concurrent rollouts).
Add one process-wide AsyncLimiter shared across rollouts, wrapping tunnel.start(),
paced to 100/min. Phrased as 1-every-0.6s (capacity 1) rather than (100, 60) so a
full bucket can't fire 100 tunnels at once and re-trip the limit within the minute.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): runtime benchmark harness + results (subprocess vs docker vs prime)
bench/run_bench.sh + bench/summarize.py drive the v1 eval CLI across runtimes and
scales (gsm8k-v1, -c 512, retries off) and report e2e wall clock + per-rollout
generation latency. bench/RESULTS.md records the ladder (32/256/512) and the prime
tunnel-rate-limit fix (before/after).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): results report (min/p50/p90/max per run)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): cap generation length to isolate runtime overhead
Add an optional MAX_TOKENS knob to run_bench.sh (--sampling.max_tokens, label -t<n>), a plot_e2e.py that renders e2e by-runtime and by-batch-size charts, and a capped (max_tokens=2048) 32/64/128 two-pass section in RESULTS.md. Capping trims the ~1.5% long-generation straggler tail (p90=554), giving cleaner runtime-overhead numbers (e.g. docker-128 319 -> ~134s, prime-128 413 -> ~178s).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(v1): bump prime tunnel limiter 100 -> 512 (Prime raised the per-token cap)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): multiplex interception servers + tunnels across rollouts
Split the per-rollout InterceptionServer into a RolloutSession (one rollout's trace/limits/stops/user) + a shared, secret-routed InterceptionServer; add an eval-level InterceptionPool that brings up ceil(concurrency/multiplex) shared servers, each exposed once (one tunnel per server behind a remote runtime), and hands each rollout a session slot (shared endpoint + its own secret). Opt-in via --multiplex (EvalConfig.interception_multiplex; 0 = per-rollout, unchanged). The harness is untouched — it already authenticates with its per-rollout secret, which is what the server routes by. Drops remote tunnels from O(N) to O(N/multiplex).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): add MULTIPLEX knob to run_bench.sh; run from the script's own worktree
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): plot baseline vs multiplex gen-duration distribution
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): add multiplex=64 to the base-vs-mux plot (mux=32 is the sweet spot)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): plot p10/p50/p90 gen-duration band (baseline vs mux32 vs mux64)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): make multiplex a first-class EnvConfig field; multiplex the env server too
Move multiplex from EvalConfig to EnvConfig (ge=1, default 32) so both the eval CLI and the env server (EnvServerConfig inherits it) multiplex — prime-rl, which drives the server, now benefits. Wire an elastic InterceptionPool into EnvServer: created once for the server's lifetime and grown on demand, so tunnels are reused across requests rather than re-created per call (v1 only; the legacy v0 bridge is skipped). The pool is now elastic (no upfront concurrency sizing) to fit the server's unbounded request load. Restructure interception.py + interception_pool.py into interception/{server,pool}.py with re-exporting __init__.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): centralize the interception pool on Environment.interception_pool()
Both the eval runner and the env server built InterceptionPool directly from env.harness.config.runtime + multiplex — a duplicated reach-through. Add Environment.interception_pool() (the env owns multiplex + the harness runtime) and call it from both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): inline interception handle_chat; drop RolloutStopped/ModelCallError
Those exceptions only existed to signal HTTP outcomes from an extracted RolloutSession.handle() back to the server. The server's handle_chat can run the loop directly on the routed session and return each response inline (as it did before multiplexing), so the extraction + both exceptions are unneeded. RolloutSession is now pure state + refused().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): inline env.interception_pool() into the runner's async with
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): general single-turn benchmark (benchmark.sh -> benchmark.json -> plot.py)
Rename run_bench.sh -> benchmark.sh: a first-class single-turn benchmark that iterates runtimes x batch sizes (default subprocess/docker/prime x 32/64/128, gsm8k-v1, max_tokens 1024, default multiplex) and writes bench/benchmark.json (metadata + per-run e2e + full per-rollout gen durations + reward/errors) via bench/aggregate.py. plot_e2e.py -> plot.py reads benchmark.json and renders p10/p50/p90 by runtime + by batch to bench/benchmark.png (gitignored). Drop the obsolete e2e_*.png; commit a prime-only benchmark.json.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): public EnvServer.interception_pool() hook (drop isinstance + or-nullcontext)
The env server's run() guarded pool creation with isinstance(self.env, Environment) and entered it via 'async with self.pool or nullcontext()'. Replace with a public interception_pool() hook (returns self.env.interception_pool()) that LegacyEnvServer overrides to a nullcontext — so run() is just 'async with self.interception_pool() as self.pool', no isinstance, no or-nullcontext. Also drop the 'for an eval or env server' phrase from the pool docstring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): drop committed mux_vs_base.png; gitignore all bench images
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(v1): public PooledServer, leak-proof pool teardown; prune stale bench scripts
- interception pool: rename _PooledServer -> PooledServer (exported); suppress per-entry teardown errors so one stuck tunnel can't leak the rest
- bench: drop plot_mux.py (one-off A/B) and summarize.py (superseded by aggregate.py); keep only the benchmark.sh -> aggregate.py -> plot.py flow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(v1): reserve interception slot atomically with register
register() under the lock before incrementing load, so a failed register can't leak a slot (the finally only runs once the slot is fully reserved).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* exp(v1): drop bench/RESULTS.md (keep only the benchmark scripts + data)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): delta-native message-graph trajectory Replace the flat `trajectory: list[Turn]` — where every turn restated the whole prompt, so storage was quadratic in turns — with a graph of `MessageNode`s, one per distinct message linked to its predecessor. `Trace.nodes` is the ground truth; `trajectory`/`branches` are views over the graph, and branching falls out of walking parent links (no post-hoc prefix-matching). Each node stores only the tokens it adds (per-message spans from the renderer; the generation-prompt scaffold + sampled completion on the assistant node), so a branch's training sample is a cheap concat and in-memory/on-disk/wire size is linear. - new verifiers/v1/graph.py: MessageNode, message_hash (mirrors branching.same_message), add_turn (build), the walk (branches/trajectory views), branch_token_sequences (concat). - trace.py: `nodes` field; trajectory/branches/num_* become graph-walk views; legacy trajectory dicts tolerated on load. - interception.py / legacy.py build via graph.add_turn; the renderer client threads per-message token spans (prompt_attribution). - branching.segment kept as the legacy / conformance oracle. On-disk size scales linearly: ~9x smaller for a 20-turn agentic trace, ~18x at 40 turns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): rename MessageNode.sampled_mask -> mask; per-field docstrings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(v1): graph.py module docstring describes the current design only Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): drop Turn/trajectory — nodes + branches are the model `Trace.trajectory` and the `Turn` view are gone; the message graph (`Trace.nodes`) plus `branches` (root→leaf node paths) are the whole model. `Branch` now holds `nodes` (not `Turn`s) and exposes `messages`/`num_turns`/`completion_len`/`prompt_len`/`total_tokens`; the trace's `assistant_messages`/`tool_messages`/`has_response`/`is_truncated` read the graph directly. `MessageNode` gains `finish_reason` (it only lived on `Turn.response`) so truncation detection survives. `branching.py` is deleted — `graph` supersedes it (`message_hash` is the message-equality). Dashboard + rollout log read node/branch fields. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): remove dead code (legacy-trajectory validator, view cache) - Drop `_drop_legacy_trajectory`: a pre-graph dict fails strict validation anyway (the computed fields it also carried), and the wire form has no `trajectory` — so the validator never actually did anything. - Drop `_view_cache`/`_cached`: the per-turn limit checks grow `nodes` each turn and invalidate it, so it bought a constant at best; `branches` is now a plain property. - Drop a stale doc reference to the removed test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: highlight the delta-native trace graph + ruff format Add a Highlights bullet for the message-graph trace (linear-not-quadratic storage, branches from the walk, training sample = concat along a path) and refresh the body references to the removed `branching` module / `trajectory` field. Format the touched files with ruff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: first-class Branch token accessors Give `Branch` self-describing token views — `token_ids`, `sampled_mask` and `logprobs` (aligned, 0.0 on non-sampled tokens) — so a branch is the single first-class unit for building a training sample or logging, no graph walking at the call site. Drop `graph.branch_token_sequences` (superseded by the Branch accessors). Ruff-format `types.py`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`Trace.branches` is the only consumer of `branches_from_nodes`, so build the branches there (walk each leaf's parents back to its root) and drop the graph-level `branches_from_nodes` / `_path_to`. `graph.leaves` stays (also used by `num_turns`/`num_branches`). Docstring no longer names the removed `trajectory`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(v1): add end-to-end eval test suite under tests/v1 - e2e reward-1 runs across the runtime matrix (subprocess/docker/prime, modal excluded): single-turn (echo), multi-turn (alphabet-sort), multi-turn + tools (glossary), agentic (agentic-echo: bash writes a file, verified in the runtime) - v0 backwards-compat: reverse_text + alphabet_sort bridged, shape parity vs a v1 run - test_configs: every root configs/*.toml parses as EvalConfig - echo + agentic-echo fixture tasksets (deterministic, no dataset/Dockerfile) - conftest: run_v1/run_v0 helpers (greedy temperature=0, generous caps), runtime fixture, e2e + prime markers, skip-without-API-key, on-demand v0 install Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): move fixture tasksets to tests/v1/fixtures; add cross-harness tests - move echo-v1 + agentic-echo-v1 out of the test dir into tests/v1/fixtures, resolved by id via pytest's pythonpath ini (drops the conftest sys.path insert) - test_harnesses.py: run echo (single-turn) and glossary (multi-turn + tools) under default + compact; assert rlm (no task-tool support) is rejected when paired with a tools taskset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): satisfy ruff (E731 lambda -> def, ruff format) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): merge harness+runtime into one matrix; gate tools on SUPPORTS_TASK_TOOLS - fold test_harnesses into test_e2e: the trivial tasks fan across the harness x runtime matrix (built-in default + rlm; compact is an example harness, excluded; rlm marked slow as it installs an agent binary) - the tools test reads each harness's SUPPORTS_TASK_TOOLS to expect a raise (rlm) vs a run (default), instead of a separate hardcoded test - alphabet-sort: similarity_power=1 (drop power scaling) so a near-perfect sort isn't sharply penalized Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): self-contained v0 legacy tests via echo fixtures; inline run helpers - add v0 echo fixtures (echo-v0 SingleTurnEnv, echo-multi-v0 MultiTurnEnv); the legacy tests use them instead of example envs, so the v0 path is exercised deterministically - drop ensure_v0: the legacy bridge imports a fixture by id off pythonpath (no runtime uv pip install, no environments/ dependency), same as the v1 fixtures - inline the run_v1/run_v0 helpers into their fixtures Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(v1): adapt legacy shape test to #1606 message-graph (trajectory -> nodes) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(v1): SUPPORTS_USER_SIM harness flag + container-safe multi-turn user-sim fixture - add SUPPORTS_USER_SIM ClassVar on Harness (default False; the default harness opts in) - a user simulator is a distinct capability from task MCP tools (rlm supports neither: it takes a single instruction, no message history) - e2e: container-safe echo-multi-v1 user-sim fixture (vf.User shipped as a uv script, staged + run via uv in any runtime); the multi-turn test is gated by SUPPORTS_USER_SIM (skips rlm) - generalize the test capability lookup to harness_supports(id, flag) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(v1): run the user simulator in its own runtime, reached host-side The user simulator is driven by the framework on the host (connect_user), not by the model. It was served colocated in the agent's runtime with a localhost URL, which the host can't reach when the agent runs in a remote prime sandbox (ConnectError; via the agent tunnel, a 421). Now serve_user runs it in its OWN runtime (host subprocess by default, or its own sandbox via TasksetConfig.user.runtime) and publishes the port back to the host (serve_tools host_reachable: a remote sandbox's public_url, else localhost). - add UserConfig(runtime) to TasksetConfig; serve_tools gains host_reachable for a host-consumed colocated server - e2e: the multi-turn user-sim test now passes on prime; add a test for the user-sim in its own (docker) sandbox Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(v1): matrix task-tools + user-sim across server runtimes - server_runtime fixture + test_task_tools_own_runtime / test_user_own_runtime: a tool/user-sim server in its OWN runtime (subprocess/docker/prime), agent on subprocess - skip_if_unexposable: skip the prime server case when the sandbox region can't publish a port (a known prime infra limit, surfaced by the matrix) - TODO in prime.public_url to lift that limit (then drop the skip) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(v1): rename agentic-echo fixture -> echo-agentic-v1 (echo_* naming) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LegacyEnvServer.requires_group_scoring was hardcoded False, so a v0 env whose rubric defines group/preference reward funcs was routed down the per-rollout path. rubric.score_rollout asserts there are no group reward funcs, so every rollout of such an env errored. Report the env's actual capability (env.requires_group_rollouts) and run _run_group via env.run_group once, so the rubric scores the rollouts together (score_group) and cross-rollout rewards apply. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts (#1613) _v0_client hardcoded a v0 renderer (token-in/out) client. prime-rl trains renderer-only, so that's correct today, but a MITO (chat-completions, type="openai") config would have silently built a renderer client and done the wrong inference mode instead of erroring. Raise on a non-renderer client config, and now that the type is guaranteed, read the renderer fields directly instead of via defensive getattr. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1614) * fix: serve the eval split for eval-only v0 envs in the legacy bridge LegacyEnvServer.__init__ unconditionally called env.get_dataset(), so an eval-only v0 env (e.g. aime2024, which defines only an eval split) raised ValueError: dataset is not set and crashed the v1 backward-compat path. Fall back to env.get_eval_dataset() when there's no train split. (No prime-rl wiring; get_eval_dataset itself falls back to the train split, so train-only envs are unaffected.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: eval the eval split in the in-process v0 eval (run_legacy_eval) run_legacy_eval (the `eval` CLI's `--legacy.id` path) is the eval entrypoint but called env.get_dataset(), so it crashed on eval-only v0 envs and ran on the train split for envs that define both. Use env.get_eval_dataset() — the eval split, falling back to train when unset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1615) The orchestrator drives eval rollouts of v0 envs through the same LegacyEnvServer (run_rollout/run_group -> _run_v0 -> _v0_client), and eval uses an OpenAI chat-completions client. The #1613 guard raised on any non-renderer config, so it broke v0 eval ("MITO ... not supported"). Dispatch on the config type instead: a renderer config (training, TITO) builds a v0 renderer client; an OpenAI config (eval) builds a v0 chat-completions client. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): add scaleswe taskset + per-task setup/workdir hooks scaleswe-v1 ports the v0 ComposableEnv Scale-SWE taskset to v1: each row carries its per-task image + workdir, runs its pre_commands in setup() before the agent, and scores with a single `solved` reward that restores the test files to base, applies the f2p test, and runs the merged F2P+P2P pytest ids through a self-contained scorer (1.0 iff every expected id passes). Two small, general framework hooks enable it: - Task.workdir, injected into the runtime config (symmetric with Task.image), so the agent and scoring run in the row's repo dir. - Taskset.setup(task, runtime), run by the rollout after runtime.start() and before the harness, for per-task runtime prep. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: rename scaleswe _scorer.py -> score.py Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(v1): NEEDS_CONTAINER taskset flag; trim scaleswe config - add Taskset.NEEDS_CONTAINER ClassVar; the Environment refuses the subprocess runtime for a taskset that sets it. scaleswe-v1 sets NEEDS_CONTAINER = True. - drop scaleswe's dataset_name/split knobs (hardcode AweAI-Team/Scale-SWE train); the taskset uses the base TasksetConfig. - drop the pre_commands guard — all 20181 Scale-SWE rows carry pre_commands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop the GCP registry prefix from scaleswe images The prime sandbox pulls the raw Docker Hub image (aweaiteam/scaleswe:<tag>) directly — verified in a smoke — so the us-central1 prod-sandbox prefix the v0 env prepended is unnecessary. Use the row's image_url as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(v1): honor cli/toml workdir over the task's runtime_for injected task.workdir unconditionally, overriding a user-set --harness.runtime.workdir. Apply the task's workdir only when the runtime config's is still the default — matching the "cli/toml > task > default" precedence the resources loop already uses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop redundant comment on workdir precedence Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: add v1 #1559-vs-#1576 comparison doc Feature parity, branch-unique features, and validation done for the two open v1 refactor PRs (#1559 codex/v1-nano-refactor-draft, #1576 feat/nano-as-v1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: trim comparison (drop names, some #1559 items, validation section) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: scope the size row to the verifiers/ module diff Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Revert "docs: scope the size row to the verifiers/ module diff" This reverts commit 500bd30. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-v1 (#1618) * feat(v1): carry multimodal images through the message graph Make a VLM trainable through the v1 message-graph trace, in two layers. Ingress: a content-part union (TextContentPart / ImageUrlContentPart, MessageContent) lets user/system messages hold images, and Task.instruction becomes `str | Messages` so a taskset can seed an image-bearing initial prompt (the default harness opts in via SUPPORTS_MESSAGE_INSTRUCTION; others reject a Messages instruction). The interception server and the v0 legacy bridge preserve image parts (shared `content_to_parts`) instead of flattening them to text, and `message_hash` hashes list content stably. Egress: TurnTokens / MessageNode carry the renderer's MultiModalData as a transient, serialization-excluded sidecar (offsets stored node-local); add_turn attributes each image to the node that introduced it; Branch.multi_modal_data merges the nodes' items and rebases offsets to branch-global. The pixel tensors never reach the wire or results.jsonl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(v1): colocate the user simulator in the agent runtime By default the user simulator now runs inside the agent's (harness's) already- started runtime — reusing it via serve_tools(colocated=True, host_reachable=True) with its port published back to the host — instead of spawning a separate runtime per rollout. This removes the per-rollout runtime start/stop churn (and the startup races it caused) for multi-turn tasksets. UserConfig gains `colocated` (default True); set it False to give the user its own runtime, e.g. a remote sandbox that can't publish the colocated port back to the host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(v1): add the color-codeword-v1 taskset A multi-turn VLM decoding task (the v1 port of the v0 `color-codeword` env): each turn shows colored squares mapping to letters; the model accumulates the codeword and outputs it in full on the final turn. Turn-0 squares ride in the task's `Messages` instruction; later turns are injected by a colocated `vf.User`. Reward is an exact match of the final codeword, with a partial-match metric. Exercises multimodal images end-to-end through the v1 message graph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: occasional malloc_trim to bound v1 worker RSS (#1621) * perf(v1): occasional malloc_trim to bound worker RSS A rollout parses large base64 request bodies (e.g. screenshots) per turn and frees them, but glibc retains the freed arenas, so a long-lived eval / env-server worker's resting RSS climbs and never drops. Call malloc_trim(0) once every Nth finished rollout (gated in Episode.run) to hand those arenas back to the OS. Best-effort and resolved once: a no-op off glibc (musl, macOS). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(v1): run malloc_trim off the event loop malloc_trim(0) walks every arena's free lists and can block for tens of ms on a large heap. Called inline it stalls the whole event loop — and under the multiplexed env server, every concurrent rollout with it. Offload to a worker thread via asyncio.to_thread; ctypes releases the GIL during the call, so the heap walk runs concurrently with the loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): review-pass cleanups for multimodal support - harness: express the capability flags (APPENDS_SYSTEM_PROMPT, SUPPORTS_*) as docstrings; tighten resolve_prompt. - graph/types: trim the multimodal transient-carrier comments to the essentials. - color-codeword-v1: hard-code MAX_TURNS / SEED as module constants, move the >=1 check into a Field(ge=...) validator, drop the redundant assert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(v1): attribute multimodal items by content part, drop offset machinery Each image's renderer item is now attributed to the node whose message introduced it (by counting media content parts in prompt order) instead of mapping placeholder token offsets to node spans. Removes `_node_for_offset` and the node-local → branch-global offset rebasing in both `graph.add_turn` and `Branch.multi_modal_data` — those placeholders were never read (training uses `mm_items` + the token→type map). The reused prefix is skipped via a cursor (`num_reused`), so earlier turns' images aren't overwritten. Also: default harness drops the redundant inline flag comments, and program.py reads `sys.argv[1]` only in the no-INITIAL_MESSAGES branch (an image-prompt rollout passes INITIAL_MESSAGES and no argv, which previously raised IndexError). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(v1): ruff-format graph.py multimodal attribution Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(v1): add r2e-gym-v1 taskset (R2E-Gym on rlm harness / prime runtime) Port of the v0 ComposableEnv R2EGymTaskSet to a v1 taskset: loads R2E-Gym/R2E-Gym-Subset, symlinks the repo venv + clears pycache + hides the ground-truth /r2e_tests in setup, and scores via run_tests.sh -> pytest-summary parse vs the row's expected_output_json. Includes gold-patch reconstruction and a dummy_rollout end-to-end check (base->0.0, gold->1.0, verified on 2 R2E-Gym sandboxes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(r2e-gym-v1): address review — robust parsing, drop unused fields, no dummy script - parse_log_pytest: ERROR lines without '::' fall back to the full line instead of silently producing an empty-string key (the old try/except was dead code). - extract_gold_patch: guard empty/missing parsed_commit_content (return '') so json.loads doesn't raise before the empty-patch check. - R2EGymTask: drop unused commit_hash/repo_name fields (name carries the id). - remove dummy_rollout.py (validate via 'uv run eval' instead). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: ruff format r2e_gym_v1 (line-length 88) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(r2e-gym-v1): drop undeclared task kwargs, mirror composable resources load_tasks passed commit_hash/repo_name to R2EGymTask, which doesn't declare them (extra_forbidden) — every eval crashed in load_tasks before any rollout ran. Drop the unused kwargs; commit_hash still surfaces via the task name. Also request the v0 ComposableEnv SandboxSpec defaults (cpu=4, memory=4, disk=10) per task, so R2E-Gym sandboxes aren't sized at the v1 prime-runtime defaults (cpu=1, memory=2, disk=5). Verified on a real prime sandbox (4 CPU / 4 GB / 10 GB). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scaleswe-v1): mirror composable sandbox resources ScaleSWETask requested no resources, so Scale-SWE sandboxes ran at the v1 prime-runtime defaults (cpu=1, memory=2, disk=5). The v0 ComposableEnv ScaleSWETaskSet inherits the SandboxSpec defaults (cpu=4, memory=4, disk=10); request the same per task to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(swe-v1): drop resources comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Mika Senghaas <mail@mikasenghaas.de>
* fix(v1): treat overlong prompts as clean truncated rollouts An overlong prompt (prompt + requested completion exceeding the model's context window) is a budget limit, not a crash. Previously the model call failed, the interception server returned a 502, and the rollout was recorded as an error. - Add OverlongPromptError(ModelError). Detection lives in the clients: the openai client phrase-matches the provider's 4xx context-length message; the renderer client rebadges the renderers-native client-side OverlongPromptError (raised pre-flight from GET /v1/models, not an OpenAIError) as well as the engine 4xx. - The interception server catches it and ends the rollout cleanly with a `context_length` truncation stop: it returns the last good turn, or refuses the call to halt the harness when there isn't one (the same shape as the existing `refused` path). - Trace.is_truncated counts `context_length`. Mirrors the v0 path (handle_openai_overlong_prompt + renderer_client's RendererOverlongPromptError rebadge). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): drop redundant comment on the renderer overlong rebadge Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v1): trim the overlong-prompt comment in handle_chat Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Use native JSON bytes for upstream eval traffic * Validate non-finite JSON structurally
* Await scoring hooks directly * Preserve concurrent multi-hook scoring
* Persist large traces off the event loop * Preserve queued trace writes on cancellation * Use public Pydantic JSON serialization
…'s verifiers dep (#1824) * fix(v1): declare verifiers dep in compact harness package The compact harness package imports verifiers.v1 but declared dependencies = [], so installing/publishing the wheel on its own could fail at import time. Match the other v1 environment packages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(v1): carry v0 truncation flag through the legacy bridge Trace.is_truncated is derived from the v1 stop-condition vocabulary and the final turn's finish_reason. v0 stop names (e.g. max_turns_reached, prompt_too_long) don't map onto that vocabulary, so legacy traces reported is_truncated=False even when the v0 rollout was truncated. Add an explicit, serialized `truncated` override on Trace (None for native v1, so the derivation is unchanged) that is_truncated honors first, and set it from the v0 rollout's own is_truncated flag in the bridge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(v1): derive legacy truncation from stop condition, not a Trace field Keep Trace.is_truncated purely derived (no stored field). Instead, the v0->v1 bridge translates a truncated v0 rollout's stop name into v1's truncation vocabulary (max_turns_reached -> max_turns, prompt_too_long -> context_length, ...; unmapped truncated stops fall back to max_output_tokens) so the property derives True. Untruncated rollouts keep their v0 stop condition unchanged. This reverts the `truncated` field added to Trace in the previous commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mikasenghaas
requested review from
samsja,
willccbb and
xeophon
and removed request for
willccbb
June 22, 2026 19:04
Member
|
lgtm |
samsja
previously approved these changes
Jun 22, 2026
mikasenghaas
marked this pull request as ready for review
June 22, 2026 19:06
Contributor
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e57c064. Configure here.
…es (#1826) * refactor(v1): move built-in tasksets/harnesses into verifiers.v1 The standalone `tasksets` and `harnesses` packages (under packages/) now live inside verifiers as `verifiers.v1.tasksets` / `verifiers.v1.harnesses`, so the built-in tasksets and harnesses ship with verifiers rather than as separate editable/PyPI packages. - move packages/{tasksets,harnesses}/* -> verifiers/v1/{tasksets,harnesses}/* - loaders resolve built-in ids under verifiers.v1.{tasksets,harnesses} - drop the tasksets/harnesses dependency-groups, [tool.uv.sources] entries, and default-groups; the textarena extra is now verifiers[ta] - point the harbor_v1/textarena_v1 consumer envs (terminal-bench-2-v1, swebench-verified-v1, wordle-v1) at verifiers.v1.tasksets, depending on verifiers - remove the publish-{harnesses,tasksets} PyPI workflows - drop the now-stale taskset/harness packaging docs from the READMEs Verified: eval --dry-run resolves all README swappable-harness commands and the tb2 fix-git example; real runs of gsm8k-v1 (default/rlm/codex) and the terminal-bench/fix-git task (docker + rlm) load and run end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(v1): pin textarena==0.7.4 and rename the `ta` extra to `textarena` The old standalone `tasksets[textarena]` extra pinned `textarena==0.7.4`, but the verifiers extra it folded into (`ta`) declared textarena unpinned — so fresh installs of wordle-v1 (`verifiers[textarena]`) could resolve a newer TextArena release and break textarena_v1 / Wordle-v0 behavior. Re-pin `textarena==0.7.4` (+ `nltk>=3.9.2`) and rename the extra `ta` -> `textarena`, updating every reference: the v0 TextArenaEnv integration, the dev group, docs, and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(v1): keep the textarena extra named `ta`, keep the 0.7.4 pin Revert the `ta` -> `textarena` extra rename (back to `ta`, restoring the v0 TextArenaEnv integration's extra name and all references), but keep the `textarena==0.7.4` (+ `nltk>=3.9.2`) pin from the previous commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
samsja
approved these changes
Jun 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Note
Squashed history of
feat/nano-as-v1for the merge intomain. Identical content (tree-identical tofeat/nano-as-v1), with runs of adjacent standalone (non-PR) commits squashed so the merge carries fewer commits while every individually-merged PR stays its own commit. Merge with a merge commit (not squash) to preserve the PRs. Full original history: #1576.Note
High Risk
Large framework and packaging shift with removed workflows, docs, and environments (bfcl-v3, v1 init path); hub publish and test matrix changes can affect releases and downstream consumers.
Overview
This merge brings in the verifiers v1 stack (
import verifiers.v1 as vf): composable taskset × harness configs, swappable runtimes, typedTask/Tracerollouts, and newuv run eval/validate/initentrypoints. Public docs drop the old BYO Harness /prime env init --v1path in favor of packaged*_v1environments and TOML underconfigs/.Environments move from monolithic
load_environment+ inline v1 shims to installable packages (e.g.aime24_v1,alphabet_sort_v1,code_golf_v1,color_codeword_v1, examplecompactharness). Legacybfcl_v3and rootalphabet_sort_v1.pyare removed; v0alphabet_sortno longer accepts av1=flag.CI and release narrow: Semgrep policy and
publish-tasksets/publish-harnessesworkflows are deleted; ty checks onlyverifiers; tests drop Python 3.10, addtests/v1, and swap Prime-sandbox tests for v1 runs. Hub auto-publish skips*_v1dirs andcompactuntil those packages are hub-ready.Reviewed by Cursor Bugbot for commit 71c81bc. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Introduce the verifiers v1 environment framework with tasksets, harnesses, runtimes, and CLI
verifiers/v1/replacing v0-era abstractions with typedTaskset,Harness,Runtime,Rollout,Episode, andTracecomponents, including MCP-based tool and user simulator servers.BashHarness,DefaultHarness,CodexHarness,KimiCodeHarness,MiniSWEAgentHarness,RLMHarness,Terminus2Harness) and runtimes (SubprocessRuntime,DockerRuntime,ModalRuntime,PrimeRuntime) as installable packages.eval,validate,serve, andinitCLI entrypoints underverifiers/v1/cli/with Rich dashboards, resumable runs, dry-run mode, and in-process or worker-pool execution.env_utils,scripts/init.py,scripts/eval.py) and strips previously available symbols fromverifiers.__init__andverifiers.types.State.verifiersandverifiers.v1are no longer available;verifiers.typesnow requires therendererspackage at import time;Stateno longer provides runtime/endpoint/tool handle APIs;load_environmentraises if the module lacksload_environment; Python 3.10 is no longer supported.Changes since #1825 opened
packages/harnessespackage intoverifiers.v1.harnessesnamespace [71c81bc]packages/tasksetspackage intoverifiers.v1.tasksetsnamespace [71c81bc]verifiers.v1namespace [71c81bc]tasksetsandharnessespackages [71c81bc]verifierspackage with new extras [71c81bc]Macroscope summarized e57c064.