diff --git a/strix/core/execution.py b/strix/core/execution.py index c8ab54ce..7272a8fe 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -603,6 +603,11 @@ async def _run_cycle( # noqa: PLR0912, PLR0915 return await _handle_content_guardrail( coordinator, agent_id, exc, interactive=interactive ) + if isinstance(exc, ProviderRefusalError): + logger.warning("agent %s refused by the model provider: %s", agent_id, exc) + await coordinator.set_status(agent_id, "failed", error=str(exc)) + await _notify_parent_on_terminal(coordinator, agent_id, "failed") + return None if not interactive: raise if isinstance(exc, MaxTurnsExceeded): diff --git a/tests/test_execution.py b/tests/test_execution.py index 99712d9f..8fa043f1 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -599,6 +599,41 @@ async def test_structured_provider_refusal_fails_interactive_agent( assert coordinator.errors["root"] == refusal +@pytest.mark.asyncio +async def test_structured_provider_refusal_fails_noninteractive_child( + tmp_path: Any, + monkeypatch: pytest.MonkeyPatch, +) -> None: + refusal = "This request was blocked under the provider's usage policy." + stream = _StructuredRefusalStream(refusal) + monkeypatch.setattr(execution.Runner, "run_streamed", lambda *_args, **_kwargs: stream) + coordinator = AgentCoordinator() + await coordinator.register("root", "strix", parent_id=None) + await coordinator.register("child", "recon", parent_id="root") + session = SQLiteSession("root", tmp_path / "agents.db") + await coordinator.attach_runtime("root", session=session) + + result = await execution._run_cycle( + MagicMock(), + coordinator, + "child", + input_data="task", + run_config=MagicMock(), + context={"parent_id": "root"}, + max_turns=5, + session=None, + interactive=False, + event_sink=None, + hooks=None, + ) + + assert result is None + assert coordinator.statuses["child"] == "failed" + assert coordinator.errors["child"] == refusal + assert coordinator.pending_counts.get("root", 0) > 0 + session.close() + + @pytest.mark.asyncio async def test_resume_revives_guardrail_parked_child_but_not_plain_waiting( tmp_path: Any, monkeypatch: pytest.MonkeyPatch