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.
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:
- Rollout Request — Atropos framework calls
get_next_item()on theTerminalBenchenvironment, requesting a new task from the dataset. - Dataset Item — The environment returns a dataset item containing a terminal challenge prompt (e.g., "Find all Python files larger than 1MB in /data").
- Prompt Formatting —
format_prompt()converts the dataset item into an OpenAI-format user message with system prompt and enabled toolsets. - Toolset Sampling —
HermesAgentEnvConfigsamples the tool distribution for this rollout group. All rollouts in the same group share the same tool configuration for fair comparison. - Agent Loop Start —
HermesAgentLoopbegins the multi-turn loop, calling the LLM with thetools=parameter listing available tool schemas. - Tool Call Parsing — The appropriate parser (e.g.,
HermesParserfor Hermes models) extracts structured tool calls from the LLM response. Phase 2 mode uses client-side parsing for lower latency. - Tool Execution — Each tool call is dispatched to
handle_function_call()in aThreadPoolExecutor(128 workers). The tool runs in a sandbox scoped to the rollout'stask_id. - Loop Continuation — Tool results are appended to the conversation history. The loop continues until
max_agent_turnsis reached or the model stops calling tools. - AgentResult Capture — The full conversation history, turns used, tool errors, and reasoning per turn are captured atomically in an
AgentResultdataclass. - ToolContext Creation — A
ToolContextis created for the reward verifier, giving it unrestricted access to all Hermes tools scoped to this rollout'stask_id. - Reward Computation —
compute_reward()uses theToolContextto verify the model's work (e.g., run the same command and check output), returning a score from 0.0 to 1.0. - Cleanup and Scoring — Sandbox resources (terminal session, browser profile) are cleaned up in a try-finally block. The
ScoredDataGroupis 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:
| Parser | Model Family | Format |
|---|---|---|
HermesParser | Hermes / NousResearch | XML-style <tool_call> tags |
DeepSeekParser | DeepSeek | JSON in code blocks |
QwenParser | Qwen / Alibaba | Function call blocks |
LLaMAParser | LLaMA / Meta | Structured JSON output |
MistralParser | Mistral AI | Tool use format |
GLMParser | GLM / Zhipu | Custom tags |
KimiParser | Kimi / Moonshot | Bracketed calls |
🏗️ Design Decisions
| Decision | Choice Made | Alternatives Considered | Why This Approach |
|---|---|---|---|
| Tool execution threading | ThreadPoolExecutor with 128 workers | asyncio tasks, subprocess per tool | Tools 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 boundary | Task ID scoping | Container per rollout, namespace isolation | Task 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 selection | Config-driven distribution sampling | Hard-coded tool lists, per-rollout random | Enables research experiments (e.g., "does web access help SWE tasks?") without code changes. Per-group sampling ensures fair comparison. |
| Agent result format | Immutable AgentResult dataclass | Mutable state dict, live query API | Atomic capture makes it easy to persist, replay, and analyze rollouts without querying live state. |
| Phase 1 vs Phase 2 | Dual-mode abstraction | Single OpenAI API mode | Phase 2 (ManagedServer) offers lower latency for dense rollouts. Abstraction hides backend differences from environment code. |
| Reward verifier access | Unrestricted ToolContext | Limited read-only access | Verifiers 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 Mode | Trigger | Detection | Recovery / Mitigation |
|---|---|---|---|
| Thread pool exhaustion | More 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 timeout | Terminal command hangs beyond terminal_timeout (120s) | Timeout exception in thread | Captured as ToolError in AgentResult.tool_errors. Loop continues; reward function decides severity. |
| Parser format mismatch | Model outputs unexpected format (e.g., mixed XML/JSON) | Parser returns empty list | Agent loop treats turn as "no tool calls" and continues or stops naturally. Logged for debugging. |
| Sandbox cross-contamination | Bug in task_id scoping | Reward verifier finds unexpected files/state | Each rollout's task_id isolates terminal sessions, file paths, browser profiles. Cleanup in try-finally. |
| ManagedServer crash (Phase 2) | VLLM backend OOM or crash | Connection error during LLM call | AgentResult captures partial state. Rollout scored as incomplete; Atropos handles partial data. |
⚡ Performance Profile
Hot Paths
The most performance-sensitive code paths in this module:
- Tool dispatch via ThreadPoolExecutor: O(1) submission, but tool execution time varies from milliseconds (file read) to minutes (terminal commands). The 128-worker pool is sized for high-concurrency benchmarks.
- Tool call parsing: O(n) per response where n is response length. Dedicated parsers avoid regex backtracking on complex outputs.
- Reward verification: Variable cost — may involve running test suites, compiling code, or browsing. Scoped by
ToolContextto prevent accidental global state access.
Resource Usage
- Memory: Each rollout holds its
AgentResult(full conversation history). For 30-turn rollouts with tool outputs, this can be 1-5 MB per rollout. Freed after scoring. - I/O: Heavy terminal and file I/O during tool execution. Sandboxed per
task_id. Network I/O for LLM calls (Phase 1) or shared memory for VLLM (Phase 2). - Threads: 128-thread pool shared across all environments.
resize_tool_pool()allows per-environment tuning.
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