environments — RL Training Environments

RL Training Environments

Integrates Hermes Agent's tool-calling capabilities with the Atropos RL training framework. Provides a layered abstraction bridging OpenAI-spec tool calling with Atropos dataset rollouts, reward scoring, and training loops. Includes 5 benchmark environments and 11 tool call parsers for different LLM formats.

🎯 Why This Module Exists

Without the environments module, every RL training pipeline would need to implement its own agent loop, tool dispatch, reward verification, and sandbox isolation from scratch. The core Hermes agent (run_agent.py) is designed for interactive CLI use — single query, single session. RL training requires batch rollouts across hundreds of parallel tasks, each with isolated sandboxes, configurable tool distributions, and reward scoring. This module bridges that gap by wrapping the agent's tool-calling engine in Atropos-compatible BaseEnv subclasses. It provides ToolContext for reward verifiers to inspect rollout state (e.g., "did the model actually create that file?"), manages thread pool executors for concurrent tool calls, and abstracts away Phase 1 (OpenAI server) vs Phase 2 (VLLM ManagedServer) differences. The benchmark environments (TerminalBench, TBLite, YCBench, SWE, WebResearch) provide standardized evaluation harnesses, while the 11 tool call parsers handle the diverse output formats of models like Hermes, DeepSeek, Qwen, LLaMA, Mistral, GLM, and Kimi.

💡 Real-world analogy

Think of the environments module as a standardized flight simulator for training pilots. The core agent is the pilot; the environments module provides the simulator cockpit (agent loop), flight scenarios (benchmark environments), scoring rubrics (reward functions), and translator headsets (parsers) so pilots who speak different languages (LLM formats) can all train in the same simulator. Without it, you'd need a separate simulator for each pilot language and each training scenario.

20
Files
~8K
Lines of Code
5
Benchmark Envs
Python
Language

Atropos RL 5 Benchmark Envs 11 Parsers Trajectory Gen

Architecture

flowchart LR
  subgraph Core["Core Agent Loop"]
    AL["HermesAgentLoop"] --> AR["AgentResult"]
    AL --> TC["ToolContext"]
  end
  subgraph Base["Base Environment"]
    BE["HermesAgentBaseEnv"] --> CF["HermesAgentEnvConfig"]
    BE --> AL
  end
  subgraph Benchmarks["Benchmark Envs"]
    TB["TerminalBench"]
    TBL["TBLite"]
    YC["YCBench"]
    SWE["HermesSweEnv"]
    WR["WebResearchEnv"]
  end
  subgraph Parsers["Tool Call Parsers"]
    HP["HermesParser"]
    DP["DeepSeekParser"]
    QP["QwenParser"]
    LP["LLaMAParser"]
    MP["MistralParser"]
    GP["GLMParser"]
  end
  BE --> TB
  BE --> TBL
  BE --> YC
  BE --> SWE
  BE --> WR
  AL --> HP
  AL --> DP
  AL --> QP
  AL --> LP
  AL --> MP
  AL --> GP

Data Flow

flowchart TD
  A["Atropos Rollout Request"] --> B["get_next_item()"]
  B --> C["format_prompt()"]
  C --> D["HermesAgentLoop"]
  D --> E["LLM Call with tools="]
  E --> F["Parse tool_calls"]
  F --> G["handle_function_call()"]
  G --> D
  D --> H["AgentResult"]
  H --> I["ToolContext Created"]
  I --> J["compute_reward()"]
  J --> K["Score 0.0 - 1.0"]
  K --> L["ScoredDataGroup"]
  L --> M["Atropos Training Pipeline"]

🚶 Step-by-Step Narrated Walkthrough

Follow a single RL training rollout for a TerminalBench task as it flows through the entire environments pipeline:

  1. Rollout Request — Atropos framework calls get_next_item() on the TerminalBench environment, requesting a new task from the dataset.
  2. Dataset Item — The environment returns a dataset item containing a terminal challenge prompt (e.g., "Find all Python files larger than 1MB in /data").
  3. Prompt Formatting — format_prompt() converts the dataset item into an OpenAI-format user message with system prompt and enabled toolsets.
  4. Toolset Sampling — HermesAgentEnvConfig samples the tool distribution for this rollout group. All rollouts in the same group share the same tool configuration for fair comparison.
  5. Agent Loop Start — HermesAgentLoop begins the multi-turn loop, calling the LLM with the tools= parameter listing available tool schemas.
  6. Tool Call Parsing — The appropriate parser (e.g., HermesParser for Hermes models) extracts structured tool calls from the LLM response. Phase 2 mode uses client-side parsing for lower latency.
  7. Tool Execution — Each tool call is dispatched to handle_function_call() in a ThreadPoolExecutor (128 workers). The tool runs in a sandbox scoped to the rollout's task_id.
  8. Loop Continuation — Tool results are appended to the conversation history. The loop continues until max_agent_turns is reached or the model stops calling tools.
  9. AgentResult Capture — The full conversation history, turns used, tool errors, and reasoning per turn are captured atomically in an AgentResult dataclass.
  10. ToolContext Creation — A ToolContext is created for the reward verifier, giving it unrestricted access to all Hermes tools scoped to this rollout's task_id.
  11. Reward Computation — compute_reward() uses the ToolContext to verify the model's work (e.g., run the same command and check output), returning a score from 0.0 to 1.0.
  12. Cleanup and Scoring — Sandbox resources (terminal session, browser profile) are cleaned up in a try-finally block. The ScoredDataGroup is built from ManagedServer state and returned to Atropos for training.

Key Components

HermesAgentLoop

The reusable multi-turn agent engine at the heart of every environment. Runs the tool-calling loop using standard OpenAI-spec tools= parameter, dispatches calls via handle_function_call(), and captures results in AgentResult. Supports both Phase 1 (OpenAI API server) and Phase 2 (VLLM ManagedServer with client-side parsing).

AgentResult

Immutable dataclass capturing the full outcome of an agent loop run:

@dataclass
class AgentResult:
    messages: List[Dict[str, Any]]       # Full conversation history
    managed_state: Optional[Dict] = None # ManagedServer state (Phase 2)
    turns_used: int = 0                  # LLM call count
    finished_naturally: bool = False     # Model stopped vs max_turns
    reasoning_per_turn: List[Optional[str]] = field(default_factory=list)
    tool_errors: List[ToolError] = field(default_factory=list)

ToolContext

Per-rollout handle giving reward functions unrestricted access to ALL Hermes tools (terminal, file, web, browser) scoped to the rollout's task_id. Unlike agent-facing tools which may be restricted by permissions, reward verifiers need full access to inspect the agent's work.

HermesAgentBaseEnv

Abstract BaseEnv subclass for Atropos integration. Orchestrates the agent loop, ToolContext creation, and ScoredDataGroup construction. Concrete environments (TerminalBench, SWE, etc.) extend this base with task-specific reward functions and dataset loading.

Tool Call Parsers

Eleven parsers handle the diverse tool call output formats of different LLM families. Each parser extracts structured tool calls from raw model output, normalizing them into a standard format for the agent loop:

ParserModel FamilyFormat
HermesParserHermes / NousResearchXML-style <tool_call> tags
DeepSeekParserDeepSeekJSON in code blocks
QwenParserQwen / AlibabaFunction call blocks
LLaMAParserLLaMA / MetaStructured JSON output
MistralParserMistral AITool use format
GLMParserGLM / ZhipuCustom tags
KimiParserKimi / MoonshotBracketed calls

🏗️ Design Decisions

DecisionChoice MadeAlternatives ConsideredWhy This Approach
Tool execution threadingThreadPoolExecutor with 128 workersasyncio tasks, subprocess per toolTools use sync APIs internally (subprocess, file I/O). Thread pool prevents deadlock when tools call asyncio.run(). Large pool prevents starvation during parallel evals.
Isolation boundaryTask ID scopingContainer per rollout, namespace isolationTask ID is lightweight; all tool calls for a rollout share one sandbox (terminal, files, browser). Containers are too heavy for hundreds of parallel rollouts.
Toolset selectionConfig-driven distribution samplingHard-coded tool lists, per-rollout randomEnables research experiments (e.g., "does web access help SWE tasks?") without code changes. Per-group sampling ensures fair comparison.
Agent result formatImmutable AgentResult dataclassMutable state dict, live query APIAtomic capture makes it easy to persist, replay, and analyze rollouts without querying live state.
Phase 1 vs Phase 2Dual-mode abstractionSingle OpenAI API modePhase 2 (ManagedServer) offers lower latency for dense rollouts. Abstraction hides backend differences from environment code.
Reward verifier accessUnrestricted ToolContextLimited read-only accessVerifiers need to inspect arbitrary state (run commands, read files, check test results). Read-only access is insufficient for complex reward computation.

🔬 Algorithm Spotlight

Tool Call Parsing Strategy

The Problem: Different LLMs produce tool calls in radically different formats — XML tags, JSON blocks, structured outputs, custom delimiters. The agent loop needs a unified interface.

The Approach: A parser registry maps model identifiers to parser classes. Each parser implements extract_tool_calls(text) -> List[ToolCall]. Phase 2 mode selects the parser at environment init based on model config; Phase 1 relies on the OpenAI API's native parsing.

Why This Algorithm: A single regex-based parser would break on edge cases across 11 formats. Dedicated parsers per model family isolate format-specific logic. The registry pattern keeps the agent loop agnostic to model details.

graph TD
    A["Raw LLM Output"] --> B{"Phase 1 or 2?"}
    B -->|"Phase 1"| C["OpenAI API Native Parsing"]
    B -->|"Phase 2"| D["Parser Registry Lookup"]
    D --> E["Model-Specific Parser"]
    E --> F["extract_tool_calls()"]
    C --> G["Normalized ToolCall List"]
    F --> G
    G --> H["Agent Loop Dispatch"]
Trace through an example

Given: A DeepSeek model returns: ```json\n{"name": "terminal", "arguments": {"command": "ls -la"}}\n```

Step 1: Phase 2 mode is active. Parser registry looks up "deepseek" and selects DeepSeekParser.

Step 2: Parser scans for JSON code blocks, extracts the object, validates name and arguments fields.

Step 3: Returns [ToolCall(name="terminal", arguments={"command": "ls -la"})] in the normalized format.

Result: The agent loop dispatches this identically to a Hermes-format tool call, despite the completely different wire format.

⚠️ What Could Go Wrong

Failure ModeTriggerDetectionRecovery / Mitigation
Thread pool exhaustionMore concurrent tool calls than pool workers (128)Blocking on executor.submit()LLM calls block until workers free up. Backpressure prevents task loss. Pool size configurable via resize_tool_pool().
Tool execution timeoutTerminal command hangs beyond terminal_timeout (120s)Timeout exception in threadCaptured as ToolError in AgentResult.tool_errors. Loop continues; reward function decides severity.
Parser format mismatchModel outputs unexpected format (e.g., mixed XML/JSON)Parser returns empty listAgent loop treats turn as "no tool calls" and continues or stops naturally. Logged for debugging.
Sandbox cross-contaminationBug in task_id scopingReward verifier finds unexpected files/stateEach rollout's task_id isolates terminal sessions, file paths, browser profiles. Cleanup in try-finally.
ManagedServer crash (Phase 2)VLLM backend OOM or crashConnection error during LLM callAgentResult captures partial state. Rollout scored as incomplete; Atropos handles partial data.

⚡ Performance Profile

Hot Paths

The most performance-sensitive code paths in this module:

Resource Usage

Benchmark Environments

TerminalBench

Full terminal benchmark environment for evaluating agent terminal skills.

  • Complex multi-step terminal challenges
  • Full sandbox isolation per rollout

TBLite

Lightweight terminal benchmark variant for faster iteration.

  • Simplified scoring rubric
  • Lower resource requirements

YCBench

Y Combinator-style startup benchmark tasks.

  • Real-world coding challenges
  • Multi-tool evaluation

HermesSweEnv

SWE-bench integration for software engineering evaluation.

  • GitHub issue resolution tasks
  • Test suite verification

WebResearchEnv

Web research and information gathering environment.

  • Browser-based research tasks
  • Multi-source fact verification

Directory Map

Source tree for environments
environments/
├── __init__.py                    ← Module init, apply_patches()
├── agent_loop.py                  ← HermesAgentLoop, AgentResult, ToolError
├── tool_context.py                ← ToolContext for reward verifiers
├── hermes_base_env.py             ← HermesAgentBaseEnv, HermesAgentEnvConfig
├── agentic_opd_env.py             ← AgenticOpdEnv benchmark
├── hermes_swe_env.py              ← HermesSweEnv (SWE-bench)
├── web_research_env.py            ← WebResearchEnv
├── terminal_bench.py              ← TerminalBench full benchmark
├── terminal_bench_lite.py         ← TBLite lightweight variant
├── yc_bench.py                    ← YCBench startup challenges
├── tool_call_parsers/             ← 11 LLM format parsers
│   ├── __init__.py                ← Parser registry
│   ├── hermes_parser.py           ← Hermes XML-style parser
│   ├── deepseek_parser.py         ← DeepSeek JSON parser
│   ├── qwen_parser.py             ← Qwen function call parser
│   ├── llama_parser.py            ← LLaMA structured output parser
│   ├── mistral_parser.py          ← Mistral tool use parser
│   ├── glm_parser.py              ← GLM custom tag parser
│   ├── kimi_parser.py             ← Kimi bracketed call parser
│   ├── functionary_parser.py      ← Functionary format parser
│   ├── command_r_parser.py        ← Command-R format parser
│   ├── firefunction_parser.py     ← FireFunction format parser
│   └── generic_json_parser.py     ← Fallback JSON parser
├── patches.py                     ← Async-safety patches (legacy no-op)
└── README.md                      ← Module documentation