From 62e2cfcee74540174b2e0c20f55561901e0f5947 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Sat, 7 Mar 2026 13:27:00 +0100 Subject: [PATCH] fix(bedrock): reorder reasoning blocks first in assistant messages for multi-turn thinking Bedrock requires reasoningContent blocks to precede all other content blocks in assistant messages when thinking is enabled. Session managers or message reconstruction can produce blocks in wrong order, causing ValidationException: 'If an assistant message contains any thinking blocks, the first block must be thinking.' Defensively reorder content blocks in _format_bedrock_messages() so reasoning always comes first in assistant messages, regardless of storage order. Fixes #1698 --- src/strands/models/bedrock.py | 9 +++++++ tests/strands/models/test_bedrock.py | 35 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 3fa907995..83141b856 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -448,6 +448,15 @@ def _format_bedrock_messages(self, messages: Messages) -> list[dict[str, Any]]: # Create new message with cleaned content (skip if empty) if cleaned_content: + # Bedrock requires reasoningContent blocks to come first in assistant messages + # when thinking is enabled. Session managers or message reconstruction may + # produce blocks in wrong order, so we defensively reorder here. + if message["role"] == "assistant": + reasoning = [b for b in cleaned_content if "reasoningContent" in b] + other = [b for b in cleaned_content if "reasoningContent" not in b] + if reasoning and other and cleaned_content[0] != reasoning[0]: + cleaned_content = reasoning + other + cleaned_messages.append({"content": cleaned_content, "role": message["role"]}) if filtered_unknown_members: diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 66fe8ab00..99967cd39 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -2817,3 +2817,38 @@ def test_guardrail_latest_message_disabled_does_not_wrap(model): assert "text" in formatted assert "guardContent" not in formatted + + +def test_format_bedrock_messages_reorders_reasoning_blocks_first(): + """Test that reasoning blocks are reordered to come first in assistant messages. + + Bedrock requires reasoningContent blocks to precede all other blocks in assistant + messages when thinking is enabled. Session managers may restore messages with blocks + in wrong order, so _format_bedrock_messages must defensively reorder them. + + Regression test for https://github.com/strands-agents/sdk-python/issues/1698 + """ + model = BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514-v1:0") + + messages = [ + {"role": "user", "content": [{"text": "What is the status?"}]}, + { + "role": "assistant", + "content": [ + {"text": "I'll check the status."}, + {"reasoningContent": {"reasoningText": {"text": "Let me think...", "signature": "sig"}}}, + {"toolUse": {"toolUseId": "t1", "name": "check", "input": {}}}, + ], + }, + {"role": "user", "content": [{"toolResult": {"toolUseId": "t1", "content": [{"text": "OK"}]}}]}, + ] + + result = model._format_bedrock_messages(messages) + + assistant_msg = result[1] + assert assistant_msg["role"] == "assistant" + assert "reasoningContent" in assistant_msg["content"][0], ( + "reasoningContent must be the first block in assistant messages" + ) + assert "text" in assistant_msg["content"][1] + assert "toolUse" in assistant_msg["content"][2]