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
68 changes: 34 additions & 34 deletions src/strands/session/repository_session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,17 +245,16 @@ def initialize(self, agent: "Agent", **kwargs: Any) -> None:
def _fix_broken_tool_use(self, messages: list[Message]) -> list[Message]:
"""Fix broken tool use/result pairs in message history.

This method handles two issues:
This method handles three issues:
1. Orphaned toolUse messages without corresponding toolResult.
Before 1.15.0, strands had a bug where they persisted sessions with a potentially broken messages array.
This method retroactively fixes that issue by adding a tool_result outside of session management.
After 1.15.0, this bug is no longer present.
2. Orphaned toolResult messages without corresponding toolUse (e.g., when pagination truncates messages)
2. Orphaned toolUse as the last message (e.g., process terminated during tool execution).
3. Orphaned toolResult messages without corresponding toolUse (e.g., when pagination truncates messages)

Args:
messages: The list of messages to fix
agent_id: The agent ID for fetching previous messages
removed_message_count: Number of messages removed by the conversation manager

Returns:
Fixed list of messages with proper tool use/result pairs
Expand All @@ -273,37 +272,38 @@ def _fix_broken_tool_use(self, messages: list[Message]) -> list[Message]:

# Then check for orphaned toolUse messages
for index, message in enumerate(messages):
# Check all but the latest message in the messages array
# The latest message being orphaned is handled in the agent class
if not any("toolUse" in content for content in message["content"]):
continue

tool_use_ids = [content["toolUse"]["toolUseId"] for content in message["content"] if "toolUse" in content]

if index + 1 < len(messages):
if any("toolUse" in content for content in message["content"]):
tool_use_ids = [
content["toolUse"]["toolUseId"] for content in message["content"] if "toolUse" in content
]

# Check if there are more messages after the current toolUse message
tool_result_ids = [
content["toolResult"]["toolUseId"]
for content in messages[index + 1]["content"]
if "toolResult" in content
]

missing_tool_use_ids = list(set(tool_use_ids) - set(tool_result_ids))
# If there are missing tool use ids, that means the messages history is broken
if missing_tool_use_ids:
logger.warning(
"Session message history has an orphaned toolUse with no toolResult. "
"Adding toolResult content blocks to create valid conversation."
)
# Create the missing toolResult content blocks
missing_content_blocks = generate_missing_tool_result_content(missing_tool_use_ids)

if tool_result_ids:
# If there were any toolResult ids, that means only some of the content blocks are missing
messages[index + 1]["content"].extend(missing_content_blocks)
else:
# The message following the toolUse was not a toolResult, so lets insert it
messages.insert(index + 1, {"role": "user", "content": missing_content_blocks})
tool_result_ids = [
content["toolResult"]["toolUseId"]
for content in messages[index + 1]["content"]
if "toolResult" in content
]

missing_tool_use_ids = list(set(tool_use_ids) - set(tool_result_ids))
if missing_tool_use_ids:
logger.warning(
"Session message history has an orphaned toolUse with no toolResult. "
"Adding toolResult content blocks to create valid conversation."
)
missing_content_blocks = generate_missing_tool_result_content(missing_tool_use_ids)

if tool_result_ids:
messages[index + 1]["content"].extend(missing_content_blocks)
else:
messages.insert(index + 1, {"role": "user", "content": missing_content_blocks})
else:
# Last message is an orphaned toolUse — typically caused by process termination during tool execution
logger.warning(
"Session message history ends with an orphaned toolUse. "
"Adding toolResult to create valid conversation."
)
messages.append({"role": "user", "content": generate_missing_tool_result_content(tool_use_ids)})

return messages

def sync_multi_agent(self, source: "MultiAgentBase", **kwargs: Any) -> None:
Expand Down
12 changes: 8 additions & 4 deletions tests/strands/session/test_repository_session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,8 @@ def test_fix_broken_tool_use_handles_multiple_orphaned_tools(existing_session_ma
assert tool_use_ids == {"orphaned-123", "orphaned-456"}


def test_fix_broken_tool_use_ignores_last_message(session_manager):
"""Test that orphaned toolUse in the last message is not fixed."""
def test_fix_broken_tool_use_fixes_last_message(session_manager):
"""Test that orphaned toolUse in the last message is fixed by appending a toolResult."""
messages = [
{"role": "user", "content": [{"text": "Hello"}]},
{
Expand All @@ -430,8 +430,12 @@ def test_fix_broken_tool_use_ignores_last_message(session_manager):

fixed_messages = session_manager._fix_broken_tool_use(messages)

# Should remain unchanged since toolUse is in last message
assert fixed_messages == messages
assert len(fixed_messages) == 3
appended = fixed_messages[2]
assert appended["role"] == "user"
assert len(appended["content"]) == 1
assert appended["content"][0]["toolResult"]["toolUseId"] == "last-message-123"
assert appended["content"][0]["toolResult"]["status"] == "error"


def test_fix_broken_tool_use_does_not_change_valid_message(session_manager):
Expand Down