Skip to content

feat(v1): training run metadata on episode - #2252

Draft
mikasenghaas wants to merge 17 commits into
mainfrom
feat/env-name-and-group-info
Draft

feat(v1): training run metadata on episode#2252
mikasenghaas wants to merge 17 commits into
mainfrom
feat/env-name-and-group-info

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Enough on the episode that a consumer stops keeping its own copy of where it came from and what it
is to the run. Everything is optional or defaulted, so existing records and readers are unaffected.

class EnvInfo:    id: str = ""             # what the env IS      — reverse-text-v1
                  name: str | None = None  # what a run CALLS it  — reverse-text

class GroupInfo:  id, size                 # the episodes planned together from one task

class Episode:    env, group, run

A group is what a consumer compares against each other — pass@k over it, a GRPO baseline within it.
utils/platform.py reconstructs that ad hoc today. The id is per group, not per task: planning
the same task again is a new group, so two rounds never merge into one comparison.

The run, and what an episode is to it

A training run's evals are its own — same id, same policy as the rollouts they are measured against
— so one run record, with the per-episode part nested under it:

class Metadata:        policy: PolicySpan | None       # the versions generation spanned
class TrainMetadata:   step: int | None                # the batch window it landed in
class EvalMetadata:    step: int                       # the epoch it was dispatched for

class TrainRunInfo:    id, metadata: TrainMetadata | EvalMetadata
class EvalRunInfo:     id                              # a standalone eval

Splitting the metadata is what lets each say something true. step means two different things and
only the eval's is knowable upfront, so under a single kind flag it had to be optional for both.
And off_policy_steps differs the same way:

PolicySpan.drift              -> end - start                  # what moved mid-generation
TrainMetadata.off_policy_steps -> (step - 1) - policy.start   # behind what is training
EvalMetadata.off_policy_steps  -> policy.drift                # what moved under it

Nothing trains on an eval, so there is no policy for it to be behind; what makes it off-policy is
the policy moving under it. Its step cannot say that — fixed at dispatch, it reports an eval that
outlived three updates as on-policy, and one that measured a single version cleanly as one behind.

Both readings derive from the span, so nothing stored can contradict it. policy is None when the
episode did not come from the live policy at all (a frozen sampler), so staleness reads None
rather than a 0 that looks fresh.

The run types also move from trace.py to episode.py — they were written when the run was a field
on Trace, and #2244 moved the field without them.

Verification

  • uv run pytest tests — full suite passes; round-trips through
    WireEpisode.model_validate(episode.model_dump(mode="json")).

  • The eval cases that motivated the split:

    before now
    dispatched at step 6 under v5, ran to v8 0 3
    measured one version cleanly 1 0
  • Live in prime-rl (feat(orchestrator)!: align multi-agent types prime-rl#3183), 6-step runs with eval throughout.

Consumer

prime-rl carried all of this on Episode and Trace subclasses and now extends neither. Its two
staleness mechanisms — an in-flight counter, then a recompute at ship — collapse into the derived
property.

Not included

An earlier revision had Env.slots mint a group so -r k was a group of k. Dropped: nothing here
reads a group, and it broke resume — slots() sized the group by what it was asked to plan, but a
resume asks only for what it still owes, so one -r k split in two. Worth doing when something here
consumes a group, with the resume threading it needs.

🤖 Generated with Claude Code

EnvInfo.name is what the caller knows the env by when that differs from
its id — the key it was configured under, which a run over several envs
keys its metrics by.

GroupInfo is the cohort: the episodes planned together from one task,
which a consumer compares against each other. Env.slots mints one per
call, so -r k is a group of k, and replanning the same task later is a
new group rather than a merge into the old one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread verifiers/v1/env.py Outdated
mikasenghaas and others added 3 commits August 4, 2026 20:51
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
slots() minted a group from the count it was asked to plan, but a resume
asks only for what it still owes — so its replacements got a fresh id and
a smaller size while the kept episodes stayed in the original group, and
one -r k split into two partial groups. Group-keyed pass@k read those
halves as whole groups.

slots() now accepts a group to join, and the eval runner recovers each
task's group from its kept episodes (resume.groups_by_key).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
group joins run as a consumer-stamped field and nothing in verifiers sets
it. Minting one in slots() was speculative — no verifiers code reads it,
pass@k there groups by task.data.idx — and it dragged in a resume bug:
slots() sized the group by what it was asked to plan, but a resume asks
only for what it owes, so one -r k split into two partial groups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikasenghaas mikasenghaas changed the title feat(v1): name an env, and the cohort an episode was planned in feat(v1): optional env name and group on the episode Aug 4, 2026
mikasenghaas and others added 5 commits August 4, 2026 21:20
…to_record

policy_version and off_policy_steps are the training path's own facts —
which policy generated an episode, and how far behind the step that
trains on it — so they sit on TrainRunInfo beside its id and step.

to_record is the episode form of Trace.to_record: the same tensor
exclusions applied through traces, which is the unit traces.jsonl stores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A training run's evals are its own: same run id, same step, same policy
as the rollouts it trains on. They were an EvalRunInfo, which made those
shared facts optional on both records and left a standalone eval carrying
a step and a policy version it has none of.

TrainRunInfo now covers both through kind, and EvalRunInfo is what it
should always have been: a model measured once, against nothing that is
training.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Staleness was a number a consumer had to compute and store, and it only
ever described one end of the episode. The run now records the policy
versions generation spanned — TimeSpan's shape over updates instead of
seconds — and both readings fall out: drift is what changed mid-episode,
off_policy_steps how far behind training the generating policy was.

Neither is stored, so neither can disagree with the span. A frozen
sampler follows no version, so its policy is None and staleness with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikasenghaas mikasenghaas changed the title feat(v1): optional env name and group on the episode feat(v1): env name, group, and a run that derives its own staleness Aug 4, 2026
mikasenghaas and others added 3 commits August 4, 2026 22:12
to_record had no caller here — write_episode dumps the episode itself, and
the eval path has no tensors to exclude. It was added for a consumer that
can hold it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Queue time is only in it on the training path, where step is the window
the episode landed in. An eval is placed by the epoch it was dispatched
for, so its lag is the one it had when it started.

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

kind was a field, and step meant one thing under it and another under
the other value: the window an episode landed in, or the epoch an eval
was dispatched for. Two meanings, one field, and only one of them could
be required.

They become TrainMetadata and EvalMetadata, nested under the run because
that is the relationship — one training run, one id, two things an
episode can be to it. Each states its own step, and an eval's is
required, since it is known the moment the eval is dispatched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread verifiers/v1/trace.py Outdated
mikasenghaas and others added 4 commits August 4, 2026 22:56
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An eval's step is fixed when it is dispatched, so measuring staleness
against it undercounts: an eval that outlives three updates still looked
on-policy, while one that measured a single version cleanly reported the
gap between the step index and that version.

Nothing trains on an eval, so there is no policy for it to be behind.
What makes it off-policy is the policy moving under it. Each metadata now
owns that definition, which is what splitting them was for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They were written when the run was a field on Trace. #2244 moved the
field to Episode; the types stayed behind, and Trace has not referred to
one since.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikasenghaas mikasenghaas changed the title feat(v1): env name, group, and a run that derives its own staleness feat(v1): env name, group, and what an episode is to its run Aug 4, 2026
@mikasenghaas mikasenghaas changed the title feat(v1): env name, group, and what an episode is to its run feat(v1): training run metadata Aug 5, 2026
It sat beside the run id, where it was the one per-episode fact among
shared ones — the same mixing that nesting the metadata was meant to
undo. Moving it completes that: a run is an id, and everything true of a
single episode is its metadata.

off_policy_steps follows it, so each kind owns both the definition and
the data it reads, and it is a property rather than a method the run has
to hand the span to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikasenghaas mikasenghaas changed the title feat(v1): training run metadata feat(v1): training run metadata on episode Aug 5, 2026
@mikasenghaas
mikasenghaas requested a review from hallerite August 5, 2026 00:51
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.

1 participant