Skip to content

feat: delta-native message-graph trajectory - #1606

Merged
mikasenghaas merged 8 commits into
feat/nano-as-v1from
feat/trace-message-graph
Jun 10, 2026
Merged

feat: delta-native message-graph trajectory#1606
mikasenghaas merged 8 commits into
feat/nano-as-v1from
feat/trace-message-graph

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Jun 10, 2026

Copy link
Copy Markdown
Member

Summary

Move the v1 trajectory from a flat trajectory: list[Turn] — where every turn restated the whole prompt (and its tokens), so size was quadratic in turns — to a delta-native message graph. Trace.nodes (one MessageNode per distinct message, each linked to its predecessor) is the only stored model; branches and the conversation accessors are computed views over it. Each message is stored once, so in-memory / on-disk / wire size is linear; branching falls out of walking the graph; a training sample is a cheap concat along a branch path.

Changes

  • verifiers/v1/graph.py (new)MessageNode (parent, message, token_ids, mask, logprobs, finish_reason); message_hash (message equality); add_turn (reuse prefix nodes by (parent, hash), attribute each new message's tokens from the renderer's per-message spans, append the assistant node = generation-prompt scaffold + sampled completion); branches_from_nodes / leaves (the per-branch token views live on Branch).
  • trace.pyTrace.nodes is the ground truth. Turn and Trace.trajectory are removed. Branch holds nodes and is self-describing: messages + the aligned token views token_ids/sampled_mask/logprobs, plus num_turns/prompt_len/completion_len/total_tokens; assistant_messages/tool_messages/has_response/is_truncated read the graph directly.
  • interception/server.py, legacy.py build via graph.add_turn; clients/renderer.py threads the per-message token spans (message_token_spans()) it previously discarded.
  • branching.py deleted — the graph supersedes it (message_hash is the message-equality).
  • Token attribution is exact: concat(node.token_ids along a path) == prompt_ids + completion_ids the model saw (the previous assistant's closing scaffold anchors to the cumulative stored prefix, not message spans).

Data structure

A trace is a flat list of MessageNodes — one per distinct message — each pointing at its predecessor (parent, an index into nodes). The conversation is a root→leaf path; branches (compaction / subagents / resampling) are simply multiple leaves, so branching falls out of walking the graph. Each node stores only the tokens it adds (token_ids), so concatenating a path reproduces the full sequence — storage is linear, not quadratic.

nodes (flat list; parent = index into nodes)        branches = leaf→root paths
────────────────────────────────────────────        ──────────────────────────
 [0] system     parent=–  (root)                      0─1─2─3─4   → one branch
 [1] user       parent=0                              (S)(U)(A0)(U1)(A1)
 [2] assistant  parent=1   ◄ token_ids + mask + logprobs (sampled)
 [3] user       parent=2                              a resample/compaction forks it:
 [4] assistant  parent=3                                0─1─2─3─4      (leaf 4)
                                                              └─5─6    (leaf 6)  → two branches

On disk (results.jsonl — one trace per line)

{
  "id": "", "task": {  },
  "nodes": [
    { "message": {"role":"system","content":"You sort."},          // root: "parent" omitted (= None)
      "token_ids":[1,2,3], "mask":[false,false,false], "logprobs":[] },
    { "parent":0, "message": {"role":"user","content":"cba?"},
      "token_ids":[4,5], "mask":[false,false], "logprobs":[] },
    { "parent":1, "message": {"role":"assistant","content":"abc"},  // a turn (assistant node)
      "token_ids":[9,8], "mask":[true,true], "logprobs":[-0.2,-0.2], "finish_reason":"stop" }
  ],
  "rewards": {  }, "reward": , "metrics": {  },
  "is_completed": true, "stop_condition": "", "is_truncated": false,
  "timing": {  }, "errors": []
}
  • parent — index of the predecessor node; omitted for a root (None).
  • token_ids — this message's delta contribution (leading template scaffold + its own tokens; for an assistant, the generation-prompt scaffold + the sampled completion).
  • mask[i]True ⇒ a model-sampled (trainable) token; False ⇒ scaffold / input. Only assistant nodes have Trues.
  • logprobs — sampling logprobs, aligned to the True entries.
  • finish_reason — assistant nodes only (truncation detection).
  • branches / num_turns / assistant_messages / … are not stored — they're computed views over nodes.

Loading + building a training sample

import json, verifiers.v1 as vf

trace = vf.Trace[vf.Task].model_validate(json.loads(line))   # from results.jsonl or the wire

# branches — one training sample per branch; a view over the graph (no stored copies)
for b in trace.branches:
    conversation = b.messages          # list[Message] for this root→leaf path

# each branch is first-class and carries its own flat training sequence:
for branch in trace.branches:
    ids, mask, logprobs = branch.token_ids, branch.sampled_mask, branch.logprobs
    first = mask.index(True)                       # prompt | completion split = first sampled token
    sample = dict(
        prompt_ids          = ids[:first],
        completion_ids      = ids[first:],
        completion_mask     = mask[first:],        # True where the model generated (trainable)
        completion_logprobs = logprobs[first:],
    )

This is exactly what prime-rl's trace_to_samples does — one TrainingSample per branch.

Verification

  • Size (linear vs quadratic) — same trace, new (graph) results.jsonl vs the old per-turn-full-prompt + expanded-branches layout:
    • real terminal-bench-2 (bash agent), same trace both ways:

      turns new (graph) old (trajectory) reduction
      8 47 KB 0.40 MB 8.7×
      16 74 KB 1.35 MB 18.7×
      32 236 KB 7.70 MB 33.5×
      64 ~0.5 MB ~31 MB ~67× (projected)
      128 ~0.9 MB ~123 MB ~134× (projected)
      256 ~1.8 MB ~493 MB ~268× (projected)

      8/16/32 are measured (same trace, both layouts); 64/128/256 are projected (old ∝ turns², new ∝ turns). The reduction tracks turn count — at 256 turns the old layout would be ~0.5 GB/trace vs ~1.8 MB.

    • synthetic 5→40 turns: 2.6× → 17.6×.

  • Training samples correct end-to-end (with prime-rl #2763): RL on alphabet-sort trains cleanly through graph → trace_to_samples → trainer at 0% error on both v1 (native renderer path, 3.2 turns, 432/512 trainable) and v0 (legacy bridge, 2 turns, 368/512).
  • Existing trace / serve / env tests green; ruff clean.

Notes

  • Renderer-only training: per-message attribution comes from the renderer client; the chat/MITO client carries no spans → eval-only (non-trainable). Paired with prime-rl #2763 (graph-walk trace_to_samples).
  • Multimodal is left out (not in this base, feat/nano-as-v1); MessageNode is trivially mm-extendable when mm lands.

Note

High Risk
Breaking change to v1 trace shape and serialization (trajectory/Turn removed); training and downstream consumers must read nodes/Branch instead of per-turn lists.

Overview
Replaces the v1 trace’s flat trajectory: list[Turn] (full prompt restated every turn → quadratic storage) with a delta-native message graph: Trace.nodes is the only persisted model; Turn and branching.py are removed.

verifiers/v1/graph.py adds MessageNode (parent link, per-message token_ids / mask / logprobs), add_turn (prefix dedup via message_hash, renderer span attribution), and graph walks for branches. Branch now holds nodes and exposes training-ready token_ids, sampled_mask, and logprobs by concatenating the path. The interception server and legacy v0 bridge record turns via graph.add_turn; the renderer client threads message_spans on TurnTokens (transient, exclude=True on persist).

Docs and the eval dashboard are updated for graph-based branching and token display (b.prompt_len / b.completion_len).

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

Note

Replace flat trajectory with a delta-native message graph in trace representation

  • Introduces MessageNode in graph.py as the atomic unit of the trace, storing token_ids, mask, logprobs, and finish_reason per message.
  • Replaces Trace.trajectory: list[Turn] with Trace.nodes: list[MessageNode]; branches, turn counts, and token aggregations are now derived from root→leaf paths through this graph.
  • graph.add_turn reuses shared prompt prefixes via a hash-keyed head index, avoiding duplication of repeated messages across turns.
  • Updates interception/server.py and legacy.py to write into the message graph rather than appending Turn objects.
  • Risk: Turn and Trace.trajectory are removed; any code referencing these will break.

Macroscope summarized dae561f.

mikasenghaas and others added 7 commits June 10, 2026 18:54
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>
…trings

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.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>
…-message-graph

# Conflicts:
#	verifiers/v1/interception/server.py
- 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>
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>
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>
@mikasenghaas
mikasenghaas marked this pull request as ready for review June 10, 2026 21:16
@mikasenghaas
mikasenghaas merged commit 17f7e02 into feat/nano-as-v1 Jun 10, 2026
4 checks passed
@macroscopeapp

macroscopeapp Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR replaces core data structures (flat trajectory list → message graph) and changes how traces are stored, branches are computed, and training samples are built. Major refactors that restructure shared abstractions warrant human review.

You can customize Macroscope's approvability policy. Learn more.

mikasenghaas added a commit that referenced this pull request Jun 10, 2026
…-> nodes)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mikasenghaas added a commit that referenced this pull request Jun 10, 2026
* 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>
mikasenghaas added a commit that referenced this pull request Jun 11, 2026
The --rich dashboard's per-rollout "X/Y tokens" was derived from the branch's
token-id lengths, which are 0 for endpoints that don't return token ids (e.g.
plain OpenAI completions) — so eval rows showed "0/0 tokens".

Carry the response's usage onto the assistant node (transient, excluded from
wire/disk, like the multimodal sidecar) and fall back to it when token ids are
absent: input = final turn's usage.prompt_tokens, output = sum of
usage.completion_tokens across turns. The renderer path (token ids present) is
unchanged. Regression from #1606, which switched _tokens off response.usage.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pull Bot pushed a commit to Stars1233/verifiers that referenced this pull request Jun 23, 2026
* 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>
pull Bot pushed a commit to Stars1233/verifiers that referenced this pull request Jun 23, 2026
* 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 PrimeIntellect-ai#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>
pull Bot pushed a commit to Stars1233/verifiers that referenced this pull request Jun 23, 2026
…eIntellect-ai#1627)

The --rich dashboard's per-rollout "X/Y tokens" was derived from the branch's
token-id lengths, which are 0 for endpoints that don't return token ids (e.g.
plain OpenAI completions) — so eval rows showed "0/0 tokens".

Carry the response's usage onto the assistant node (transient, excluded from
wire/disk, like the multimodal sidecar) and fall back to it when token ids are
absent: input = final turn's usage.prompt_tokens, output = sum of
usage.completion_tokens across turns. The renderer path (token ids present) is
unchanged. Regression from PrimeIntellect-ai#1606, which switched _tokens off response.usage.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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