Skip to content

refactor(robot): unify the robot stack - one sim process shape, batched-first bridge/agent, grouped evals#481

Draft
lukass16 wants to merge 14 commits into
mainfrom
lukass/phys-experimental
Draft

refactor(robot): unify the robot stack - one sim process shape, batched-first bridge/agent, grouped evals#481
lukass16 wants to merge 14 commits into
mainfrom
lukass/phys-experimental

Conversation

@lukass16

@lukass16 lukass16 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Issue

The robot framework maintained two parallel implementations - single-env and vectorized - with duplicated agent loops, recorders, bridges, and eval paths. Sim execution had three separate threading mechanisms, and the vectorized eval path leaked robot-specific arguments (contract=, vectorized=) into the generic Taskset.run API.

Solution

  • SimThread replaces the three SimRunner strategies: the sim always owns the main thread, serving runs on a background thread. The same mechanism covers CPU envs and Isaac.
  • RobotBridge is now batched-first; VecRobotBridge and IsaacBridge are removed. result() returns per-slot scores under "slots".
  • Recording moves to a shared hud.telemetry.robot module, replacing the three previous recorder classes. LeRobot dataset writing is split into agents/robot/dataset.py.
  • RobotAgent drives any number of env slots with a single loop; VecRobotAgent is removed.
  • Taskset.run(agent, runtime=..., num_envs=N) runs vectorized evals through a generic rollout_group; the robot-specific kwargs are removed and hud/eval no longer imports robot code.
  • New surfaces: env.gym(make_env) serves a gym-style sim declaratively (contract and capability derived from the factory), and hud.wrap(env) streams traces from a loop the user owns.

Outcome / Verification

Roughly 1,500 lines of duplicated code removed; the diff against main is net negative despite the new features. Test suite and lint pass. Verified end-to-end by serving a vectorized stub env in a child process and running Taskset.run(num_envs=3) to three graded traces. Downstream consumers (assembly_bench, bench, rl) updated.


Note

High Risk
Large refactor across sim threading, the robot wire protocol (scalar vs batched terminated), eval orchestration, and telemetry/recording; behavior changes for Isaac-style sims and vectorized grading.

Overview
This PR collapses the parallel single-env and vectorized robot paths into one stack: SimThread replaces the three SimRunner strategies so every sim uses the same shape (sim on main, serving on a background loop). hud serve / serve_blocking pick that automatically when env.gym(...) is attached.

Environment: env.gym(make_env) plus GymBridge derive the contract, capability, and robot WebSocket from a gym factory; bridge result() now returns per-slot scores under "slots". hud.wrap(env) is exposed lazily from hud for in-process trace streaming.

Agent: RobotAgent is the only harness—drive(runs, client) drives N slots with one open-loop chunk loop; telemetry goes through shared TraceRecorder in hud.telemetry.robot, with LeRobot saving in DatasetWriter (per-trace shards; vectorized runs stream telemetry only). Adapters gain adapt_chunk and batched LeRobotAdapter support.

Eval: Taskset.run(..., num_envs=N) routes to generic vec_rollout, grading each trace from slots[i]; BatchedAgent defaults batch_size to live concurrency.

Reviewed by Cursor Bugbot for commit 5736eb2. Bugbot is set up for automated code reviews on this repo. Configure here.

lukass16 added 8 commits July 3, 2026 17:49
The sim always owns the process main thread; serving (control channel +
robot WebSocket) runs on a background loop thread, with every sim touch
queued back to main. Cheap CPU envs block on the queue; Isaac/Omniverse
gets its Kit pump from the idle hook - no per-sim threading choices.
serve_blocking is the single entry hud serve and the module runner use,
replacing the serve_pumped/asyncio.run branching.
…y.robot

Recording is shared infrastructure (agent harness, hud.wrap, gym bridges),
not agent code. TraceRecorder (one trace, span emission only - lifecycle
stays with the caller) and JobRecorder (a vectorized env as one Job of
per-episode traces) replace Recorder/VecRecorder/EpisodeRecorder and their
dual-mode flags; video streaming moves alongside. The LeRobot dataset half
of record.py becomes agents/robot/dataset.py (DatasetWriter). InferenceStep
gains contract-derived per-dim action names so the viewer can label plots.
…mBridge

One bridge serves num_envs slots in lockstep; a plain single env is a
batch of one speaking the scalar wire framing on the same code path, so
VecRobotBridge and IsaacBridge collapse into the base. GymBridge is the
generic bridge over any gym-style factory (arg partitioning by factory
signature, rebuild-on-instance-change, success probing, torch/int action
handling). Grading becomes result() -> {score, success, slots: [...]}
(one dict per slot), replacing the result_batch plumbing through the
endpoint's JSON-RPC. SimRunner strategies are gone - bridges route every
sim touch through the shared SimThread - and the endpoint gains
serve_blocking() for split-process sims (replacing serve_forever).
env.gym(make_env) turns any gym-style factory into a served robot sim:
contract derived from a sample observation (round-tripped through an
editable contract.json), capability minted, lifecycle wired to the env's
hooks, episodes driven via the returned handle (sim.reset / sim.result).
A factory accepting num_envs is the vectorization declaration. hud.wrap
is the loop-owning counterpart: wrap an env you drive yourself and every
episode streams to the platform as a trace under one job (lazy top-level
attr so core hud stays free of the robot extra).
One open-loop chunk queue drives single and vectorized envs alike: the
wire framing (scalar terminated vs [N] mask) sets the batch size, spent
slots refill from one batched forward, adapt_chunk converts per slot at
inference time. __call__(run) is the generic rollout contract (a group
of one, still coalescing through ainfer so BatchedAgent works unchanged);
drive(runs, client) is the grouped-eval entry recording spans per slot.
VecRobotAgent and VecLeRobotAdapter are deleted - LeRobotAdapter maps
batched and unbatched observations with the same wiring.
…vs=)

One vectorized env instance becomes num_envs graded runs sharing a
group_id, behind the same Taskset.run surface and with no robot concepts
in hud/eval: the atom goes through the normal env client and template
(num_envs injected into task args; an env that doesn't accept it fails
loudly), the contract rides the capability manifest, the agent's
drive(runs, client) entry drives the slots, and run i grades from the
template result's slots[i]. Replaces vec_rollout and the vectorized=/
contract= kwargs; num_envs composes with group= (k instances x N traces,
group ids per instance) and requires a self-managed runtime.
env.gym-first environment side, the one sim-serving process shape
(SimRunner section replaced), slots-based grading, serve_blocking for
split-process sims, a new vectorized-envs-and-grouped-evals section, and
a refreshed API summary.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3d341a1. Configure here.

Comment thread hud/environment/robot/introspect.py
Comment thread hud/agents/robot/agent.py Outdated
@mintlify

mintlify Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hud 🟢 Ready View Preview Jul 6, 2026, 9:29 PM

lukass16 added 6 commits July 6, 2026 23:03
batch_size is now an optional cap: with no value the worker stacks every
ainfer call queued within the max_wait_s window, so the forward is as wide
as whatever is in flight and max_concurrent never needs manual pairing.
A set cap still flushes the window early once reached.
"group" already means statistical repeats (group=k); the vectorized atom
deserves its own word. Docstrings and error messages now say "vectorized"
throughout and the atom's docstring is tightened. Also narrows the minted
trace id before trace_enter (pyright).
hud/agents is strict-typed in CI: restore the stubless-torch Any alias in
LeRobotAdapter, type the chunk queues as deque[ActionArray], narrow the
optional model to a local before the loop, and suppress the deliberate
private _client check when picking recorder mode.
Cameras in derived gym contracts are tagged type: rgb (no dtype: image),
so frames mapped to non-video LeRobot columns. Image detection now accepts
both conventions; missing dtypes default to float32 and missing shapes are
filled from the first real frame before the dataset is created. Also
imports importlib.util explicitly (pyright).
Finished slots keep acting so the [N, A] action frame stays complete, but
their chunk refills no longer emit InferenceSteps - matching the existing
observation guard, so a finished slot's trace gains no orphan inference.
Adds a Taskset.run parameter reference (runtime / group / num_envs /
max_concurrent / rollout_timeout / job) under the vectorized-evals section,
updates the batching example for the self-sizing BatchedModel, and aligns
wording with the vec_rollout rename.
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.

1 participant