diff --git a/docs/configuration/Chapter_15.md b/docs/configuration/Chapter_15.md index 4dac43c..533ed6d 100644 --- a/docs/configuration/Chapter_15.md +++ b/docs/configuration/Chapter_15.md @@ -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 diff --git a/src/strands_compose/converters/openai.py b/src/strands_compose/converters/openai.py index 7540e08..56a8557 100644 --- a/src/strands_compose/converters/openai.py +++ b/src/strands_compose/converters/openai.py @@ -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, @@ -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. """ @@ -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", {})) diff --git a/src/strands_compose/hooks/event_publisher.py b/src/strands_compose/hooks/event_publisher.py index a18ed3a..328d11c 100644 --- a/src/strands_compose/hooks/event_publisher.py +++ b/src/strands_compose/hooks/event_publisher.py @@ -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 @@ -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`. @@ -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", @@ -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 diff --git a/src/strands_compose/renderers/ansi.py b/src/strands_compose/renderers/ansi.py index 2eaf340..b7188cd 100644 --- a/src/strands_compose/renderers/ansi.py +++ b/src/strands_compose/renderers/ansi.py @@ -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, @@ -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 diff --git a/src/strands_compose/types.py b/src/strands_compose/types.py index c179ac7..54a3b13 100644 --- a/src/strands_compose/types.py +++ b/src/strands_compose/types.py @@ -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 @@ -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 diff --git a/src/strands_compose/wire.py b/src/strands_compose/wire.py index 38175ed..cff0615 100644 --- a/src/strands_compose/wire.py +++ b/src/strands_compose/wire.py @@ -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, diff --git a/tests/unit/converters/test_openai.py b/tests/unit/converters/test_openai.py index d00843e..758c5e7 100644 --- a/tests/unit/converters/test_openai.py +++ b/tests/unit/converters/test_openai.py @@ -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 @@ -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={}), ], ) @@ -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={}), ], ) @@ -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}, ), ], @@ -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={}), ], ) @@ -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, @@ -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" @@ -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={}), ], ) @@ -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 == [] diff --git a/tests/unit/converters/test_raw.py b/tests/unit/converters/test_raw.py index b79da5b..1a9d0b2 100644 --- a/tests/unit/converters/test_raw.py +++ b/tests/unit/converters/test_raw.py @@ -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) @@ -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.""" @@ -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 == {} diff --git a/tests/unit/hooks/test_event_publisher.py b/tests/unit/hooks/test_event_publisher.py index c774bdf..54b37b2 100644 --- a/tests/unit/hooks/test_event_publisher.py +++ b/tests/unit/hooks/test_event_publisher.py @@ -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 @@ -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.""" @@ -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") @@ -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") @@ -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.""" diff --git a/tests/unit/renderers/test_ansi.py b/tests/unit/renderers/test_ansi.py index 23fdf7a..5313db0 100644 --- a/tests/unit/renderers/test_ansi.py +++ b/tests/unit/renderers/test_ansi.py @@ -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 @@ -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") diff --git a/tests/unit/test_golden_outputs.py b/tests/unit/test_golden_outputs.py index 3c7f447..091a25a 100644 --- a/tests/unit/test_golden_outputs.py +++ b/tests/unit/test_golden_outputs.py @@ -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: @@ -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 @@ -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: @@ -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" @@ -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: @@ -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 # --------------------------------------------------------------------------- @@ -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: @@ -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 @@ -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: diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 3bb0de3..299f5c4 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -37,7 +37,7 @@ def test_is_str_enum_with_string_values(self): [ ("TOKEN", "token"), ("AGENT_START", "agent_start"), - ("COMPLETE", "complete"), + ("AGENT_COMPLETE", "agent_complete"), ("ERROR", "error"), ("TOOL_START", "tool_start"), ("TOOL_END", "tool_end"), @@ -73,7 +73,7 @@ def test_all_members_present(self): "TOOL_END", "REASONING", "INTERRUPT", - "COMPLETE", + "AGENT_COMPLETE", "ERROR", "NODE_START", "NODE_STOP", diff --git a/tests/unit/test_wire.py b/tests/unit/test_wire.py index 3429370..522e13b 100644 --- a/tests/unit/test_wire.py +++ b/tests/unit/test_wire.py @@ -38,7 +38,7 @@ def test_eq_ignores_timestamp(self): def test_eq_different_type_not_equal(self): e1 = StreamEvent(type=EventType.TOKEN, agent_name="a") - e2 = StreamEvent(type=EventType.COMPLETE, agent_name="a") + e2 = StreamEvent(type=EventType.AGENT_COMPLETE, agent_name="a") assert e1 != e2 def test_eq_different_data_not_equal(self):