Skip to content

Commit c2ea68d

Browse files
feat(summarizing_conversation_manager): implement summarization strategy
1 parent ffc7c5e commit c2ea68d

File tree

5 files changed

+1180
-2
lines changed

5 files changed

+1180
-2
lines changed

src/strands/agent/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@
88

99
from .agent import Agent
1010
from .agent_result import AgentResult
11-
from .conversation_manager import ConversationManager, NullConversationManager, SlidingWindowConversationManager
11+
from .conversation_manager import (
12+
ConversationManager,
13+
NullConversationManager,
14+
SlidingWindowConversationManager,
15+
SummarizingConversationManager,
16+
)
1217

1318
__all__ = [
1419
"Agent",
1520
"AgentResult",
1621
"ConversationManager",
1722
"NullConversationManager",
1823
"SlidingWindowConversationManager",
24+
"SummarizingConversationManager",
1925
]

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 implementation that summarizes 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+
]
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
"""Summarizing conversation history management with configurable options."""
2+
3+
import logging
4+
from typing import TYPE_CHECKING, List, Optional
5+
6+
from ...types.content import Message
7+
from ...types.exceptions import ContextWindowOverflowException
8+
from .conversation_manager import ConversationManager
9+
10+
if TYPE_CHECKING:
11+
from ..agent import Agent
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
DEFAULT_SUMMARIZATION_PROMPT = """You are a conversation summarizer. Provide a concise summary of the conversation \
18+
history.
19+
20+
Format Requirements:
21+
- You MUST create a structured and concise summary in bullet-point format.
22+
- You MUST NOT respond conversationally.
23+
- You MUST NOT address the user directly.
24+
25+
Task:
26+
Your task is to create a structured summary document:
27+
- It MUST contain bullet points with key topics and questions covered
28+
- It MUST contain bullet points for all significant tools executed and their results
29+
- It MUST contain bullet points for any code or technical information shared
30+
- It MUST contain a section of key insights gained
31+
- It MUST format the summary in the third person
32+
33+
Example format:
34+
35+
## Conversation Summary
36+
* Topic 1: Key information
37+
* Topic 2: Key information
38+
*
39+
## Tools Executed
40+
* Tool X: Result Y"""
41+
42+
43+
class SummarizingConversationManager(ConversationManager):
44+
"""Extends sliding window manager with optional summarization of older messages.
45+
46+
This manager provides a configurable option to summarize older context instead of
47+
simply trimming it, helping preserve important information while staying within
48+
context limits.
49+
"""
50+
51+
def __init__(
52+
self,
53+
summary_ratio: float = 0.3,
54+
preserve_recent_messages: int = 10,
55+
summarization_agent: Optional["Agent"] = None,
56+
summarization_system_prompt: Optional[str] = None,
57+
):
58+
"""Initialize the summarizing conversation manager.
59+
60+
Args:
61+
summary_ratio: Ratio of messages to summarize vs keep when context overflow occurs.
62+
Value between 0.1 and 0.8. Defaults to 0.3 (summarize 30% of oldest messages).
63+
preserve_recent_messages: Minimum number of recent messages to always keep.
64+
Defaults to 10 messages.
65+
summarization_agent: Optional agent to use for summarization instead of the parent agent.
66+
If provided, this agent can use tools as part of the summarization process.
67+
summarization_system_prompt: Optional system prompt override for summarization.
68+
If None, uses the default summarization prompt.
69+
"""
70+
if summarization_agent is not None and summarization_system_prompt is not None:
71+
raise ValueError(
72+
"Cannot provide both summarization_agent and summarization_system_prompt. "
73+
"Agents come with their own system prompt."
74+
)
75+
76+
self.summary_ratio = max(0.1, min(0.8, summary_ratio))
77+
self.preserve_recent_messages = preserve_recent_messages
78+
self.summarization_agent = summarization_agent
79+
self.summarization_system_prompt = summarization_system_prompt
80+
81+
def apply_management(self, agent: "Agent") -> None:
82+
"""Apply management strategy to conversation history.
83+
84+
For the summarizing conversation manager, no proactive management is performed.
85+
Summarization only occurs when there's a context overflow that triggers reduce_context.
86+
87+
Args:
88+
agent: The agent whose conversation history will be managed.
89+
The agent's messages list is modified in-place.
90+
"""
91+
# No proactive management - summarization only happens on context overflow
92+
pass
93+
94+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None) -> None:
95+
"""Reduce context using summarization.
96+
97+
Args:
98+
agent: The agent whose conversation history will be reduced.
99+
The agent's messages list is modified in-place.
100+
e: The exception that triggered the context reduction, if any.
101+
102+
Raises:
103+
ContextWindowOverflowException: If the context cannot be summarized.
104+
"""
105+
try:
106+
# Calculate how many messages to summarize
107+
messages_to_summarize_count = max(1, int(len(agent.messages) * self.summary_ratio))
108+
109+
# Ensure we don't summarize recent messages
110+
messages_to_summarize_count = min(
111+
messages_to_summarize_count, len(agent.messages) - self.preserve_recent_messages
112+
)
113+
114+
if messages_to_summarize_count <= 0:
115+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization")
116+
117+
# Adjust split point to avoid breaking ToolUse/ToolResult pairs
118+
messages_to_summarize_count = self._adjust_split_point_for_tool_pairs(
119+
agent.messages, messages_to_summarize_count
120+
)
121+
122+
if messages_to_summarize_count <= 0:
123+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization")
124+
125+
# Extract messages to summarize
126+
messages_to_summarize = agent.messages[:messages_to_summarize_count]
127+
remaining_messages = agent.messages[messages_to_summarize_count:]
128+
129+
# Generate summary
130+
summary_message = self._generate_summary(messages_to_summarize, agent)
131+
132+
# Replace the summarized messages with the summary
133+
agent.messages[:] = [summary_message] + remaining_messages
134+
135+
except Exception as summarization_error:
136+
logger.error("Summarization failed: %s", summarization_error)
137+
raise summarization_error from e
138+
139+
def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message:
140+
"""Generate a summary of the provided messages.
141+
142+
Args:
143+
messages: The messages to summarize.
144+
agent: The agent instance to use for summarization.
145+
146+
Returns:
147+
A message containing the conversation summary.
148+
149+
Raises:
150+
Exception: If summary generation fails.
151+
"""
152+
# Choose which agent to use for summarization
153+
summarization_agent = self.summarization_agent if self.summarization_agent is not None else agent
154+
155+
# Save original system prompt and messages to restore later
156+
original_system_prompt = summarization_agent.system_prompt
157+
original_messages = summarization_agent.messages.copy()
158+
159+
try:
160+
# Only override system prompt if no agent was provided during initialization
161+
if self.summarization_agent is None:
162+
# Use custom system prompt if provided, otherwise use default
163+
system_prompt = (
164+
self.summarization_system_prompt
165+
if self.summarization_system_prompt is not None
166+
else DEFAULT_SUMMARIZATION_PROMPT
167+
)
168+
# Temporarily set the system prompt for summarization
169+
summarization_agent.system_prompt = system_prompt
170+
summarization_agent.messages = messages
171+
172+
# Use the agent to generate summary with rich content (can use tools if needed)
173+
result = summarization_agent("Please summarize this conversation.")
174+
175+
return result.message
176+
177+
finally:
178+
# Restore original agent state
179+
summarization_agent.system_prompt = original_system_prompt
180+
summarization_agent.messages = original_messages
181+
182+
def _adjust_split_point_for_tool_pairs(self, messages: List[Message], split_point: int) -> int:
183+
"""Adjust the split point to avoid breaking ToolUse/ToolResult pairs.
184+
185+
Uses the same logic as SlidingWindowConversationManager for consistency.
186+
187+
Args:
188+
messages: The full list of messages.
189+
split_point: The initially calculated split point.
190+
191+
Returns:
192+
The adjusted split point that doesn't break ToolUse/ToolResult pairs.
193+
194+
Raises:
195+
ContextWindowOverflowException: If no valid split point can be found.
196+
"""
197+
if split_point > len(messages):
198+
raise ContextWindowOverflowException("Split point exceeds message array length")
199+
200+
if split_point >= len(messages):
201+
return split_point
202+
203+
# Find the next valid split_point
204+
while split_point < len(messages):
205+
if (
206+
# Oldest message cannot be a toolResult because it needs a toolUse preceding it
207+
any("toolResult" in content for content in messages[split_point]["content"])
208+
or (
209+
# Oldest message can be a toolUse only if a toolResult immediately follows it.
210+
any("toolUse" in content for content in messages[split_point]["content"])
211+
and split_point + 1 < len(messages)
212+
and not any("toolResult" in content for content in messages[split_point + 1]["content"])
213+
)
214+
):
215+
split_point += 1
216+
else:
217+
break
218+
else:
219+
# If we didn't find a valid split_point, then we throw
220+
raise ContextWindowOverflowException("Unable to trim conversation context!")
221+
222+
return split_point
223+
224+
def _message_contains_tool_use(self, message: Message) -> bool:
225+
"""Check if a message contains a ToolUse."""
226+
return any("toolUse" in content for content in message.get("content", []))
227+
228+
def _message_contains_tool_result(self, message: Message) -> bool:
229+
"""Check if a message contains a ToolResult."""
230+
return any("toolResult" in content for content in message.get("content", []))

0 commit comments

Comments
 (0)