Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,10 @@ The Dataset column links to publicly available datasets (e.g., on HuggingFace).
| Swe Agents | | - | - | ✓ | ✓ | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_multi_tools.yaml'>swebench_multi_tools.yaml</a> | - |
| Swe Agents | | - | - | ✓ | ✓ | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_openhands.yaml'>swebench_openhands.yaml</a> | - |
| Swe Agents | | - | - | ✓ | ✓ | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_openhands_training.yaml'>swebench_openhands_training.yaml</a> | - |
| Swe Agents | coding | SWE-bench driven by the opencode agent framework for RL training. | Train software engineering capabilities on SWE tasks using opencode rollouts. | ✓ | - | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_opencode_training.yaml'>swebench_opencode_training.yaml</a> | - |
| Swe Agents | coding | SWE-bench driven by the opencode agent framework. | Eval software engineering capabilities on SWE-bench using opencode. | ✓ | ✓ | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_opencode.yaml'>swebench_opencode.yaml</a> | - |
| Swe Agents | coding | SWE-bench driven by the opencode agent framework for RL training. | Train software engineering capabilities on SWE tasks using opencode rollouts. | ✓ | - | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_nv_opencode_training.yaml'>swebench_nv_opencode_training.yaml</a> | - |
| Swe Agents | coding | SWE-bench driven by the opencode agent framework. | Eval software engineering capabilities on SWE-bench using opencode. | ✓ | ✓ | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_nv_opencode.yaml'>swebench_nv_opencode.yaml</a> | - |
| Swe Agents | coding | SWE-bench driven by upstream opencode for RL training. | Train software engineering capabilities on SWE tasks using upstream opencode rollouts. | ✓ | - | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_opencode_training.yaml'>swebench_opencode_training.yaml</a> | - |
| Swe Agents | coding | SWE-bench driven by upstream opencode. | Eval software engineering capabilities on SWE-bench using upstream opencode. | ✓ | ✓ | Apache 2.0 | <a href='responses_api_agents/swe_agents/configs/swebench_opencode.yaml'>swebench_opencode.yaml</a> | - |
| Swe Pivot | agent | SWE pivot verifier for PivotRL on coding agent trajectories | Improve coding agent fix-design decisions | ✓ | ✓ | Apache 2.0 | <a href='resources_servers/swe_pivot/configs/swe_pivot.yaml'>swe_pivot.yaml</a> | - |
| Swerl Gen | coding | Running sandboxed evaluation for SWE-style tasks (either patch generation or reproduction test generation) | Improve SWE capabilities useful for benchmarks like SWE-bench | ✓ | ✓ | Apache 2.0 | <a href='resources_servers/swerl_gen/configs/swerl_gen.yaml'>swerl_gen.yaml</a> | - |
| Swerl Llm Judge | coding | SWE-style multiple-choice LLM-judge tasks scored via <solution>...</solution> choice. | Improve SWE capabilities useful for benchmarks like SWE-bench | ✓ | ✓ | MIT | <a href='resources_servers/swerl_llm_judge/configs/swerl_llm_judge.yaml'>swerl_llm_judge.yaml</a> | - |
Expand Down
66 changes: 59 additions & 7 deletions nemo_gym/switchyard_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
masks the sample rather than emitting partially annotated training data.
"""

import json
import math
from typing import Any, Dict, List, NamedTuple
from typing import Any, Dict, List, NamedTuple, Optional

from openai.types.responses.function_tool import FunctionTool

Expand All @@ -53,6 +54,9 @@ class SwitchyardTrace(NamedTuple):
tools: List[FunctionTool]
record_uuids: List[str]
model: str
# Set when reconstruction stopped at a validation break and emitted only the
# longest valid prefix. Carriers of this MUST be masked from the loss.
partial_reason: Optional[str] = None


def map_switchyard_tools(raw_tools: Any) -> List[FunctionTool]:
Expand All @@ -78,12 +82,22 @@ def map_switchyard_tools(raw_tools: Any) -> List[FunctionTool]:
return tools


def reconstruct_switchyard_rollout(envelope: Any, session_id: str, converter: ResponsesConverter) -> SwitchyardTrace:
def reconstruct_switchyard_rollout(
envelope: Any, session_id: str, converter: ResponsesConverter, allow_partial: bool = True
) -> SwitchyardTrace:
"""Validate a retrieval envelope and rebuild one token-annotated Gym rollout.

Records are processed in the endpoint's returned order; each later prompt
must strictly extend the already assembled history with a non-empty
environment-message suffix. Raises `SwitchyardTraceError` on any deviation.
environment-message suffix. By default (``allow_partial=True`` — the
training pipeline is the primary consumer, and masking a partial sample is
its correct semantics) a deviation at record i > 0 stops the walk and emits
the longest valid prefix with ``partial_reason`` set; pass
``allow_partial=False`` for fail-fast certification (analysis, tests). The prefix is the only token-continuous object that exists once the
chain breaks (each record's tokens are deltas over the accumulated history),
and emitting it lets the sample mask itself from the loss instead of
aborting its whole prompt group downstream. A record-0 failure still raises:
there is no valid prefix to emit.
"""
records = _validate_envelope(envelope, session_id)

Expand All @@ -103,12 +117,12 @@ def reconstruct_switchyard_rollout(envelope: Any, session_id: str, converter: Re
# violation (e.g. a template that re-renders history differently) masks the
# sample instead of crashing the training step.
token_history: List[int] = []
partial_reason: Optional[str] = None
for i, record in enumerate(records):
try:
_validate_record(record, i, session_id, record_uuids)
if record["prompt_token_ids"][: len(token_history)] != token_history:
raise SwitchyardTraceError(
f"record {i} prompt_token_ids do not extend the prior prompt+generation tokens"
)
raise SwitchyardTraceError(f"record {i} prompt_token_ids do not extend the prior prompt+generation tokens")
token_history = record["prompt_token_ids"] + record["generation_token_ids"]
if record["model"] != model:
raise SwitchyardTraceError(f"record {i} model {record['model']!r} != session model {model!r}")
Expand All @@ -123,7 +137,11 @@ def reconstruct_switchyard_rollout(envelope: Any, session_id: str, converter: Re
if i == 0:
new_context = prompt
else:
if prompt[: len(assembled)] != assembled:
# Compare with tool-call arguments canonicalized so a client that
# re-serializes JSON differently across turns (compact vs. pretty)
# does not fail the extension check. The reconstructed items keep the
# original arguments — this normalization is comparison-only.
if _canonicalize_tool_args(prompt[: len(assembled)]) != _canonicalize_tool_args(assembled):
raise SwitchyardTraceError(f"record {i} prompt does not extend the reconstructed history")
new_context = prompt[len(assembled) :]
if not new_context:
Expand All @@ -145,6 +163,11 @@ def reconstruct_switchyard_rollout(envelope: Any, session_id: str, converter: Re
}
)
record_uuids.append(record["uuid"])
except SwitchyardTraceError as e:
if not allow_partial or i == 0:
raise
partial_reason = f"record {i} of {len(records)}: {e}"
break

items = converter.chat_completions_messages_to_responses_items(annotated)
input_items, output_items = split_responses_input_output_items(items)
Expand All @@ -155,6 +178,7 @@ def reconstruct_switchyard_rollout(envelope: Any, session_id: str, converter: Re
tools=map_switchyard_tools(tools),
record_uuids=record_uuids,
model=model,
partial_reason=partial_reason,
)


Expand Down Expand Up @@ -255,6 +279,34 @@ def _content_to_text(content: Any, i: int) -> str:
raise SwitchyardTraceError(f"record {i} contains unsupported content of type {type(content).__name__}")


def _canonicalize_tool_args(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Copy of *messages* with assistant tool-call ``arguments`` canonicalized to
sorted-key JSON, used only for the history-extension equality check.

A client may re-serialize the same tool-call JSON differently across turns
(compact vs. pretty-printed); that whitespace difference must not fail the
extension check. The reconstructed training items keep the original arguments
— this normalization never reaches them.
"""
out: List[Dict[str, Any]] = []
for message in messages:
tool_calls = message.get("tool_calls")
if not tool_calls:
out.append(message)
continue
canonical_calls = []
for call in tool_calls:
function = call["function"]
arguments = function["arguments"]
try:
arguments = json.dumps(json.loads(arguments), sort_keys=True)
except (json.JSONDecodeError, TypeError):
pass # not valid JSON — compare as-is
canonical_calls.append({**call, "function": {**function, "arguments": arguments}})
out.append({**message, "tool_calls": canonical_calls})
return out


def _normalize_tool_calls(tool_calls: Any, i: int) -> List[Dict[str, Any]]:
if not isinstance(tool_calls, list):
raise SwitchyardTraceError(f"record {i} contains non-list tool_calls")
Expand Down
Loading