From ab85a6cf9aa743218e978d1ee79d62510b0fb556 Mon Sep 17 00:00:00 2001 From: Konippi Date: Wed, 1 Apr 2026 13:46:00 +0900 Subject: [PATCH 1/2] fix: repair orphaned toolUser in last message during session restore --- .../session/repository_session_manager.py | 66 ++++++++++--------- .../test_repository_session_manager.py | 12 ++-- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/strands/session/repository_session_manager.py b/src/strands/session/repository_session_manager.py index c1032a85e..4f27ebc7d 100644 --- a/src/strands/session/repository_session_manager.py +++ b/src/strands/session/repository_session_manager.py @@ -245,12 +245,13 @@ 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 @@ -273,37 +274,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: diff --git a/tests/strands/session/test_repository_session_manager.py b/tests/strands/session/test_repository_session_manager.py index 1d5048113..986d9f5de 100644 --- a/tests/strands/session/test_repository_session_manager.py +++ b/tests/strands/session/test_repository_session_manager.py @@ -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"}]}, { @@ -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): From 34cce61560a6c45d3fde144246fd7995f15b3bfa Mon Sep 17 00:00:00 2001 From: Konippi Date: Wed, 1 Apr 2026 23:52:16 +0900 Subject: [PATCH 2/2] docs: remove stale params from _fix_broken_tool_use docstring --- src/strands/session/repository_session_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/strands/session/repository_session_manager.py b/src/strands/session/repository_session_manager.py index 4f27ebc7d..303df14e7 100644 --- a/src/strands/session/repository_session_manager.py +++ b/src/strands/session/repository_session_manager.py @@ -255,8 +255,6 @@ def _fix_broken_tool_use(self, messages: list[Message]) -> list[Message]: 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