Skip to content

refactor: Remove _remove_dangling_messages from SlidingWindowConversationManager #418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,15 @@ def __init__(self, window_size: int = 40, should_truncate_results: bool = True):
def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
"""Apply the sliding window to the agent's messages array to maintain a manageable history size.

This method is called after every event loop cycle, as the messages array may have been modified with tool
results and assistant responses. It first removes any dangling messages that might create an invalid
conversation state, then applies the sliding window if the message count exceeds the window size.

Special handling is implemented to ensure we don't leave a user message with toolResult
as the first message in the array. It also ensures that all toolUse blocks have corresponding toolResult
blocks to maintain conversation coherence.
This method is called after every event loop cycle to apply a sliding window if the message count
exceeds the window size.

Args:
agent: The agent whose messages will be managed.
This list is modified in-place.
**kwargs: Additional keyword arguments for future extensibility.
"""
messages = agent.messages
self._remove_dangling_messages(messages)

if len(messages) <= self.window_size:
logger.debug(
Expand All @@ -81,37 +75,6 @@ def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
return
self.reduce_context(agent)

def _remove_dangling_messages(self, messages: Messages) -> None:
"""Remove dangling messages that would create an invalid conversation state.

After the event loop cycle is executed, we expect the messages array to end with either an assistant tool use
request followed by the pairing user tool result or an assistant response with no tool use request. If the
event loop cycle fails, we may end up in an invalid message state, and so this method will remove problematic
messages from the end of the array.

This method handles two specific cases:

- User with no tool result: Indicates that event loop failed to generate an assistant tool use request
- Assistant with tool use request: Indicates that event loop failed to generate a pairing user tool result

Args:
messages: The messages to clean up.
This list is modified in-place.
"""
# remove any dangling user messages with no ToolResult
if len(messages) > 0 and is_user_message(messages[-1]):
if not any("toolResult" in content for content in messages[-1]["content"]):
messages.pop()

# remove any dangling assistant messages with ToolUse
if len(messages) > 0 and is_assistant_message(messages[-1]):
if any("toolUse" in content for content in messages[-1]["content"]):
messages.pop()
# remove remaining dangling user messages with no ToolResult after we popped off an assistant message
if len(messages) > 0 and is_user_message(messages[-1]):
if not any("toolResult" in content for content in messages[-1]["content"]):
messages.pop()

def reduce_context(self, agent: "Agent", e: Optional[Exception] = None, **kwargs: Any) -> None:
"""Trim the oldest messages to reduce the conversation context size.

Expand Down
12 changes: 7 additions & 5 deletions tests/strands/agent/test_conversation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,21 @@ def conversation_manager(request):
{"role": "user", "content": [{"toolResult": {"toolUseId": "123", "content": [], "status": "success"}}]},
],
),
# 2 - Remove dangling user message with no tool result
# 2 - Keep user message
(
{"window_size": 2},
[
{"role": "user", "content": [{"text": "Hello"}]},
],
[],
[{"role": "user", "content": [{"text": "Hello"}]}],
),
# 3 - Remove dangling assistant message with tool use
# 3 - Keep dangling assistant message with tool use
(
{"window_size": 3},
[
{"role": "assistant", "content": [{"toolUse": {"toolUseId": "123", "name": "tool1", "input": {}}}]},
],
[],
[{"role": "assistant", "content": [{"toolUse": {"toolUseId": "123", "name": "tool1", "input": {}}}]}],
),
# 4 - Remove dangling assistant message with tool use - User tool result remains
(
Expand All @@ -83,6 +83,7 @@ def conversation_manager(request):
],
[
{"role": "user", "content": [{"toolResult": {"toolUseId": "123", "content": [], "status": "success"}}]},
{"role": "assistant", "content": [{"toolUse": {"toolUseId": "123", "name": "tool1", "input": {}}}]},
],
),
# 5 - Remove dangling assistant message with tool use and user message without tool result
Expand All @@ -95,8 +96,9 @@ def conversation_manager(request):
{"role": "assistant", "content": [{"toolUse": {"toolUseId": "123", "name": "tool1", "input": {}}}]},
],
[
{"role": "user", "content": [{"text": "First"}]},
{"role": "assistant", "content": [{"text": "First response"}]},
{"role": "user", "content": [{"text": "Use a tool"}]},
{"role": "assistant", "content": [{"toolUse": {"toolUseId": "123", "name": "tool1", "input": {}}}]},
],
),
# 6 - Message count above max window size - Basic drop
Expand Down