Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion src/lerobot/envs/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import importlib
from functools import partial
from typing import Any

import gymnasium as gym
Expand Down Expand Up @@ -163,7 +164,9 @@ def make_env(
if n_envs < 1:
raise ValueError("`n_envs` must be at least 1")

env_cls = gym.vector.AsyncVectorEnv if use_async_envs else gym.vector.SyncVectorEnv
env_cls = (
partial(gym.vector.AsyncVectorEnv, context="spawn") if use_async_envs else gym.vector.SyncVectorEnv
)
Comment on lines +167 to +169
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Using partial(...) means env_cls is no longer a class when use_async_envs=True. If any downstream code expects class-like behavior (e.g., isinstance(..., env_cls), env_cls.__name__, type comparisons), this will break. A more robust approach is to keep env_cls as the class and pass context='spawn' at the construction site (or branch on async vs sync for instantiation), so the variable’s type stays consistent.

Copilot uses AI. Check for mistakes.

if "libero" in cfg.type:
from lerobot.envs.libero import create_libero_envs
Expand Down
20 changes: 17 additions & 3 deletions src/lerobot/envs/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,21 @@ def env_to_policy_features(env_cfg: EnvConfig) -> dict[str, PolicyFeature]:
return policy_features


def _has_env_attr(env: gym.vector.VectorEnv, attr: str) -> bool:
"""Check if sub-environments have an attribute, compatible with sync and async vector envs."""
Comment on lines +133 to +134
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The helper name/docstring says 'attr', but the async path effectively validates a no-arg callable via env.call(attr). Renaming to something like _has_env_method() (or clarifying in the docstring that this is specifically for env.call-invoked methods) would make expectations clearer and reduce misuse.

Copilot uses AI. Check for mistakes.
if hasattr(env, "envs"):
return hasattr(env.envs[0], attr)
try:
env.call(attr)
return True
except Exception:
return False
Comment on lines +133 to +141
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

_has_env_attr() swallows all exceptions from env.call(attr). This can mask real failures inside an existing method (e.g., task_description() raising), incorrectly treating it as 'missing'. Prefer catching only the 'missing attribute/method' error (typically AttributeError propagated from workers, or the specific vector-env error type) and re-raising other exceptions so actual bugs aren’t hidden.

Copilot uses AI. Check for mistakes.


def are_all_envs_same_type(env: gym.vector.VectorEnv) -> bool:
if not hasattr(env, "envs"):
# AsyncVectorEnv: cannot inspect subprocess env types directly
return True
first_type = type(env.envs[0]) # Get type of first env
return all(type(e) is first_type for e in env.envs) # Fast type check

Expand All @@ -139,7 +153,7 @@ def check_env_attributes_and_types(env: gym.vector.VectorEnv) -> None:
with warnings.catch_warnings():
warnings.simplefilter("once", UserWarning) # Apply filter only in this function

if not (hasattr(env.envs[0], "task_description") and hasattr(env.envs[0], "task")):
if not (_has_env_attr(env, "task_description") or _has_env_attr(env, "task")):
warnings.warn(
"The environment does not have 'task_description' and 'task'. Some policies require these features.",
UserWarning,
Comment on lines +156 to 159
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

This changes the warning condition from 'warn unless both are present' (previous not (A and B)) to 'warn only if neither is present' (not (A or B)). If the intent is to preserve prior behavior, this should remain an and check (or the warning message should be updated to match the new semantics).

Copilot uses AI. Check for mistakes.
Expand All @@ -155,7 +169,7 @@ def check_env_attributes_and_types(env: gym.vector.VectorEnv) -> None:

def add_envs_task(env: gym.vector.VectorEnv, observation: RobotObservation) -> RobotObservation:
"""Adds task feature to the observation dict with respect to the first environment attribute."""
if hasattr(env.envs[0], "task_description"):
if _has_env_attr(env, "task_description"):
task_result = env.call("task_description")
Comment on lines +172 to 173
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

In the async case, _has_env_attr() calls env.call(...) and then add_envs_task() calls env.call(...) again, doubling cross-process IPC and potentially invoking the method twice (side effects / non-determinism). Consider restructuring to attempt env.call('task_description') once (handling only the 'method missing' case), otherwise fall back to env.call('task').

Copilot uses AI. Check for mistakes.

if isinstance(task_result, tuple):
Expand All @@ -167,7 +181,7 @@ def add_envs_task(env: gym.vector.VectorEnv, observation: RobotObservation) -> R
raise TypeError("All items in task_description result must be strings")

observation["task"] = task_result
elif hasattr(env.envs[0], "task"):
elif _has_env_attr(env, "task"):
task_result = env.call("task")
Comment on lines +184 to 185
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

In the async case, _has_env_attr() calls env.call(...) and then add_envs_task() calls env.call(...) again, doubling cross-process IPC and potentially invoking the method twice (side effects / non-determinism). Consider restructuring to attempt env.call('task_description') once (handling only the 'method missing' case), otherwise fall back to env.call('task').

Copilot uses AI. Check for mistakes.

if isinstance(task_result, tuple):
Expand Down
Loading