Skip to content

Commit 9632ed5

Browse files
authored
conversation manager - summarization - noop tool (#1003)
1 parent aada326 commit 9632ed5

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

src/strands/agent/conversation_manager/summarizing_conversation_manager.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from typing_extensions import override
77

8+
from ...tools import tool
9+
from ...tools.registry import ToolRegistry
810
from ...types.content import Message
911
from ...types.exceptions import ContextWindowOverflowException
1012
from .conversation_manager import ConversationManager
@@ -23,6 +25,10 @@
2325
- You MUST create a structured and concise summary in bullet-point format.
2426
- You MUST NOT respond conversationally.
2527
- You MUST NOT address the user directly.
28+
- You MUST NOT comment on tool availability.
29+
30+
Assumptions:
31+
- You MUST NOT assume tool executions failed unless otherwise stated.
2632
2733
Task:
2834
Your task is to create a structured summary document:
@@ -182,9 +188,10 @@ def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message:
182188
# Choose which agent to use for summarization
183189
summarization_agent = self.summarization_agent if self.summarization_agent is not None else agent
184190

185-
# Save original system prompt and messages to restore later
191+
# Save original system prompt, messages, and tool registry to restore later
186192
original_system_prompt = summarization_agent.system_prompt
187193
original_messages = summarization_agent.messages.copy()
194+
original_tool_registry = summarization_agent.tool_registry
188195

189196
try:
190197
# 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:
197204
)
198205
# Temporarily set the system prompt for summarization
199206
summarization_agent.system_prompt = system_prompt
207+
208+
# Add no-op tool if agent has no tools to satisfy tool spec requirement
209+
if not summarization_agent.tool_names:
210+
tool_registry = ToolRegistry()
211+
tool_registry.register_tool(self._noop_tool)
212+
summarization_agent.tool_registry = tool_registry
213+
200214
summarization_agent.messages = messages
201215

202216
# 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:
207221
# Restore original agent state
208222
summarization_agent.system_prompt = original_system_prompt
209223
summarization_agent.messages = original_messages
224+
summarization_agent.tool_registry = original_tool_registry
210225

211226
def _adjust_split_point_for_tool_pairs(self, messages: List[Message], split_point: int) -> int:
212227
"""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
249264
raise ContextWindowOverflowException("Unable to trim conversation context!")
250265

251266
return split_point
267+
268+
@tool(name="noop", description="MUST NOT call or summarize")
269+
def _noop_tool(self) -> None:
270+
"""No-op tool to satisfy tool spec requirement when tool messages are present.
271+
272+
Some model provides (e.g., Bedrock) will return an error response if tool uses and tool results are present in
273+
messages without any tool specs configured. Consequently, if the summarization agent has no registered tools,
274+
summarization will fail. As a workaround, we register the no-op tool.
275+
"""
276+
pass

tests/strands/agent/test_summarizing_conversation_manager.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def __init__(self, summary_response="This is a summary of the conversation."):
1919
self.messages = []
2020
self.model = Mock()
2121
self.call_tracker = Mock()
22+
self.tool_registry = Mock()
23+
self.tool_names = []
2224

2325
def __call__(self, prompt):
2426
"""Mock agent call that returns a summary."""
@@ -608,3 +610,30 @@ def test_summarizing_conversation_manager_properly_records_removed_message_count
608610
# so we dont count this toward the total:
609611
# 4 (Previously removed messages) + 2 (removed messages) - 1 (Previous summary message) = 5
610612
assert manager.removed_message_count == 5
613+
614+
615+
@patch("strands.agent.conversation_manager.summarizing_conversation_manager.ToolRegistry")
616+
def test_summarizing_conversation_manager_generate_summary_with_noop_tool(mock_registry_cls, summarizing_manager):
617+
mock_registry = mock_registry_cls.return_value
618+
619+
messages = [{"role": "user", "content": [{"text": "test"}]}]
620+
agent = create_mock_agent()
621+
622+
original_tool_registry = agent.tool_registry
623+
summarizing_manager._generate_summary(messages, agent)
624+
625+
assert original_tool_registry == agent.tool_registry
626+
mock_registry.register_tool.assert_called_once()
627+
628+
629+
@patch("strands.agent.conversation_manager.summarizing_conversation_manager.ToolRegistry")
630+
def test_summarizing_conversation_manager_generate_summary_with_tools(mock_registry_cls, summarizing_manager):
631+
mock_registry = mock_registry_cls.return_value
632+
633+
messages = [{"role": "user", "content": [{"text": "test"}]}]
634+
agent = create_mock_agent()
635+
agent.tool_names = ["test_tool"]
636+
637+
summarizing_manager._generate_summary(messages, agent)
638+
639+
mock_registry.register_tool.assert_not_called()

tests_integ/test_summarizing_conversation_manager_integration.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,39 @@ def test_dedicated_summarization_agent(model, summarization_model):
372372
break
373373

374374
assert summary_text
375+
376+
377+
def test_summarization_with_tool_messages_and_no_tools():
378+
agent = Agent(
379+
messages=[
380+
{"role": "user", "content": [{"text": "What is the current time?"}]},
381+
{
382+
"role": "assistant",
383+
"content": [{"toolUse": {"toolUseId": "t1", "name": "time_tool", "input": {}}}],
384+
},
385+
{
386+
"role": "user",
387+
"content": [
388+
{
389+
"toolResult": {
390+
"toolUseId": "t1",
391+
"content": [{"text": "12:00"}],
392+
"status": "success",
393+
}
394+
}
395+
],
396+
},
397+
{"role": "assistant", "content": [{"text": "The current time is 12:00."}]},
398+
{"role": "user", "content": [{"text": "Thank you"}]},
399+
{"role": "assistant", "content": [{"text": "You are welcome."}]},
400+
],
401+
)
402+
403+
conversation_manager = SummarizingConversationManager(summary_ratio=1, preserve_recent_messages=2)
404+
conversation_manager.reduce_context(agent)
405+
406+
assert len(agent.tool_names) == 0
407+
assert len(agent.messages) == 3
408+
409+
summary = str(agent.messages[0]).lower()
410+
assert "12:00" in summary

0 commit comments

Comments
 (0)