diff --git a/agent/transports/anthropic.py b/agent/transports/anthropic.py index 7ffa71a6f..8588ea0b0 100644 --- a/agent/transports/anthropic.py +++ b/agent/transports/anthropic.py @@ -87,14 +87,20 @@ class AnthropicTransport(ProviderTransport): return normalize_anthropic_response_v2(response, strip_tool_prefix=strip_tool_prefix) def validate_response(self, response: Any) -> bool: - """Check Anthropic response structure is valid.""" + """Check Anthropic response structure is valid. + + An empty content list is legitimate when ``stop_reason == "end_turn"`` + — the model's canonical way of signalling "nothing more to add" after + a tool turn that already delivered the user-facing text. Treating it + as invalid falsely retries a completed response. + """ if response is None: return False content_blocks = getattr(response, "content", None) if not isinstance(content_blocks, list): return False if not content_blocks: - return False + return getattr(response, "stop_reason", None) == "end_turn" return True def extract_cache_stats(self, response: Any) -> Optional[Dict[str, int]]: diff --git a/tests/agent/transports/test_transport.py b/tests/agent/transports/test_transport.py index b51336d96..75b3a2c70 100644 --- a/tests/agent/transports/test_transport.py +++ b/tests/agent/transports/test_transport.py @@ -114,6 +114,14 @@ class TestAnthropicTransport: r = SimpleNamespace(content=[]) assert transport.validate_response(r) is False + def test_validate_response_empty_content_with_end_turn_is_valid(self, transport): + r = SimpleNamespace(content=[], stop_reason="end_turn") + assert transport.validate_response(r) is True + + def test_validate_response_empty_content_with_tool_use_is_invalid(self, transport): + r = SimpleNamespace(content=[], stop_reason="tool_use") + assert transport.validate_response(r) is False + def test_validate_response_valid(self, transport): r = SimpleNamespace(content=[SimpleNamespace(type="text", text="hello")]) assert transport.validate_response(r) is True