Skip to content
Open
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 @@ -139,6 +139,10 @@ def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
This method is called after every event loop cycle to apply a sliding window if the message count
exceeds the window size.

Unlike reduce_context (which prioritizes content truncation for context overflow recovery),
this method prioritizes sliding window trimming to remove old messages. This prevents false
positives where tool results inside the kept window are unnecessarily truncated.

Args:
agent: The agent whose messages will be managed.
This list is modified in-place.
Expand All @@ -151,7 +155,7 @@ def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
"message_count=<%s>, window_size=<%s> | skipping context reduction", len(messages), self.window_size
)
return
self.reduce_context(agent)
self._slide_window(messages)

def reduce_context(self, agent: "Agent", e: Exception | None = None, **kwargs: Any) -> None:
"""Trim the oldest messages to reduce the conversation context size.
Expand Down Expand Up @@ -184,7 +188,22 @@ def reduce_context(self, agent: "Agent", e: Exception | None = None, **kwargs: A
logger.debug("message_index=<%s> | tool results truncated", oldest_message_idx_with_tool_results)
return

# Try to trim index id when tool result cannot be truncated anymore
# Try to trim messages when tool result cannot be truncated anymore
self._slide_window(messages, e)

def _slide_window(self, messages: Messages, e: Exception | None = None) -> None:
"""Remove the oldest messages using a sliding window.

Handles special cases where trimming the messages leads to toolResult
with no corresponding toolUse or toolUse with no corresponding toolResult.

Args:
messages: The conversation message history (modified in-place).
e: The exception that triggered the context reduction, if any.

Raises:
ContextWindowOverflowException: If no valid trim index can be found.
"""
# If the number of messages is less than the window_size, then we default to 2, otherwise, trim to window size
trim_index = 2 if len(messages) <= self.window_size else len(messages) - self.window_size

Expand Down
48 changes: 48 additions & 0 deletions tests/strands/agent/test_conversation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,3 +591,51 @@ def test_boundary_text_in_tool_result_not_truncated():

assert not changed
assert messages[0]["content"][0]["toolResult"]["content"][0]["text"] == boundary_text


def test_apply_management_does_not_falsely_truncate_tool_results_in_window():
"""apply_management should slide the window, not truncate tool results that would be kept.

Regression test for https://github.com/strands-agents/sdk-python/issues/702:
With window_size=5 and 8 messages, apply_management should remove the 3 oldest
messages via sliding window, NOT truncate tool results that fall inside the
kept window.
"""
manager = SlidingWindowConversationManager(window_size=5)
messages = [
{"role": "user", "content": [{"text": "Hello, my name is Strands!"}]},
{"role": "assistant", "content": [{"text": "Hi there!"}]},
{"role": "user", "content": [{"text": "What's my name?"}]},
{"role": "assistant", "content": [{"text": "Your name is Strands!"}]},
{"role": "user", "content": [{"text": "direct tool call"}]},
{"role": "assistant", "content": [{"toolUse": {"toolUseId": "calc1", "name": "calculator", "input": {}}}]},
{
"role": "user",
"content": [
{"toolResult": {"toolUseId": "calc1", "content": [{"text": "Result: 56088"}], "status": "success"}}
],
},
{"role": "assistant", "content": [{"text": "calculator was called."}]},
]
test_agent = Agent(messages=messages)

manager.apply_management(test_agent)

# Should have trimmed to window_size (5 messages kept)
assert len(test_agent.messages) <= 5

# The tool result inside the window must NOT be truncated or marked as error
tool_result_msgs = [
m for m in test_agent.messages
if any("toolResult" in c for c in m.get("content", []))
]
for msg in tool_result_msgs:
for content in msg["content"]:
if "toolResult" in content:
assert content["toolResult"]["status"] == "success", (
"Tool result status should remain 'success', not changed to 'error'"
)
result_text = content["toolResult"]["content"][0]["text"]
assert "truncated" not in result_text.lower(), (
"Tool result should not be truncated when it falls inside the window"
)