From f77402c7802af4cb0876c6696202437fdf7a0ed4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 13:52:12 +0000 Subject: [PATCH 1/3] Initial plan From 9312f733e59b6ed5288c558a2536889e20773dd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 13:55:29 +0000 Subject: [PATCH 2/3] feat: stream agent interrupt events Agent-Logs-Url: https://github.com/strands-compose/sdk-python/sessions/8e9b0572-1021-4350-9815-8757ec1b3312 Co-authored-by: galuszkm <53984802+galuszkm@users.noreply.github.com> --- README.md | 2 +- docs/configuration/Chapter_15.md | 1 + examples/12_streaming/README.md | 1 + src/strands_compose/hooks/event_publisher.py | 16 +++++++++ src/strands_compose/types.py | 3 +- src/strands_compose/wire.py | 2 +- tests/unit/hooks/test_event_publisher.py | 37 ++++++++++++++++++++ 7 files changed, 59 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4d5777d..3a4e528 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,7 @@ async def main(): asyncio.run(main()) ``` -Event types: `AGENT_START`, `TOKEN`, `REASONING`, `TOOL_START`, `TOOL_END`, `NODE_START`, `NODE_STOP`, `HANDOFF`, `COMPLETE`, `MULTIAGENT_START`, `MULTIAGENT_COMPLETE`, `ERROR` — each carrying `{type, agent_name, timestamp, data}`. Enough for a real-time frontend, a log aggregator, or a debugging dashboard. The `AnsiRenderer` gives you coloured terminal output out of the box — agent names, tool calls, reasoning traces, all streaming live. +Event types: `AGENT_START`, `TOKEN`, `REASONING`, `TOOL_START`, `TOOL_END`, `INTERRUPT`, `NODE_START`, `NODE_STOP`, `HANDOFF`, `COMPLETE`, `MULTIAGENT_START`, `MULTIAGENT_COMPLETE`, `ERROR` — each carrying `{type, agent_name, timestamp, data}`. Enough for a real-time frontend, a log aggregator, or a debugging dashboard. The `AnsiRenderer` gives you coloured terminal output out of the box — agent names, tool calls, reasoning traces, all streaming live. --- diff --git a/docs/configuration/Chapter_15.md b/docs/configuration/Chapter_15.md index 03e300a..baffeef 100644 --- a/docs/configuration/Chapter_15.md +++ b/docs/configuration/Chapter_15.md @@ -39,6 +39,7 @@ asyncio.run(main()) | `REASONING` | Reasoning/thinking content from LLM | | `TOOL_START` | Tool execution begins | | `TOOL_END` | Tool execution completes | +| `INTERRUPT` | Agent pauses for human input | | `COMPLETE` | Agent finishes (with usage metrics) | | `ERROR` | Model or execution error | | `NODE_START` | Graph/swarm node begins | diff --git a/examples/12_streaming/README.md b/examples/12_streaming/README.md index 18aa831..ab77bf6 100644 --- a/examples/12_streaming/README.md +++ b/examples/12_streaming/README.md @@ -38,6 +38,7 @@ renderer.flush() | `reasoning` | Streaming reasoning chunk | | `tool_start` | Tool call begins | | `tool_end` | Tool call finished | +| `interrupt` | Agent pauses for human input | | `complete` | Agent finished (includes token usage) | | `node_start` / `node_stop` | Swarm / Graph enters/leaves a node | | `handoff` | Swarm transfers control | diff --git a/src/strands_compose/hooks/event_publisher.py b/src/strands_compose/hooks/event_publisher.py index d3c18db..a18ed3a 100644 --- a/src/strands_compose/hooks/event_publisher.py +++ b/src/strands_compose/hooks/event_publisher.py @@ -230,6 +230,22 @@ def _on_complete(self, event: AfterInvocationEvent) -> None: if self._errored: return + result = event.result + if result is not None and result.stop_reason == "interrupt": + for interrupt in result.interrupts or []: + self._callback( + StreamEvent( + type=EventType.INTERRUPT, + agent_name=self._agent_name, + data={ + "interrupt_id": interrupt.id, + "name": interrupt.name, + "reason": interrupt.reason, + }, + ), + ) + return + metrics = event.agent.event_loop_metrics # Usage from the latest invocation (current turn only). diff --git a/src/strands_compose/types.py b/src/strands_compose/types.py index 5fb2f28..504babb 100644 --- a/src/strands_compose/types.py +++ b/src/strands_compose/types.py @@ -33,6 +33,7 @@ class EventType(StrEnum): TOOL_START = "tool_start" TOOL_END = "tool_end" REASONING = "reasoning" + INTERRUPT = "interrupt" COMPLETE = "complete" ERROR = "error" @@ -50,7 +51,7 @@ class StreamEvent: Produced by :class:`~strands_compose.hooks.EventPublisher` for all agent activity: ``TOKEN``, ``REASONING``, ``TOOL_START``, ``TOOL_END``, - ``COMPLETE``, ``ERROR``, ``NODE_START``, ``NODE_STOP``, + ``INTERRUPT``, ``COMPLETE``, ``ERROR``, ``NODE_START``, ``NODE_STOP``, ``HANDOFF``, ``MULTIAGENT_COMPLETE``. Attributes: diff --git a/src/strands_compose/wire.py b/src/strands_compose/wire.py index 456ed9c..7e2385d 100644 --- a/src/strands_compose/wire.py +++ b/src/strands_compose/wire.py @@ -6,7 +6,7 @@ :class:`EventQueue` is a thin async queue wrapper that hides the sentinel pattern from callers. :func:`make_event_queue` attaches :class:`~strands_compose.hooks.EventPublisher` hooks to every agent -so all events (TOKEN, REASONING, TOOL_START, TOOL_END, COMPLETE, +so all events (TOKEN, REASONING, TOOL_START, TOOL_END, INTERRUPT, COMPLETE, and — for Swarm/Graph — NODE_START, NODE_STOP, HANDOFF, MULTIAGENT_COMPLETE) flow into the shared queue. diff --git a/tests/unit/hooks/test_event_publisher.py b/tests/unit/hooks/test_event_publisher.py index 3961456..c774bdf 100644 --- a/tests/unit/hooks/test_event_publisher.py +++ b/tests/unit/hooks/test_event_publisher.py @@ -2,6 +2,7 @@ from __future__ import annotations +from types import SimpleNamespace from unittest.mock import MagicMock import pytest @@ -97,6 +98,42 @@ def test_complete_emits_with_usage(self): assert events[0].data["usage"]["output_tokens"] == 5 assert events[0].data["usage"]["total_tokens"] == 15 + def test_complete_with_interrupt_result_emits_interrupt_events(self) -> None: + events = [] + pub = EventPublisher(callback=events.append, agent_name="test") + + complete_event = MagicMock() + complete_event.result = SimpleNamespace( + stop_reason="interrupt", + interrupts=[ + SimpleNamespace(id="approval-1", name="approve_delete", reason="Delete file?"), + SimpleNamespace(id="approval-2", name="approve_deploy", reason={"env": "prod"}), + ], + ) + pub._on_complete(complete_event) + + assert [event.type for event in events] == [EventType.INTERRUPT, EventType.INTERRUPT] + assert events[0].data == { + "interrupt_id": "approval-1", + "name": "approve_delete", + "reason": "Delete file?", + } + assert events[1].data == { + "interrupt_id": "approval-2", + "name": "approve_deploy", + "reason": {"env": "prod"}, + } + + def test_complete_with_interrupt_result_does_not_emit_complete(self) -> None: + events = [] + pub = EventPublisher(callback=events.append, agent_name="test") + + complete_event = MagicMock() + complete_event.result = SimpleNamespace(stop_reason="interrupt", interrupts=[]) + pub._on_complete(complete_event) + + assert [event.type for event in events] == [] + class TestHandoffEvent: def test_callback_handler_emits_handoff_on_multiagent_handoff_type(self) -> None: From 0fdcfebd0a23a6ebe84016d0a77f6a487501df75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 13:57:24 +0000 Subject: [PATCH 3/3] test: include interrupt event type Agent-Logs-Url: https://github.com/strands-compose/sdk-python/sessions/8e9b0572-1021-4350-9815-8757ec1b3312 Co-authored-by: galuszkm <53984802+galuszkm@users.noreply.github.com> --- tests/unit/test_types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 34a89a8..e8aaf73 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -29,6 +29,7 @@ def test_is_str_enum_with_string_values(self): ("TOOL_START", "tool_start"), ("TOOL_END", "tool_end"), ("REASONING", "reasoning"), + ("INTERRUPT", "interrupt"), ("NODE_START", "node_start"), ("NODE_STOP", "node_stop"), ("HANDOFF", "handoff"), @@ -47,6 +48,7 @@ def test_all_members_present(self): "TOOL_START", "TOOL_END", "REASONING", + "INTERRUPT", "COMPLETE", "ERROR", "NODE_START",