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 docs/configuration/Chapter_15.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The `SESSION_START` payload is the full wired topology at invocation time. Use i
| `TOOL_START` | Tool execution begins |
| `TOOL_END` | Tool execution completes |
| `INTERRUPT` | Agent pauses for human input |
| `COMPLETE` | Agent finishes (with usage metrics) |
| `AGENT_COMPLETE` | Agent finishes (with usage metrics) |
| `ERROR` | Model or execution error |

### Multi-agent events
Expand Down
8 changes: 4 additions & 4 deletions src/strands_compose/converters/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def convert(self, event: StreamEvent) -> list[dict[str, Any]]:
EventType.REASONING: self._handle_reasoning,
EventType.TOOL_START: self._handle_tool_start,
EventType.TOOL_END: self._handle_tool_end,
EventType.COMPLETE: self._handle_complete,
EventType.AGENT_COMPLETE: self._handle_agent_complete,
EventType.MULTIAGENT_COMPLETE: self._handle_multiagent_complete,
EventType.ERROR: self._handle_error,
EventType.NODE_START: self._handle_node_start,
Expand Down Expand Up @@ -186,7 +186,7 @@ def _terminal_chunks(
"""Build the terminal finish_reason chunk and optional trailing usage chunk.

Always emits ``finish_reason: "stop"``. ``"tool_calls"`` is never used
because by the time COMPLETE fires every tool has already run inside the
because by the time AGENT_COMPLETE fires every tool has already run inside the
strands loop — emitting ``"tool_calls"`` would cause clients to wait for
results that never arrive.
"""
Expand Down Expand Up @@ -362,8 +362,8 @@ def _handle_node_stop(self, event: StreamEvent, is_entry: bool) -> list[dict[str
]
return []

def _handle_complete(self, event: StreamEvent, is_entry: bool) -> list[dict[str, Any]]:
"""COMPLETE → terminal chunks for entry agent; silent for sub-agents."""
def _handle_agent_complete(self, event: StreamEvent, is_entry: bool) -> list[dict[str, Any]]:
"""AGENT_COMPLETE → terminal chunks for entry agent; silent for sub-agents."""
if not is_entry:
return []
return self._terminal_chunks(event.data.get("usage", {}))
Expand Down
8 changes: 4 additions & 4 deletions src/strands_compose/hooks/event_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def __init__(
"""Initialize the EventPublisher.

Converts strands hook events into :class:`StreamEvent` objects and
delivers them to an external callback. Emits a COMPLETE event at
delivers them to an external callback. Emits an AGENT_COMPLETE event at
the end of each invocation with usage metrics from ``EventLoopMetrics``.

For TOKEN and REASONING events use :meth:`as_callback_handler` to
Expand Down Expand Up @@ -222,7 +222,7 @@ def _on_tool_end(self, event: AfterToolCallEvent) -> None:
)

def _on_complete(self, event: AfterInvocationEvent) -> None:
"""Emit COMPLETE with usage metrics from EventLoopMetrics.
"""Emit AGENT_COMPLETE with usage metrics from EventLoopMetrics.

Suppressed when the invocation errored — an ERROR event was
already emitted via :meth:`_on_model_error`.
Expand Down Expand Up @@ -254,7 +254,7 @@ def _on_complete(self, event: AfterInvocationEvent) -> None:

self._callback(
StreamEvent(
type=EventType.COMPLETE,
type=EventType.AGENT_COMPLETE,
agent_name=self._agent_name,
data={
"type": "agent",
Expand All @@ -274,7 +274,7 @@ def _on_model_error(self, event: AfterModelCallEvent) -> None:

Fires for provider-level exceptions such as expired credentials,
throttling, network errors, or any other model API failure.
Sets ``_errored`` to suppress the subsequent misleading COMPLETE.
Sets ``_errored`` to suppress the subsequent misleading AGENT_COMPLETE.
"""
if event.exception is None:
return
Expand Down
4 changes: 2 additions & 2 deletions src/strands_compose/renderers/ansi.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def __init__(
EventType.AGENT_START: self._handle_agent_start,
EventType.TOOL_START: self._handle_tool_start,
EventType.TOOL_END: self._handle_tool_end,
EventType.COMPLETE: self._handle_complete,
EventType.AGENT_COMPLETE: self._handle_agent_complete,
EventType.ERROR: self._handle_error,
EventType.NODE_START: self._handle_node_start,
EventType.NODE_STOP: self._handle_node_stop,
Expand Down Expand Up @@ -187,7 +187,7 @@ def _handle_tool_end(self, event: StreamEvent) -> None:
self._out.write(f" {self._green}✓{self._reset} [{agent}] tool done\n")
self._out.flush()

def _handle_complete(self, event: StreamEvent) -> None:
def _handle_agent_complete(self, event: StreamEvent) -> None:
self._break()
self._mode = None
self._active_agent = None
Expand Down
4 changes: 2 additions & 2 deletions src/strands_compose/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class EventType(StrEnum):
TOOL_END = "tool_end"
REASONING = "reasoning"
INTERRUPT = "interrupt"
COMPLETE = "complete"
AGENT_COMPLETE = "agent_complete"
ERROR = "error"

# Multi-agent events
Expand All @@ -61,7 +61,7 @@ class StreamEvent:
"""A typed event from agent or multi-agent execution.

Per-agent activity (``AGENT_START``, ``TOKEN``, ``REASONING``,
``TOOL_START``, ``TOOL_END``, ``INTERRUPT``, ``COMPLETE``, ``ERROR``,
``TOOL_START``, ``TOOL_END``, ``INTERRUPT``, ``AGENT_COMPLETE``, ``ERROR``,
``NODE_START``, ``NODE_STOP``, ``HANDOFF``, ``MULTIAGENT_START``,
``MULTIAGENT_COMPLETE``) is produced by
:class:`~strands_compose.hooks.EventPublisher`. Session-level events
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 @@ -9,7 +9,7 @@

:func:`make_event_queue` attaches :class:`~strands_compose.hooks.EventPublisher`
hooks to every agent so all per-agent events (TOKEN, REASONING, TOOL_START,
TOOL_END, INTERRUPT, COMPLETE, and — for Swarm/Graph — NODE_START, NODE_STOP,
TOOL_END, INTERRUPT, AGENT_COMPLETE, and — for Swarm/Graph — NODE_START, NODE_STOP,
HANDOFF, MULTIAGENT_COMPLETE) flow into the shared queue.

Hooks are wired **once per session**. Between requests on the same session,
Expand Down
22 changes: 11 additions & 11 deletions tests/unit/converters/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def entry_name_converter() -> OpenAIStreamConverter:


class TestPlainTextStream:
"""Simple text-only response: tokens then COMPLETE."""
"""Simple text-only response: tokens then AGENT_COMPLETE."""

def test_token_events_produce_content_delta_chunks(
self, converter: OpenAIStreamConverter
Expand Down Expand Up @@ -136,13 +136,13 @@ def test_role_assistant_sent_on_first_chunk_only(
assert "role" not in _delta(chunks[1])

def test_complete_emits_stop_finish_reason(self, converter: OpenAIStreamConverter) -> None:
"""COMPLETE produces a single chunk with finish_reason='stop' and empty delta."""
"""AGENT_COMPLETE produces a single chunk with finish_reason='stop' and empty delta."""
conv = converter
chunks = _flush(
conv,
[
_ev(EventType.TOKEN, text="hi"),
_ev(EventType.COMPLETE, usage={}),
_ev(EventType.AGENT_COMPLETE, usage={}),
],
)

Expand All @@ -160,7 +160,7 @@ def test_completion_id_is_consistent_across_stream(
[
_ev(EventType.TOKEN, text="x"),
_ev(EventType.TOKEN, text="y"),
_ev(EventType.COMPLETE, usage={}),
_ev(EventType.AGENT_COMPLETE, usage={}),
],
)

Expand All @@ -175,7 +175,7 @@ def test_usage_fields_mapped_to_openai_names(self, converter: OpenAIStreamConver
conv,
[
_ev(
EventType.COMPLETE,
EventType.AGENT_COMPLETE,
usage={"input_tokens": 10, "output_tokens": 20, "total_tokens": 30},
),
],
Expand All @@ -195,7 +195,7 @@ def test_all_chunks_carry_required_openai_envelope_fields(
conv,
[
_ev(EventType.TOKEN, text="hi"),
_ev(EventType.COMPLETE, usage={}),
_ev(EventType.AGENT_COMPLETE, usage={}),
],
)

Expand Down Expand Up @@ -315,7 +315,7 @@ def test_tool_end_emits_details_closer_with_result(
assert "4" in html

def test_finish_reason_is_stop_never_tool_calls(self, converter: OpenAIStreamConverter) -> None:
"""COMPLETE after tool use emits stop, never tool_calls (would cause client loop)."""
"""AGENT_COMPLETE after tool use emits stop, never tool_calls (would cause client loop)."""
conv = converter
_flush(
conv,
Expand All @@ -324,7 +324,7 @@ def test_finish_reason_is_stop_never_tool_calls(self, converter: OpenAIStreamCon
_ev(EventType.TOOL_END, tool_use_id="c", result="ok"),
],
)
chunks = conv.convert(_ev(EventType.COMPLETE, usage={}))
chunks = conv.convert(_ev(EventType.AGENT_COMPLETE, usage={}))

assert _finish(chunks[0]) == "stop"

Expand All @@ -340,7 +340,7 @@ def test_no_native_tool_calls_delta_is_ever_emitted(
_ev(EventType.TOOL_END, tool_use_id="x1", tool_result="1"),
_ev(EventType.TOOL_START, tool_name="b", tool_use_id="x2", tool_input={}),
_ev(EventType.TOOL_END, tool_use_id="x2", tool_result="2"),
_ev(EventType.COMPLETE, usage={}),
_ev(EventType.AGENT_COMPLETE, usage={}),
],
)

Expand Down Expand Up @@ -381,9 +381,9 @@ def test_token_from_sub_agent_produces_no_chunks(
def test_complete_from_sub_agent_produces_no_chunks(
self, converter: OpenAIStreamConverter
) -> None:
"""COMPLETE from a sub-agent does not close the stream."""
"""AGENT_COMPLETE from a sub-agent does not close the stream."""
conv = converter
chunks = conv.convert(_ev(EventType.COMPLETE, agent=SUB, usage={}))
chunks = conv.convert(_ev(EventType.AGENT_COMPLETE, agent=SUB, usage={}))

assert chunks == []

Expand Down
8 changes: 4 additions & 4 deletions tests/unit/converters/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_convert_returns_event_asdict(self) -> None:
def test_convert_returns_list_with_single_element(self) -> None:
"""convert() always returns a list of length 1."""
conv = RawStreamConverter()
event = _event("complete")
event = _event("agent_complete")
result = conv.convert(event)

assert isinstance(result, list)
Expand All @@ -53,7 +53,7 @@ def test_convert_preserves_payload_fields(self) -> None:

@pytest.mark.parametrize(
"event_type",
["token", "reasoning", "tool_start", "tool_end", "complete", "error"],
["token", "reasoning", "tool_start", "tool_end", "agent_complete", "error"],
)
def test_convert_works_for_all_event_types(self, event_type: str) -> None:
"""convert() handles any event type without raising."""
Expand Down Expand Up @@ -94,9 +94,9 @@ def test_from_dict_with_only_type_field(self) -> None:

def test_from_dict_missing_optional_fields_uses_defaults(self) -> None:
"""from_dict() sets agent_name='' and data={} when those keys are absent."""
event = StreamEvent.from_dict({"type": "complete"})
event = StreamEvent.from_dict({"type": "agent_complete"})

assert event.type == "complete"
assert event.type == "agent_complete"
assert event.agent_name == ""
assert event.data == {}

Expand Down
12 changes: 6 additions & 6 deletions tests/unit/hooks/test_event_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def test_complete_emits_with_usage(self):
pub._on_complete(complete_event)

assert len(events) == 1
assert events[0].type == EventType.COMPLETE
assert events[0].type == EventType.AGENT_COMPLETE
assert events[0].data["usage"]["input_tokens"] == 10
assert events[0].data["usage"]["output_tokens"] == 5
assert events[0].data["usage"]["total_tokens"] == 15
Expand Down Expand Up @@ -189,7 +189,7 @@ def test_non_handoff_type_does_not_emit_handoff_event(self) -> None:


class TestModelErrorCapture:
"""EventPublisher emits ERROR events on model failures and suppresses COMPLETE."""
"""EventPublisher emits ERROR events on model failures and suppresses AGENT_COMPLETE."""

def test_model_error_emits_error_event(self) -> None:
"""AfterModelCallEvent with exception emits ERROR."""
Expand Down Expand Up @@ -230,7 +230,7 @@ def test_successful_model_call_does_not_emit_error(self) -> None:
assert pub._errored is False

def test_complete_suppressed_after_error(self) -> None:
"""COMPLETE is not emitted when the invocation errored."""
"""AGENT_COMPLETE is not emitted when the invocation errored."""
events: list = []
pub = EventPublisher(callback=events.append, agent_name="test")

Expand All @@ -247,12 +247,12 @@ def test_complete_suppressed_after_error(self) -> None:
complete_event.agent.event_loop_metrics = metrics
pub._on_complete(complete_event)

# Only the ERROR event, no COMPLETE
# Only the ERROR event, no AGENT_COMPLETE
assert len(events) == 1
assert events[0].type == EventType.ERROR

def test_complete_emitted_when_no_error(self) -> None:
"""COMPLETE is emitted normally when there was no error."""
"""AGENT_COMPLETE is emitted normally when there was no error."""
events: list = []
pub = EventPublisher(callback=events.append, agent_name="test")

Expand All @@ -264,7 +264,7 @@ def test_complete_emitted_when_no_error(self) -> None:
pub._on_complete(complete_event)

assert len(events) == 1
assert events[0].type == EventType.COMPLETE
assert events[0].type == EventType.AGENT_COMPLETE

def test_errored_flag_resets_on_next_invocation(self) -> None:
"""_errored resets to False when a new invocation starts."""
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/renderers/test_ansi.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_tool_end_error(self) -> None:

def test_complete(self) -> None:
r, buf = self._renderer()
r.render(_event(EventType.COMPLETE, usage={"input_tokens": 42, "output_tokens": 80}))
r.render(_event(EventType.AGENT_COMPLETE, usage={"input_tokens": 42, "output_tokens": 80}))
output = buf.getvalue()
assert "complete" in output
assert "42" in output
Expand Down Expand Up @@ -177,7 +177,7 @@ def test_structured_event_after_tokens_inserts_newline(self) -> None:
"""Any non-token event must break the inline token stream."""
r, buf = self._renderer()
r.render(_event(EventType.TOKEN, text="partial"))
r.render(_event(EventType.COMPLETE, usage={}))
r.render(_event(EventType.AGENT_COMPLETE, usage={}))
output = buf.getvalue()
# "partial" followed by "\n" followed by the complete line
idx_partial = output.index("partial")
Expand Down
20 changes: 10 additions & 10 deletions tests/unit/test_golden_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class TestGoldenSimpleTextResponse:
1. AGENT_START
2. TOKEN("Hello")
3. TOKEN(" world")
4. COMPLETE(usage)
4. AGENT_COMPLETE(usage)
"""

def test_simple_text_produces_correct_event_sequence(self) -> None:
Expand Down Expand Up @@ -61,7 +61,7 @@ def test_simple_text_produces_correct_event_sequence(self) -> None:
assert events[1].data["text"] == "Hello"
assert events[2].type == EventType.TOKEN
assert events[2].data["text"] == " world"
assert events[3].type == EventType.COMPLETE
assert events[3].type == EventType.AGENT_COMPLETE
assert events[3].data["usage"]["input_tokens"] == 50
assert events[3].data["usage"]["output_tokens"] == 10

Expand All @@ -79,7 +79,7 @@ class TestGoldenToolCallingAgent:
2. TOOL_START(search)
3. TOOL_END(search, success)
4. TOKEN("Based on the search...")
5. COMPLETE(usage)
5. AGENT_COMPLETE(usage)
"""

def test_tool_calling_produces_correct_event_sequence(self) -> None:
Expand Down Expand Up @@ -124,7 +124,7 @@ def test_tool_calling_produces_correct_event_sequence(self) -> None:
EventType.TOOL_START,
EventType.TOOL_END,
EventType.TOKEN,
EventType.COMPLETE,
EventType.AGENT_COMPLETE,
]
assert events[1].data["tool_name"] == "search"
assert events[1].data["tool_use_id"] == "call_abc"
Expand All @@ -144,7 +144,7 @@ class TestGoldenReasoningThenResponse:
1. AGENT_START
2. REASONING("Let me think...")
3. TOKEN("The answer is 42")
4. COMPLETE
4. AGENT_COMPLETE
"""

def test_reasoning_then_response_produces_correct_sequence(self) -> None:
Expand All @@ -169,7 +169,7 @@ def test_reasoning_then_response_produces_correct_sequence(self) -> None:
assert events[1].data["text"] == "Let me think..."
assert events[2].type == EventType.TOKEN
assert events[2].data["text"] == "The answer is 42"
assert events[3].type == EventType.COMPLETE
assert events[3].type == EventType.AGENT_COMPLETE


# ---------------------------------------------------------------------------
Expand All @@ -178,12 +178,12 @@ def test_reasoning_then_response_produces_correct_sequence(self) -> None:


class TestGoldenModelError:
"""Golden test: model call fails → ERROR emitted, COMPLETE suppressed.
"""Golden test: model call fails → ERROR emitted, AGENT_COMPLETE suppressed.

Expected event sequence:
1. AGENT_START
2. ERROR(expired credentials)
(no COMPLETE)
(no AGENT_COMPLETE)
"""

def test_model_error_produces_correct_sequence(self) -> None:
Expand All @@ -205,7 +205,7 @@ def test_model_error_produces_correct_sequence(self) -> None:
complete.agent.event_loop_metrics = metrics
pub._on_complete(complete)

# Only AGENT_START + ERROR, no COMPLETE
# Only AGENT_START + ERROR, no AGENT_COMPLETE
assert len(events) == 2
assert events[0].type == EventType.AGENT_START
assert events[1].type == EventType.ERROR
Expand Down Expand Up @@ -296,7 +296,7 @@ class TestGoldenToolError:
2. TOOL_START(db_query)
3. TOOL_END(db_query, error)
4. TOKEN("I encountered an error...")
5. COMPLETE
5. AGENT_COMPLETE
"""

def test_tool_error_produces_correct_sequence(self) -> None:
Expand Down
Loading
Loading