Skip to content

feat: RL infrastructure — agent environment, trajectory export (ATIF), benchmark loaders (SWE-bench / Terminal-Bench / SWE-smith / EdgeBench), and verl / TRL adapters - #48

Open
Enderfga wants to merge 18 commits into
Rath-Team:mainfrom
Enderfga:feat/rl-infra-four-dimensions

Conversation

@Enderfga

Copy link
Copy Markdown
Contributor

Turns OpenRath from an agent orchestration framework into infrastructure you can collect RL rollouts with, evaluate against public benchmarks with, and hand to a trainer.

This is a large, opinionated addition. It is offered as much as a reference implementation as a merge request — if it is out of scope for the main line, the branch stands on its own and can be used from a fork. Everything below is verifiable from the tests in this PR.


What you can do with it

# 1. Run an episode under an enforced permission boundary
env = OpenRathEnv(OpenRathEnvConfig(
    backend="opensandbox",
    tool_policy=ToolPolicy(fs_roots=("/workspace",), command_deny=("rm",), max_calls=200),
))

# 2. Collect rollouts in parallel and hand them to a trainer
batch = collect_benchmark_rollouts(tasks, policy, max_workers=8)
proto   = to_verl_data_proto(batch)   # verl DataProto
dataset = to_trl_dataset(batch)       # TRL / GRPO / PPO

# 3. Load a public benchmark, and be told what your backend cannot run
report = load_terminal_bench(tasks_dir, features=backend.capabilities().features)
print(report.summary())   # "229 runnable, 12 skipped (95% coverage)"
for skipped in report.skipped:
    print(skipped.task_id, skipped.reason)   # "... backend lacks: compose"

# 4. Mine forked branches for preference pairs
pairs = extract_preference_pairs(build_session_graph(sessions, rewards=rewards))

What is in it

Environment and permissions. OpenRathEnv is a transactional episode state
machine over the existing Session/Sandbox/FlowToolCall stack: reset() rolls back completely on failure, a started action keeps its trajectory record even if reward or verification then fails, and normal terminal paths close the session WAL while faults abandon it.

ToolPolicy bounds what a model under training can reach — tool allowlist, filesystem roots, command allow/deny, per-episode call budget. It is enforced in dispatch_flow_tool(), the single point every tool call passes through, so a permission cannot be talked around by the model. A denied call never reaches the tool body; it returns as a recorded ToolExecutionFailure the model can react to, rather than an exception that kills the episode.

Trajectories. A compact, versioned episode protocol (episode_start → step* → episode_end) whose size grows linearly with the transcript, rather than storing a full observation per step. to_atif() exports ATIF-v1.7, the interchange format Harbor, TRL, and SkyRL already read.

rath.data exports the Session lineage DAG — the piece ATIF cannot express. ATIF nests subagents but has no fork/merge graph; OpenRath's lineage has both. extract_preference_pairs() turns scored siblings of a common parent into chosen/rejected pairs for DPO or reward-model training: siblings saw the same state before diverging, which is the only comparison here not confounded by a different starting point.

Benchmarks. Four loaders map external datasets onto one task/verifier protocol.
Each returns a LoaderReport, never a bare task list — a score computed over a silently truncated subset is not a score:

Loader Verifier Requires
load_swebench (SWE-bench Verified) FAIL_TO_PASS / PASS_TO_PASS, binary per-task image
load_terminal_bench (Terminal-Bench) task's own test suite per-task image; compose for 12 of 241 tasks
load_swesmith (SWE-smith) binary test lists per-task image (repository-level, ~250 total)
load_edgebench (EdgeBench / SForge) judge container host Docker daemon

Trainer adapters. to_verl_data_proto() produces a real verl.protocol.DataProto;
to_trl_dataset() produces a datasets.Dataset for TRL. Both import lazily — import rath.training still pulls in no verl, torch, numpy, tensordict, trl, or datasets.

Decisions worth arguing with

These are the places where the obvious implementation is wrong, and the code deliberately does something else.

Network isolation is a backend capability, not a policy field. Denying curl
by name cannot stop socket.connect inside an interpreter. A bound that can be stepped over is worse than no bound, because it reads like protection. So ToolPolicy has no network flag; BackendCapability.NETWORK_ISOLATION exists instead, and a task declaring internet=False is skipped on a backend that cannot enforce it.

Loaders publish coverage, including zero. EdgeBench scores inside a second
container the harness starts, and its own documentation says running the harness inside a container hits Docker-in-Docker problems. On a container-based sandbox every EdgeBench task is therefore reported as skipped, with host_docker named as the missing capability. The loader exists for protocol compatibility; it does not claim a full EdgeBench run is possible here.

All 241 Terminal-Bench tasks ship a docker-compose.yaml, so the file's
existence says nothing about whether a task needs multi-container orchestration. Only the service count does — 229 declare one service, 12 declare more. Keying on the file would have skipped 229 runnable tasks.

Built-in tools are process-wide singletons, so serializing "unsafe" tools per
instance would make run_shell_command in one rollout wait for run_shell_command in an unrelated one. Measured: four independent sandboxes running sleep 1 concurrently took 4.68 s. Built-ins now declare sandbox_scoped = True, which narrows their lane to the calling session's sandbox — same four sandboxes now take
1.10 s, while two sessions sharing one sandbox still take turns.

ATIF is pinned at v1.7. It gained fields at 1.3, 1.4, 1.6, and 1.7; a version
bump is a code change, not a silent reinterpretation of the same document.

Exactly one DAG extractor ships. Linearizing search-and-backtrack traces and
reconstructing multi-agent provenance are both possible from the exported graph, but no consumer has asked for either, and an abstraction built for an unknown consumer is waste. They are example recipes, not APIs.

What this is not

No rollout service, no Ray, no scheduler — parallelism stays with whatever the user already runs. No Gym/Gymnasium API and no claim of Gym compatibility. No trainer tensors, tokenization, advantages, or optimization: OpenRath is the sampling layer, not the learner.

Limits, stated plainly

  • No loader has been executed against a real Docker image in this branch. Field
    names and image-naming conventions were pinned against the live datasets and Docker Hub rather than written from memory, and the mapping is tested against committed fixtures — but "runs a real SWE-bench instance end to end" is not demonstrated here.
  • The verl adapter is tested against a fake module implementing
    DataProto.from_dict, plus an optional CI job; a real verl install was not exercised locally.
  • Credentialed suites (opensandbox, openviking, live_llm) were not run.
  • The compact trajectory schema is bumped to v2 (records now carry a UTC
    created_at, which ATIF requires per step). The schema was never released, so there is no migration.

Verification

1070 passed, 10 skipped on the non-credentialed matrix; ruff check, ruff format --check, mypy src (strict) clean; uv build succeeds; examples 13–18 exit zero. 109 files, +10,818 / −254.

The 15 commits are individually reviewable and ordered by dependency: backend capabilities → per-task sandbox spec → tool policy → capability gate → trajectory timestamps → ATIF → lineage DAG → preference pairs → four loaders → TRL adapter → end-to-end and docs.

Docs

ONLINE_ENV.md covers the tool policy and why network isolation is not in it, the three trajectory layers and the ATIF version pin, and the coverage contract for every loader.

kangkangzi2025 and others added 18 commits July 8, 2026 14:22
…compile-visual

docs: add workflow compile visual
Adds an environment-style transactional executor over OpenRath tools, a compact
versioned trajectory protocol, a benchmark task/verifier/runner state machine,
validated episode-owned rollout batches with bounded collection, and an optional
verl DataProto adapter.

Shared seams extracted along the way: flow-tool dispatch and result projection,
a controlled Session chunk-append path, and reusable JSONL persistence.
A permission that lives in a prompt is one the model can walk around. Every tool
call passes through dispatch_flow_tool, so the bound is enforced there: a denied
call never reaches the tool body and comes back as an auditable failure the model
can react to.

Network isolation is deliberately not a policy field. Denying curl by name cannot
stop socket.connect inside an interpreter, and a bound that can be stepped over is
worse than none. It is a backend capability instead.
ATIF requires an ISO-8601 timestamp on every step, and the compact trajectory never
recorded time. The schema is unreleased, so bump it rather than bolt a parallel
clock onto the exporter.
Harbor, TRL, and SkyRL read ATIF. A trajectory format nobody else can read is a
parallel universe; this is the passport out of ours. The version is pinned: ATIF
adds fields between minor versions, so a bump is a code change.
ATIF can express a parent embedding subagents, but not fork/merge. The lineage DAG
is the structure no competing framework exports, so it is exported losslessly and
interpreted nowhere else.
Two branches from one parent saw the same state and then diverged, so their reward
gap is the only comparison in the system that is not confounded by a different
starting point. Branches of different parents are never paired.
…fier

Fields and the image-name escaping were pinned against the real dataset and Docker
Hub, not written from memory.
Pinned against the real repository: all 241 tasks ship a docker-compose file, so
its existence is not the signal — the service count is. 229 declare one service;
only the other 12 need multi-container orchestration. task.yaml declares no network
policy, so the loader does not claim tasks are offline.
…ibility

SWE-smith's images are repository-level, an order of magnitude fewer than one per
instance, which is what makes a large rollout batch affordable.

EdgeBench scores in a second container started by the harness, which needs the host
Docker daemon; its own docs say running inside a container hits Docker-in-Docker
problems. On a container backend every task is skipped with that reason recorded
rather than attempted and failed.
One row per episode: prompt, completion, reward, and the ATIF document for anything
that wants the trajectory rather than the summary.
Adds the cross-dimension integration test, two examples, and the documentation for
the tool policy, trajectory interop, and honest benchmark coverage.
…cover both optional extras

Two failures the local environment was hiding.

tests/benchmark/datasets/ was importable as a namespace package named `datasets`,
which shadowed Hugging Face's package and made `datasets.Dataset` vanish inside the
TRL adapter. Renamed to tests/benchmark/loaders/.

PyYAML reached the local run as a transitive dependency, so the Terminal-Bench
loader's tests passed here and failed on a clean CI runner. It now ships in the dev
group so the standard matrix exercises the loader, the tests skip when it is absent,
and users still only get it through the optional benchmarks extra.

Adds a TRL adapter CI job mirroring the verl one, so to_trl_dataset is executed
somewhere rather than skipped everywhere.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants