Repository navigation
Description
Summary
NeMo Gym today standardizes rollout collection on an extended OpenAI Responses API contract (responses_create_params + response + reward). A separate model-server traces capture feature (#1715) records per-LLM-call observability in wire-native dialects. These answer different questions, live at different layers, and are not automatically unified.
For blackbox agents (Claude Code, Codex, etc.) the gap is acute: the only reliable signal is intercepted model calls, yet training and eval still expect a coherent rollout/trajectory artifact joined with reward and (optionally) environment/tool context. The intended architecture (capture store + trajectory builder + verify) is described in internal design discussions and preview docs, but the pieces are at different maturity levels and the contract boundaries are easy to conflate.
This issue tracks clarifying the problem, documenting contract relationships/gaps, and driving the missing assembly layer.
What NeMo Gym produces today
NeMo Gym standardizes on NeMo Gym Responses API types, which wrap OpenAI’s Responses API:
NeMoGymResponseCreateParamsNonStreaming— copy of OpenAIResponseCreateParamsNonStreamingwith server-side validation (nemo_gym/openai_utils.py)NeMoGymResponse— extends OpenAIResponsewith Gym output item types and usage details
The rollout record written by rollout collection is a verify response:
BaseRunRequest.responses_create_params— task inputBaseVerifyRequest.response— accumulated agent trajectory asNeMoGymResponseBaseVerifyResponse.reward— verifier score
(nemo_gym/base_resources_server.py)
So each rollout JSONL row is essentially:
| Field | Meaning |
|---|---|
responses_create_params |
Task input (messages, tools, sampling params) |
response |
Accumulated agent trajectory as a NeMoGymResponse (output item list + usage) |
reward |
Verifier score |
| + agent-specific extras | e.g. Harbor’s instance_id, error flags |
Datasets also use this contract — docs explicitly say rows require responses_create_params following the OpenAI Responses API schema.
Gym extensions on top of OpenAI Responses API:
*ForTrainingvariants addprompt_token_ids,generation_token_ids,generation_log_probs, optionalrouted_experts(for RL)
See also the repo’s Responses API evolution engineering note.
There is no single unified trajectory format inside Gym today:
- SimpleAgent — native Responses loop
- HarborAgent — ATIF internally → converted to Responses via
HarborAgentUtils.trajectory_to_responses() - VerifiersAgent — Chat-style completion → Responses
- BrowsecompAgent — custom JSONL event log (agent-specific)
All converge to Responses at the rollout boundary, but via different adapters.
StepRecord / trajectory_capture is not in main — it lives in preview docs / #1715.
Where the formats sit (different layers)
flowchart TB
subgraph dataset ["Dataset / task spec"]
RCP["responses_create_params<br/>(Responses API input)"]
end
subgraph agent ["Agent harness (varies)"]
SA["SimpleAgent: native Responses loop"]
HA["HarborAgent: ATIF internally"]
VA["VerifiersAgent: Chat-style completion"]
end
subgraph model ["Model server (wire dialect)"]
R["/v1/responses"]
C["/v1/chat/completions"]
M["/v1/messages"]
TC["StepRecord capture middleware<br/>(preview, opt-in)"]
end
subgraph rollout ["Rollout record (today)"]
VR["BaseVerifyResponse<br/>responses_create_params + response + reward"]
end
RCP --> agent
agent --> R & C & M
R & C & M --> TC
agent --> VR
Key insight: NeMo Gym’s rollout contract is agent-assembled content in Responses API form. StepRecord is model-server middleware that logs each LLM HTTP call in its native wire dialect. They are complementary, not the same thing.
Problem statement
What users expect
When running agentic rollouts — especially external / opaque agents we cannot instrument — we want:
- Complete interaction history for debugging, eval, and visualization
- Per-call efficiency metrics (tokens, latency, cache, turns)
- Training-ready trajectories (sampled token ids, logprobs, loss mask) without re-tokenizing from text
- Verifier reward attached to the same rollout attempt
- Harness-agnostic collection (same shape whether the agent is native Gym, Harbor/ATIF, or a CLI in a sandbox)
What we have today
Multiple overlapping “trajectory” concepts with no single canonical assembly path:
| Layer | Contract | Granularity | In main today? |
|---|---|---|---|
| Rollout record | BaseVerifyResponse: responses_create_params + NeMoGymResponse + reward |
One assembled transcript per trial | Yes |
| Model-call capture | StepRecord in capture store JSONL |
One record per HTTP model call | Preview / #1715 (not in main) |
| Harbor harness | ATIF trajectory.json |
Agent steps + observations + metrics | Yes (Harbor agent only) |
| NAT runtime | ATOF events → ATIF v1.7 | Scope/event stream | External (NeMo Agent Toolkit) |
| Interchange target | ATIF | Standard agent trajectory | Ecosystem (Harbor) |
Why this is a problem now
-
Blackbox agents cannot populate training fields in
response
For opaque CLIs (Anthropic Messages dialect), token ids exist only momentarily inside the model server after conversion to the served format; they are dropped before the response returns to the CLI (e.g.responses_to_anthropic_response()intentionally drops token-id fields). The agent’s parsed stdout /NeMoGymResponseis a text view for scoring, not an authoritative training trajectory. -
StepRecord does not subsume agent-level information
Capture records the model-call portion only. It does not capture:- Tool execution inside an opaque CLI shell (unless routed via Gym MCP)
- Harvest artifacts (e.g. git patch) sent to verify
- Reward from the resources server
- Harness-specific error flags
-
StepRecord alone does not subsume full agentic trajectories
Rolloutresponselacks reliable multi-turn stitching and token-id alignment for blackbox paths; ATIF includes observations but is not Gym-native; ATOF normalizes to ATIF, not Responses. -
No documented join contract in main
Preview trajectory-capture docs describe folding capture into rollout records asng_trajectory_capture = {rollout_id, metrics, steps}(sanitized; raw req/resp stay in capture store). The trajectory builder that stitches captures into masked training trajectories (prefix matching, loss mask, sub-agent forests) is designed but not shipped. -
Terminology collision
“Rollout”, “trajectory”, “StepRecord”, “record”, and “step” mean different things across Gym, capture middleware, Harbor ATIF, and internal design docs — causing mismatched expectations.
Activity
ffrujeri commentedon Jun 30, 2026
Contract-by-contract comparison
1. OpenAI Responses API ↔ NeMo Gym Responses API
Responsewith typedoutput[]itemsNeMoGymResponseResponseCreateParams.inputNeMoGymResponseCreateParamsNonStreamingreasoningitemsNeMoGymResponseReasoningItemfunction_call/function_call_output*ForTrainingitems with token IDs / logprobsGap: Gym is a superset for RL. Plain OpenAI Responses consumers won’t expect
generation_token_idsetc.2. Anthropic Messages API (
/v1/messages)messages[]with content blocks (text,tool_use,tool_result,thinking)input/outputitem list with different type namesAnthropicConverterbidirectionally maps Gym ↔ AnthropicEvery Gym model server exposes
/v1/messagesby default, mapping Messages ↔ Responses around its ownresponses()implementation — so blackbox harnesses that require an Anthropic endpoint (e.g. Claude Code CLI) can target any model server directly (nemo_gym/base_responses_api_model.py).Gap: Anthropic is a wire dialect at the model server. Gym agents speak Responses API internally; StepRecord would store
dialect: "messages"with raw Anthropic payloads; Gym rollouts are Responses-shaped after conversion.3. Chat Completions (
/v1/chat/completions)tool_calls)role: "tool"messagesfunction_call_outputitemscontentstringreasoningitemsResponsesConverterat model-server boundaryGap: Chat is still widely used (Verifiers agent, many OSS models). Gym normalizes to Responses at the agent output boundary, but StepRecord captures the actual wire format per call (
dialect: "chat").4. StepRecord (preview trajectory capture)
From Trajectory Capture docs:
request+responseper dialectoutput[]items; raw HTTP omittedfunction_call_outputfrom agent/envturn_indexfrom user-message boundaries (naive)assemble_rollouteval view*ForTrainingitems<rollout_id>.capture.jsonl)observability_enabled: trueon model server/run)Terms from trajectory capture docs:
turn_indexderived from user-message boundaries in captured requestsGap: StepRecord is observability/eval at the inference boundary. Gym rollouts are training/eval at the agent boundary. StepRecord sees every LLM call even from opaque harnesses; Gym rollouts only see what the agent chooses to expose (unless capture is folded into the rollout record).
Reading captured data (preview API):
When rollouts are collected with capture enabled,
rollout_collectionfolds each rollout's captured trajectory into its record asng_trajectory_capture = {rollout_id, metrics, steps}— same shape for every agent harness; raw request/response remain in the capture store.5. ATIF (Harbor)
From Harbor’s ATIF docs:
source: user/agent/systemwithmessage,tool_calls,observation,metricsObservation.results[]function_call_outputsubagent_trajectories,trajectory_id(v1.7)Metrics+final_metricsresponse.usageIn-repo bridge: Harbor writes
agent/trajectory.json(ATIF), thenHarborAgentUtils.trajectory_to_responses()converts to NeMo Gym Responses output — each agent step becomes assistant message + function_call(s) + function_call_output(s).Gaps ATIF → Gym conversion:
responses_create_params.input, notoutput<think>tags (training alignment), not separatereasoningitemsfinal_metricsmaps tousage, but per-step metrics can be lossy6. ATOF (NeMo Agent Toolkit)
From NAT’s ATOF→ATIF converter:
ScopeEvent,MarkEvent)subagent_trajectories(v1.7)data_schema+ pluggable extractorsGap: ATOF is the broadest runtime telemetry format (NAT workflows). It normalizes to ATIF, not directly to Gym Responses API. There is no in-repo ATOF→Gym path today; the chain would be ATOF → ATIF → (Harbor-style conversion) → Responses API.
Summary: relationships
Main gaps
No single trajectory format inside Gym today. Simple agents emit Responses natively; Harbor uses ATIF internally; Verifiers uses Chat-style completion. All converge to Responses at the rollout boundary — but via different adapters.
StepRecord vs rollout record answer different questions. StepRecord = “what did the model server see on each HTTP call?” Rollout = “what transcript + reward does training/eval consume?” Tool results and env state live in rollouts (or ATIF), not in StepRecord.
Turn/step semantics don’t align. StepRecord’s
turn_indexis server-derived and explicitly “naive”; ATIF steps are agent-centric with observations attached; Gymoutput[]is a flat item stream.Training token IDs are Gym-specific. StepRecord’s assembled eval view deliberately omits raw req/resp and token IDs; on-policy RL is a separate pipeline consuming capture. Gym puts token IDs on
*ForTrainingoutput items when the model server provides them.ATIF/ATOF are interchange standards, not Gym natives. Harbor already bridges ATIF→Responses. ATOF→Gym would need ATOF→ATIF→Responses (or a new converter). StepRecord could eventually backfill opaque harness trajectories without agent cooperation.
Preview vs shipped. Trajectory capture (
StepRecord,ng_trajectory_capture,assemble_rollout) is documented on the preview branch but not present in main — so today you only get the Responses API rollout contract from agents.ATIF ↔ NeMo Gym Responses: conversion semantics
steps[]withsource: user | agent | systeminput/output[]typed itemsobservation.results[]on the same agent step astool_calls[]function_call_outputitemsschema_version,session_id,agent,final_metrics, subagents (v1.7)id,usage, tools, …)reasoning_contentfield on agent stepstype: "reasoning"items (Gym-native); Harbor merge path uses inline tags insteadmetrics*ForTrainingfields on output itemsATIF carries more trajectory/session structure. Responses carries more API/request envelope and Gym RL extensions. For the core interaction payload (prompt, assistant text, tool calls, tool outputs, token ids), ATIF usually has enough to derive a Gym Responses view.
ATIF → Responses: supported today (Harbor)
Yes — this is the intended direction. Implemented in
responses_api_agents/harbor_agent/utils.py:response.output[]source: "agent"stepstrajectory_to_responses()responses_create_params.inputsource: "user"stepsextract_input_from_trajectory()response.usagefinal_metrics(fallback: trialagent_result)extract_usage()Per agent step, the mapper emits:
type: "message"item —message+ optionalreasoning_contentmerged as<think>…</think>prefix (not a separate reasoning item)type: "function_call"pertool_calls[]entry (argumentsobject → JSON string)type: "function_call_output"perobservation.results[]entry (linked by index /call_id)Token ids, logprobs, and
routed_expertsfrom stepmetricsare preserved on*ForTrainingmessage items when present.Harbor already keeps
agent/trajectory.json(ATIF) as the canonical rich artifact and derives the Gym rollout record (responses_create_params+response+reward) from it inHarborAgent.run().Responses → ATIF: not supported; not lossless
No reverse converter exists in the repo. ATIF → Responses → ATIF is not identity:
reasoning_contentmerged into message textinput, notoutputinput+outputto reconstruct stepssource: "system"steps skippedstep_id,timestamp,agent,schema_versionsubagent_trajectories,ToolCall.extra,ObservationResult.extra(v1.7)step.message(raw_contentmode)call_{n}_{i}ids; fields liketask_completenot in Responses outputPractical rule: need faithful ATIF → keep
trajectory.json. Need Gym training/eval rollouts → derive Responses from ATIF (Harbor path) or assemble natively (SimpleAgent). Do not round-trip through Responses expecting to recover ATIF.Native Gym agents that never produced ATIF cannot be losslessly converted to ATIF without adopting conventions (step grouping, observation bundling, metadata defaults).
How ATIF models agent–environment interaction (relevant to mapping)
ATIF does not use separate environment steps. One agent–environment micro-cycle is one
source: "agent"step:Harbor’s ATIF → Responses conversion splits this bundled step into flat Responses items (message, then calls, then outputs). That split is reversible with grouping heuristics, but the Harbor forward mapper is the defined ATIF → Responses contract.
Implication for this issue
Proposed direction (for discussion)
ng_trajectory_captureshape)responses_create_params,reward, …) but clarifyresponseprovenance:Acceptance criteria (suggested)
BaseVerifyResponseand blackbox agentsng_trajectory_capture(or successor field) consistentlyHarborAgentUtils); state that Responses → ATIF is not lossless and no reverse converter existsReferences
Open questions
parse()as fallback indefinitely?ng_trajectory_capturethe long-term field name, or should we align eval rollup shape with ATIF for cross-ecosystem tooling?nemo_gymlibrary, model server, or separate service (gateway vs middleware)?response, or treat agentresponseas sufficient when the loop is observable?function_call_outputsynthesized from MCP logs?ffrujeri commentedon Jul 1, 2026
Joining StepRecord with agent-level information
For blackbox agents, StepRecord capture and agent-level information must be joined — but not by merging into one schema at capture time. The intended model is layered assembly with a shared join key.
Two separate artifacts
Per internal design terminology: one rollout can yield more than one trajectory (prefix-consistent segments). Per-call observability records are kept separately and called records.
StepRecord is not the final rollout contract. It is the authoritative substrate for model calls; agent-level info fills in what capture cannot see.
Scattered records joined at read time
A rollout's record is scattered — many model calls, a verify, and (for training) an assembled trajectory — across processes and time. Nothing in an opaque CLI's request says which rollout a call belongs to.
Join key: rollout id from
(task_index, rollout_index), carried on:/ng-rollout/<rollout_id>/v1URL prefix (opaque CLIs) orx-nemo-gym-rollout-idheader (Gym-orchestrated callers)flowchart LR subgraph scattered ["Scattered across the rollout"] R1["StepRecord #1"] R2["StepRecord #2"] RN["StepRecord #N"] V["verify → reward"] H["harvest artifact<br/>(patch, stdout, …)"] MCP["optional MCP tool calls"] end JK["rollout_id join key"] JK --> R1 & R2 & RN & V & H & MCP subgraph assembly ["Assembly (not at capture time)"] TB["Trajectory builder<br/>(training)"] EV["Per-rollout metrics<br/>(eval)"] RR["Rollout record<br/>ng_trajectory_capture + reward"] end R1 & R2 & RN --> TB & EV V --> TB & RR TB --> RRWhat each layer contributes
StepRecord / capture store (model-server layer):
Agent-level (run-in-box / harness layer):
git diff HEAD)harness.parse()) — eval-only fallback when there is no recording layer; not training source when capture existsNeMoGymResponse-shaped return — for Claude Code has no token ids by constructionResources-server layer:
Trajectory builder (the actual “union” for training):
rollout_idAnthropic / multi-turn stitching (the hard case)
Anthropic Messages is stateless: every turn the CLI resends the whole conversation so far plus the latest tool results.
Flow for Claude Code (hardest case):
Training assembly requires:
anthropic_request_to_responses)Token ids cannot flow back through the CLI; capture must happen inside the model-server proxy/gateway.
Prefix matching only works if the Anthropic→served conversion is deterministic — the same conversation prefix must produce the same served token ids every turn.
Rollout JSONL vs capture store
BaseVerifyResponse)ng_trajectory_capture)response.usageaggregates)assemble_rolloutresponse.output[](agent-built today)*ForTrainingwhen agent controls loopTwo external-agent shapes (same join logic)
Both use
seed_session(optional spec) +verify; harness-specific code is confined to install/launch/harvest.Intended end state for a rollout record
Not:
ng_trajectory_capture = {rollout_id, metrics, steps}+ existingrewardRecording placement: middleware vs gateway
Both are proxy-layer options (separate from the inference engine):
For training, recording may need to inject token-id params and assemble masked trajectories — whether that lives in middleware or a dedicated gateway is an open architectural choice; the capture store substrate is shared either way.
At high concurrency, all of one rollout's calls must reach the same capture (sticky routing by join key, or shared store keyed by rollout id).
Implementation maturity
/v1/messagesdialect converterng_trajectory_capturerollout fold-inresponses_create_params+response+rewardassembled by the agentresponse/trajectory for blackbox from capture + builder, not CLI parse*ForTrainingitems when agent controls the loopfunction_call_output(native agents) or ATIF (Harbor)response.usagePractical mental model
responses_create_params+response+reward)request/responseperdialect, or model-server convertersffrujeri commentedon Jul 1, 2026
Adding the architecture/naming distinction from the #1715 review discussion here too, since I think it is central to this issue.
I think we should be careful not to let “trajectory” mean both “model-call evidence captured at the model-server boundary” and “agent-level rollout trajectory.” The model server is the right owner for model-call capture: request/response payloads, served wire dialect, token usage, latency, status/error classification, cache signals, and a rollout correlation id. That is the substrate we need for black-box harnesses because every model call crosses this boundary.
But the model server should not be treated as owning the agent trajectory. The agent/rollout layer owns the semantic structure: which calls are part of the task attempt versus auxiliary harness calls, how tool execution and observations map between calls, what a turn means, how compaction/history rewriting should be interpreted, and which final verify/reward record is authoritative.
So one useful contract distinction might be:
That framing keeps #1715’s ASGI capture design intact, but avoids implying that
StepRecordorng_trajectory_capturealone is the canonical agent trajectory contract. If we keep the current names, I think the docs should explicitly say “trajectory capture” is model-call capture attached to a rollout, not the full agent-level trajectory assembly layer.ffrujeri commentedon Jul 1, 2026
Converging on Issue #1867