Skip to content

Commit 5ea97f9

Browse files
authored
Remove toolUse message when its missing due to pagination in session manager (strands-agents#1274)
* Remove toolUse message when its missing due to pagination in session manager
1 parent 62534de commit 5ea97f9

File tree

3 files changed

+105
-5
lines changed

3 files changed

+105
-5
lines changed

src/strands/session/repository_session_manager.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,35 @@ def initialize(self, agent: "Agent", **kwargs: Any) -> None:
165165
agent.messages = self._fix_broken_tool_use(agent.messages)
166166

167167
def _fix_broken_tool_use(self, messages: list[Message]) -> list[Message]:
168-
"""Add tool_result after orphaned tool_use messages.
168+
"""Fix broken tool use/result pairs in message history.
169169
170-
Before 1.15.0, strands had a bug where they persisted sessions with a potentially broken messages array.
171-
This method retroactively fixes that issue by adding a tool_result outside of session management. After 1.15.0,
172-
this bug is no longer present.
170+
This method handles two issues:
171+
1. Orphaned toolUse messages without corresponding toolResult.
172+
Before 1.15.0, strands had a bug where they persisted sessions with a potentially broken messages array.
173+
This method retroactively fixes that issue by adding a tool_result outside of session management.
174+
After 1.15.0, this bug is no longer present.
175+
2. Orphaned toolResult messages without corresponding toolUse (e.g., when pagination truncates messages)
176+
177+
Args:
178+
messages: The list of messages to fix
179+
agent_id: The agent ID for fetching previous messages
180+
removed_message_count: Number of messages removed by the conversation manager
181+
182+
Returns:
183+
Fixed list of messages with proper tool use/result pairs
173184
"""
185+
# First, check if the oldest message has orphaned toolResult (no preceding toolUse) and remove it.
186+
if messages:
187+
first_message = messages[0]
188+
if first_message["role"] == "user" and any("toolResult" in content for content in first_message["content"]):
189+
logger.warning(
190+
"Session message history starts with orphaned toolResult with no preceding toolUse. "
191+
"This typically happens when messages are truncated due to pagination limits. "
192+
"Removing orphaned toolResult message to maintain valid conversation structure."
193+
)
194+
messages.pop(0)
195+
196+
# Then check for orphaned toolUse messages
174197
for index, message in enumerate(messages):
175198
# Check all but the latest message in the messages array
176199
# The latest message being orphaned is handled in the agent class
@@ -188,7 +211,7 @@ def _fix_broken_tool_use(self, messages: list[Message]) -> list[Message]:
188211
]
189212

190213
missing_tool_use_ids = list(set(tool_use_ids) - set(tool_result_ids))
191-
# If there area missing tool use ids, that means the messages history is broken
214+
# If there are missing tool use ids, that means the messages history is broken
192215
if missing_tool_use_ids:
193216
logger.warning(
194217
"Session message history has an orphaned toolUse with no toolResult. "

tests/strands/session/test_repository_session_manager.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,3 +552,46 @@ def test_bidi_agent_messages_with_offset_zero(session_manager, mock_bidi_agent):
552552

553553
# Verify all messages restored (offset=0, no removed_message_count)
554554
assert len(mock_bidi_agent.messages) == 5
555+
556+
557+
def test_fix_broken_tool_use_removes_orphaned_tool_result_at_start(session_manager):
558+
"""Test that orphaned toolResult at the start of conversation is removed."""
559+
messages = [
560+
{
561+
"role": "user",
562+
"content": [
563+
{
564+
"toolResult": {
565+
"toolUseId": "orphaned-result-123",
566+
"status": "success",
567+
"content": [{"text": "Seattle, USA"}],
568+
}
569+
}
570+
],
571+
},
572+
{"role": "assistant", "content": [{"text": "You live in Seattle, USA."}]},
573+
{"role": "user", "content": [{"text": "I like pizza"}]},
574+
]
575+
576+
fixed_messages = session_manager._fix_broken_tool_use(messages)
577+
578+
# Should remove the first message with orphaned toolResult
579+
assert len(fixed_messages) == 2
580+
assert fixed_messages[0]["role"] == "assistant"
581+
assert fixed_messages[0]["content"][0]["text"] == "You live in Seattle, USA."
582+
assert fixed_messages[1]["role"] == "user"
583+
assert fixed_messages[1]["content"][0]["text"] == "I like pizza"
584+
585+
586+
def test_fix_broken_tool_use_does_not_affect_normal_conversations(session_manager):
587+
"""Test that normal conversations without orphaned toolResults are unaffected."""
588+
messages = [
589+
{"role": "user", "content": [{"text": "Hello"}]},
590+
{"role": "assistant", "content": [{"text": "Hi there!"}]},
591+
{"role": "user", "content": [{"text": "How are you?"}]},
592+
]
593+
594+
fixed_messages = session_manager._fix_broken_tool_use(messages)
595+
596+
# Should remain unchanged
597+
assert fixed_messages == messages
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Tests for tool helper functions."""
2+
3+
from strands.tools._tool_helpers import generate_missing_tool_result_content
4+
5+
6+
class TestGenerateMissingToolResultContent:
7+
"""Tests for generate_missing_tool_result_content function."""
8+
9+
def test_single_tool_use_id(self):
10+
"""Test generating content for a single tool use ID."""
11+
tool_use_ids = ["tool_123"]
12+
result = generate_missing_tool_result_content(tool_use_ids)
13+
14+
assert len(result) == 1
15+
assert "toolResult" in result[0]
16+
assert result[0]["toolResult"]["toolUseId"] == "tool_123"
17+
assert result[0]["toolResult"]["status"] == "error"
18+
assert result[0]["toolResult"]["content"] == [{"text": "Tool was interrupted."}]
19+
20+
def test_multiple_tool_use_ids(self):
21+
"""Test generating content for multiple tool use IDs."""
22+
tool_use_ids = ["tool_123", "tool_456", "tool_789"]
23+
result = generate_missing_tool_result_content(tool_use_ids)
24+
25+
assert len(result) == 3
26+
for i, tool_id in enumerate(tool_use_ids):
27+
assert "toolResult" in result[i]
28+
assert result[i]["toolResult"]["toolUseId"] == tool_id
29+
assert result[i]["toolResult"]["status"] == "error"
30+
31+
def test_empty_list(self):
32+
"""Test generating content for empty list."""
33+
result = generate_missing_tool_result_content([])
34+
assert result == []

0 commit comments

Comments
 (0)