feat(orchestrator)!: align multi-agent types - #3183
Closed
mikasenghaas wants to merge 65 commits into
Closed
Conversation
Adapt to verifiers#2187 (Episode derived aggregates): episode.last_error
rename, submodule bump. Mirror the episode/trace hierarchy in the wandb
layout: count metrics read at the episode level (per-episode sums), and
a new {scope}/{subset}/<agent>/<metric>/<stat> level reports trace-level
metrics per agent, averaging in-episode fan-outs first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s -> TraceMetrics Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation span renamed agent
Env level now carries only episode-level facts (count sums, pipeline
rates, eval scores); reward, truncation, errors, stop conditions, timing,
custom metrics and solve rates move under {scope}/{subset}/<agent>/.
Adapts to the vf span rename (Timing.agent, AgentSpan, split_agent_time)
and re-pins deps/verifiers. Overview reward/error/truncation panels match
the per-agent keys by regex.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…udge debug config Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
verifiers f646beb37 -> f6e420b99 (0.2.2.dev66, 17 commits) and
research-environments 6a2dee6fc -> ccb375ce5 (main + the scicode-v1
pin relax from research-environments#740).
Adaptations to the verifiers changes:
- Episode.error is now Episode.errors + a last_error property (vf#2187)
- verifiers.v1.loaders / verifiers.v1.push moved into
verifiers.v1.utils.{loaders,platform} (vf#2204)
- verifiers floors bumped to 0.2.2.dev66 (root and prime-rl-configs)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ccb375ce5 -> 9a57c713c: research-environments#740 refreshed on current main, picking up the ViRL39K v1 taskset (RE#689) and the taskset prompt config fixes (RE#742). verifiers main is unchanged since f6e420b99. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f6e420b99 -> 576506d66 (7 commits). vf#2218 builds one client per rollout with a process-wide elastic renderer pool: TrainClientConfig drops pool_size for a multiplex knob, and the config module moved from clients.config to configs.client. The orchestrator config follows suit (pool_size -> multiplex, None = client default). Floors to dev73. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rchestrator.multiplex deps/research-environments -> 21a284618 (research-environments#740 on main). The orchestrator-level renderer knob (pool_size, briefly multiplex) is removed instead of renamed: clients resolve inside env- server workers, whose concurrency is already bounded per env by serve.pool.multiplex (default 128, below the client-side renderer default of 256), so each worker warms one renderer and the elastic pool grows on demand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etrics Takes #3172's newer verifiers pin (576506d66, contains the vf#2187 merge). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopts main's timing kwarg name in the metrics test fixture; the timing assertions stay on the agent subtree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
is_trainable / is_filtered / filters and avg@k / pass@k all score a single trace, so they read per agent like every other trace-level metric instead of pooling an episode's seats. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
is_truncated / is_completed rode the per-episode mean path with the distributions, so an uneven fan-out — what the effective subset leaves whenever a sibling errors — reweighted them away from the plain fraction their siblings and the docs promise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One aggregation per level: the episode level sums an episode's traces, the agent level takes each trace as a sample. Reward, avg@k and the per-agent counts had been collapsing each episode's fan-out to a mean first, which left them the odd ones out among the seat's own metrics and made avg@k's k — a trace count — disagree with what it averaged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keeps #3182's unscored-as-zero coverage, rekeyed onto the agent subtree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A dispatched episode that never produced a trace used to be reported as a stand-in Rollout carrying a fake error. That trace had no real agent, so it defaulted to the 'agent' seat and its failure landed under a phantom subtree in a multi-agent env, inflating that seat's error rate while the real seats looked clean. EpisodeResult now carries an episode's scheduling facts plus either its traces or an EpisodeFailure, so the outcome is representable without inventing a rollout, and failures are counted at the episode level where they belong. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng it envs.py used to read .traces/.id/.ok/.last_error off the episode and throw the envelope away, so metrics.py regrouped the flattened traces on episode_id and hand-rolled by_agent plus the token/turn sums that vf.Episode already exposes. The episode now rides through: Env.run returns it with its traces re-typed, EpisodeResult carries it, the sinks bucket episodes rather than loose rollouts, and the metric containers narrow an episode to its surviving traces for a subset view. The count metrics and by_agent are now reads off vf.Episode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
main carries #3165 as a squash; this branch was cut from its pre-squash head, so the conflicts are the same content seen twice and resolve to ours (which already has it, plus this PR's work). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… traceless episodes EmptyEpisode was never a failure mode of its own — vf's run_episode already raises when run() mints no trace, records it on episode.errors and returns the episode — and the code that reported it threw that real error away for a generic string. So there is one shape for every failure that has no trace to ride: an episode with no traces and the reason on errors. EpisodeFailure and EpisodeResult both go; prl's Episode subclasses vf's and adds only the six scheduling facts prime-rl actually contributes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A sampled node shared by several branches is trainable only in the first one; the branch view still spreads its credit everywhere it appears, so zero the positions the sample does not train on — the layout the trainer has always been given. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The train/eval split was a type distinction the producer already knows: the dispatcher is what decides which path an episode is on. It now writes that into vf's own RunInfo when the episode lands, so TrainEpisode, EvalEpisode and the KIND ClassVar all collapse into one Episode and the main loop reads run.type instead of asking isinstance. An eval episode's step rides in run.step; a train one's is filled by the loop that knows which batch window is collecting. Also re-pins deps/verifiers to vf#2245 as merged on main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TraceMetrics took a loose trace list, so a trace had to carry the example it answered for solve_rates and pass@k to bucket by. It now takes the episodes narrowed to one agent (Episode.narrow, keyed by the agent names vf.Episode.by_agent reports), which is where group_id already lives — so the field comes off the trace entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gibberish and repetition were filters with an enforce flag, which meant their monitoring numbers were censored: apply_filters stopped at the first hit, so a rollout flagged as gibberish was never measured for repetition. They are now detectors — pure per-trace measurements that all run on every trace and report per agent. Zero-advantage stops being a plugin. It is not a property of the generation (it is only knowable after the group scores, and it is what is_trainable already means), so it becomes the drop policy's built-in default. Rollouts that were never scored stay: opd/opsd assign no credit and train through reference KL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything the orchestrator adds to a dispatch now has a place on vf's own episode: the env it ran (env.name), the group it was planned in (group), and the run it belongs to — which on the training path carries the policy version and how stale it got, and tells train from online eval by kind. Episode is an alias for vf.WireEpisode, and what were methods are free functions over it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
log_samples/log_eval_samples took loose traces, which is why a trace had to carry the episode it came from and the env it ran in. They take episodes now: the platform monitor reads the episode id off the episode, and the wandb table gains agent and branch_idx columns so a multi-agent episode reads as its seats, one branch per row. Rollout is an alias for vf.Trace. TrainRollout is what remains — the one place prime-rl extends a verifiers type, for trainer-bound state that has nowhere else to live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Online eval moved onto the training run, but the sink still asserted on EvalRunInfo. Nothing unit-tested the sink, so only a live run caught it — covered now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sors Five reads still went at the fields the subclass used to have, which a pydantic model answers with AttributeError at runtime — in the sinks and the ship path, none of which the unit suite covers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dispatcher counted weight updates per in-flight episode, then the main loop threw that away and recomputed staleness at ship — two mechanisms writing one field. The dispatcher now records the span generation covered and every reading derives from it: the cancel check subtracts against the live version, the in-flight gauges do the same, and off_policy_steps is a property of the run. Eval is measured the same way rather than pinned to 0, and a frozen sampler records no span at all, so its staleness reads None instead of a zero indistinguishable from fresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only prime-rl writes episodes carrying training tensors, so only it needs the exclusion; vf's own writer dumps the episode whole. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Whether a trace is gibberish or stuck in a loop is a fact about it, not something a run opts into computing: measuring it behind config made the rate depend on what was configured. Both run on every trace with fixed thresholds, and the only choice left is whether to act on one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three lost imports and one stale reference in the prime monitor, which would have raised on the first sample upload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rollout aliased the bare generic while Episode aliased the wire form, and TrainRollout resolved its agent config to the strict default — so the traces prime-rl declared were a different specialization from the ones WireEpisode carries. model_construct skipped validation, so nothing ever said so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e.info Verifiers has no notion of a group, so carrying one there was a field it held for a single consumer. It rides in info, which is where an episode takes a consumer's metadata, and it still lands on the saved record so a row stays placeable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The detector/drop-policy split is a change of its own and does not need to ride along with the episode work — it goes in a follow-up. filters.py, its config, its tests and its docs are back to main; TrainRollout keeps is_filtered and filter_results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Member
Author
|
Closing in favour of a minimal rewrite — this branch accumulated several changes (filters, the trace-type collapse, group-in-info, accessor helpers) that belong in their own PRs. Superseded by a fresh PR scoped to the episode work alone. |
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.
Summary
Follow-up to #3165. The orchestrator passes episodes end to end — the env's own
vf.Episode,extended only with what prime-rl genuinely adds — instead of flattening them into loose traces at
the boundary and rebuilding the grouping downstream. Three changes, each one falling out of the last.
1. An episode that produced nothing needs no stand-in trace
A cancellation, or a task that raised before reaching the env, was reported as a fabricated
Rolloutcarrying a fake error. That trace has no real agent, so it tookAgentInfo's defaultname, and once #3165 keyed metrics by agent those failures landed under a phantom
agentsubtree —in a proposer-solver env, cancellations inflated a seat nobody ran. (Reported by bugbot on #3165.)
verifiers already has the shape:
run_episoderecords the reason onepisode.errorsand returnsthe episode with
okfalse. Prime-rl's own outcomes are minted the same way, so one vocabularycovers every cause.
envs.pystops unwrapping the episode,metrics.pystops rebuilding it, andvf's own
by_agent/num_turns/num_*_tokensreplace the hand-rolled versions.traces.jsonlnow stores one episode per line — what verifiers writes and what itsread_episodesexpects; prime-rl was writing the legacy bare-trace form. A traceless episode gets arow too, with the reason on
errors.2. prime-rl stops extending the episode and the trace
Everything the orchestrator adds to a dispatch now has a place on verifiers' own types
(PrimeIntellect-ai/verifiers#2252): the env it ran (
env.name) and the run it belongs to (run—an id, plus a
metadatasaying whether the run trains on the episode or measures itself with it,and carrying the step and policy span that go with that). The comparison group is prime-rl's alone —
verifiers has no notion of one — so it rides in
episode.info, read through one accessor.TrainEpisode,EvalEpisode,Episode.KINDand theRolloutbase are gone; what were methods arefree functions over the vf types.
TrainRolloutis the one extension left, because trainer-boundstate has nowhere else to live —
samplesin particular cannot go intrace.info, whichserializes.
The main loop routes on
run.metadataand fills in a train episode's step, the one fact thedispatcher can't know: it is the batch window collecting when the episode lands, not when it was
dispatched. An eval's is known at dispatch, so
EvalMetadata.stepis required.Staleness is derived, not stored. It used to be written twice — the dispatcher counted weight
updates per in-flight episode, then the main loop discarded that and recomputed
(step - 1) - policy_versionat ship. The dispatcher now records the policy span generationcovered, and every reading derives from it: the cancel check subtracts against the live version, the
in-flight gauges do the same, and
off_policy_stepsis a property of each metadata: versions behindwhat is training for an episode trained on, and what drifted under it for an eval, which is the only
sense in which an eval can be off-policy. A frozen sampler records no span at all, so its staleness
reads
Noneinstead of a zero that looks fresh.InflightRolloutbecomesInflightEpisode, andstamp()is the single place a dispatch's factsbecome an episode's.
3. Algorithms score episodes; credit lives on the graph's nodes
score_group/finalize_grouptake the group'sEpisodes, so an algorithm can compare withinan episode as well as across them —
hierarchical_grpokeys its solver baselines offepisode.idinstead of a foreign key copied onto every trace.
group_rollouts(group)flattens for GRPO / MaxRL /RAE, which only compare across.
Per-agent metrics are built from episodes too (
narrow, keyed byvf.Episode.by_agent), sosolve_ratesandpass_at_kbucket on the episode's group — andRollout.group_idis gone. Themetrics stay flat over traces; the episode is only what they are built from.
Credit moves onto
MessageNode(PrimeIntellect-ai/verifiers#2245, merged and pinned here).TrainRollout.advantagesbecomes a derived read,assign_advantages(value)writes each node'strainable tokens, and
stamp_advantagescopiesbranch.advantagesonto the sample built from thatbranch rather than slicing one flat stream back apart by offset. Unassigned stays distinct from
assigned-zero all the way to the trainer.
Breaking
Types
EpisodeandRolloutare aliases forvf.WireEpisode/vf.Trace.TrainEpisode,EvalEpisodeandEpisode.KINDare gone; read the path offrun.metadata.type, the step offrun.metadata.step, the env offenv.name, the group offinfo["group_id"], staleness offrun.metadata.off_policy_steps.Episodemethods are free functions inorchestrator.types:narrow,rollouts_of,group_id_of,env_name_of,run_of.Rolloutno longer carrieskind,policy_version,off_policy_steps,eval_step,group_idor
episode_id.env_name,samples,is_filteredandfilter_resultsare onTrainRollout.Monitor.log_samples/log_eval_samplestake episodes, not traces. The wandb sample table gainsagentandbranch_idxcolumns, one branch per row.TrainRollout.advantagesis read-only (derived from the nodes) andassign_advantagestakes ascalar only — the full-length per-token list is gone. No shipped algorithm used it.
InflightRollout→InflightEpisode(rollout_count→episodes_owed);stamp()takes arun_id, andRolloutDispatchera matching ctor argument.Algorithms
Algorithm.score_group/finalize_grouptakelist[Episode], notlist[TrainRollout]. A customalgorithm comparing across the whole cohort wraps its body in
group_rollouts(group).TraceMetrics(...)andpass_at_k(...)take episodes.Metrics / on-disk
traces.jsonlrows are episodes, not traces. verifiers'read_episodesreads both.{scope}/{subset}/<agent>/has_error/meanno longer counts cancellations or task failures — anepisode nobody ran belongs to no seat. Those report under
dispatcher/{cancelled,errored}/{train,eval}.Verification
uv run pytest tests/unit— 481 passed. Two exclusions on this box, both pre-existing andunrelated:
tests/unit/train/models(GPU crash) andtest_qwen3_vl_e2e(stale fixture, fix: token_id-formatted logprob tokens in the qwen3-vl fake engine #3161).credit — 12 steps, reward 0.17 → 0.75,
Trainable 128/128throughout, so node-assignedcredit reaches the trainer.
episode collapse — train+eval, evals at steps 2/4/6;
traces.jsonlrows carryrun={'type': 'train', 'step': <batch window>}/{'type': 'eval', 'step': <eval epoch>}withthe dispatch facts in
info.metrics rework —
solved_*,avg@kand the per-agent subtree land unchanged on bothsubsets.
no subclasses — a 6-step run with eval. Both kinds of episode carry the same run id and are
told apart by their metadata, and everything prime-rl used to keep on a subclass is on the
record:
Two latent bugs surfaced on the way and are fixed here: both sinks read a group's env and step off
group[0]— a trace — whichIndexErrors when a whole group is cancelled and produces none. Notest covered that path.
Follow-ups
deps/verifiersis pinned to that branch and needs are-pin to
mainonce it merges.mainas of feat!: standalone env servers #3162 (standalone env servers), verified with a live run.TrainRolloutis the remaining extension. Retiring it means movingsamples/is_filtered/filter_resultsinto a sink-owned table keyed by trace id, which metrics would then have tocarry — a subclass traded for threading, so I would keep it.
trace) from the drop policy (a decision) is a change of its own — it fixes
apply_filtersstoppingat the first hit, which makes a monitoring rate depend on filter order — and it goes in a
follow-up rather than riding along with the episode work.
AttributeErrors during this work reached a live run through a green suite, each one a read of afield the subclass used to have. Scripted sweeps now check every episode and run attribute read
against the vf surface, but that is a stopgap for real tests.
dropped/all/<name>/rateis only emitted in steps where that reason fired, so the per-reasonseries is sparse. Pre-existing under the old key.
🤖 Generated with Claude Code