[Real-world active learning] PR 7: human-gated task rebuild on the stock loop - #104
Merged
Conversation
Between episodes a human rearranges the dominoes, and the episode's task is
rebuilt from what the cameras then see. That makes the stock online-learning
loop work on real hardware: every episode faces a physically different scene
instead of replanning against one frozen capture.
The ordering is the crux and it dictates where this lives. The loop is
env_task = env.get_train_tasks()[i] # (1)
cogman.reset(env_task) # (2) the approach SOLVES here
run_episode_and_get_observations(...) # (3) calls env.reset(...)
so a human reset performed in env.reset would plan against a bench that no
longer exists. It therefore happens in (1): for a real environment "give me
the train task" honestly means "look at the bench". BaseEnv caches tasks, so
PyBulletEnv overrides get_train_tasks / get_test_tasks to ask the executor
first, via a third ActionExecutor hook, tasks_for.
_reset_pending is set by after_reset rather than at the end of an episode --
there is no end-of-episode hook, and the loop always resets before it steps,
so marking it on reset yields exactly one prompt per episode.
real_robot_human_reset (default True) governs it. Off keeps the captured
scene, which is what replay_plan needs: rebuilding from a live look would
rename and re-place the very objects a recorded plan refers to.
replay_plan is rewired onto CogMan's episode loop with an override policy --
the same shape main.py uses online -- instead of the deprecated
utils.run_policy. With an override set CogMan never asks the approach to
solve, so the plan being replayed is the plan that executes; the approach and
monitor are therefore the cheap ones, keeping an LLM out of a debug replay.
exp_domino_real.yaml drops to 2 cycles x 1 seed: every episode costs a
physical reset by a human, so common.yaml's 10 x 3 is days, not hours.
Found and fixed along the way: reset_env with no joints resolves to None,
which fails ResetArmReply validation in dry mode and raises outright on
hardware unless the RealRobot was built with home_joints. The executor now
captures the twin's home configuration at attach time -- the simulated arm is
still at home then -- and passes it explicitly.
amburger66
marked this pull request as ready for review
July 30, 2026 15:05
The ordering justification in tasks_for and the online test overstated the case: it claimed the approach solves inside cogman.reset, full stop. That is true on the evaluation path (_solve_task resets with no override policy, so _reset_policy calls approach.solve) but NOT on the exploration path, where main.py sets an override policy first and _reset_policy takes that branch. The conclusion is unchanged -- the task must still be rebuilt at task-request time -- but for a second reason on the exploration path: the task is what env.reset initializes the simulated twin from, so a stale one starts every episode from the captured scene rather than the one just arranged. Both reasons are now written down. Also renames "bench" to "scene" throughout the files this PR touches (36 occurrences), matching the terminology used elsewhere for the real-world domino setup. Two phrases were reworded rather than substituted, because the swap would have collapsed a contrast into "a physical scene is one scene". Plus comment trims, and a rewrap of a docstring line that ran past 80.
Collaborator
|
LGTM! Congrats on completing the plan! |
yichao-liang
approved these changes
Jul 30, 2026
Author
|
Cross-link, per CONTRIBUTING: the babyrobot side of this migration finishes in BasisResearch/BabyRobotPredicator#55 (plan PR 8), which deletes the old JSON |
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.
PR 7 of the real-world active learning plan. Depends on #95.
What this adds
Between episodes a human rearranges the dominoes, and that episode's task is rebuilt from what the cameras then see. That is what makes the stock online-learning loop work on the bench: every episode faces a physically different scene instead of replanning against one frozen capture. No new entrypoint.
Why the rebuild happens at task-request time
The task must be rebuilt at (1), for a different reason on each of the two paths:
_solve_task(main.py:774) callscogman.reset(env_task)with no override policy, so_reset_policyrunsapproach.solve(task)— beforeenv.resetis ever called. A human reset performed at (3) would plan against a bench that no longer exists.main.py:551), so_reset_policytakes that branch. The task still matters, because it is whatenv.resetinitializes the simulated twin from — a stale one starts every episode from the captured scene rather than the arranged bench.BaseEnvcaches tasks, soPyBulletEnvoverridesget_train_tasks/get_test_tasksto ask the executor first through a thirdActionExecutorhook,tasks_for._reset_pendingis set byafter_reset, not at episode end: there is no end-of-episode hook, and the loop always resets before it steps, so marking it on reset gives exactly one prompt per episode.Deviation from the plan
The plan puts this on
RealWorldEnv.get_train_tasks. PR 6 replaced that wrapper with an injected executor, so it lands as a third port hook instead. The ordering argument, the_reset_pendingbookkeeping and the "not in the domain env" rule are unchanged —PyBulletDominoRealEnvstill contributes onlytask_from_observation, with no notion of a human.real_robot_human_reset(default True)Off keeps the captured scene, which is what
replay_planneeds: rebuilding from a live look would rename and re-place the very objects a recorded plan refers to. It is refused at construction if the robot has no perception, rather than raising after the human has been asked to stand by.replay_plan
Rewired onto CogMan's episode loop with an override policy — the same shape
main.pyuses online — instead of the deprecatedutils.run_policy. With an override set CogMan never asks the approach to solve, so the plan being replayed is the plan that executes; that is also why the approach and monitor are the cheap ones (oracle+trivial), keeping an LLM out of a debug replay._parse_planand the plan.txtformat are untouched.Budget
exp_domino_real.yamldrops to 2 cycles × 1 seed from common.yaml's 10 × 3 — every episode costs a physical reset by a human. Verified by loading the config, not by reading it.A bug found on the way
reset_envwith no joints resolves toNone, which failsResetArmReplyvalidation in dry mode and raises outright on hardware unless theRealRobotwas constructed withhome_joints— whichmake_real_robotdoesn't do. The executor now captures the twin's home configuration at attach time (the simulated arm is still at home then, verified) and passes it explicitly. Mutation M5 covers it.Tests
test_domino_real_online.py— 6 tests, stub robot, never skips: the rebuild happens before the task is handed over, one prompt per episode, a new look replaces the cached task, the test split too, human-reset-off keeps the capture, and an unattached env never prompts.test_domino_real_offline_e2e.py— a genuineRealRobot(dry=True)with mock perception and an auto-confirming reset, the real executor, realCogMan, realrun_episode_and_get_observations. Asserts one prompt per episode, one look per option boundary (6 looks over 2 episodes = 2 resets + 4 boundaries), and well-formed gripper commands. It reproducesmain.py:551-565's call sequence rather than invokingmain(), which would add arg parsing and result IO without testing more. Skips without the submodule.Seven mutations checked, all caught: no rebuild; next reset never owed; no reset at startup;
human_resetignored;reset_envsent no home joints; each split hook removed.Gates:
tests/envs+tests/pybullet_helpers+test_main= 292 passed / 15 skipped without the submodule, 302 passed / 0 skipped with it; mypy clean over 709 files; pylint 88/88; isort/yapf/docformatter verified by exit code; submodule untouched.Still not bench-validated
Nothing here has run on hardware. The human wait, the arm homing before a person reaches in, and per-option shipping are exercised only against a dry robot and mock perception. Worth a first session with
real_robot_dryon, to hear the prompts without motion.