Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
1 change: 1 addition & 0 deletions docs/configuration/Chapter_15.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions examples/12_streaming/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
16 changes: 16 additions & 0 deletions src/strands_compose/hooks/event_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
3 changes: 2 additions & 1 deletion src/strands_compose/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class EventType(StrEnum):
TOOL_START = "tool_start"
TOOL_END = "tool_end"
REASONING = "reasoning"
INTERRUPT = "interrupt"
COMPLETE = "complete"
ERROR = "error"

Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/strands_compose/wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
37 changes: 37 additions & 0 deletions tests/unit/hooks/test_event_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -47,6 +48,7 @@ def test_all_members_present(self):
"TOOL_START",
"TOOL_END",
"REASONING",
"INTERRUPT",
"COMPLETE",
"ERROR",
"NODE_START",
Expand Down
Loading