Skip to content

Agent bilevel: LLM-proposed params, domino domain, and agent-SDK tooling overhaul#40

Merged
yichao-liang merged 270 commits into
masterfrom
sim-learning
Jun 25, 2026
Merged

Agent bilevel: LLM-proposed params, domino domain, and agent-SDK tooling overhaul#40
yichao-liang merged 270 commits into
masterfrom
sim-learning

Conversation

@yichao-liang

Copy link
Copy Markdown
Collaborator

Summary

Integration of the sim-learning work since #38 (Info-seeking active experiments). This batch advances the agent-driven bilevel pipeline and brings the pybullet domino domain to a working state, plus a substantial agent-SDK tooling overhaul and supporting debug/analysis scripts.

Highlights

Agent SDK & bilevel sketching

  • Major rework of agent_sdk/tools.py and agent_sdk/bilevel_sketch.py (the tool surface the agent uses to propose, vet, and refine plan sketches).
  • LLM-proposed initial continuous params: the LLM can now propose per-step params in the sketch; refinement tries them first and falls back to uniform backtracking (gated by agent_bilevel_use_llm_initial_params). Wired through agent_bilevel_approach.py and agent_sim_learning_approach.py.
  • Removed the unused save_low_level_action_images option from evaluate_option_plan (used in ~2% of calls, never read by the agent) and its docs.

Domino domain

  • Restructured pybullet_domino (composed_env.pyenv.py, component refactors for grid/domino), reworked predicates/processes and skill factories.
  • Substantially reworked domino_task_generator.py and added GT sampler tests; oracle/LLM-param configs reach the high-20s/25 success range.

Approaches / configs

  • Predicate-invention and partially-observable approaches advanced; removed obsolete agent_abstraction_learning_approach.py, agent_closed_loop_approach.py, and chain_reward.py.
  • Updated predicatorv3 configs (agents.yaml, envs/all.yaml) and settings.py flags.

Tooling & tests

  • New scripts/domino_debug/ analysis suite (turn diversity/counting, failure reproduction, state rendering, ik-val sweeps, cascade/infront-drift probes).
  • New/expanded tests: tests/agent_sdk/test_bilevel_sketch_samplers.py, domino GT sampler tests, skill-factory integration, agent-SDK tools.

Stats

80 files changed, +6476 / −2877, 266 commits since the #38 merge-base.

Checks

The 4 README CI checks (pytest, mypy, pylint, autoformat) run in CI. Local spot-checks on the most recent changes pass; any pre-existing local-vs-CI formatter/mypy version deltas are unaffected by this batch.

yichao-liang and others added 30 commits April 16, 2026 10:16
- New setting agent_bilevel_explorer_max_samples_per_step (default 50),
  separate from the solve-path budget, so the explorer's backtracking
  cost is independently tunable.
- Log the actual experiment plan (option names, objects, params) after
  refinement so the explorer's output is visible alongside the
  existing sketch/truncation log lines.
- Test config updated to set both budgets explicitly.
AgentSimLearningApproach extends AgentBilevelApproach to learn process
dynamics online. Each cycle: the agent synthesizes parameterized
process rules via Claude (using run_python / evaluate_simulator /
test_simulator MCP tools), parameters are fitted via emcee MCMC, and
the learned dynamics are composed with a kinematics-only PyBullet
oracle into a combined option model for plan refinement.

Key pieces:
- predicators/approaches/agent_sim_learning_approach.py: the approach.
  Initialises with a kinematics-only option model (so
  AgentBilevelExplorer sees disagreements at process-dynamic subgoals
  like JugFilled/Boiled), and replaces it with the kin+learned model
  after each successful synthesis cycle.
- predicators/agent_sdk/tools.py: create_synthesis_tools() builds the
  three MCP tools the synthesis agent uses; extra_mcp_tools field and
  get_allowed_tool_list(extra_names=) plumbing lets the approach
  inject them into the session.
- predicators/code_sim_learning/: ParamSpec, fit_params (emcee MCMC),
  compute_mse, LearnedSimulator.
- predicators/ground_truth_models/boil/gt_simulator.py: ground-truth
  process-dynamics simulator for the boil environment.
- tests/: approach and param-fitting tests.
- agents.yaml: comment out agent_bilevel preset, add agent_sim_learning
  with explorer=agent_bilevel and skip_test_until_last_ite_or_early_stopping.
- common.yaml: disable failure/test video recording, set
  num_online_learning_cycles=1 for faster iteration.
Simulation primitives (code_sim_learning/utils.py):
- apply_rules(state, rules, params) → ProcessUpdate
- merge_updates(base_state, updates, process_features) → State
- simulate_step(state, action, base_env, rules, params, features) → State
These replace _build_fitted_step_fn, merge_process_updates,
_sim_fn_from_rules, and the body of _build_combined_simulator.

GT simulator factory (ground_truth_models):
- GroundTruthSimulatorFactory ABC + get_gt_simulator(env_name) discovery,
  following the existing get_gt_options / get_gt_nsrts pattern.
- PyBulletBoilGroundTruthSimulatorFactory registered in boil/.
- Replaces the hardcoded _load_oracle_simulator in the approach.

Oracle ablation flags (settings.py):
- agent_sim_learn_oracle_sim_program: load GT rules, skip synthesis.
- agent_sim_learn_oracle_sim_params: use GT param values, skip MCMC.

Also: kin_env → base_env rename throughout, redundant self._types
assignment removed, process_features computed once in __init__.
- yapf + isort autoformatting applied to all touched files.
- pylint: fix logging-not-lazy in agent_bilevel_explorer, add
  broad-except and reimported disables in agent_sim_learning_approach.
- mypy: fix base/env variable name collision, add type: ignore on
  lambda inference, add return type annotations to GT factory methods.
Use utils.abstract to evaluate expected atoms in low-level search so
that DerivedPredicates — which require a Set[GroundAtom] rather than a
State — are handled correctly alongside regular predicates.
When sequential simulate calls differ only in process features (as in
the combined kinematic+learned simulator), reapplying joint positions
and tearing down/recreating grasp constraints causes visible arm
jitter. Compare robot poses first and skip the kinematic reset path
when they already match.
Factor simulator synthesis into a shared _learn_simulator helper so
that both learn_from_offline_dataset and learn_from_interaction_results
can trigger it on their respective trajectory sources. Also create a
separate headless env for parameter fitting so MCMC's thousands of
_set_state calls don't thrash the GUI env during training.
Replace the silent run_mcmc call with a manual sample loop that logs
step count and best log-probability roughly five times per run, and
flushes handlers so the updates appear promptly under buffered
logging.
Type-annotate **kwargs on PyBullet env __init__ overrides so mypy
doesn't flag them. Initialize attrs used by _domain_specific_step in
__init__ (pybullet_coffee, pybullet_switch) to silence
defined-outside-init. Type-ignore the emcee import. Fix encoding,
unused, protected-access, and redefined-outer-name warnings in the
sim-learning tests and agent-SDK tooling.
When a held object's grasp constraint is recreated via _set_state, the
gripper frame must match the original world pose exactly — otherwise
the recorded base_link->object offset is rotated and the object lands
at the wrong world position when the gripper next moves. The State
representation only carries (x, y, z, tilt, wrist), so IK during reset
can pick a different wrist-roll solution and corrupt the constraint.

Thread joint_positions from PyBulletState.simulator_state through
reset_state so we skip IK and restore the exact arm configuration.
Falls back to IK when joints aren't available (plain State).

Also wire wait-termination so refinement and execution can stop Wait
when expected atoms hold instead of running to
max_num_steps_option_rollout: set _abstract_function on the option
model in BilevelPlanningApproach (mirrors AgentPlannerApproach), pass
abstract_function into option_plan_to_policy in
BilevelProcessPlanningApproach, and inject wait_target_atoms per
sample in run_low_level_search.
After resetJointState, PyBullet's getLinkState returns a stale link
pose from the previous FK cache, producing 50-500μm drift in the EE
pose readback. Pass computeForwardKinematics=1 so world poses are
recomputed from current joints on every call.

Also skip the explicit finger reset in reset_state when joint_positions
are provided: arm_joints already includes the finger joints, so
set_joints has restored them to their exact continuous values, and the
subsequent loop was overwriting them with the discrete-snapped value
from _fingers_state_to_joint. The finger reset still runs on the IK
path where set_joints leaves fingers untouched.

Together these eliminate the "Could not reconstruct state exactly in
reset" warning noise (24 -> 0 on the boil-oracle run).
common.yaml: switch to one demonstration per task with no online
learning cycle so launch_simp.py exercises only the offline pipeline.

agents.yaml (agent_sim_learning): turn on oracle_sim_program with
oracle_sim_params disabled so synthesis fits parameters but starts
from the ground-truth program structure.
…flag)

Investigation found no measurable difference in reported Cartesian world
position or orientation whether the flag is True or False, so the
override introduced earlier was not needed.
ParamSpec gains optional lo/hi fields for clamping sampled values.
fit_params now reads num_steps from CFG.code_sim_learning_num_mcmc_steps
by default; passing 0 (or setting the flag to 0) skips emcee entirely
and returns the initial parameter values as the fit result.  burn_in is
also clamped to num_steps-1 to avoid emcee errors on very short runs.
Adds a test covering the skip-MCMC path via CFG.
Replace the module-level BOIL_PARAM_SPECS list with _build_param_specs()
so water_fill_speed is derived from CFG.boil_water_fill_speed at call
time rather than import time.  All specs now carry lo=0.0 to prevent
MCMC from sampling physically invalid negative values.  get_param_specs()
is updated to call _build_param_specs() so per-run CFG values are always
reflected.
Oracle parameter perturbation now uses the relative scale from
CFG.agent_sim_learn_oracle_sim_param_noise_scale (default 0.2) instead
of a hard-coded 20 % figure, and clamps perturbed values to the lo/hi
bounds declared in each ParamSpec.  Also improves the log message when
MCMC is skipped (num_mcmc_steps == 0) so it is clear no fitting occurred.
Converts _build_combined_simulator to an instance method so it can
capture self, recreate the base env on pybullet.error, and retry once.
Also catches pybullet.error in the oracle option model alongside
OptionExecutionFailure. Updates agents.yaml config for testing.
Reproduces the exact domino test tasks from a run (same seed,
test_env_seed_offset, and domino flags) and saves a PNG of each test
task's initial state, labeling solved vs failed tasks.
Extract resolve_refine_timeout and refine_and_validate_report into
bilevel_sketch as the shared refinement + forward-validation + report
core. Synthesis (run_refinement_for_synthesis) and the new planner
refine_plan_sketch tool both call it, differing only in setup glue:
synthesis fits PARAM_SPECS and rebuilds the option model per call,
while the planner uses the prebuilt ctx.option_model. Wire
refine_plan_sketch into the planner's solve tools when a simulator
is available.
…ve prompt

At the start of each _solve, render the task's initial state to
test_images/{taskNNN_}initial_state.png so the agent sees the scene
layout before planning. The prompt now includes a '## Initial State
Image' section pointing to the file when available.

Handles both PyBullet envs (_set_state + render()) and general envs
(render_state) with graceful fallback on failure.
After grasping, the held object may start in shallow penetration from
grasp settling. Add allow_shallow_held_object_contacts flag to Phase
and wire it through make_move_to_phase, PhaseSkill, and BiRRT. When
enabled, initial contacts shallower than the configurable
pybullet_birrt_shallow_held_contact_margin (-0.003) are excluded from
collision checking so the lift can escape without failing.

Applied to the LiftSlightly phase of pick skills. Also adds min contact
distance to collision log messages for easier debugging.
Replace the fixed-row staging layout with a grid search that uses
oriented-rectangle overlap tests to avoid placing movable dominoes on
top of start/target blocks. Returns None (triggering retry) when no
collision-free slot is found.

Adds _placement_collides, _placement_rect, and _rectangles_overlap
helpers with a separating-axis overlap test.
Update domino env __main__ test defaults (seed=1, 1 test task,
unfinished state). Rename agent config entry for clarity.
The unfinished-state staging loop placed movable dominoes with an
overlap-only collision check, which could leave one inside the gripper's
swept grasp footprint of the start block or a target -- especially a
perpendicular neighbor a few cm away in y. The domino then lands placed
but un-pickable: BiRRT finds no collision-free descent for
Pick/MoveToGrasp.

Add a grasp-clearance check (_grasp_clearance_blocked): reject a staging
spot unless the gripper's swept footprint -- an oriented rectangle with
half-extents 0.85x domino width along the long axis and 1.45x along the
finger/depth axis, measured from the Fetch gripper -- is clear of every
other object.

Verified across seeds 0-4: previously seed1 t3, seed2 t4 and seed2 t5
each had an un-pickable movable domino; now every movable domino in all
25 tasks is graspable from init, with no generation slowdown.
Debugging/repro tooling for the domino oracle-samplers runs:
- reproduce_domino_failures.py: deterministic, LLM-free reproduction of
  grasp/place BiRRT infeasibility and the Push parser-drop bug.
- replay_domino_sketches.py: replay recorded LLM sketches through the
  real bilevel refinement to reproduce solve-time failures.
- render_unsolved_domino_states.py: annotated init-state PNGs for the
  unsolved tasks.
- plan_sketches/domino_repro_s1t0.txt: example sketch for
  --agent_bilevel_plan_sketch_file.
Keep these predicates in oracle.yaml (test oracle) but drop them for
agent runs. Achieved via a deep-merged ENVS.domino override in
agents.yaml instead of the shared envs/all.yaml.
Add a per-phase Phase.validate_ik flag and set it for Pick's MoveToGrasp.
When CFG.pybullet_ik_validate is False, unvalidated PyBullet IK can return a
grasp goal config whose EE pose is numerically close but whose gripper finger
slightly penetrates the very domino being grasped (~1-11mm). BiRRT then rejects
the otherwise-reachable grasp ("no collision-free path"), failing the option
mid-plan even though the grasp pose is feasible (validated IK clears it).

_plan_with_simulator now validates the goal IK when the phase requests it,
without globally enabling validation (which slows transport/place/retreat and
introduces Place/Retreat collision + refinement-budget regressions). Replaying
the recorded domino oracle-samplers sketches confirms this clears the mid-plan
Pick/MoveToGrasp failures (e.g. no_demo seed1.t3 4/5 -> 5/5) with no new
regressions, where global ik_validate=True regressed the same seed to 3/5.
Move the domino reproduction/rendering harnesses into a dedicated
scripts/domino_debug/ package and update their usage strings and the
cross-import in the probes accordingly. No behavior change.
- probe_cascade.py: run a recorded Pick/Place/Push/Wait sketch through the
  real option model and log each domino's roll, locating where a topple
  cascade dies (e.g. Toppled subgoal never propagating past the start block).
- probe_infront_drift.py: place a domino at the oracle sampler's
  generator-faithful pose via the real forward sim and compare the settled
  pose to the InFront tolerance window.
- replay_ikval_sweep.py: replay recorded sketches per task with a configurable
  ik_validate setting and report FIXED/REGRESSION vs the recorded outcome.
Render the initial-state image at the start of _solve and reference it from
the plan-sketch prompt so the sketcher can see the scene layout. Extract the
inline image-section construction in agent_planner_approach into a reusable
_initial_image_section() helper and thread it through build_solve_prompt.
…oach

Drop the global max_initial_demos: 1 default from common.yaml and set it
per-env instead (domino: 0; others commented alongside their env blocks).
Rename the agent approach entry agent_oracle_hybrid_sim_oracle_samplers_demo
to agent_oracle_hybrid_sim_oracle_samplers.
Move domino_restricted_push: True out of the shared envs/all.yaml and into
oracle.yaml's ENVS.domino.FLAGS. Agent configs now fall back to the codebase
default (False), so the restricted Push option is registered as
types=[robot, domino] and the LLM's Push(robot, domino_X) sketch line parses
instead of being silently dropped (the seed0/task1 no_demo failure).

Also rename the agent approach to agent_oracle_hybrid_sim_oracle_samplers_no_demo
and drop its redundant approach-level override.
…pts for analyzing turn percentages in domino tasks
The plain AgentPlannerApproach is the open-loop agent that selects continuous
parameters itself, so it should not get refine_plan_sketch (backtracking
refinement on a param-free sketch). Move that tool out of the base
_get_solve_tool_names and add it via an override on AgentBilevelApproach
(still gated on agent_planner_use_simulator), where the agent hands continuous
refinement to a search procedure. AgentSimLearningApproach inherits it.
Bound the model's deliberation (default 16000 tokens) so a single
response's thinking + text stays under the harness output-token cap,
preventing the intermittent 'exceeded the output token maximum' (32000)
overflow on hard tasks. Wired through the local sandbox, the shared
AgentSessionManager, and the docker runner; gated by the new
agent_sdk_thinking_budget_tokens setting (0 leaves it unset).
Let the agent propose per-step continuous parameters inside the plan
sketch (`Option(obj:type)[p1, p2] -> {subgoals}`) instead of always
recovering them by backtracking search. Refinement tries the proposed
params first, then falls back to the registered sampler / uniform
backtracking on failure. Gated by agent_bilevel_use_llm_initial_params
(default False keeps the param-free sketch); agent_bilevel_refine_fallback
controls whether the approach still runs its own post-agent refinement or
the agent must deliver a refine_plan_sketch-validated plan.

When the info-seeking explorer is also on, the proposed params are seeded
as the FIRST candidate of the info-seeking pool (rolled forward,
subgoal-checked, scored) so the ensemble-disagreement argmax chooses among
{LLM guess} U sampled draws, rather than the guess short-circuiting the
probe. One-shot per step, counts toward the node's rollout budget.

- bilevel_sketch: SketchStep.initial_params, parse_initial_params, the
  sample_fn LLM-params branch + info-seeking seeding, refine_and_validate
  _report now returns the grounded plan.
- agent_bilevel_approach: prompt + tool plumbing for proposing params and
  for the tool-validated-only delivery path.
- tools / explorer / synthesis_validation: thread parse_continuous_params
  and the new return arity through.
- tests: pooling/seed-wins/seed-loses/infeasible-seed + parser cases.
Turn on agent_bilevel_use_llm_initial_params for the
agent_po_predicate_invention_al approach, add InFront to domino's
excluded_predicates, drop the now-redundant early-stopping / warm-start
flags from that block, and refresh the commented-out A/B reference
configs (oracle-samplers vs LLM-params hybrid-sim).
The save_low_level_action_images param on evaluate_option_plan was used in
only ~2% of calls across the agent_model_based_planning and
agent_oracle_hybrid_sim_llm_params_no_demo runs, and in every case the agent
never read the generated per-low-level-action frames (it reasoned off the
always-saved per-option scene images instead). Each True triggered hundreds of
pybullet renders per task for no consumed benefit.

Remove the param from the tool schema, its retrieval, and the low-level
rendering branch; per-option image saving is unchanged. Drop the agent-facing
docs that pointed at the flag and ./test_images_low_level/.
Run the three CI-pinned autoformatters (yapf 0.32.0, docformatter 1.4,
isort 5.10.1) over files accumulated on sim-learning that were never
autoformatted. Whitespace/import-ordering only, no logic changes.
Make mypy/pylint pass across the branch's accumulated changes:
- agent_sdk: coerce per-solve cost to float (Optional[float] + operand
  fix) in session_manager/local_sandbox; rename a thinking loop-local in
  docker_agent_runner that shadowed the SDK thinking config.
- approaches: make human_option_control._solve accept _allow_replan so it
  matches the BilevelProcessPlanningApproach signature (fixes [override]
  + arguments-differ); type:ignore[override] on maple_q's intentional
  train_or_test signature; resolve agent_bilevel_plan_sketch_file to an
  absolute path when given one (fixes test_sketch_from_file); wrap long
  lines and guard _agent_session_id (defined via the session mixin).
- skill_factories/base: drop a shadowed get_link_state import, del the
  unused objects arg, reflow comments.
- domino_debug scripts: add type annotations + docstrings, hoist lazy
  imports, and mark intentional protected-member probes.
- tests: arg-type ignores for the classifier stubs; booleaness fixup.
The task-generator rework on this branch changed the seed-0 chain: the
start block (domino_0) now sits at yaw=0 with a wider x-footprint, and the
chain bends through a 45-degree turn. The scripted Place params were tuned
to the old straight chain, so the second place drove the gripper ~2.5mm
into domino_0 and BiRRT's Place/Retreat start was in collision.

Set the two Place poses to the generator's intended finished-chain poses
(domino_1 -> 0.7854 rad, domino_2 -> 1.5708 rad); Pick/Push unchanged. No
collision-margin or skill-factory changes -- the overlap was a real
penetration, now eliminated. Fixes
test_human_option_control_scripted_domino_solves_task.
isort reordered the imports such that the fully-qualified
domino_task_generator import exceeded 80 cols (pylint C0301). Alias the
parent package as dtg, matching the other domino_debug modules.
@yichao-liang yichao-liang merged commit a364aa2 into master Jun 25, 2026
14 checks passed
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