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
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
What is in it
Environment and permissions.
OpenRathEnvis a transactional episode statemachine 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.ToolPolicybounds what a model under training can reach — tool allowlist, filesystem roots, command allow/deny, per-episode call budget. It is enforced indispatch_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 recordedToolExecutionFailurethe 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.dataexports 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:load_swebench(SWE-bench Verified)FAIL_TO_PASS/PASS_TO_PASS, binaryload_terminal_bench(Terminal-Bench)load_swesmith(SWE-smith)load_edgebench(EdgeBench / SForge)Trainer adapters.
to_verl_data_proto()produces a realverl.protocol.DataProto;to_trl_dataset()produces adatasets.Datasetfor TRL. Both import lazily —import rath.trainingstill pulls in noverl,torch,numpy,tensordict,trl, ordatasets.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
curlby name cannot stop
socket.connectinside an interpreter. A bound that can be stepped over is worse than no bound, because it reads like protection. SoToolPolicyhas nonetworkflag;BackendCapability.NETWORK_ISOLATIONexists instead, and a task declaringinternet=Falseis 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_dockernamed 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'sexistence 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_commandin one rollout wait forrun_shell_commandin an unrelated one. Measured: four independent sandboxes runningsleep 1concurrently took 4.68 s. Built-ins now declaresandbox_scoped = True, which narrows their lane to the calling session's sandbox — same four sandboxes now take1.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
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.
DataProto.from_dict, plus an optional CI job; a real verl install was not exercised locally.opensandbox,openviking,live_llm) were not run.created_at, which ATIF requires per step). The schema was never released, so there is no migration.Verification
1070 passed, 10 skippedon the non-credentialed matrix;ruff check,ruff format --check,mypy src(strict) clean;uv buildsucceeds; 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.mdcovers 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.