Skip to content

Commit 9aef5ae

Browse files
feat(summarizing_conversation_manager): implement summarization of older messages
1 parent 63cef21 commit 9aef5ae

File tree

7 files changed

+693
-6
lines changed

7 files changed

+693
-6
lines changed

src/strands/agent/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ def _execute_event_loop_cycle(self, callback_handler: Callable, kwargs: dict[str
488488
except ContextWindowOverflowException as e:
489489
# Try reducing the context size and retrying
490490

491-
self.conversation_manager.reduce_context(self.messages, e=e)
491+
self.conversation_manager.reduce_context(self.messages, e=e, agent=self)
492492
return self._execute_event_loop_cycle(callback_handler, kwargs)
493493

494494
def _record_tool_execution(

src/strands/agent/conversation_manager/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- NullConversationManager: A no-op implementation that does not modify conversation history
77
- SlidingWindowConversationManager: An implementation that maintains a sliding window of messages to control context
88
size while preserving conversation coherence
9+
- SummarizingConversationManager: An extension of sliding window that can optionally summarize older context instead
10+
of simply trimming it
911
1012
Conversation managers help control memory usage and context length while maintaining relevant conversation state, which
1113
is critical for effective agent interactions.
@@ -14,5 +16,11 @@
1416
from .conversation_manager import ConversationManager
1517
from .null_conversation_manager import NullConversationManager
1618
from .sliding_window_conversation_manager import SlidingWindowConversationManager
19+
from .summarizing_conversation_manager import SummarizingConversationManager
1720

18-
__all__ = ["ConversationManager", "NullConversationManager", "SlidingWindowConversationManager"]
21+
__all__ = [
22+
"ConversationManager",
23+
"NullConversationManager",
24+
"SlidingWindowConversationManager",
25+
"SummarizingConversationManager",
26+
]

src/strands/agent/conversation_manager/conversation_manager.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Abstract interface for conversation history management."""
22

33
from abc import ABC, abstractmethod
4-
from typing import Optional
4+
from typing import TYPE_CHECKING, Optional
55

66
from ...types.content import Messages
77

8+
if TYPE_CHECKING:
9+
from ..agent import Agent
10+
811

912
class ConversationManager(ABC):
1013
"""Abstract base class for managing conversation history.
@@ -34,7 +37,9 @@ def apply_management(self, messages: Messages) -> None:
3437

3538
@abstractmethod
3639
# pragma: no cover
37-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
40+
def reduce_context(
41+
self, messages: Messages, e: Optional[Exception] = None, agent: Optional["Agent"] = None
42+
) -> None:
3843
"""Called when the model's context window is exceeded.
3944
4045
This method should implement the specific strategy for reducing the window size when a context overflow occurs.
@@ -51,5 +56,6 @@ def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> N
5156
messages: The conversation history to reduce.
5257
This list is modified in-place.
5358
e: The exception that triggered the context reduction, if any.
59+
agent: The agent instance, if available for advanced reduction strategies.
5460
"""
5561
pass

src/strands/agent/conversation_manager/null_conversation_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ def apply_management(self, messages: Messages) -> None:
2525
"""
2626
pass
2727

28-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
28+
def reduce_context(self, messages: Messages, e: Optional[Exception] = None, **kwargs) -> None:
2929
"""Does not reduce context and raises an exception.
3030
3131
Args:
3232
messages: The conversation history that will remain unmodified.
3333
e: The exception that triggered the context reduction, if any.
34+
**kwargs: Additional keyword arguments (ignored).
3435
3536
Raises:
3637
e: If provided.

src/strands/agent/conversation_manager/sliding_window_conversation_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def _remove_dangling_messages(self, messages: Messages) -> None:
107107
if not any("toolResult" in content for content in messages[-1]["content"]):
108108
messages.pop()
109109

110-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
110+
def reduce_context(self, messages: Messages, e: Optional[Exception] = None, **kwargs) -> None:
111111
"""Trim the oldest messages to reduce the conversation context size.
112112
113113
The method handles special cases where tool results need to be converted to regular content blocks to maintain
@@ -117,6 +117,7 @@ def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> N
117117
messages: The messages to reduce.
118118
This list is modified in-place.
119119
e: The exception that triggered the context reduction, if any.
120+
**kwargs: Additional keyword arguments (ignored).
120121
121122
Raises:
122123
ContextWindowOverflowException: If the context cannot be reduced further.
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""Summarizing conversation history management with configurable options."""
2+
3+
import json
4+
import logging
5+
from typing import TYPE_CHECKING, List, Optional, cast
6+
7+
from ...types.content import ContentBlock, Message, Messages
8+
from ...types.exceptions import ContextWindowOverflowException
9+
from ...types.tools import ToolResult
10+
from .conversation_manager import ConversationManager
11+
12+
if TYPE_CHECKING:
13+
from ..agent import Agent
14+
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class SummarizingConversationManager(ConversationManager):
20+
"""Extends sliding window manager with optional summarization of older messages.
21+
22+
This manager provides a configurable option to summarize older context instead of
23+
simply trimming it, helping preserve important information while staying within
24+
context limits.
25+
"""
26+
27+
def __init__(
28+
self,
29+
window_size: int = 40,
30+
summary_ratio: float = 0.3,
31+
preserve_recent_messages: int = 10,
32+
summarization_agent: Optional["Agent"] = None,
33+
summarization_system_prompt: Optional[str] = None,
34+
):
35+
"""Initialize the summarizing conversation manager.
36+
37+
Args:
38+
window_size: Maximum number of messages to keep in history.
39+
Defaults to 40 messages.
40+
summary_ratio: Ratio of messages to summarize vs keep when window is exceeded.
41+
Value between 0.1 and 0.8. Defaults to 0.3 (summarize 30% of oldest messages).
42+
preserve_recent_messages: Minimum number of recent messages to always keep.
43+
Defaults to 10 messages.
44+
summarization_agent: Optional agent to use for summarization instead of the parent agent.
45+
If provided, this agent can use tools as part of the summarization process.
46+
summarization_system_prompt: Optional system prompt override for summarization.
47+
If None, uses the default summarization prompt.
48+
"""
49+
self.window_size = window_size
50+
self.summary_ratio = max(0.1, min(0.8, summary_ratio))
51+
self.preserve_recent_messages = preserve_recent_messages
52+
self.summarization_agent = summarization_agent
53+
self.summarization_system_prompt = summarization_system_prompt
54+
55+
def apply_management(self, messages: Messages) -> None:
56+
"""Apply management strategy when message count exceeds window size.
57+
58+
Args:
59+
messages: The conversation history to manage.
60+
This list is modified in-place.
61+
"""
62+
if len(messages) > self.window_size:
63+
# Call reduce_context (agent will be provided by the caller when needed)
64+
self.reduce_context(messages)
65+
66+
def reduce_context(
67+
self, messages: Messages, e: Optional[Exception] = None, agent: Optional["Agent"] = None
68+
) -> None:
69+
"""Reduce context using summarization.
70+
71+
Args:
72+
messages: The messages to reduce.
73+
This list is modified in-place.
74+
e: The exception that triggered the context reduction, if any.
75+
agent: The agent instance to use for summarization.
76+
If None, context overflow will raise an exception.
77+
78+
Raises:
79+
ContextWindowOverflowException: If the context cannot be summarized.
80+
"""
81+
if agent is None:
82+
raise ContextWindowOverflowException("No agent provided for context reduction") from e
83+
84+
try:
85+
# Calculate how many messages to summarize
86+
messages_to_summarize_count = max(1, int(len(messages) * self.summary_ratio))
87+
88+
# Ensure we don't summarize recent messages
89+
messages_to_summarize_count = min(
90+
messages_to_summarize_count, len(messages) - self.preserve_recent_messages
91+
)
92+
93+
if messages_to_summarize_count <= 0:
94+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization") from e
95+
96+
# Extract messages to summarize
97+
messages_to_summarize = messages[:messages_to_summarize_count]
98+
remaining_messages = messages[messages_to_summarize_count:]
99+
100+
# Generate summary
101+
summary_content = self._generate_summary(messages_to_summarize, agent)
102+
103+
# Create a summary message
104+
summary_message: Message = {"role": "assistant", "content": [ContentBlock(text=summary_content)]}
105+
106+
# Replace the summarized messages with the summary
107+
messages[:] = [summary_message] + remaining_messages
108+
109+
except Exception as summarization_error:
110+
logger.error("Summarization failed: %s", summarization_error)
111+
raise
112+
113+
def _generate_summary(self, messages: List[Message], agent: "Agent") -> str:
114+
"""Generate a summary of the provided messages.
115+
116+
Args:
117+
messages: The messages to summarize.
118+
agent: The agent instance to use for summarization.
119+
120+
Returns:
121+
A text summary of the conversation history.
122+
123+
Raises:
124+
Exception: If summary generation fails.
125+
"""
126+
# Choose which agent to use for summarization
127+
summarization_agent = self.summarization_agent if self.summarization_agent is not None else agent
128+
129+
# Format messages for summarization, preserving rich content
130+
formatted_messages = self._format_messages_for_summarization(messages)
131+
132+
# Use custom system prompt if provided, otherwise use default
133+
system_prompt = (
134+
self.summarization_system_prompt
135+
if self.summarization_system_prompt is not None
136+
else (
137+
"You are a conversation summarizer. Provide a concise summary of the conversation history.\n\n"
138+
"Format Requirements:\n"
139+
"- You MUST create a structured and concise summary in bullet-point format.\n"
140+
"- You MUST NOT respond conversationally.\n"
141+
"- You MUST NOT address the user directly.\n\n"
142+
"Task:\n"
143+
"Your task is to create a structured summary document:\n"
144+
"- It MUST contain bullet points with key topics and questions covered\n"
145+
"- It MUST contain bullet points for all significant tools executed and their results\n"
146+
"- It MUST contain bullet points for any code or technical information shared\n"
147+
"- It MUST contain a section of key insights gained\n"
148+
"- It MUST format the summary in the third person\n\n"
149+
"Example format:\n\n"
150+
"## Conversation Summary\n"
151+
"* Topic 1: Key information\n"
152+
"* Topic 2: Key information\n"
153+
"*\n"
154+
"## Tools Executed\n"
155+
"* Tool X: Result Y"
156+
)
157+
)
158+
159+
# Save original system prompt and messages to restore later
160+
original_system_prompt = summarization_agent.system_prompt
161+
original_messages = summarization_agent.messages.copy()
162+
163+
try:
164+
# Temporarily set the system prompt and set formatted messages for summarization
165+
summarization_agent.system_prompt = system_prompt
166+
summarization_agent.messages = formatted_messages
167+
168+
# Use the agent to generate summary with rich content (can use tools if needed)
169+
result = summarization_agent("Please summarize this conversation.")
170+
summary_response = ""
171+
172+
# Extract text content from the result
173+
if hasattr(result, "message") and result.message and "content" in result.message:
174+
for content_block in result.message["content"]:
175+
if "text" in content_block:
176+
summary_response += content_block["text"]
177+
178+
return summary_response.strip()
179+
180+
finally:
181+
# Restore original agent state
182+
summarization_agent.system_prompt = original_system_prompt
183+
summarization_agent.messages = original_messages
184+
185+
def _format_messages_for_summarization(self, messages: List[Message]) -> List[Message]:
186+
"""Format messages for summarization, preserving rich content.
187+
188+
Args:
189+
messages: The messages to format.
190+
191+
Returns:
192+
A list of formatted messages with rich content preserved for agent analysis.
193+
"""
194+
formatted_messages = []
195+
196+
for _, message in enumerate(messages):
197+
role = message["role"]
198+
formatted_content = []
199+
200+
for content in message["content"]:
201+
if "text" in content:
202+
formatted_content.append(ContentBlock(text=content["text"]))
203+
elif "toolUse" in content:
204+
tool_use = content["toolUse"]
205+
formatted_content.append(ContentBlock(text=f"[Used tool: {tool_use.get('name', 'unknown')}]"))
206+
elif "toolResult" in content:
207+
tool_result = cast(ToolResult, content["toolResult"])
208+
status = tool_result.get("status", "")
209+
if tool_result.get("content"):
210+
for tr_content in tool_result["content"]:
211+
if "text" in tr_content:
212+
text = tr_content["text"]
213+
formatted_content.append(ContentBlock(text=f"[Tool result text: {text}]"))
214+
elif "json" in tr_content:
215+
json_str = json.dumps(tr_content["json"])
216+
formatted_content.append(ContentBlock(text=f"[Tool result JSON: {json_str}]"))
217+
elif "image" in tr_content:
218+
formatted_content.append(ContentBlock(image=tr_content["image"]))
219+
elif "document" in tr_content:
220+
formatted_content.append(ContentBlock(document=tr_content["document"]))
221+
else:
222+
formatted_content.append(ContentBlock(text=f"[Tool result: {status}]"))
223+
elif "image" in content:
224+
formatted_content.append(ContentBlock(image=content["image"]))
225+
elif "document" in content:
226+
formatted_content.append(ContentBlock(document=content["document"]))
227+
228+
if formatted_content:
229+
formatted_message = {"role": role, "content": formatted_content}
230+
formatted_messages.append(formatted_message)
231+
232+
return formatted_messages

0 commit comments

Comments
 (0)