diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index ace35640a..b86b7ee6c 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -34,6 +34,11 @@ "too many total text bytes", ] +# Models that should include tool result status (include_tool_result_status = True) +_MODELS_INCLUDE_STATUS = [ + "anthropic.claude", +] + T = TypeVar("T", bound=BaseModel) @@ -68,6 +73,8 @@ class BedrockConfig(TypedDict, total=False): guardrail_redact_output_message: If a Bedrock Output guardrail triggers, replace output with this message. max_tokens: Maximum number of tokens to generate in the response model_id: The Bedrock model ID (e.g., "us.anthropic.claude-sonnet-4-20250514-v1:0") + include_tool_result_status: Flag to include status field in tool results. + True includes status, False removes status, "auto" determines based on model_id. Defaults to "auto". stop_sequences: List of sequences that will stop generation when encountered streaming: Flag to enable/disable streaming. Defaults to True. temperature: Controls randomness in generation (higher = more random) @@ -89,6 +96,7 @@ class BedrockConfig(TypedDict, total=False): guardrail_redact_output_message: Optional[str] max_tokens: Optional[int] model_id: str + include_tool_result_status: Optional[Literal["auto"] | bool] stop_sequences: Optional[list[str]] streaming: Optional[bool] temperature: Optional[float] @@ -114,7 +122,7 @@ def __init__( if region_name and boto_session: raise ValueError("Cannot specify both `region_name` and `boto_session`.") - self.config = BedrockModel.BedrockConfig(model_id=DEFAULT_BEDROCK_MODEL_ID) + self.config = BedrockModel.BedrockConfig(model_id=DEFAULT_BEDROCK_MODEL_ID, include_tool_result_status="auto") self.update_config(**model_config) logger.debug("config=<%s> | initializing", self.config) @@ -163,6 +171,17 @@ def get_config(self) -> BedrockConfig: """ return self.config + def _should_include_tool_result_status(self) -> bool: + """Determine whether to include tool result status based on current config.""" + include_status = self.config.get("include_tool_result_status", "auto") + + if include_status is True: + return True + elif include_status is False: + return False + else: # "auto" + return any(model in self.config["model_id"] for model in _MODELS_INCLUDE_STATUS) + def format_request( self, messages: Messages, @@ -276,10 +295,18 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # Create a new content block with only the cleaned toolResult tool_result: ToolResult = content_block["toolResult"] - # Keep only the required fields for Bedrock - cleaned_tool_result = ToolResult( - content=tool_result["content"], toolUseId=tool_result["toolUseId"], status=tool_result["status"] - ) + if self._should_include_tool_result_status(): + # Include status field + cleaned_tool_result = ToolResult( + content=tool_result["content"], + toolUseId=tool_result["toolUseId"], + status=tool_result["status"], + ) + else: + # Remove status field + cleaned_tool_result = ToolResult( # type: ignore[typeddict-item] + toolUseId=tool_result["toolUseId"], content=tool_result["content"] + ) cleaned_block: ContentBlock = {"toolResult": cleaned_tool_result} cleaned_content.append(cleaned_block) @@ -290,7 +317,6 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # Create new message with cleaned content cleaned_message: Message = Message(content=cleaned_content, role=message["role"]) cleaned_messages.append(cleaned_message) - return cleaned_messages def _has_blocked_guardrail(self, guardrail_data: dict[str, Any]) -> bool: diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 09e508845..ff150925f 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1211,7 +1211,6 @@ async def test_stream_logging(bedrock_client, model, messages, caplog, alist): def test_format_request_cleans_tool_result_content_blocks(model, model_id): - """Test that format_request cleans toolResult blocks by removing extra fields.""" messages = [ { "role": "user", @@ -1231,9 +1230,77 @@ def test_format_request_cleans_tool_result_content_blocks(model, model_id): formatted_request = model.format_request(messages) - # Verify toolResult only contains allowed fields in the formatted request tool_result = formatted_request["messages"][0]["content"][0]["toolResult"] - expected = {"content": [{"text": "Tool output"}], "toolUseId": "tool123", "status": "success"} + expected = {"toolUseId": "tool123", "content": [{"text": "Tool output"}]} assert tool_result == expected assert "extraField" not in tool_result assert "mcpMetadata" not in tool_result + assert "status" not in tool_result + + +def test_format_request_removes_status_field_when_configured(model, model_id): + model.update_config(include_tool_result_status=False) + + messages = [ + { + "role": "user", + "content": [ + { + "toolResult": { + "content": [{"text": "Tool output"}], + "toolUseId": "tool123", + "status": "success", + } + }, + ], + } + ] + + formatted_request = model.format_request(messages) + + tool_result = formatted_request["messages"][0]["content"][0]["toolResult"] + expected = {"toolUseId": "tool123", "content": [{"text": "Tool output"}]} + assert tool_result == expected + assert "status" not in tool_result + + +def test_auto_behavior_anthropic_vs_non_anthropic(bedrock_client): + model_anthropic = BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514-v1:0") + assert model_anthropic.get_config()["include_tool_result_status"] == "auto" + + model_non_anthropic = BedrockModel(model_id="amazon.titan-text-v1") + assert model_non_anthropic.get_config()["include_tool_result_status"] == "auto" + + +def test_explicit_boolean_values_preserved(bedrock_client): + model = BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514-v1:0", include_tool_result_status=True) + assert model.get_config()["include_tool_result_status"] is True + + model2 = BedrockModel(model_id="amazon.titan-text-v1", include_tool_result_status=False) + assert model2.get_config()["include_tool_result_status"] is False + """Test that format_request keeps status field by default for anthropic.claude models.""" + # Default model is anthropic.claude, so should keep status + model = BedrockModel() + + messages = [ + { + "role": "user", + "content": [ + { + "toolResult": { + "content": [{"text": "Tool output"}], + "toolUseId": "tool123", + "status": "success", + } + }, + ], + } + ] + + formatted_request = model.format_request(messages) + + # Verify toolResult contains status field by default + tool_result = formatted_request["messages"][0]["content"][0]["toolResult"] + expected = {"content": [{"text": "Tool output"}], "toolUseId": "tool123", "status": "success"} + assert tool_result == expected + assert "status" in tool_result