Skip to content

feat: speak the v1 episode wire (verifiers multi-agent restructure)#3088

Closed
hallerite wants to merge 8 commits into
mainfrom
feat/episode-wire
Closed

feat: speak the v1 episode wire (verifiers multi-agent restructure)#3088
hallerite wants to merge 8 commits into
mainfrom
feat/episode-wire

Conversation

@hallerite

@hallerite hallerite commented Jul 20, 2026

Copy link
Copy Markdown
Member

Companion to PrimeIntellect-ai/verifiers#1939 (multi-agent v1). Do not merge before #1939deps/verifiers is pinned to the PR-branch head for now and should be re-pinned to the merge commit once it lands.

What changed in verifiers

  • run_rollout now returns an Episode — the env-rollout atom: the task, a rollout-level errors list, and the run's traces (each stamped role/trainable) — instead of a single Trace. run_group survives only for the legacy (v0) bridge; a v1 server refuses it (sibling-dependent signals now score inside the env's own rollout).
  • EnvServerConfig composes instead of inherits: the environment is a nested env block (env.taskset, per-seat settings like env.agent.harness, run limits like env.timeout/env.max_output_tokens/env.max_concurrent), with pool and the legacy id/args/extra_env_kwargs beside it. verifiers refuses the old flat taskset/harness keys with pointer messages.

What this PR changes

Multi-agent episodes are trainable

  • Episode-shaped pipelineEnv.run_rollout yields one Rollout per trace, all stamped episode_id/episode_rollouts; the sinks finalize groups on complete episodes (singleton stamps keep single-agent and legacy counting bit-identical); the dispatcher's group bookkeeping runs in scheduling units so a fanned episode is one dispatch however many traces it returns. Train drops frozen (trainable=False) seats; episode-level errors fail every sibling (the episode is the failure atom, matching the env's own retries); eval keeps only the policy-played seats so a frozen judge can't poison env metrics.
  • Algorithms declare the env shape they acceptAlgorithm.multi_seat (False on every existing type). A multi-trace episode under a single-seat algorithm raises UntrainableEnvError, which fails the run: the mistake is categorical, so the dispatcher re-raises it instead of absorbing it into error markers (an all-refused stream would never fill a batch), and the main loop now surfaces any dead background component instead of polling an empty queue forever. A sole trainable seat beside frozen ones (the agentic-judge shape) is single-seat from the trainer's perspective and trains under plain grpo.
  • algo.type = "hierarchical_grpo" — GRPO for multi-seat envs: each role baselines against whoever attempted the same prompt. A role fanned within an episode (proposer-solver's n solver attempts at one minted problem) baselines within its episode; a once-per-episode role (the proposer) across the group's episodes. Degenerates to plain GRPO on single-agent envs. configs/debug/v1/proposer_solver.toml is the runnable smoke.
  • algo.type = "rae" — SPIRAL's Role-conditioned Advantage Estimation (arXiv:2506.24119): reward minus a per-role EMA baseline maintained across the run, the self-play estimator for zero-sum multi-seat games (a sibling-relative baseline would couple the opponents' credit). Second multi_seat algorithm — zero pipeline changes needed. Validated live on a self-play Kuhn-poker env (turn-per-run, standalone): 6 training steps, per-episode zero-sum payoffs on role-stamped decision traces.
  • Grouping semantics live in the algorithm over the wire stamps (role/episode_id), deliberately not deepening the pipeline's group machinery.

Wire adaptation

  • orchestrator/envs.py — unwraps episodes as above; an episode that failed before any trace completed becomes an error marker carrying the real task. run_group is the legacy-only route (the dispatcher already gates it on requires_group_scoring, which only the v0 bridge sets). Spawned servers receive the nested env block as their config.
  • configs/orchestrator.py — drops the local is_legacy/env_id overrides (vf's EnvServerConfig provides them) and retargets the legacy-kwargs resolution at self.env.…; flat run-limit keys on an env entry get pointer refusals; the deprecated [[orchestrator.env]] shorthand combined with explicit train.envs is a hard conflict.
  • Env entry lists renamed envenvs[[orchestrator.train.envs]] / [[orchestrator.eval.envs]]: each entry wraps one nested env block, so the list reads as what it is and sub-tables avoid an env.env stutter. The deprecated [[orchestrator.env]] shorthand still translates (now onto the renamed field). No alias for the old list name — every entry's inner layout changes in this PR anyway, so it's one migration, not two.
  • Config TOMLs (56 files)taskset/harness/timeout/token limits move under the nested env table; prime-rl keys (name, ratio, sampling, group_size, algo, …) stay top-level.
  • scripts/export_sft.py — handles both traces.jsonl shapes (episode-per-line eval runs AND flat trace-per-line trainer dumps / pre-episode runs); drops untrainable traces by default (--include-untrainable keeps them); malformed lines report file:line.
  • Docs/skillsdocs/configuration.md env-layout section, docs/algorithms.md + skills/configs/SKILL.md gain hierarchical_grpo and the multi-seat declaration rule, stale num_workers pointer in docs/inference.md.
  • deps/verifiers + uv.lock — submodule bump to the fix env server task cancellation #1939 head; relock picks up its new example env package.
  • tests/conftest.py — the zombie-cleanup pkill torchrun/VLLM sweep is CI-gated; it matched every such process on the machine, so any local pytest run killed live training runs beside it.

Behavior notes

  • Single-agent training and eval are unchanged: a single-agent episode wraps exactly one trace with singleton episode stamps, so every downstream reader sees the same Rollout flow as before.
  • batch_size counts traces (what the trainer consumes); group_size counts episodes per task (what the dispatcher schedules). On single-agent envs these coincide, as before.
  • Known v1 limits (documented, deliberate): the permit counter paces dispatches, not traces (bound server-side fan-out with env.max_concurrent); the difficulty filter and per-env reward metrics mix roles on multi-agent envs; hierarchical_grpo has no length penalty yet.
  • Free new knob: env.max_concurrent bounds a served worker's concurrent agent runs (an env's internal fan-out counts), riding the same nested config.

Verification

  • uv run pytest tests/unit -m "not gpu" — 508 passed (includes test_load_configs over every TOML, the unwrap-seam contract, the episode counting rule, and the hierarchical partition math).
  • ruff check / ruff format --check clean.
  • E2E: 20-step single-env RL debug run and a 12-step two-train/two-eval multi-env run, both green end-to-end; multi-agent training smoke (proposer_solver.toml: proposer-solver + hierarchical_grpo on 2 GPUs) runs episodes through dispatch → episode-counted groups → hierarchical advantages → trainer steps; the same env under plain grpo fails the run in seconds with the UntrainableEnvError pointer (verified live).

🤖 Generated with Claude Code


Note

High Risk
Touches core rollout dispatch, group/batch accounting, and all RL configs—mis-counting episodes or env migration mistakes would break training silently or at scale; behavior change is gated on verifiers #1939 merge.

Overview
Adapts prime-rl to verifiers v1 episodes (one env rollout → multiple role-stamped traces) and restructures how environments are configured in TOML.

Orchestrator pipelinerun_rollout expands each episode into one Rollout per trace with shared episode_id / episode_rollouts. Train/eval sinks and group finalization use complete episodes (not raw rollout count). Train drops trainable=False seats; eval on multi-seat episodes keeps policy-played traces only. Multi-trace training under a single-seat algorithm raises UntrainableEnvError (fails the run; dispatcher re-raises). Algorithms gain multi_seat; new types hierarchical_grpo (per-role GRPO baselines) and rae (per-role EMA baselines for self-play).

Configtrain.env / eval.env lists become train.envs / eval.envs. Each entry nests verifiers settings under env.* (env.taskset, env.agent.harness, run limits on env). Flat keys like timeout on the entry get validation pointers. ~56 TOMLs migrated; adds configs/debug/v1/proposer_solver.toml smoke.

Otherexport_sft.py sniffs episode vs flat trace lines and drops untrainable traces by default; docs/skills updated; deps/verifiers + proposer-solver-v1 in lockfile; CI-only pytest zombie cleanup.

Reviewed by Cursor Bugbot for commit 984ad90. Bugbot is set up for automated code reviews on this repo. Configure here.

hallerite and others added 8 commits July 20, 2026 10:19
Companion to PrimeIntellect-ai/verifiers#1939: run_rollout returns an
Episode (run_group is legacy-only), the environment nests under
EnvServerConfig.env (env.taskset / env.agent.harness / env.timeout), and
traces.jsonl records are episodes. Single-agent behavior is unchanged --
an episode wraps exactly one trace; multi-trace episodes are refused
(training on multi-seat episodes lands separately).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[[orchestrator.train.envs]] entries each wrap one nested env block, so
the list reads as what it is; sub-tables lose the env.env stutter
([orchestrator.train.envs.env.taskset]). The deprecated [[orchestrator.env]]
shorthand now lands on the renamed field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TrainEnv refuses multi-agent envs at START when the env class resolves
  orchestrator-side (mirrors verifiers' replay guard) — previously each
  dispatched rollout burned a full episode server-side before the per-episode
  refusal, and the dispatcher's error-marker conversion turned that into a
  batch that never fills. The per-episode guard stays for external servers.
  TrainEnv also refuses an untrainable sole trace (a frozen seat is not
  training data); eval accepts it.
- export_sft sniffs each line's shape: episode-per-line (v1 eval runs) and
  flat trace-per-line (the trainer's own rollout dumps, pre-episode eval
  runs) both work again; untrainable traces are dropped by default with a
  logged count (--include-untrainable keeps them); malformed lines report
  file:line instead of a raw pydantic wall.
- Config entries refuse flat run-limit keys (timeout, max_*_tokens,
  max_turns, max_concurrent, multiplex) with pointers to their env./pool.
  homes; [[orchestrator.env]] shorthand + explicit train.envs together is
  now a hard conflict instead of a silent discard behind an "auto-
  translating" warning.
- First direct unit coverage of the unwrap seam (episodes built in-test:
  1-trace, 0-trace marker, multi-trace refusal, untrainable train/eval
  split) + export_sft shape tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cleanup_zombies is autouse at module scope and pkills EVERY torchrun/vLLM
on the machine — running any pytest locally killed live training runs
beside it (two debug RL runs died this way today). Gate it on CI, which
the docstring already declared as its purpose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hierarchical GRPO

An env-rollout now flows through the pipeline as what it is: one episode,
one to many trainable traces. run_rollout yields one Rollout per trace,
stamped episode_id/episode_rollouts; the sinks finalize groups on COMPLETE
EPISODES (singleton stamps keep single-agent and legacy counting
bit-identical); the dispatcher's group bookkeeping moves to scheduling
units so a fanned episode can't pop its group while dispatches are still
owed. Train drops frozen (trainable=False) seats and restamps; episode-
level errors fail every sibling (the episode is the failure atom, matching
the env's own retries); eval keeps only the policy-played seats so a frozen
judge can't poison env metrics.

Which episodes an algorithm can credit is the algorithm's declaration:
Algorithm.multi_seat (False on every existing type), enforced at
TrainEnv.start when the env class resolves orchestrator-side and per
episode otherwise. New algo type hierarchical_grpo (multi_seat) baselines
each role against whoever attempted the same prompt — a role fanned within
an episode (n solver attempts at one minted problem) within its episode, a
once-per-episode role (the proposer) across the group's episodes; on a
single-agent env it degenerates to plain GRPO.

configs/debug/v1/proposer_solver.toml is the runnable smoke (proposer-
solver + hierarchical_grpo, both seats on the policy). Unit tests cover
the unwrap contract, the episode counting rule, and the partition math.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Picks up the serve-layer hang fixes, the recipe-env correctness gating, the
CLI ergonomics wave, and the proposer contract's LaTeX tolerance
(6d66eaa41..61d0dfe13 on the #1939 branch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ollout

The start-time class gate is gone (per Konstantin): it false-positived any
env whose extra seats are frozen — agentic judging is single-seat from the
trainer's perspective and now trains under plain grpo. The per-episode gate
is the one gate, and its mistake class is categorical, so it raises
UntrainableEnvError: the dispatcher re-raises it instead of absorbing it
into error markers (an all-refused stream never fills a batch), and the
main loop now surfaces any dead background component instead of polling an
empty queue forever — which also turns previously-silent dispatcher/watcher
crashes into loud run failures.

Verified live: proposer-solver under plain grpo dies in seconds with the
hierarchical_grpo pointer; under hierarchical_grpo it trains (prior smoke).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
advantage = reward minus a per-role EMA baseline maintained across the run:
the self-play estimator for zero-sum multi-seat games, where a sibling-
relative baseline (GRPO-style) would couple the opponents' credit. The
algorithm instance lives per-env, so the paper's game×role conditioning
reduces to role. Second multi_seat algorithm — the episode machinery now
carries two credit-assignment schemes with no pipeline changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hallerite
hallerite marked this pull request as ready for review July 20, 2026 16:01

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 984ad90. Configure here.


def batch_size_for(self, env_name: str) -> int:
"""``num_examples × group_size`` — total rollouts expected for one
"""``num_examples × group_size`` — total episodes expected for one

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eval progress counts traces not episodes

Medium Severity

Eval batch completion now uses complete_episodes, but batch_progress still reports len(pending_batches) against an episode-based batch_size_for. Multi-seat evals can show 100% progress while the epoch is only half finished, because each episode contributes several traces.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 984ad90. Configure here.

@mikasenghaas

Copy link
Copy Markdown
Member

Closing — superseded by the multi-agent v1 API that landed in verifiers (PrimeIntellect-ai/verifiers#1939); the prime-rl adoption will be redone against main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants