From 1d76d58a433225bea869f75e63e044a1c74466db Mon Sep 17 00:00:00 2001 From: Patrick Gray Date: Tue, 7 Oct 2025 19:00:07 -0400 Subject: [PATCH] conversation manager - summarization - noop tool --- .../summarizing_conversation_manager.py | 27 +++++++++++++- .../test_summarizing_conversation_manager.py | 29 +++++++++++++++ ...rizing_conversation_manager_integration.py | 36 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/strands/agent/conversation_manager/summarizing_conversation_manager.py b/src/strands/agent/conversation_manager/summarizing_conversation_manager.py index b08b6853e..117626fbe 100644 --- a/src/strands/agent/conversation_manager/summarizing_conversation_manager.py +++ b/src/strands/agent/conversation_manager/summarizing_conversation_manager.py @@ -5,6 +5,8 @@ from typing_extensions import override +from ...tools import tool +from ...tools.registry import ToolRegistry from ...types.content import Message from ...types.exceptions import ContextWindowOverflowException from .conversation_manager import ConversationManager @@ -23,6 +25,10 @@ - You MUST create a structured and concise summary in bullet-point format. - You MUST NOT respond conversationally. - You MUST NOT address the user directly. +- You MUST NOT comment on tool availability. + +Assumptions: +- You MUST NOT assume tool executions failed unless otherwise stated. Task: Your task is to create a structured summary document: @@ -182,9 +188,10 @@ def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message: # Choose which agent to use for summarization summarization_agent = self.summarization_agent if self.summarization_agent is not None else agent - # Save original system prompt and messages to restore later + # Save original system prompt, messages, and tool registry to restore later original_system_prompt = summarization_agent.system_prompt original_messages = summarization_agent.messages.copy() + original_tool_registry = summarization_agent.tool_registry try: # Only override system prompt if no agent was provided during initialization @@ -197,6 +204,13 @@ def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message: ) # Temporarily set the system prompt for summarization summarization_agent.system_prompt = system_prompt + + # Add no-op tool if agent has no tools to satisfy tool spec requirement + if not summarization_agent.tool_names: + tool_registry = ToolRegistry() + tool_registry.register_tool(self._noop_tool) + summarization_agent.tool_registry = tool_registry + summarization_agent.messages = messages # Use the agent to generate summary with rich content (can use tools if needed) @@ -207,6 +221,7 @@ def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message: # Restore original agent state summarization_agent.system_prompt = original_system_prompt summarization_agent.messages = original_messages + summarization_agent.tool_registry = original_tool_registry def _adjust_split_point_for_tool_pairs(self, messages: List[Message], split_point: int) -> int: """Adjust the split point to avoid breaking ToolUse/ToolResult pairs. @@ -249,3 +264,13 @@ def _adjust_split_point_for_tool_pairs(self, messages: List[Message], split_poin raise ContextWindowOverflowException("Unable to trim conversation context!") return split_point + + @tool(name="noop", description="MUST NOT call or summarize") + def _noop_tool(self) -> None: + """No-op tool to satisfy tool spec requirement when tool messages are present. + + Some model provides (e.g., Bedrock) will return an error response if tool uses and tool results are present in + messages without any tool specs configured. Consequently, if the summarization agent has no registered tools, + summarization will fail. As a workaround, we register the no-op tool. + """ + pass diff --git a/tests/strands/agent/test_summarizing_conversation_manager.py b/tests/strands/agent/test_summarizing_conversation_manager.py index 6003a1710..4b69e6653 100644 --- a/tests/strands/agent/test_summarizing_conversation_manager.py +++ b/tests/strands/agent/test_summarizing_conversation_manager.py @@ -19,6 +19,8 @@ def __init__(self, summary_response="This is a summary of the conversation."): self.messages = [] self.model = Mock() self.call_tracker = Mock() + self.tool_registry = Mock() + self.tool_names = [] def __call__(self, prompt): """Mock agent call that returns a summary.""" @@ -608,3 +610,30 @@ def test_summarizing_conversation_manager_properly_records_removed_message_count # so we dont count this toward the total: # 4 (Previously removed messages) + 2 (removed messages) - 1 (Previous summary message) = 5 assert manager.removed_message_count == 5 + + +@patch("strands.agent.conversation_manager.summarizing_conversation_manager.ToolRegistry") +def test_summarizing_conversation_manager_generate_summary_with_noop_tool(mock_registry_cls, summarizing_manager): + mock_registry = mock_registry_cls.return_value + + messages = [{"role": "user", "content": [{"text": "test"}]}] + agent = create_mock_agent() + + original_tool_registry = agent.tool_registry + summarizing_manager._generate_summary(messages, agent) + + assert original_tool_registry == agent.tool_registry + mock_registry.register_tool.assert_called_once() + + +@patch("strands.agent.conversation_manager.summarizing_conversation_manager.ToolRegistry") +def test_summarizing_conversation_manager_generate_summary_with_tools(mock_registry_cls, summarizing_manager): + mock_registry = mock_registry_cls.return_value + + messages = [{"role": "user", "content": [{"text": "test"}]}] + agent = create_mock_agent() + agent.tool_names = ["test_tool"] + + summarizing_manager._generate_summary(messages, agent) + + mock_registry.register_tool.assert_not_called() diff --git a/tests_integ/test_summarizing_conversation_manager_integration.py b/tests_integ/test_summarizing_conversation_manager_integration.py index b205c723f..91fb5b910 100644 --- a/tests_integ/test_summarizing_conversation_manager_integration.py +++ b/tests_integ/test_summarizing_conversation_manager_integration.py @@ -372,3 +372,39 @@ def test_dedicated_summarization_agent(model, summarization_model): break assert summary_text + + +def test_summarization_with_tool_messages_and_no_tools(): + agent = Agent( + messages=[ + {"role": "user", "content": [{"text": "What is the current time?"}]}, + { + "role": "assistant", + "content": [{"toolUse": {"toolUseId": "t1", "name": "time_tool", "input": {}}}], + }, + { + "role": "user", + "content": [ + { + "toolResult": { + "toolUseId": "t1", + "content": [{"text": "12:00"}], + "status": "success", + } + } + ], + }, + {"role": "assistant", "content": [{"text": "The current time is 12:00."}]}, + {"role": "user", "content": [{"text": "Thank you"}]}, + {"role": "assistant", "content": [{"text": "You are welcome."}]}, + ], + ) + + conversation_manager = SummarizingConversationManager(summary_ratio=1, preserve_recent_messages=2) + conversation_manager.reduce_context(agent) + + assert len(agent.tool_names) == 0 + assert len(agent.messages) == 3 + + summary = str(agent.messages[0]).lower() + assert "12:00" in summary