Skip to content

Commit 319c18e

Browse files
committed
fix: repair orphaned toolUse in last session message during restore
_fix_broken_tool_use skipped the last message with an explicit guard, relying on the agent-class fallback (_has_tool_use_in_latest_message). That fallback only works within the same process. When a new process restores a session that ended with an orphaned toolUse (e.g. after a runtime timeout), the guard causes the broken history to be sent to the model, producing a ValidationException. Remove the guard and handle the last-message case by appending a synthetic toolResult with status 'error'. The tool execution context is already lost at restore time, so letting the model decide how to proceed is the correct behavior. Updates the test that asserted the old (incorrect) behavior.
1 parent 94fc8dd commit 319c18e

2 files changed

Lines changed: 51 additions & 35 deletions

File tree

src/strands/session/repository_session_manager.py

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -273,37 +273,48 @@ def _fix_broken_tool_use(self, messages: list[Message]) -> list[Message]:
273273

274274
# Then check for orphaned toolUse messages
275275
for index, message in enumerate(messages):
276-
# Check all but the latest message in the messages array
277-
# The latest message being orphaned is handled in the agent class
278-
if index + 1 < len(messages):
279-
if any("toolUse" in content for content in message["content"]):
280-
tool_use_ids = [
281-
content["toolUse"]["toolUseId"] for content in message["content"] if "toolUse" in content
282-
]
283-
284-
# Check if there are more messages after the current toolUse message
285-
tool_result_ids = [
286-
content["toolResult"]["toolUseId"]
287-
for content in messages[index + 1]["content"]
288-
if "toolResult" in content
289-
]
290-
291-
missing_tool_use_ids = list(set(tool_use_ids) - set(tool_result_ids))
292-
# If there are missing tool use ids, that means the messages history is broken
293-
if missing_tool_use_ids:
294-
logger.warning(
295-
"Session message history has an orphaned toolUse with no toolResult. "
296-
"Adding toolResult content blocks to create valid conversation."
297-
)
298-
# Create the missing toolResult content blocks
299-
missing_content_blocks = generate_missing_tool_result_content(missing_tool_use_ids)
300-
301-
if tool_result_ids:
302-
# If there were any toolResult ids, that means only some of the content blocks are missing
303-
messages[index + 1]["content"].extend(missing_content_blocks)
304-
else:
305-
# The message following the toolUse was not a toolResult, so lets insert it
306-
messages.insert(index + 1, {"role": "user", "content": missing_content_blocks})
276+
if not any("toolUse" in content for content in message["content"]):
277+
continue
278+
279+
tool_use_ids = [
280+
content["toolUse"]["toolUseId"] for content in message["content"] if "toolUse" in content
281+
]
282+
283+
if index + 1 >= len(messages):
284+
# The last message has an orphaned toolUse. The in-process fallback
285+
# (_has_tool_use_in_latest_message) only works within the same process.
286+
# On cross-process restore the tool execution context is lost, so report
287+
# an error to the model and let it decide how to proceed.
288+
logger.warning(
289+
"Session message history ends with an orphaned toolUse with no toolResult. "
290+
"Adding toolResult content blocks to create valid conversation."
291+
)
292+
missing_content_blocks = generate_missing_tool_result_content(tool_use_ids)
293+
messages.append({"role": "user", "content": missing_content_blocks})
294+
else:
295+
# Check if the next message already has tool results
296+
tool_result_ids = [
297+
content["toolResult"]["toolUseId"]
298+
for content in messages[index + 1]["content"]
299+
if "toolResult" in content
300+
]
301+
302+
missing_tool_use_ids = list(set(tool_use_ids) - set(tool_result_ids))
303+
# If there are missing tool use ids, that means the messages history is broken
304+
if missing_tool_use_ids:
305+
logger.warning(
306+
"Session message history has an orphaned toolUse with no toolResult. "
307+
"Adding toolResult content blocks to create valid conversation."
308+
)
309+
# Create the missing toolResult content blocks
310+
missing_content_blocks = generate_missing_tool_result_content(missing_tool_use_ids)
311+
312+
if tool_result_ids:
313+
# If there were any toolResult ids, that means only some of the content blocks are missing
314+
messages[index + 1]["content"].extend(missing_content_blocks)
315+
else:
316+
# The message following the toolUse was not a toolResult, so lets insert it
317+
messages.insert(index + 1, {"role": "user", "content": missing_content_blocks})
307318
return messages
308319

309320
def sync_multi_agent(self, source: "MultiAgentBase", **kwargs: Any) -> None:

tests/strands/session/test_repository_session_manager.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,8 @@ def test_fix_broken_tool_use_handles_multiple_orphaned_tools(existing_session_ma
416416
assert tool_use_ids == {"orphaned-123", "orphaned-456"}
417417

418418

419-
def test_fix_broken_tool_use_ignores_last_message(session_manager):
420-
"""Test that orphaned toolUse in the last message is not fixed."""
419+
def test_fix_broken_tool_use_repairs_orphaned_last_message(session_manager):
420+
"""Test that orphaned toolUse in the last message is repaired with a synthetic toolResult."""
421421
messages = [
422422
{"role": "user", "content": [{"text": "Hello"}]},
423423
{
@@ -430,8 +430,13 @@ def test_fix_broken_tool_use_ignores_last_message(session_manager):
430430

431431
fixed_messages = session_manager._fix_broken_tool_use(messages)
432432

433-
# Should remain unchanged since toolUse is in last message
434-
assert fixed_messages == messages
433+
# A synthetic toolResult should be appended for the orphaned toolUse
434+
assert len(fixed_messages) == 3
435+
assert fixed_messages[2]["role"] == "user"
436+
tool_result_ids = [
437+
c["toolResult"]["toolUseId"] for c in fixed_messages[2]["content"] if "toolResult" in c
438+
]
439+
assert "last-message-123" in tool_result_ids
435440

436441

437442
def test_fix_broken_tool_use_does_not_change_valid_message(session_manager):

0 commit comments

Comments
 (0)