Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ The Dataset column links to publicly available datasets (e.g., on HuggingFace).
| Rdkit Chemistry | knowledge | [DEPRECATED — use litmus_agent] Molecular chemistry question answering: calculate properties of SMILES. Includes a mix of tool-use (python + rdkit) and no-tool-use questions. Superseded by the domain-agnostic litmus_agent server, which generalizes this server's scoring path. | Improve molecular reasoning and SMILES parsing. | ✓ | - | TBD | <a href='resources_servers/rdkit_chemistry/configs/rdkit_chemistry.yaml'>rdkit_chemistry.yaml</a> | - |
| Reasoning Gym | knowledge | Claude Code agent harness for reasoning gym tasks | Evaluate model capabilities in the Claude Code agent harness | ✓ | - | Creative Commons Attribution 4.0 International | <a href='resources_servers/reasoning_gym/configs/reasoning_gym_claude_code_agent.yaml'>reasoning_gym_claude_code_agent.yaml</a> | <a href='https://huggingface.co/datasets/nvidia/Nemotron-RL-ReasoningGym-v1'>Nemotron-RL-ReasoningGym-v1</a> |
| Reasoning Gym | knowledge | Claude Code agent harness for reasoning gym tasks, via a Gym model server's /v1/messages | Showcase Claude Code running against any Gym model backend | - | - | - | <a href='resources_servers/reasoning_gym/configs/reasoning_gym_claude_code_agent_model_server.yaml'>reasoning_gym_claude_code_agent_model_server.yaml</a> | - |
| Reasoning Gym | knowledge | Codex agent harness for reasoning gym tasks | Evaluate model capabilities in the Codex agent harness | - | - | - | <a href='resources_servers/reasoning_gym/configs/reasoning_gym_codex_agent.yaml'>reasoning_gym_codex_agent.yaml</a> | - |
| Reasoning Gym | knowledge | Codex agent harness for reasoning gym tasks, via a Gym model server's /v1/responses | Showcase Codex running against any Gym model backend | - | - | - | <a href='resources_servers/reasoning_gym/configs/reasoning_gym_codex_agent_model_server.yaml'>reasoning_gym_codex_agent_model_server.yaml</a> | - |
| Reasoning Gym | knowledge | LangGraph orchestrator agent compatible with resource servers that do not use tools; enables diverse agent training data and test time scaling vs a simple agent, extensible to use tools or other agent architectures | Iterative test time scaling for improved performance in reasoning tasks | ✓ | - | Apache 2.0 | <a href='resources_servers/reasoning_gym/configs/orchestrator_agent.yaml'>orchestrator_agent.yaml</a> | - |
| Reasoning Gym | knowledge | LangGraph parallel thinking agent compatible with resource servers that do not use tools; enables diverse agent training data and test time scaling vs a simple agent, extensible to use tools or other agent architectures | Iterative test time scaling for improved performance in reasoning tasks | ✓ | - | Apache 2.0 | <a href='resources_servers/reasoning_gym/configs/parallel_thinking_agent.yaml'>parallel_thinking_agent.yaml</a> | - |
| Reasoning Gym | knowledge | LangGraph reflection agent compatible with resource servers that do not use tools; provides iterative reflection for diverse agent training data and test time scaling, extensible to use tools or other agent architectures | Iterative test time scaling for improved performance in reasoning tasks | ✓ | - | Apache 2.0 | <a href='resources_servers/reasoning_gym/configs/reflection_agent.yaml'>reflection_agent.yaml</a> | - |
Expand Down
4 changes: 2 additions & 2 deletions fern/versions/latest/pages/agent-server/agent-skills.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ To support skills in a new agent, implement the three-step adapter. NeMo Gym pro
| Agent | Discovery location |
|---|---|
| Claude Code | `CLAUDE_CONFIG_DIR/skills/` |
| Codex CLI | the Codex config home (e.g. `$CODEX_HOME`) |
| Codex CLI | `CODEX_HOME/skills/` |
| MCP-backed agent | configure the MCP skill server to serve from the directory |

Prefer staging into a **per-request** ephemeral location so concurrent requests with different skills do not contaminate one another, and so nothing leaks between rollouts.
Expand All @@ -94,7 +94,7 @@ To support skills in a new agent, implement the three-step adapter. NeMo Gym pro
Note that enabling discovery is usually all-or-nothing: with Claude Code, dropping `--bare` so skills load also re-enables *all* other auto-discovery — hooks, plugins, MCP servers, memory, and `CLAUDE.md` — not just skills. If you are measuring a skill's isolated impact, be aware that a baseline (`--bare`, no skills) versus a skills run may differ by more than the skill content alone.

<Note>
The reference implementation is the [Claude Code agent](https://github.com/NVIDIA-NeMo/Gym/tree/main/responses_api_agents/claude_code_agent). It stages skills into a fresh per-request `CLAUDE_CONFIG_DIR/skills/` and drops `--bare` when skills are active.
The reference implementation is the [Claude Code agent](https://github.com/NVIDIA-NeMo/Gym/tree/main/responses_api_agents/claude_code_agent). It stages skills into a fresh per-request `CLAUDE_CONFIG_DIR/skills/` and drops `--bare` when skills are active. The [Codex agent](https://github.com/NVIDIA-NeMo/Gym/tree/main/responses_api_agents/codex_agent) follows the same pattern, staging into a fresh per-request `CODEX_HOME/skills/` (Codex discovers them natively — no discovery flag needed).
</Note>

## What NeMo Gym does not do
Expand Down
59 changes: 57 additions & 2 deletions nemo_gym/base_responses_api_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@

import orjson
from fastapi import Body, FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, Field, ValidationError, model_validator

from nemo_gym.anthropic_converter import AnthropicConverter
from nemo_gym.config_types import ROLLOUT_PATH_PREFIX, ModelServerRef
Expand All @@ -58,6 +59,12 @@
NeMoGymResponse,
NeMoGymResponseCreateParamsNonStreaming,
)
from nemo_gym.responses_streaming import (
sanitize_streaming_responses_body,
synthesize_responses_failure_sse,
synthesize_responses_sse,
validate_streaming_responses_params,
)
from nemo_gym.server_utils import (
BaseRunServerInstanceConfig,
BaseServer,
Expand Down Expand Up @@ -90,7 +97,7 @@ def setup_webserver(self) -> FastAPI:

app.post("/v1/chat/completions")(self.chat_completions)

app.post("/v1/responses")(self.responses)
app.post("/v1/responses")(self.responses_dispatch)

# Every Gym model server speaks the Anthropic Messages API by default, mapping
# Messages <-> Responses around its own responses() implementation. This lets blackbox
Expand All @@ -110,6 +117,46 @@ async def chat_completions(
async def responses(self, body: NeMoGymResponseCreateParamsNonStreaming = Body()) -> NeMoGymResponse:
pass

async def responses_dispatch(self, request: Request, body: dict = Body()):
"""Default ``/v1/responses`` entrypoint shared by every Gym model server.

A plain JSON request validates strictly against
``NeMoGymResponseCreateParamsNonStreaming`` and delegates to this server's own
``responses()``, preserving the historical non-streaming behavior. When the client
requests ``stream: true`` (blackbox Responses-over-SSE harnesses like the Codex CLI
always do), the request is first sanitized from the streaming wire dialect (extra
bookkeeping fields, ``namespace`` tool specs — see ``nemo_gym.responses_streaming``),
delegated to the same ``responses()``, and the complete response is re-emitted as a
synthesized Responses SSE event stream. A ``responses()`` failure on this path is turned
into a terminal ``response.failed`` event rather than an HTTP 500 (bad-request validation
still fails eagerly, before the stream is committed).
"""
if not body.get("stream"):
params = _validate_responses_params(body)
return await self._invoke_responses(request, params)

cleaned, ns_map = sanitize_streaming_responses_body(body)
try:
params = validate_streaming_responses_params(cleaned)
except ValidationError as exc:
raise RequestValidationError([{**error, "loc": ("body", *error["loc"])} for error in exc.errors()])

try:
response = await self._invoke_responses(request, params)
response_json = response.model_dump(mode="json") if isinstance(response, BaseModel) else dict(response)
except Exception as exc:
# The streaming contract is already the response's shape, so a backend failure must be a
# terminal response.failed event, not an HTTP 500 the client would see as a broken stream.
logger.exception("responses() failed while serving a streaming /v1/responses request")
return StreamingResponse(
synthesize_responses_failure_sse(str(exc)),
media_type="text/event-stream",
)
return StreamingResponse(
synthesize_responses_sse(response_json, ns_map),
media_type="text/event-stream",
)

async def messages(self, request: Request, body: dict = Body()):
"""Default Anthropic Messages <-> Responses mapping shared by every Gym model server.

Expand Down Expand Up @@ -141,6 +188,14 @@ async def _invoke_responses(
return await self.responses(body=params)


def _validate_responses_params(body: dict) -> NeMoGymResponseCreateParamsNonStreaming:
"""Validate a /v1/responses body dict, surfacing failures as FastAPI's standard 422."""
try:
return NeMoGymResponseCreateParamsNonStreaming.model_validate(body)
except ValidationError as exc:
raise RequestValidationError([{**error, "loc": ("body", *error["loc"])} for error in exc.errors()])


# --- Capture configuration + rollout-keyed storage ---


Expand Down
23 changes: 23 additions & 0 deletions nemo_gym/responses_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@
)


def _message_content_to_text(content: Any) -> str:
"""Plain text of a chat message content (a string, or a list of text parts)."""
if isinstance(content, str):
return content
return "".join(part.get("text", "") for part in content or [] if isinstance(part, dict))


class ResponsesConverterState(BaseModel):
return_token_id_information: bool

Expand Down Expand Up @@ -166,6 +173,22 @@ def responses_to_chat_completion_create_params(

state.flush_assistant()

# The Responses API inserts `instructions` as a system message at the start of the model's
# context. Chat Completions has no such parameter, so map it explicitly — otherwise it is
# silently dropped when the remaining params are passed through (extra fields are ignored).
# The leading run of system/developer messages is folded into the same single system
# message: chat backends commonly admit only one system message, at position 0 (harnesses
# like the Codex CLI send instructions plus a leading developer message).
instructions = responses_create_params.pop("instructions", None)
if instructions:
leading_parts = [instructions]
while state.messages and state.messages[0]["role"] in ("system", "developer"):
leading_parts.append(_message_content_to_text(state.messages.pop(0)["content"]))
state.messages.insert(
0,
NeMoGymChatCompletionSystemMessageParam(content="\n\n".join(leading_parts), role="system"),
)

model = responses_create_params.pop("model", None)
if model is not None:
responses_create_params["model"] = model
Expand Down
Loading
Loading