Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ...tools._tool_helpers import noop_tool
from ...tools.registry import ToolRegistry
from ...types.content import Message
from ...types.content import ContentBlock, Message
from ...types.exceptions import ContextWindowOverflowException
from ...types.tools import AgentTool
from .conversation_manager import ConversationManager
Expand Down Expand Up @@ -216,7 +216,22 @@ def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message:

# Use the agent to generate summary with rich content (can use tools if needed)
result = summarization_agent("Please summarize this conversation.")
return cast(Message, {**result.message, "role": "user"})

# Filter content to only include blocks valid for user messages.
# User messages cannot contain toolUse or reasoningContent blocks (those are assistant-only).
# This is important when using structured_output_model, which adds toolUse blocks to responses.
filtered_content: List[ContentBlock] = [
content_block
for content_block in result.message.get("content", [])
if "toolUse" not in content_block and "reasoningContent" not in content_block
]

# If no valid content remains after filtering, create a text block from the result
if not filtered_content:
# Use the string representation of the result as fallback
filtered_content = [cast(ContentBlock, {"text": str(result)})]

return cast(Message, {"role": "user", "content": filtered_content})

finally:
# Restore original agent state
Expand Down
157 changes: 157 additions & 0 deletions tests/strands/agent/test_summarizing_conversation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,160 @@ def test_summarizing_conversation_manager_generate_summary_with_tools(mock_regis
summarizing_manager._generate_summary(messages, agent)

mock_registry.register_tool.assert_not_called()


def test_generate_summary_filters_tool_use_from_structured_output():
"""Test that toolUse blocks from structured_output_model are filtered from summary message.

When an agent uses structured_output_model, the response contains toolUse blocks.
Since the summary is converted to a user message, we must filter out toolUse blocks
because user messages cannot contain them (Bedrock API constraint).

This addresses issue #1160: ValidationException when using structured_output_model
with SummarizingConversationManager.
"""

# Create a mock agent that returns a response with both text and toolUse (simulating structured_output)
class StructuredOutputMockAgent:
def __init__(self):
self.system_prompt = None
self.messages = []
self.model = Mock()
self.tool_registry = Mock()
self.tool_names = []

def __call__(self, prompt):
result = Mock()
# Simulate a structured_output response with toolUse block
result.message = {
"role": "assistant",
"content": [
{"text": "Here is the summary of the conversation."},
{
"toolUse": {
"toolUseId": "structured_output_123",
"name": "structured_output_tool",
"input": {"field": "value"},
}
},
],
}
return result

mock_agent = cast("Agent", StructuredOutputMockAgent())
manager = SummarizingConversationManager()

messages: Messages = [
{"role": "user", "content": [{"text": "Hello"}]},
{"role": "assistant", "content": [{"text": "Hi there"}]},
]

summary = manager._generate_summary(messages, mock_agent)

# Summary should be a user message
assert summary["role"] == "user"

# Summary should NOT contain toolUse blocks
for content_block in summary["content"]:
assert "toolUse" not in content_block, "User message should not contain toolUse blocks"

# Summary should contain the text content
assert any("text" in content_block for content_block in summary["content"])
text_content = next(cb for cb in summary["content"] if "text" in cb)
assert text_content["text"] == "Here is the summary of the conversation."


def test_generate_summary_filters_reasoning_content():
"""Test that reasoningContent blocks are also filtered from summary message.

Reasoning content is another assistant-only content type that should be filtered.
"""

class ReasoningMockAgent:
def __init__(self):
self.system_prompt = None
self.messages = []
self.model = Mock()
self.tool_registry = Mock()
self.tool_names = []

def __call__(self, prompt):
result = Mock()
result.message = {
"role": "assistant",
"content": [
{"text": "Summary text."},
{"reasoningContent": {"reasoningText": {"text": "Internal reasoning..."}}},
],
}
return result

mock_agent = cast("Agent", ReasoningMockAgent())
manager = SummarizingConversationManager()

messages: Messages = [
{"role": "user", "content": [{"text": "Hello"}]},
]

summary = manager._generate_summary(messages, mock_agent)

# Summary should NOT contain reasoningContent blocks
for content_block in summary["content"]:
assert "reasoningContent" not in content_block, "User message should not contain reasoningContent"

# Should still have text content
assert any("text" in cb for cb in summary["content"])


def test_generate_summary_fallback_when_only_tool_use():
"""Test fallback behavior when response contains ONLY toolUse (no text).

In edge cases where structured_output produces only toolUse without text,
we should create a fallback text representation.
"""

class OnlyToolUseMockAgent:
def __init__(self):
self.system_prompt = None
self.messages = []
self.model = Mock()
self.tool_registry = Mock()
self.tool_names = []

def __call__(self, prompt):
result = Mock()
result.message = {
"role": "assistant",
"content": [
{
"toolUse": {
"toolUseId": "only_tool_123",
"name": "structured_output_tool",
"input": {"data": "test"},
}
},
],
}
# Provide a string representation for fallback
result.__str__ = lambda self: "Structured output summary"
return result

mock_agent = cast("Agent", OnlyToolUseMockAgent())
manager = SummarizingConversationManager()

messages: Messages = [
{"role": "user", "content": [{"text": "Hello"}]},
]

summary = manager._generate_summary(messages, mock_agent)

# Summary should be a user message with fallback text content
assert summary["role"] == "user"
assert len(summary["content"]) > 0

# Should have text content (the fallback)
assert any("text" in cb for cb in summary["content"])

# Should NOT have toolUse
for content_block in summary["content"]:
assert "toolUse" not in content_block
Loading