feat(v1): per-token advantages on the node - #2245
Merged
Merged
Conversation
Credit is token-aligned, so it belongs where the tokens are. Branches share nodes, and putting it on the node makes the one thing that would be wrong — the same generated token credited two ways in two branches — unrepresentable, while Branch.advantages falls out of the same path walk as token_ids and logprobs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A group whose rewards were all equal is assigned zeros and carries no gradient; a node that was never scored is a different thing. Nullable at the node and at the branch, so the two cannot be confused by anything reading either. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mikasenghaas
marked this pull request as ready for review
August 4, 2026 18:21
ApprovabilityVerdict: Approved 7eba3e6 Additive change: new optional You can customize Macroscope's approvability policy. Learn more. |
hallerite
approved these changes
Aug 4, 2026
eligotts
added a commit
that referenced
this pull request
Aug 5, 2026
…fload One conflict in graph.py: main's per-token advantages field (#2245) landed adjacent to this branch's finish_reason on MessageNode — union of both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eligotts
added a commit
that referenced
this pull request
Aug 5, 2026
…ages One conflict in graph.py: main's per-token advantages field (#2245) landed adjacent to this branch's finish_reason on MessageNode — union of both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Credit assigned by an RL algorithm is token-aligned, so it belongs where the tokens are.
MessageNode.advantages: list[float] | None— per-sampled-token, the same compact layout aslogprobs(length equals theTrueentries inmask).Branch.advantages: list[float] | None— the node values spread ontotoken_ids, 0.0 on non-sampled positions, exactly likeBranch.logprobs.spreadhelper rather than written twice.Nothing in verifiers writes the field — like
Trace.run, it is consumer-stamped.Why the node
Branches share nodes. A consumer holding a flat per-branch advantage stream can give the same generated token different credit in two branches of one trace — compaction and subagent forks both produce that shape. On the node it is unrepresentable: one node, one value, every branch through it agrees.
It also means a consumer stops re-implementing the path walk.
branch.advantagesfalls out of the same traversal astoken_ids,sampled_maskandlogprobs, so the alignment is structural rather than asserted by the caller.Why nullable
Unassigned credit and zero credit are different facts, and a trainer acts on them differently: a group whose rewards were all equal is assigned zeros and carries no gradient, while a rollout that was never scored ships no advantage stream at all. Storing zeros for both would erase that, so the field is
Noneuntil assigned — at the node, and at the branch when no node on the path holds any:A partially assigned path spreads, with the unassigned nodes reading 0.0.
Verification
uv run pytest tests/v1 -m "not e2e"green;ruff check/ruff format --check;tyclean on the touched files.🤖 Generated with Claude Code
Note
Low Risk
Additive schema and view-layer refactor with no writers in verifiers; behavior change is limited to new optional fields and deduplicated spread logic for logprobs.
Overview
Adds per-token RL credit on the message graph so trainers can align advantages with the same token layout as
logprobs.MessageNode.advantagesis optional per-sampled-token credit (compact layout matchingmask/logprobs).Nonemeans never scored; zeros mean explicit zero credit (e.g. tied rewards)—distinct semantics for downstream RL.Branch.advantagesspreads node values onto fulltoken_idslikeBranch.logprobs, and staysNonewhen no node on the path was assigned credit so it is not confused with an all-zero branch.Branch.spread()factors out the shared widening logic;logprobsnow delegates to it. Verifiers does not populate advantages—consumers stamp them after scoring.Reviewed by Cursor Bugbot for commit 7eba3e6. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add per-token
advantagesfield toMessageNodeandBranchadvantages: list[float] | Nonefield toMessageNodeto store per-sampled-token credit assignments.Branch.advantagesproperty intrace.pythat aligns node-level advantages to the full token sequence, returningNoneif no nodes have advantages set and0.0for unassigned positions otherwise.Branch.logprobsto delegate to a newBranch.spreadhelper, which generalizes widening any per-sampled-token node field to full-token alignment.Macroscope summarized 7eba3e6.