From 79986fc0e369c91501671aca20403eb1d84d49d2 Mon Sep 17 00:00:00 2001 From: Aaron Farntrog Date: Fri, 23 Jan 2026 12:39:23 -0500 Subject: [PATCH] feat(agent): update AgentResult __str__ priority order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change string representation priority to: 1. Interrupts (if present) → stringified list of interrupt dicts 2. Structured output (if present) → JSON string 3. Text content from message → concatenated text blocks Previously, structured output was only used as fallback when no text content existed. Now structured output and interrupts take precedence over raw text content for more predictable serialization behavior. --- src/strands/agent/agent_result.py | 21 ++--- tests/strands/agent/test_agent_result.py | 98 ++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 14 deletions(-) diff --git a/src/strands/agent/agent_result.py b/src/strands/agent/agent_result.py index 8f9241a67..63b7a0d4a 100644 --- a/src/strands/agent/agent_result.py +++ b/src/strands/agent/agent_result.py @@ -36,17 +36,23 @@ class AgentResult: structured_output: BaseModel | None = None def __str__(self) -> str: - """Get the agent's last message as a string. + """Return a string representation of the agent result. - This method extracts and concatenates all text content from the final message, ignoring any non-text content - like images or structured data. If there's no text content but structured output is present, it serializes - the structured output instead. + Priority order: + 1. Interrupts (if present) → stringified list of interrupt dicts + 2. Structured output (if present) → JSON string + 3. Text content from message → concatenated text blocks Returns: - The agent's last message as a string. + String representation based on the priority order above. """ - content_array = self.message.get("content", []) + if self.interrupts: + return str([interrupt.to_dict() for interrupt in self.interrupts]) + + if self.structured_output: + return self.structured_output.model_dump_json() + content_array = self.message.get("content", []) result = "" for item in content_array: if isinstance(item, dict): @@ -59,9 +65,6 @@ def __str__(self) -> str: if isinstance(content, dict) and "text" in content: result += content.get("text", "") + "\n" - if not result and self.structured_output: - result = self.structured_output.model_dump_json() - return result @classmethod diff --git a/tests/strands/agent/test_agent_result.py b/tests/strands/agent/test_agent_result.py index 6e4c2c91a..fa9ec4ad9 100644 --- a/tests/strands/agent/test_agent_result.py +++ b/tests/strands/agent/test_agent_result.py @@ -5,6 +5,7 @@ from pydantic import BaseModel from strands.agent.agent_result import AgentResult +from strands.interrupt import Interrupt from strands.telemetry.metrics import EventLoopMetrics from strands.types.content import Message from strands.types.streaming import StopReason @@ -185,7 +186,7 @@ def test__init__structured_output_defaults_to_none(mock_metrics, simple_message: def test__str__with_structured_output(mock_metrics, simple_message: Message): - """Test that str() is not affected by structured_output.""" + """Test that str() returns structured output JSON when structured_output is present.""" structured_output = StructuredOutputModel(name="test", value=42) result = AgentResult( @@ -196,11 +197,11 @@ def test__str__with_structured_output(mock_metrics, simple_message: Message): structured_output=structured_output, ) - # The string representation should only include the message text, not structured output + # When structured_output is present, it takes priority over message text message_string = str(result) - assert message_string == "Hello world!\n" - assert "test" not in message_string - assert "42" not in message_string + assert message_string == structured_output.model_dump_json() + assert "test" in message_string + assert "42" in message_string def test__str__empty_message_with_structured_output(mock_metrics, empty_message: Message): @@ -283,3 +284,90 @@ def test__str__mixed_text_and_citations_content(mock_metrics, mixed_text_and_cit message_string = str(result) assert message_string == "Introduction paragraph\nCited content here.\nConclusion paragraph\n" + + +def test__str__with_interrupts(mock_metrics, simple_message: Message): + """Test that str() returns stringified interrupts when present.""" + interrupts = [ + Interrupt(id="int-1", name="approval", reason="Need user approval"), + Interrupt(id="int-2", name="input", reason="Need more info"), + ] + + result = AgentResult( + stop_reason="end_turn", + message=simple_message, + metrics=mock_metrics, + state={}, + interrupts=interrupts, + ) + + message_string = str(result) + + # Should contain stringified interrupt dicts + assert "int-1" in message_string + assert "approval" in message_string + assert "Need user approval" in message_string + assert "int-2" in message_string + assert "input" in message_string + assert "Need more info" in message_string + + +def test__str__interrupts_priority_over_structured_output(mock_metrics, simple_message: Message): + """Test that interrupts take priority over structured_output in str().""" + interrupts = [Interrupt(id="int-1", name="approval", reason="Needs approval")] + structured_output = StructuredOutputModel(name="test", value=42) + + result = AgentResult( + stop_reason="end_turn", + message=simple_message, + metrics=mock_metrics, + state={}, + interrupts=interrupts, + structured_output=structured_output, + ) + + message_string = str(result) + + # Should return interrupts, not structured output + assert "int-1" in message_string + assert "approval" in message_string + # Should NOT contain structured output + assert "test" not in message_string or "approval" in message_string # "test" might appear but not from structured + assert '"value": 42' not in message_string + + +def test__str__interrupts_priority_over_text_content(mock_metrics, simple_message: Message): + """Test that interrupts take priority over message text content in str().""" + interrupts = [Interrupt(id="int-1", name="confirm", reason="Please confirm")] + + result = AgentResult( + stop_reason="end_turn", + message=simple_message, + metrics=mock_metrics, + state={}, + interrupts=interrupts, + ) + + message_string = str(result) + + # Should return interrupts, not message text + assert "int-1" in message_string + assert "confirm" in message_string + assert "Hello world!" not in message_string + + +def test__str__empty_interrupts_returns_agent_message(mock_metrics, simple_message: Message): + """Test that empty interrupts list falls through to other content.""" + result = AgentResult( + stop_reason="end_turn", + message=simple_message, + metrics=mock_metrics, + state={}, + interrupts=[], + ) + + message_string = str(result) + + # Empty list is falsy, should fall through to text content + assert message_string == "Hello world!\n" +