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
73 changes: 42 additions & 31 deletions src/strands/session/repository_session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,37 +273,48 @@ 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 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})
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):
# The last message has an orphaned toolUse. The in-process fallback
# (_has_tool_use_in_latest_message) only works within the same process.
# On cross-process restore the tool execution context is lost, so report
# an error to the model and let it decide how to proceed.
logger.warning(
"Session message history ends with an orphaned toolUse with no toolResult. "
"Adding toolResult content blocks to create valid conversation."
)
missing_content_blocks = generate_missing_tool_result_content(tool_use_ids)
messages.append({"role": "user", "content": missing_content_blocks})
else:
# Check if the next message already has tool results
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})
return messages

def sync_multi_agent(self, source: "MultiAgentBase", **kwargs: Any) -> None:
Expand Down
13 changes: 9 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_repairs_orphaned_last_message(session_manager):
"""Test that orphaned toolUse in the last message is repaired with a synthetic toolResult."""
messages = [
{"role": "user", "content": [{"text": "Hello"}]},
{
Expand All @@ -430,8 +430,13 @@ 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
# A synthetic toolResult should be appended for the orphaned toolUse
assert len(fixed_messages) == 3
assert fixed_messages[2]["role"] == "user"
tool_result_ids = [
c["toolResult"]["toolUseId"] for c in fixed_messages[2]["content"] if "toolResult" in c
]
assert "last-message-123" in tool_result_ids


def test_fix_broken_tool_use_does_not_change_valid_message(session_manager):
Expand Down