From 8be0ba67a66eb22c0148e524b14a97eab1dc7840 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:03:06 +0530 Subject: [PATCH 1/2] fix(controller): handle empty action chunks gracefully by raising PolicyError --- src/inspect_robots/controller.py | 7 +++++ tests/test_controller.py | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/inspect_robots/controller.py b/src/inspect_robots/controller.py index 2acb742c..55cd00ed 100644 --- a/src/inspect_robots/controller.py +++ b/src/inspect_robots/controller.py @@ -23,6 +23,7 @@ import numpy as np +from inspect_robots.errors import PolicyError from inspect_robots.policy import Policy from inspect_robots.spaces import Box from inspect_robots.types import Action, Observation @@ -58,6 +59,8 @@ def next_action( buffer: deque[Action] = store.setdefault(_BUFFER_KEY, deque()) if not buffer: chunk = policy.act(observation) + if not chunk.actions: + raise PolicyError("policy emitted an empty ActionChunk (0 actions)") take = self.replan_interval or len(chunk) taken = list(chunk.actions)[:take] buffer.extend(taken) @@ -163,6 +166,8 @@ def next_action( ) -> Action: """Query once for step ``t`` and blend every retained prediction for that step.""" chunk = policy.act(observation) + if not chunk.actions: + raise PolicyError("policy emitted an empty ActionChunk (0 actions)") store.setdefault(_INFER_KEY, []).append((chunk.inference_latency_s, len(chunk))) buffer: list[tuple[int, list[Any], dict[str, Any]]] = store.setdefault(_ENSEMBLE_KEY, []) @@ -179,6 +184,8 @@ def next_action( buffer.sort(key=lambda e: e[0]) predictions = [acts[t - q] for (q, acts, _meta) in buffer] + if not predictions: + raise PolicyError("no valid action predictions available for current step") weights = np.exp(-self.m * np.arange(len(predictions))) weights = weights / weights.sum() blended = np.average(np.stack(predictions), axis=0, weights=weights) diff --git a/tests/test_controller.py b/tests/test_controller.py index 29a8bbc7..24a481e0 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -99,3 +99,53 @@ def test_stacked_smoothing_controllers_keep_separate_state() -> None: expected.append(outer_prev) assert np.allclose(actual, expected) + + +class _EmptyChunkPolicy: + """Emits an empty ActionChunk with zero actions.""" + + def __init__(self) -> None: + self.info = PolicyInfo( + name="empty", + action_space=Box(shape=(2,), semantics=ActionSemantics(control_mode="joint_delta")), + ) + self.config = PolicyConfig() + + def reset(self, scene: Scene) -> None: + pass + + def act(self, observation: Observation) -> ActionChunk: + chunk = ActionChunk(actions=[Action(data=np.zeros(2))]) + object.__setattr__(chunk, "actions", []) + return chunk + + +def test_default_controller_raises_policy_error_on_empty_chunk() -> None: + import pytest + + from inspect_robots.errors import PolicyError + + policy = _EmptyChunkPolicy() + embodiment = CubePickEmbodiment() + obs = _obs(embodiment) + store: dict[str, object] = {} + ctrl = DefaultController() + + with pytest.raises(PolicyError, match="empty ActionChunk"): + ctrl.next_action(policy, obs, 0, store) + + +def test_ensembling_controller_raises_policy_error_on_empty_chunk() -> None: + import pytest + + from inspect_robots.controller import EnsemblingController + from inspect_robots.errors import PolicyError + + policy = _EmptyChunkPolicy() + embodiment = CubePickEmbodiment() + obs = _obs(embodiment) + store: dict[str, object] = {} + ctrl = EnsemblingController(policy.info.action_space) + + with pytest.raises(PolicyError, match="empty ActionChunk"): + ctrl.next_action(policy, obs, 0, store) From 52d9f5315356bda16a92ade5b5bf8fb87c9fc399 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:16:24 +0530 Subject: [PATCH 2/2] fix(controller): add pragma no cover for defensive unreachable predictions check --- src/inspect_robots/controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/inspect_robots/controller.py b/src/inspect_robots/controller.py index 55cd00ed..814e56f9 100644 --- a/src/inspect_robots/controller.py +++ b/src/inspect_robots/controller.py @@ -184,8 +184,9 @@ def next_action( buffer.sort(key=lambda e: e[0]) predictions = [acts[t - q] for (q, acts, _meta) in buffer] - if not predictions: + if not predictions: # pragma: no cover raise PolicyError("no valid action predictions available for current step") + weights = np.exp(-self.m * np.arange(len(predictions))) weights = weights / weights.sum() blended = np.average(np.stack(predictions), axis=0, weights=weights)