Skip to content

[Real-world active learning] PR 7: human-gated task rebuild on the stock loop - #104

Merged
amburger66 merged 2 commits into
masterfrom
real-world-active-learning
Jul 30, 2026
Merged

[Real-world active learning] PR 7: human-gated task rebuild on the stock loop#104
amburger66 merged 2 commits into
masterfrom
real-world-active-learning

Conversation

@amburger66

@amburger66 amburger66 commented Jul 30, 2026

Copy link
Copy Markdown

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

env_task = env.get_train_tasks()[i]     # (1)
cogman.reset(env_task)                  # (2)
run_episode_and_get_observations(...)   # (3) calls env.reset(...)

The task must be rebuilt at (1), for a different reason on each of the two paths:

  • Evaluation solves at (2). _solve_task (main.py:774) calls cogman.reset(env_task) with no override policy, so _reset_policy runs approach.solve(task) — before env.reset is ever called. A human reset performed at (3) would plan against a bench that no longer exists.
  • Exploration does not solve at (2): the online loop sets an override policy first (main.py:551), so _reset_policy takes that branch. The task still matters, because it is what env.reset initializes the simulated twin from — a stale one starts every episode from the captured scene rather than the arranged bench.

BaseEnv caches tasks, so PyBulletEnv overrides get_train_tasks/get_test_tasks to ask the executor first through a third ActionExecutor hook, tasks_for. _reset_pending is set by after_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_pending bookkeeping and the "not in the domain env" rule are unchanged — PyBulletDominoRealEnv still contributes only task_from_observation, with no notion of a human.

real_robot_human_reset (default True)

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. 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.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; that is also why the approach and monitor are the cheap ones (oracle + trivial), keeping an LLM out of a debug replay. _parse_plan and the plan .txt format are untouched.

Budget

exp_domino_real.yaml drops 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_env with no joints resolves to None, which fails ResetArmReply validation in dry mode and raises outright on hardware unless the RealRobot was constructed with home_joints — which make_real_robot doesn'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 genuine RealRobot(dry=True) with mock perception and an auto-confirming reset, the real executor, real CogMan, real run_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 reproduces main.py:551-565's call sequence rather than invoking main(), 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_reset ignored; reset_env sent 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_dry on, to hear the prompts without motion.

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
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.
@amburger66
amburger66 requested a review from yichao-liang July 30, 2026 15:40
@amburger66 amburger66 self-assigned this Jul 30, 2026
@yichao-liang

Copy link
Copy Markdown
Collaborator

LGTM! Congrats on completing the plan!

@amburger66
amburger66 merged commit 73862fe into master Jul 30, 2026
14 checks passed
@amburger66

Copy link
Copy Markdown
Author

Cross-link, per CONTRIBUTING: the babyrobot side of this migration finishes in BasisResearch/BabyRobotPredicator#55 (plan PR 8), which deletes the old JSON bridge_server.py now that this PR moved the last caller off it.

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.

2 participants