From 32393ee63a1525bd340ba7c40ef3cd2dcf7ae697 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Sat, 7 Mar 2026 05:13:43 +0100 Subject: [PATCH] fix: SlidingWindowConversationManager no longer falsely truncates tool results in window apply_management() called reduce_context() which prioritizes tool result truncation over sliding window trimming. When message count exceeded window_size, tool results inside the kept window were unnecessarily truncated or marked as errors instead of simply removing old messages. Fix: apply_management() now calls _slide_window() directly to remove old messages first. reduce_context() (used for model context overflow) retains its truncation-first strategy since content size reduction is the priority in that case. Extracted _slide_window() as a shared method for the window trimming logic. Fixes #702 --- .../sliding_window_conversation_manager.py | 23 ++++++++- .../agent/test_conversation_manager.py | 48 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/strands/agent/conversation_manager/sliding_window_conversation_manager.py b/src/strands/agent/conversation_manager/sliding_window_conversation_manager.py index b97de0b06..53da51c8a 100644 --- a/src/strands/agent/conversation_manager/sliding_window_conversation_manager.py +++ b/src/strands/agent/conversation_manager/sliding_window_conversation_manager.py @@ -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. @@ -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. @@ -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 diff --git a/tests/strands/agent/test_conversation_manager.py b/tests/strands/agent/test_conversation_manager.py index fd88954e8..e5ee753ac 100644 --- a/tests/strands/agent/test_conversation_manager.py +++ b/tests/strands/agent/test_conversation_manager.py @@ -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" + )