feat: simulation foundation — models, ABC, factory, model registry, assets#84
Conversation
e5d70ca to
6bb80bd
Compare
yinsong1986
left a comment
There was a problem hiding this comment.
All review comments addressed. LGTM.
2594b1e to
2217260
Compare
|
Fixed in ef75a5d — added the missing sim = [
"robot_descriptions>=1.11.0,<2.0.0",
]
all = [
"strands-robots[groot-service]",
"strands-robots[lerobot]",
"strands-robots[sim]",
]The code in |
There was a problem hiding this comment.
Looking good, few small final comments.
Need to rebase after #83 is merged.
For all comments in this PR, we should examine common themes and include corrections for them in AGENTS.md so that future agent runs benefit from their lessons.
Verified against the code, only the findings that hold up on re-read: simulation/factory.py - _import_backend_class: raise a descriptive ImportError when a built-in backend module is declared but not installed, instead of letting the raw ModuleNotFoundError surface to users. The default "mujoco" path now points at an unimplemented module, so create_simulation() would previously crash with a cryptic trace — now the error names the backend, suggests the extra to install, and points at register_backend(). simulation/model_registry.py - Drop the import-time _URDF_SEARCH_PATHS constant (snapshotted Path.cwd() at import, broke pytest/notebook chdir flows). Use utils.get_search_paths() at every lookup — same function already used by assets.manager, so we dedupe too. - Demote the module-level 'Asset manager available' log from INFO at import to a lazy DEBUG on first resolution. assets/download.py - _copy_and_clean: filter ignored patterns at copytree() time instead of deleting from the destination afterwards. Previous behaviour would clobber a user's own README/LICENSE/images in their asset cache. assets/manager.py - Gate _user_asset_path resolution through safe_join() so a malicious or corrupted user_robots.json cannot sneak '..' into model_xml and escape the asset cache. Matches the trust boundary of the built-in registry path. registry/user_registry.py - register_robot() now fails closed when the asset directory does not exist (previously silently accepted invalid registrations). - Warn on alias collisions at registration time (shadows-canonical, shadows-existing-alias) instead of letting resolution order silently decide the winner. tests/test_registry_integrity.py - test_aliases_unique_across_registry - test_no_alias_shadows_canonical_name - test_hardware_only_robots_declare_lerobot_type tests/test_simulation_factory.py (new) - test_default_backend_missing_raises_import_error_with_guidance (regression test for the cryptic-ModuleNotFoundError fix) - test_register_backend_loader_must_be_callable - test_register_backend_rejects_duplicate_without_force .gitignore - Ignore .ideation/ (local scratchpad for idea cycles). All 338 unit tests pass. ruff + mypy clean.
a2be111 to
3a5c14b
Compare
- assets/manager.py: remove 'import os as _os' alias — just import os at top - simulation/models.py: clarify _model vs _data vs _backend_state roles in docstring so backend implementers know which to use (each has a distinct purpose: core model handle, core state handle, catch-all dict) - utils.py: decouple get_base_dir() from STRANDS_ASSETS_DIR. Introduce STRANDS_BASE_DIR as the explicit override for the base directory so user_robots.json no longer lands in an unexpected parent of the assets path. STRANDS_ASSETS_DIR now *only* moves the assets subtree. - registry/user_registry.py: update module docstring to reflect the new STRANDS_BASE_DIR semantics - tests/test_user_registry.py: update fixture + integration tests to set STRANDS_BASE_DIR and STRANDS_ASSETS_DIR independently; add tests asserting that STRANDS_ASSETS_DIR does *not* move the registry / base dir
3a5c14b to
fc89b1f
Compare
|
Thanks for the thorough review round @awsarron 🙏 — I've captured the 5 remaining items as a follow-up issue so they don't get lost, and going to merge this one so it stops being a rebase target for #85 and #86. Follow-up: #105 (added to the project board) Items tracked:
All polish / minor correctness — none of them block the foundation landing. Will pick them up in a small follow-up PR before starting the MuJoCo backend rebase. 👍 🤖 AI agent response. Strands Agents. Feedback welcome! |
Stale — all threads from this review have been resolved and addressed in subsequent commits. Superseded by @awsarron's approval on 2026-04-22.
Post-strands-labs#84 merge: SimWorld no longer carries MuJoCo-specific private fields (_xml, _robot_base_xml, _recording, _trajectory, _dataset_recorder, _tmpdir, _push_to_hub). These are MuJoCo backend implementation details and now live in world._backend_state, as the SimWorld docstring requests (prefer _backend_state over new fields). Migrated call sites: - mjcf_builder.py: tmpdir - policy_runner.py: recording, trajectory, dataset_recorder - recording.py: recording, trajectory, dataset_recorder, push_to_hub - scene_ops.py: robot_base_xml - simulation.py: xml, robot_base_xml, recording, trajectory Reads use dict[] where preceded by a guard that guarantees initialization (e.g. start_recording() sets before policy_runner reads), and .get() with sensible defaults where the key may be unset. Tests: 392 passed, 2 skipped (5 pre-existing test_path_validation failures are on main too — unrelated). Lint: ruff + mypy clean on 75 source files.
… registration Add Newton GPU simulation backend stub that implements SimEngine ABC. No GPU dependencies at import time — warp/newton are lazy-loaded. New files: - simulation/newton/__init__.py — lazy loading, no warp import - simulation/newton/config.py — NewtonConfig dataclass (7 solvers) - simulation/newton/simulation.py — NewtonSimulation(SimEngine) skeleton Modified files: - simulation/factory.py — register newton + aliases (warp, wp) - pyproject.toml — add [newton] extras (warp-lang, newton-sim) All 12 required SimEngine methods stubbed with NotImplementedError. Newton-specific extensions (replicate, diffsim, IK, cloth, etc.) stubbed. Factory kwargs flow through to NewtonConfig. Tests: 81 passed (config validation, factory round-trip, lazy import, ABC conformance, all stub methods verified). Part 1 of 7-PR Newton backend series (issue #314). Depends on PR strands-labs#84 (SimEngine ABC).
…spec dispatcher
Bug: Simulation._dispatch_action filtered kwargs through a hardcoded
whitelist that omitted observation_mapping, action_mapping, data_config,
host, port, api_token, trust_remote_code, actions_per_step,
use_processor, processor_overrides, and any other policy-specific kwarg.
Agents could not wire a policy (GR00T, SmolVLA, lerobot_local) to a
simulated robot through the AgentTool interface — sim joint names and
canonical model keys never got reconciled, breaking sim↔real transfer.
Fix:
- simulation.py::_dispatch_action — replace the whitelist with a
mapping-aware passthrough: for methods that declare **policy_kwargs,
forward every input field that isn't already matched to a named
parameter. Actions without **kwargs stay strict.
- tool_spec.json — advertise observation_mapping, action_mapping, host,
port, api_token, trust_remote_code, actions_per_step, use_processor,
processor_overrides, device so agents can discover and use them.
- tests/test_tool_spec_dispatch_policy_kwargs.py — 5 regression tests
pinning the forwarding for run_policy / eval_policy / start_policy
and verifying non-policy actions stay strict.
End-to-end validation (MacBook Pro M-series, MPS):
- create_world → add_robot(so100) → add_object(red_cube)
→ add_camera(camera1) → add_camera(camera2)
→ run_policy(policy_provider='lerobot_local',
pretrained_name_or_path='lerobot/smolvla_base',
device='mps',
observation_mapping={'camera1': 'observation.images.camera1',
'camera2': 'observation.images.camera2',
'joint_position': 'observation.state'},
action_mapping={'action': 'joint_position'})
- SmolVLA downloaded, loaded on MPS, produced actions, sim stepped.
2 control steps / 25.4s wall → proves the full chain works.
Quality:
- ruff + mypy: clean (77 files)
- hatch run test: 5/5 new tests pass; only pre-existing
test_path_validation failures remain (noted by author on strands-labs#84).
Post-strands-labs#84 merge: SimWorld no longer carries MuJoCo-specific private fields (_xml, _robot_base_xml, _recording, _trajectory, _dataset_recorder, _tmpdir, _push_to_hub). These are MuJoCo backend implementation details and now live in world._backend_state, as the SimWorld docstring requests (prefer _backend_state over new fields). Migrated call sites: - mjcf_builder.py: tmpdir - policy_runner.py: recording, trajectory, dataset_recorder - recording.py: recording, trajectory, dataset_recorder, push_to_hub - scene_ops.py: robot_base_xml - simulation.py: xml, robot_base_xml, recording, trajectory Reads use dict[] where preceded by a guard that guarantees initialization (e.g. start_recording() sets before policy_runner reads), and .get() with sensible defaults where the key may be unset. Tests: 392 passed, 2 skipped (5 pre-existing test_path_validation failures are on main too — unrelated). Lint: ruff + mypy clean on 75 source files.
…spec dispatcher
Bug: Simulation._dispatch_action filtered kwargs through a hardcoded
whitelist that omitted observation_mapping, action_mapping, data_config,
host, port, api_token, trust_remote_code, actions_per_step,
use_processor, processor_overrides, and any other policy-specific kwarg.
Agents could not wire a policy (GR00T, SmolVLA, lerobot_local) to a
simulated robot through the AgentTool interface — sim joint names and
canonical model keys never got reconciled, breaking sim↔real transfer.
Fix:
- simulation.py::_dispatch_action — replace the whitelist with a
mapping-aware passthrough: for methods that declare **policy_kwargs,
forward every input field that isn't already matched to a named
parameter. Actions without **kwargs stay strict.
- tool_spec.json — advertise observation_mapping, action_mapping, host,
port, api_token, trust_remote_code, actions_per_step, use_processor,
processor_overrides, device so agents can discover and use them.
- tests/test_tool_spec_dispatch_policy_kwargs.py — 5 regression tests
pinning the forwarding for run_policy / eval_policy / start_policy
and verifying non-policy actions stay strict.
End-to-end validation (MacBook Pro M-series, MPS):
- create_world → add_robot(so100) → add_object(red_cube)
→ add_camera(camera1) → add_camera(camera2)
→ run_policy(policy_provider='lerobot_local',
pretrained_name_or_path='lerobot/smolvla_base',
device='mps',
observation_mapping={'camera1': 'observation.images.camera1',
'camera2': 'observation.images.camera2',
'joint_position': 'observation.state'},
action_mapping={'action': 'joint_position'})
- SmolVLA downloaded, loaded on MPS, produced actions, sim stepped.
2 control steps / 25.4s wall → proves the full chain works.
Quality:
- ruff + mypy: clean (77 files)
- hatch run test: 5/5 new tests pass; only pre-existing
test_path_validation failures remain (noted by author on strands-labs#84).
TL;DR
Simulation foundation layer — dataclasses,
SimEngineABC, pluggable backend factory, URDF/MJCF model registry, asset manager with auto-download, user robot registration, and@toolfor asset downloads. Expands robot registry from 38 → 68 robots. Pure Python, no MuJoCo dependency.Rebased on
main(post-#83). All 50 review threads resolved. All CI green.What changed
Simulation abstractions (
strands_robots/simulation/)models.pySimWorld,SimRobot,SimObject,SimCamera,TrajectoryStep,SimStatus. Backend-agnostic — engine state stored in generic_backend_state: dict(no MuJoCo-specific fields).base.pySimEngineABC — 12 required abstract methods + 4 optional (raiseNotImplementedError). Context manager protocol.__del__logs cleanup errors instead of silencing.factory.pycreate_simulation()+register_backend()with duplicate protection (ValueErroron conflict), built-in alias protection,force=Truefor intentional overwrites.model_registry.pySTRANDS_ASSETS_DIR→~/.strands_robots/assets/→ CWD →robot_descriptionsfallback. Logs asset manager availability.__init__.py__getattr__lazy loading. No MuJoCo references.Assets (
strands_robots/assets/)__init__.pymanager.py_safe_join()path-traversal protection,_resolve_candidates()helper. Search paths documented. No bundled assets — everything fromrobot_descriptions.download.pyrobot_descriptions→ git clone fallback._shallow_clone()validates URLs via_ALLOWED_CLONE_URL_RE(HTTPS github.com only).Tools (
strands_robots/tools/)download_assets.py@toolwrapper (~78 lines) — delegates toassets.download. No duplicated logic.Registry (
strands_robots/registry/)user_registry.pyregister_robot()/unregister_robot()— persisted to~/.strands_robots/user_robots.json. Security warning on docstring re: MJCF code execution risk.loader.pyrobots.json. Publicinvalidate_cache()API (no private imports).robots.json__init__.pyregister_robot,unregister_robot,list_user_robots,invalidate_cache.Other
utils.pyget_base_dir(),get_assets_dir(),resolve_asset_path()— single source of truth.README.mdpyproject.toml[sim]extra withrobot_descriptionsdependency.Design decisions
SimEngine ABC
Asset resolution order
Customer assets always win over defaults. Single env var:
STRANDS_ASSETS_DIR.Security
_safe_join()in bothmanager.pyanddownload.py_shallow_clone()rejects non-HTTPS/non-github.com URLsregister_robot(): Library-only function, not exposed as@tool. Docstring warns about MJCF code execution risk if ever surfaced to agents.Testing
Review history
Key changes since CHANGES_REQUESTED:
main(post-chore: modernize build system — uv lockfile, Python >=3.12 #83, Python ≥3.12 + uv)SimulationBackend→SimEnginetools/→assets/download.py(tools as thin wrappers)SimWorld→ generic_backend_stateregister_robot()docstringUnblocks #85 (MuJoCo backend) and #86 (Robot factory).