Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cd94627
feat: MuJoCo simulation backend — AgentTool with 35 actions
cagataycali Apr 1, 2026
fca42b8
fix: address all review comments — ABC, thread-safety, injection, cle…
cagataycali Apr 1, 2026
b35e99e
fix: resolve lint errors — import ordering, format, strict param
cagataycali Apr 1, 2026
bf9dfe0
fix: acquire _lock around MuJoCo data mutations + sanitize all XML names
cagataycali Apr 1, 2026
37cdb19
ci: add MuJoCo system deps (libosmesa6-dev + MUJOCO_GL=osmesa)
cagataycali Apr 1, 2026
9987c23
feat: add [sim] extra with mujoco dependency
cagataycali Apr 1, 2026
8a6c510
fix: rename [sim] extra to [sim-mujoco] per review
Apr 2, 2026
2f6d6e6
fix: rebase on simulation-foundation — SimulationBackend→SimEngine, u…
cagataycali Apr 6, 2026
fb5a14a
fix: resolve all mypy errors — mixin overrides, Optional types, impor…
cagataycali Apr 6, 2026
8b8c78d
fix: properly fix mypy errors instead of blanket suppression
cagataycali Apr 6, 2026
4964643
fix: zero mypy suppressions — proper type declarations instead of dis…
cagataycali Apr 6, 2026
4ca9173
feat(sim): use require_optional for imageio in policy_runner
cagataycali Apr 6, 2026
70648d1
fix: address 8 review threads — deps, exports, init, headless, coupli…
cagataycali Apr 12, 2026
19ea1dd
fix: replace Protocol annotation with direct TYPE_CHECKING stubs in P…
strands-agent Apr 12, 2026
62fa590
refactor(mujoco): migrate SimWorld private fields to _backend_state
Apr 22, 2026
abc65e6
feat: Robot() factory + top-level lazy imports
cagataycali Apr 1, 2026
1639079
fix: address review — resource leak, exception narrowing, GL comment,…
cagataycali Apr 1, 2026
33c45eb
fix: handle None in serial port description/manufacturer during auto-…
cagataycali Apr 1, 2026
9af0a55
fix: use inline MJCF in test_sim_happy_path so CI works without assets
cagataycali Apr 1, 2026
577b00f
refactor: address 10 review threads from @awsarron on PR #86
strands-agent Apr 13, 2026
ac55331
docs: address 3 unresolved review threads from @awsarron on PR #86
cagataycali Apr 16, 2026
0917c24
fix(hardware_robot): add missing type annotations for mypy strict
cagataycali Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/test-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ jobs:
python-version: '3.12'
cache: 'pip'

- name: Install system dependencies (OpenGL for MuJoCo)
run: |
sudo apt-get update
sudo apt-get install -y libosmesa6-dev

- name: Install dependencies
run: |
pip install --no-cache-dir hatch
Expand All @@ -35,4 +40,6 @@ jobs:
run: hatch run lint

- name: Run tests
env:
MUJOCO_GL: osmesa
run: hatch run test -x --strict-markers
28 changes: 10 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,30 +486,22 @@ while True:
agent.tool.gr00t_inference(action="stop", port=8000)
```

## Configuration

### Environment Variables
## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `STRANDS_ASSETS_DIR` | Custom directory for robot model assets (MJCF, meshes) | `~/.strands_robots/assets/` |
| `GROOT_API_TOKEN` | API token for GR00T inference service | — |

### Cache Directory
| `STRANDS_ROBOT_MODE` | Override auto-detection mode (`sim` or `real`) | (auto-detect) |
| `STRANDS_ASSETS_DIR` | Custom directory for robot model assets (URDF/MJCF/meshes) | `~/.strands_robots/assets/` |
| `STRANDS_URDF_DIR` | **Deprecated** — use `STRANDS_ASSETS_DIR` instead | — |
| `STRANDS_TRUST_REMOTE_CODE` | Set to `1` to allow loading remote HuggingFace policies | (disabled) |
| `GROOT_API_TOKEN` | API token for NVIDIA GR00T cloud inference | — |
| `MUJOCO_GL` | OpenGL backend for MuJoCo rendering (`egl`, `osmesa`, `glfw`) | Auto-configured on headless Linux |

Robot model assets (MJCF XML files and meshes) are cached in:
**Cache directory:** Robot model assets (URDF, MJCF, meshes) are downloaded on first use to `~/.strands_robots/assets/`. Override with `STRANDS_ASSETS_DIR`. To clear cached assets:

```bash
rm -rf ~/.strands_robots/assets/
```
~/.strands_robots/
└── assets/ # Downloaded robot models (from robot_descriptions / MuJoCo Menagerie)
├── trs_so_arm100/
├── franka_emika_panda/
└── ...
```

To clear the cache: `rm -rf ~/.strands_robots/assets/`

To change the cache location: `export STRANDS_ASSETS_DIR=/path/to/custom/dir`

## Contributing

Expand Down
25 changes: 22 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,16 @@ groot-service = [
lerobot = [
"lerobot>=0.5.0,<0.6.0",
]
sim = [
sim-mujoco = [
"robot_descriptions>=1.11.0,<2.0.0",
"mujoco>=3.0.0,<4.0.0",
"imageio>=2.28.0,<3.0.0",
"imageio-ffmpeg>=0.4.0,<1.0.0",
]
all = [
"strands-robots[groot-service]",
"strands-robots[lerobot]",
"strands-robots[sim]",
"strands-robots[sim-mujoco]",
]
dev = [
"pytest>=6.0,<9.0.0",
Expand Down Expand Up @@ -128,7 +131,7 @@ ignore_missing_imports = false

# Third-party libs without type stubs
[[tool.mypy.overrides]]
module = ["lerobot.*", "gr00t.*", "draccus.*", "msgpack.*", "zmq.*", "huggingface_hub.*", "serial.*", "psutil.*", "torch.*", "torchvision.*", "transformers.*", "einops.*", "robot_descriptions.*"]
module = ["lerobot.*", "gr00t.*", "draccus.*", "msgpack.*", "zmq.*", "huggingface_hub.*", "serial.*", "psutil.*", "torch.*", "torchvision.*", "transformers.*", "einops.*", "robot_descriptions.*", "mujoco.*", "imageio.*"]
ignore_missing_imports = true

# @tool decorator injects runtime signatures mypy cannot check
Expand Down Expand Up @@ -161,6 +164,22 @@ module = ["strands_robots.registry.*"]
warn_return_any = false
disallow_untyped_defs = false

# MuJoCo simulation — mixins use cooperative self._world patterns
# attr-defined: Mixins access self._world/self._lock/etc. from Simulation (cooperative pattern)
# assignment: PEP 484 implicit Optional (= None on typed params)
# override: Subclass signatures extend base with extra params (orientation, mesh_path)
# misc: Multiple inheritance method resolution conflicts between mixin + ABC
[[tool.mypy.overrides]]
module = ["strands_robots.simulation.mujoco.*"]
disallow_untyped_defs = false
warn_return_any = false

# Async utils and dataset recorder — thin wrappers with dynamic types
[[tool.mypy.overrides]]
module = ["strands_robots._async_utils", "strands_robots.dataset_recorder"]
disallow_untyped_defs = false
warn_return_any = false

# Test files — relaxed type checking for mocks, fixtures, and test utilities
[[tool.mypy.overrides]]
module = ["tests.*", "tests_integ.*"]
Expand Down
51 changes: 45 additions & 6 deletions strands_robots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
- Clean separation between robot control and policy inference
- Direct policy injection for maximum flexibility
- Multi-camera support with rich configuration options
- MuJoCo simulation backend (no GPU required)

Lazy Loading:
Heavy imports (Robot, tools, Gr00tPolicy) are deferred until first access.
Heavy imports are deferred so ``import strands_robots`` stays fast when lerobot/torch
are installed but not yet needed.
Heavy imports (Robot, tools, Gr00tPolicy, Simulation) are deferred until
first access. Heavy imports are deferred so ``import strands_robots`` stays
fast when lerobot/torch/mujoco are installed but not yet needed.

Light-weight symbols (Policy, MockPolicy, create_policy) are available
immediately since they don't pull in torch/lerobot.
Expand All @@ -26,7 +27,7 @@
from typing import Any

# ------------------------------------------------------------------
# Light-weight imports — no torch / lerobot dependency
# Light-weight imports — no torch / lerobot / mujoco dependency
# ------------------------------------------------------------------
from strands_robots.policies import MockPolicy, Policy, create_policy # noqa: F401

Expand All @@ -35,8 +36,21 @@
# ------------------------------------------------------------------
# Maps public name -> (module_path, attribute_name)
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
# Hardware robot
"Robot": ("strands_robots.robot", "Robot"),
"list_robots": ("strands_robots.registry", "list_robots"),
# Policies
"Gr00tPolicy": ("strands_robots.policies.groot", "Gr00tPolicy"),
# Simulation (MuJoCo)
"Simulation": ("strands_robots.simulation", "Simulation"),
"create_simulation": ("strands_robots.simulation.factory", "create_simulation"),
"list_backends": ("strands_robots.simulation.factory", "list_backends"),
"register_backend": ("strands_robots.simulation.factory", "register_backend"),
"SimWorld": ("strands_robots.simulation", "SimWorld"),
"SimRobot": ("strands_robots.simulation", "SimRobot"),
"SimObject": ("strands_robots.simulation", "SimObject"),
"SimCamera": ("strands_robots.simulation", "SimCamera"),
# Tools
"gr00t_inference": ("strands_robots.tools.gr00t_inference", "gr00t_inference"),
"lerobot_calibrate": ("strands_robots.tools.lerobot_calibrate", "lerobot_calibrate"),
"lerobot_camera": ("strands_robots.tools.lerobot_camera", "lerobot_camera"),
Expand All @@ -53,6 +67,11 @@
# Lazy-loaded
"Robot",
"Gr00tPolicy",
"Simulation",
"SimWorld",
"SimRobot",
"SimObject",
"SimCamera",
"gr00t_inference",
"lerobot_camera",
"lerobot_teleoperate",
Expand All @@ -62,12 +81,32 @@
]


# Auto-configure MuJoCo GL backend for headless environments BEFORE any
# module imports mujoco at the top level. MuJoCo locks the OpenGL backend
# at import time, so MUJOCO_GL must be set first.
#
# WHY EAGER: This MUST run at module import time, not lazily, because:
# 1. MuJoCo reads MUJOCO_GL only on first `import mujoco`
# 2. Any downstream code doing `from strands_robots.simulation import ...`
# triggers mujoco import via the lazy-load chain
# 3. If we defer to first use, the env var would be set too late
# This is the canonical location — strands_robots/simulation/__init__.py
# intentionally does NOT duplicate this call.
try:
from strands_robots.simulation.mujoco.backend import _configure_gl_backend

_configure_gl_backend()
Comment thread
cagataycali marked this conversation as resolved.
except (ImportError, AttributeError, OSError):
pass


def __getattr__(name: str) -> Any: # noqa: N807
"""Lazy-load heavy modules on first attribute access.

This avoids importing torch, lerobot, numpy, pyserial, etc. at
This avoids importing torch, lerobot, numpy, mujoco, pyserial, etc. at
``import strands_robots`` time. The first access to e.g.
``strands_robots.Robot`` triggers the real import.
``strands_robots.Robot`` or ``strands_robots.Simulation`` triggers the
real import.
"""
if name in _LAZY_IMPORTS:
module_path, attr_name = _LAZY_IMPORTS[name]
Expand Down
36 changes: 36 additions & 0 deletions strands_robots/_async_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Async-to-sync helper for resolving coroutines in sync contexts."""

import asyncio
import atexit
from concurrent.futures import ThreadPoolExecutor

# Module-level executor reused across calls to avoid creating threads at high frequency.
# A single worker is sufficient — we only need to offload one asyncio.run() at a time.
_EXECUTOR = ThreadPoolExecutor(max_workers=1, thread_name_prefix="strands_async")

# Ensure the executor is shut down cleanly on interpreter exit to avoid
# ResourceWarning and orphaned threads.
atexit.register(_EXECUTOR.shutdown, wait=False)


def _resolve_coroutine(coro_or_result): # type: ignore[no-untyped-def]
"""Safely resolve a potentially-async result to a sync value.

Handles three cases:
1. Already a plain value → return as-is
2. Coroutine, no running loop → asyncio.run()
3. Coroutine, inside running loop → offload to reused thread

Args:
coro_or_result: Either a coroutine or an already-resolved value.

Returns:
The resolved (sync) value.
"""
if not asyncio.iscoroutine(coro_or_result):
return coro_or_result
try:
asyncio.get_running_loop()
return _EXECUTOR.submit(asyncio.run, coro_or_result).result()
except RuntimeError:
return asyncio.run(coro_or_result)
Loading
Loading