Skip to content

feat: Robot() factory + top-level lazy imports#86

Merged
cagataycali merged 13 commits into
strands-labs:mainfrom
cagataycali:feat/robot-factory
May 15, 2026
Merged

feat: Robot() factory + top-level lazy imports#86
cagataycali merged 13 commits into
strands-labs:mainfrom
cagataycali:feat/robot-factory

Conversation

@cagataycali

@cagataycali cagataycali commented Apr 1, 2026

Copy link
Copy Markdown
Member

Summary

Introduces the Robot() factory function — a single entry point that returns either a MuJoCo simulation (Simulation) or a hardware robot (HardwareRobot) depending on mode detection.

Design decisions:

  • Sim-by-defaultRobot("so100") returns a MuJoCo sim unless hardware is detected or mode="real" is explicit. Safety-first: real hardware affects the physical world.
  • STRANDS_ROBOT_MODE env var — override auto-detection with sim or real. Unrecognized values log a warning.
  • robot.pyhardware_robot.py renameHardwareRobot class moved to dedicated module; robot.py is now the factory.
  • @overload typing — IDEs resolve Simulation or HardwareRobot based on mode literal.
  • Lazy GL backendMUJOCO_GL auto-configured on headless Linux only if mujoco is installed (via importlib.util.find_spec guard).

Files Changed

Core: Robot Factory & Hardware

File Description
strands_robots/robot.py New Robot() factory function with @overload typing, mode detection, env var support
strands_robots/hardware_robot.py Renamed from old robot.pyHardwareRobot class (no emoji in result strings)
strands_robots/__init__.py Top-level lazy imports + find_spec guard for GL backend

Registry

File Description
strands_robots/registry/__init__.py Export updates
strands_robots/registry/policies.py Policy registry updates
strands_robots/registry/user_registry.py User registry updates

Simulation (policy runner, benchmarks)

File Description
strands_robots/simulation/__init__.py Simulation subpackage init
strands_robots/simulation/base.py Simulation base ABC
strands_robots/simulation/benchmark.py Benchmark orchestrator
strands_robots/simulation/benchmark_spec.py Benchmark spec DSL
strands_robots/simulation/policy_runner.py Policy runner enhancements
strands_robots/simulation/predicates.py Predicate system for benchmarks
strands_robots/simulation/mujoco/tool_spec.json Tool spec additions

Benchmarks (LIBERO)

File Description
strands_robots/benchmarks/__init__.py Benchmarks package init
strands_robots/benchmarks/libero/__init__.py LIBERO benchmark init
strands_robots/benchmarks/libero/adapter.py LIBERO MuJoCo adapter
strands_robots/benchmarks/libero/bddl_parser.py BDDL task parser
strands_robots/benchmarks/libero/suite.py LIBERO suite runner

Tests

File Description
tests/test_robot_factory.py Factory tests: mode detection, env var, sim happy-path, error handling
tests/test_registry.py Comprehensive registry tests
tests/test_registry_integrity.py Registry integrity/consistency tests
tests/test_user_registry.py User registry tests
tests/simulation/test_factory.py Factory test additions
tests/simulation/test_policy_runner_behaviour.py Policy runner test update
tests/simulation/test_benchmark.py Benchmark orchestrator tests
tests/simulation/test_benchmark_dsl.py Benchmark DSL tests
tests/simulation/test_benchmark_predicates.py Predicate system tests
tests/simulation/test_policy_runner_benchmark.py Policy runner benchmark tests
tests/simulation/mujoco/test_agenttool_contract.py Minor test update
tests/simulation/mujoco/test_backend.py Backend test additions
tests/simulation/mujoco/test_benchmark_dispatch.py Benchmark dispatch tests
tests/simulation/mujoco/test_recording_backends.py Recording test update
tests/benchmarks/libero/test_bddl_parser.py BDDL parser tests
tests/benchmarks/libero/test_libero_adapter.py LIBERO adapter tests
tests/benchmarks/libero/test_libero_e2e.py LIBERO end-to-end tests
tests/benchmarks/libero/test_libero_suite.py LIBERO suite tests

Other

File Description
AGENTS.md Review learnings from PR #86
pyproject.toml Dependency updates

Key Design Details

Mode Detection (_auto_detect_mode)

  1. Check STRANDS_ROBOT_MODE env var (case-insensitive)
  2. If env var unrecognized → warn + fall through
  3. Probe USB serial ports for known robot hardware
  4. Default: "sim" (safe default)

Error Handling

  • create_world failure → sim.destroy() + RuntimeError
  • add_robot failure → sim.destroy() + RuntimeError (no resource leak)
  • USB probing errors → caught as (ImportError, OSError) only

Environment Variables

Variable Purpose
STRANDS_ROBOT_MODE Force sim or real mode
MUJOCO_GL Auto-set to egl on headless Linux (if mujoco installed)

Review Status

All 27 review threads resolved. CI green (1018 tests pass, 53 skipped).

Follow-ups (tracked, not blocking):

  • Promote _dispatch_action to public or add thin public shorthands on Simulation
  • Add mode="real" smoke test with mocking
  • USB-found-hardware branch test for _auto_detect_mode

Part of the roadmap (#94). Depends on #85 (merged ✅) and #84 (merged ✅).

Comment thread strands_robots/factory.py Outdated
Comment thread strands_robots/factory.py Outdated
Comment thread strands_robots/__init__.py Outdated
Comment thread tests/test_robot_factory.py
@cagataycali
cagataycali force-pushed the feat/robot-factory branch 3 times, most recently from 1e2d93b to 253c01a Compare April 1, 2026 20:15

@yinsong1986 yinsong1986 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All review comments addressed. LGTM.

@cagataycali cagataycali added this to the v0.4 milestone Apr 6, 2026
Comment thread strands_robots/factory.py Outdated
Comment thread strands_robots/factory.py Outdated
Comment thread strands_robots/factory.py Outdated
Comment thread strands_robots/factory.py Outdated
Comment thread strands_robots/factory.py Outdated
Comment thread strands_robots/dataset_recorder.py
Comment thread strands_robots/dataset_recorder.py Outdated
Comment thread strands_robots/dataset_recorder.py
Comment thread strands_robots/dataset_recorder.py
@awsarron

Copy link
Copy Markdown
Member

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.

Comment thread strands_robots/_async_utils.py
@cagataycali

Copy link
Copy Markdown
Member Author

Review Thread Triage (14 unresolved)

Already fixed in code (4 threads from @yinsong1986) — need thread resolution:

Thread Status Commit
Resource leak on partial failure ✅ Fixed 7699c0e
except (ImportError, Exception) too broad ✅ Fixed 7699c0e
MUJOCO_GL env side effect docs ✅ Fixed 7699c0e
Missing happy-path sim test ✅ Fixed 7699c0e

New from @awsarron (10 threads, Apr 10) — need code work:

# Thread Type Suggested action
1 Rename factory.pyrobot.py Architecture Implement
2 Document all env vars in README Docs Implement
3 Default to sim, not hardware Architecture Implement
4 Rename Robot in robot.pyHardwareRobot Architecture Implement
5 Redundant code (line 190) Cleanup Implement
6 dataset_recorder: vague filename refs Docs Implement
7 Fixed camera shape/channels Design question Needs discussion
8 "where is this used?" (line 456) Dead code? Investigate
9 "is top-level best place for dataset_recorder?" Architecture Needs discussion
10 ThreadPoolExecutor never shut down Bug fix Implement

Additional blockers:

Recommendation: Wait for PR #84 to merge, then rebase and address @awsarron's 10 threads in one pass. Items 7 and 9 may need a design discussion before implementing.


🤖 Pipeline analysis by AI agent. Strands Agents. Feedback welcome!

cagataycali pushed a commit to cagataycali/robots that referenced this pull request Apr 13, 2026


1. Rename factory.py → robot.py, robot.py → hardware_robot.py
   Eliminates two 'Robot' classes in different files. The factory
   function now lives where users expect: strands_robots.robot.Robot

2. Default mode='sim' instead of mode='auto'
   Using real hardware should be an explicit decision since it affects
   the physical world. Robot('so100') now always returns simulation.
   Use mode='real' to explicitly opt into hardware control.

3. Fix ThreadPoolExecutor leak in _async_utils.py
   Register atexit.shutdown(wait=False) to clean up the module-level
   executor on interpreter exit.

4. Remove redundant list_robots() wrapper
   Was a 1-line passthrough to registry.list_robots(). Now __init__.py
   points directly to strands_robots.registry.list_robots.

5. Use module names in dataset_recorder docstring
   'robot.py' → 'strands_robots.hardware_robot',
   'simulation.py' → 'strands_robots.simulation'

6. Make camera shape configurable in dataset_recorder
   Added camera_shapes parameter to _build_features() instead of
   hardcoding (3, 480, 640). Default preserved for backward compat.

7. Add mode validation — invalid mode raises ValueError

8. Update __init__.py lazy imports for renamed modules

Tests: 230 passed, 10 skipped, 0 failures
Lint: ruff check + ruff format clean
@cagataycali

Copy link
Copy Markdown
Member Author

📋 Review Status Summary

Hi @awsarron — this PR has 11/14 threads resolved. Here's a summary of the 3 remaining unresolved threads to help focus the re-review:

Unresolved Thread 1: "Document all env vars in README"

"we should document all env vars used in the entire project in the README, like we started in #85"

Cross-PR dependency: This is being addressed in PR #87 (docs rewrite), which includes comprehensive env var documentation. Suggest resolving this thread with a note that #87 covers it, or merging #87 first.

Unresolved Thread 2: "Where is load_lerobot_episode used?"

"where is this used?"

Needs author clarification: Is this function consumed by #85's recording/replay pipeline, or is it forward-looking API? If it's unused, removing it simplifies the PR.

Unresolved Thread 3: "Is top-level the best place for dataset_recorder?"

"is top-level the best place for this?"

Architecture decision needed: Two alternatives:

  1. Keep top-level if dataset_recorder is a primary user-facing utility (like Robot())
  2. Move to strands_robots.recording sub-package if it's a specialized tool used mainly by simulation workflows

Overall Status


🤖 Automated review triage by Strands Agents. Feedback welcome!

cagataycali added a commit to cagataycali/robots that referenced this pull request Apr 16, 2026
…s-labs#86

- Add Environment Variables table to README documenting all 6 env vars
  used across the project (STRANDS_ROBOT_MODE, STRANDS_ASSETS_DIR,
  STRANDS_URDF_DIR, STRANDS_TRUST_REMOTE_CODE, GROOT_API_TOKEN,
  MUJOCO_GL) plus cache directory documentation
- Add module-level docstring to dataset_recorder.py explaining why it
  lives at package root (shared by both hardware and simulation paths,
  avoids circular dependency)
- Add docstring to load_lerobot_episode() documenting that it is
  consumed by simulation.mujoco.policy_runner for replay_episode
@cagataycali
cagataycali requested a review from awsarron April 17, 2026 16:30
@cagataycali cagataycali modified the milestones: v0.4.0, v0.3.9 Apr 21, 2026
cagataycali added a commit that referenced this pull request Apr 22, 2026
…ssets (#84)

Simulation foundation layer for `strands-robots`. Pure Python, no MuJoCo dependency.
Unblocks #85 (MuJoCo backend) and #86 (Robot factory).

## What's in

**Simulation abstractions** (`strands_robots/simulation/`)
- `models.py` — `SimWorld`, `SimRobot`, `SimObject`, `SimCamera`, `TrajectoryStep`, `SimStatus` dataclasses. Backend-agnostic: engine handles live in `_model`/`_data`, everything else in `_backend_state: dict`.
- `base.py` — `SimEngine` ABC. 12 required abstract methods + 4 optional (raise `NotImplementedError`). Context-manager protocol. `__del__` logs cleanup errors at warning level.
- `factory.py` — `create_simulation()` + `register_backend()` with duplicate/alias shadow protection (raises `ValueError`; `force=True` for intentional overrides). Descriptive `ImportError` when a built-in backend module isn't installed.
- `model_registry.py` — URDF/MJCF resolution: user-registered → `STRANDS_ASSETS_DIR` → `~/.strands_robots/assets/` → CWD → `robot_descriptions` fallback. Resolves search paths at call time (no import-time `Path.cwd()` snapshot).
- `__init__.py` — thin re-exports with lazy `__getattr__`.

**Assets** (`strands_robots/assets/`)
- `__init__.py` — thin exports only (repo convention).
- `manager.py` — path resolution with `safe_join()` traversal protection. `_has_meshes()` uses `os.scandir` + early-exit, cached by `(path, mtime)`. Module-level guard for optional `[sim]` extra — no circular import with `download.py`.
- `download.py` — all download logic (`robot_descriptions` → git clone fallback). `_shallow_clone()` enforces `_ALLOWED_CLONE_URL_RE` (HTTPS github.com only). `_copy_and_clean` filters ignored patterns at `copytree()` time so user files in the cache aren't clobbered.

**Tools** (`strands_robots/tools/`)
- `download_assets.py` — thin `@tool` wrapper (~78 lines) that delegates to `assets.download.download_robots()`. No duplicated logic.

**Registry** (`strands_robots/registry/`)
- `user_registry.py` — `register_robot()` / `unregister_robot()` persisted to `~/.strands_robots/user_robots.json`. Fails closed on missing asset dir. Warns on alias collisions at registration time. Docstring warns this must not be exposed as an agent `@tool` without `STRANDS_TRUST_REMOTE_CODE` gating (MJCF → MuJoCo plugin code-exec risk).
- `loader.py` — merges user-local registry on top of package `robots.json`. Public `invalidate_cache()` API (no private imports from callers).
- `robots.json` — 38 → 68 robots (adds aerial, expressive, mobile_manip categories).
- `__init__.py` — re-exports `register_robot`, `unregister_robot`, `list_user_robots`, `invalidate_cache`.

**Utils** (`strands_robots/utils.py`)
- `get_base_dir()` reads `STRANDS_BASE_DIR` — decoupled from `STRANDS_ASSETS_DIR` so setting the assets path no longer drops `user_robots.json` into an unexpected parent.
- `get_assets_dir()`, `resolve_asset_path()`, `safe_join()`, `get_search_paths()` — single source of truth; consumed by model_registry, user_registry, assets/manager.

**Docs & packaging**
- `README.md` — environment variables table (`STRANDS_BASE_DIR`, `STRANDS_ASSETS_DIR`, `GROOT_API_TOKEN`) + cache directory docs.
- `AGENTS.md` — documents nested-asset-path convention (e.g. `xmls/asimov.xml` matching upstream layout) and the `auto_download` strategy invariant.
- `pyproject.toml` — new `[sim]` extra (`robot_descriptions>=1.11.0,<2.0.0`); included in `[all]`.

## Design decisions

**SimEngine ABC contract.** 12 required methods every physics engine must implement; 4 optional (`load_scene`, `run_policy`, `randomize`, `get_contacts`) raise `NotImplementedError` so unimplemented features are explicit during development. `get_observation`/`send_action` are deliberately facade methods bridging Sim ↔ Policy — the agent tool sees a single interface without needing to know the Robot vs Sim split.

**Asset resolution order.** Customer assets always win over defaults: `STRANDS_ASSETS_DIR` → `~/.strands_robots/assets/` → `CWD/assets/` → `robot_descriptions` fallback. Single env var for the asset tree (`STRANDS_ASSETS_DIR`); separate `STRANDS_BASE_DIR` for the base dir that holds `user_robots.json`.

**Backend registration.** `register_backend()` rejects duplicates by default and blocks shadowing of built-in aliases (`mj`, `mjc`, `mjx`) unless `force=True`. Alias conflicts caught at both the `name` and `aliases` parameters.

**Security.**
- `safe_join()` applied everywhere registry values flow into filesystem paths (manager + download + user registry).
- `_shallow_clone()` URL regex rejects `ssh://`, `git://`, `file://`, non-github hosts.
- `register_robot()` is library-only; not surfaced as `@tool`. Docstring spells out the MJCF-plugin exec risk.

## Testing

- 338 unit tests pass, 6 skipped, 0 failures
- `ruff check` + `ruff format --check`: clean (57 files)
- `mypy`: 0 issues in 57 source files
- New test files:
  - `tests/test_simulation_foundation.py` — ABC contracts, factory round-trip, context-manager cleanup
  - `tests/test_simulation_factory.py` — duplicate rejection, alias shadowing, missing-backend ImportError
  - `tests/test_user_registry.py` — register/unregister, persistence, validation, path traversal; asserts `STRANDS_ASSETS_DIR` does NOT move the base dir / registry
  - `tests/test_registry_integrity.py` — auto-download invariant, alias uniqueness, canonical-shadow protection, lerobot_type presence on hardware-only robots

## Review history

| Reviewer          | Status                                | Threads         |
|-------------------|---------------------------------------|-----------------|
| @yinsong1986      | APPROVED                              | 3/3 resolved    |
| @awsarron         | CHANGES_REQUESTED → all addressed     | 50/50 addressed |
| @max-rattray-aws  | COMMENTED → all addressed             | 3/3 resolved    |

Closes #84.


---------

Co-authored-by: cagataycali <cagataycali@icloud.com>
Co-authored-by: strands-agent <217235299+strands-agent@users.noreply.github.com>
cagataycali pushed a commit to cagataycali/robots that referenced this pull request Apr 22, 2026


1. Rename factory.py → robot.py, robot.py → hardware_robot.py
   Eliminates two 'Robot' classes in different files. The factory
   function now lives where users expect: strands_robots.robot.Robot

2. Default mode='sim' instead of mode='auto'
   Using real hardware should be an explicit decision since it affects
   the physical world. Robot('so100') now always returns simulation.
   Use mode='real' to explicitly opt into hardware control.

3. Fix ThreadPoolExecutor leak in _async_utils.py
   Register atexit.shutdown(wait=False) to clean up the module-level
   executor on interpreter exit.

4. Remove redundant list_robots() wrapper
   Was a 1-line passthrough to registry.list_robots(). Now __init__.py
   points directly to strands_robots.registry.list_robots.

5. Use module names in dataset_recorder docstring
   'robot.py' → 'strands_robots.hardware_robot',
   'simulation.py' → 'strands_robots.simulation'

6. Make camera shape configurable in dataset_recorder
   Added camera_shapes parameter to _build_features() instead of
   hardcoding (3, 480, 640). Default preserved for backward compat.

7. Add mode validation — invalid mode raises ValueError

8. Update __init__.py lazy imports for renamed modules

Tests: 230 passed, 10 skipped, 0 failures
Lint: ruff check + ruff format clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants