Skip to content

Commit af25f98

Browse files
authored
refactor: Update conversation manager interface (#161)
1 parent af961b2 commit af25f98

File tree

6 files changed

+43
-30
lines changed

6 files changed

+43
-30
lines changed

src/strands/agent/agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def caller(**kwargs: Any) -> Any:
165165
self._agent._record_tool_execution(tool_use, tool_result, user_message_override, messages)
166166

167167
# Apply window management
168-
self._agent.conversation_manager.apply_management(self._agent.messages)
168+
self._agent.conversation_manager.apply_management(self._agent)
169169

170170
return tool_result
171171

@@ -439,7 +439,7 @@ def _run_loop(
439439
return self._execute_event_loop_cycle(invocation_callback_handler, kwargs)
440440

441441
finally:
442-
self.conversation_manager.apply_management(self.messages)
442+
self.conversation_manager.apply_management(self)
443443

444444
def _execute_event_loop_cycle(self, callback_handler: Callable, kwargs: dict[str, Any]) -> AgentResult:
445445
"""Execute the event loop cycle with retry logic for context window limits.
@@ -483,7 +483,7 @@ def _execute_event_loop_cycle(self, callback_handler: Callable, kwargs: dict[str
483483
except ContextWindowOverflowException as e:
484484
# Try reducing the context size and retrying
485485

486-
self.conversation_manager.reduce_context(messages, e=e)
486+
self.conversation_manager.reduce_context(self, e=e)
487487
return self._execute_event_loop_cycle(callback_handler_override, kwargs)
488488

489489
def _record_tool_execution(

src/strands/agent/conversation_manager/conversation_manager.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
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

6-
from ...types.content import Messages
6+
if TYPE_CHECKING:
7+
from ...agent.agent import Agent
78

89

910
class ConversationManager(ABC):
@@ -19,22 +20,22 @@ class ConversationManager(ABC):
1920

2021
@abstractmethod
2122
# pragma: no cover
22-
def apply_management(self, messages: Messages) -> None:
23-
"""Applies management strategy to the provided list of messages.
23+
def apply_management(self, agent: "Agent") -> None:
24+
"""Applies management strategy to the provided agent.
2425
2526
Processes the conversation history to maintain appropriate size by modifying the messages list in-place.
2627
Implementations should handle message pruning, summarization, or other size management techniques to keep the
2728
conversation context within desired bounds.
2829
2930
Args:
30-
messages: The conversation history to manage.
31+
agent: The agent whose conversation history will be manage.
3132
This list is modified in-place.
3233
"""
3334
pass
3435

3536
@abstractmethod
3637
# pragma: no cover
37-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
38+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None) -> None:
3839
"""Called when the model's context window is exceeded.
3940
4041
This method should implement the specific strategy for reducing the window size when a context overflow occurs.
@@ -48,7 +49,7 @@ def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> N
4849
- Maintaining critical conversation markers
4950
5051
Args:
51-
messages: The conversation history to reduce.
52+
agent: The agent whose conversation history will be reduced.
5253
This list is modified in-place.
5354
e: The exception that triggered the context reduction, if any.
5455
"""

src/strands/agent/conversation_manager/null_conversation_manager.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Null implementation of conversation management."""
22

3-
from typing import Optional
3+
from typing import TYPE_CHECKING, Optional
4+
5+
if TYPE_CHECKING:
6+
from ...agent.agent import Agent
47

5-
from ...types.content import Messages
68
from ...types.exceptions import ContextWindowOverflowException
79
from .conversation_manager import ConversationManager
810

@@ -17,19 +19,19 @@ class NullConversationManager(ConversationManager):
1719
- Situations where the full conversation history should be preserved
1820
"""
1921

20-
def apply_management(self, messages: Messages) -> None:
22+
def apply_management(self, _agent: "Agent") -> None:
2123
"""Does nothing to the conversation history.
2224
2325
Args:
24-
messages: The conversation history that will remain unmodified.
26+
agent: The agent whose conversation history will remain unmodified.
2527
"""
2628
pass
2729

28-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
30+
def reduce_context(self, _agent: "Agent", e: Optional[Exception] = None) -> None:
2931
"""Does not reduce context and raises an exception.
3032
3133
Args:
32-
messages: The conversation history that will remain unmodified.
34+
agent: The agent whose conversation history will remain unmodified.
3335
e: The exception that triggered the context reduction, if any.
3436
3537
Raises:

src/strands/agent/conversation_manager/sliding_window_conversation_manager.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Sliding window conversation history management."""
22

33
import logging
4-
from typing import Optional
4+
from typing import TYPE_CHECKING, Optional
5+
6+
if TYPE_CHECKING:
7+
from ...agent.agent import Agent
58

69
from ...types.content import Message, Messages
710
from ...types.exceptions import ContextWindowOverflowException
@@ -45,13 +48,13 @@ def __init__(self, window_size: int = 40):
4548
"""Initialize the sliding window conversation manager.
4649
4750
Args:
48-
window_size: Maximum number of messages to keep in history.
51+
window_size: Maximum number of messages to keep in the agent's history.
4952
Defaults to 40 messages.
5053
"""
5154
self.window_size = window_size
5255

53-
def apply_management(self, messages: Messages) -> None:
54-
"""Apply the sliding window to the messages array to maintain a manageable history size.
56+
def apply_management(self, agent: "Agent") -> None:
57+
"""Apply the sliding window to the agent's messages array to maintain a manageable history size.
5558
5659
This method is called after every event loop cycle, as the messages array may have been modified with tool
5760
results and assistant responses. It first removes any dangling messages that might create an invalid
@@ -62,17 +65,18 @@ def apply_management(self, messages: Messages) -> None:
6265
blocks to maintain conversation coherence.
6366
6467
Args:
65-
messages: The messages to manage.
68+
agent: The agent whose messages will be managed.
6669
This list is modified in-place.
6770
"""
71+
messages = agent.messages
6872
self._remove_dangling_messages(messages)
6973

7074
if len(messages) <= self.window_size:
7175
logger.debug(
7276
"window_size=<%s>, message_count=<%s> | skipping context reduction", len(messages), self.window_size
7377
)
7478
return
75-
self.reduce_context(messages)
79+
self.reduce_context(agent)
7680

7781
def _remove_dangling_messages(self, messages: Messages) -> None:
7882
"""Remove dangling messages that would create an invalid conversation state.
@@ -105,15 +109,15 @@ def _remove_dangling_messages(self, messages: Messages) -> None:
105109
if not any("toolResult" in content for content in messages[-1]["content"]):
106110
messages.pop()
107111

108-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
112+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None) -> None:
109113
"""Trim the oldest messages to reduce the conversation context size.
110114
111115
The method handles special cases where trimming the messages leads to:
112116
- toolResult with no corresponding toolUse
113117
- toolUse with no corresponding toolResult
114118
115119
Args:
116-
messages: The messages to reduce.
120+
agent: The agent whose messages will be reduce.
117121
This list is modified in-place.
118122
e: The exception that triggered the context reduction, if any.
119123
@@ -122,6 +126,7 @@ def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> N
122126
Such as when the conversation is already minimal or when tool result messages cannot be properly
123127
converted.
124128
"""
129+
messages = agent.messages
125130
# If the number of messages is less than the window_size, then we default to 2, otherwise, trim to window size
126131
trim_index = 2 if len(messages) <= self.window_size else len(messages) - self.window_size
127132

tests/strands/agent/test_agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ def test_agent__call__(
318318
)
319319

320320
callback_handler.assert_called()
321-
conversation_manager_spy.apply_management.assert_called_with(agent.messages)
321+
conversation_manager_spy.apply_management.assert_called_with(agent)
322322

323323

324324
def test_agent__call__passes_kwargs(mock_model, system_prompt, callback_handler, agent, tool, mock_event_loop_cycle):
@@ -583,7 +583,7 @@ def test_agent_tool(mock_randint, agent):
583583
}
584584

585585
assert tru_result == exp_result
586-
conversation_manager_spy.apply_management.assert_called_with(agent.messages)
586+
conversation_manager_spy.apply_management.assert_called_with(agent)
587587

588588

589589
def test_agent_tool_user_message_override(agent):

tests/strands/agent/test_conversation_manager.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
import strands
4+
from strands.agent.agent import Agent
45
from strands.types.exceptions import ContextWindowOverflowException
56

67

@@ -160,7 +161,8 @@ def conversation_manager(request):
160161
indirect=["conversation_manager"],
161162
)
162163
def test_apply_management(conversation_manager, messages, expected_messages):
163-
conversation_manager.apply_management(messages)
164+
test_agent = Agent(messages=messages)
165+
conversation_manager.apply_management(test_agent)
164166

165167
assert messages == expected_messages
166168

@@ -172,9 +174,10 @@ def test_sliding_window_conversation_manager_with_untrimmable_history_raises_con
172174
{"role": "user", "content": [{"toolResult": {"toolUseId": "789", "content": [], "status": "success"}}]},
173175
]
174176
original_messages = messages.copy()
177+
test_agent = Agent(messages=messages)
175178

176179
with pytest.raises(ContextWindowOverflowException):
177-
manager.apply_management(messages)
180+
manager.apply_management(test_agent)
178181

179182
assert messages == original_messages
180183

@@ -187,8 +190,9 @@ def test_null_conversation_manager_reduce_context_raises_context_window_overflow
187190
{"role": "assistant", "content": [{"text": "Hi there"}]},
188191
]
189192
original_messages = messages.copy()
193+
test_agent = Agent(messages=messages)
190194

191-
manager.apply_management(messages)
195+
manager.apply_management(test_agent)
192196

193197
with pytest.raises(ContextWindowOverflowException):
194198
manager.reduce_context(messages)
@@ -204,8 +208,9 @@ def test_null_conversation_manager_reduce_context_with_exception_raises_same_exc
204208
{"role": "assistant", "content": [{"text": "Hi there"}]},
205209
]
206210
original_messages = messages.copy()
211+
test_agent = Agent(messages=messages)
207212

208-
manager.apply_management(messages)
213+
manager.apply_management(test_agent)
209214

210215
with pytest.raises(RuntimeError):
211216
manager.reduce_context(messages, RuntimeError("test"))

0 commit comments

Comments
 (0)