diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index e353a7c7fa..68694e3db4 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -127,7 +127,21 @@ class A2AAgent(BaseAgent): ) factory = ClientFactory(config) interceptors = [auth_interceptor] if auth_interceptor is not None else None - self.client = factory.create(agent_card, interceptors=interceptors) # type: ignore + + # Attempt transport negotiation with the provided agent card + try: + self.client = factory.create(agent_card, interceptors=interceptors) # type: ignore + except Exception as transport_error: + # Transport negotiation failed - fall back to minimal agent card with JSONRPC + fallback_card = minimal_agent_card(agent_card.url, [TransportProtocol.jsonrpc]) + try: + self.client = factory.create(fallback_card, interceptors=interceptors) # type: ignore + except Exception as fallback_error: + raise RuntimeError( + f"A2A transport negotiation failed. " + f"Primary error: {transport_error}. " + f"Fallback error: {fallback_error}" + ) from transport_error async def __aenter__(self) -> "A2AAgent": """Async context manager entry.""" diff --git a/python/packages/a2a/tests/test_a2a_agent.py b/python/packages/a2a/tests/test_a2a_agent.py index b8fe97be60..f703c92a96 100644 --- a/python/packages/a2a/tests/test_a2a_agent.py +++ b/python/packages/a2a/tests/test_a2a_agent.py @@ -2,10 +2,22 @@ from collections.abc import AsyncIterator from typing import Any -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 -from a2a.types import Artifact, DataPart, FilePart, FileWithUri, Message, Part, Task, TaskState, TaskStatus, TextPart +from a2a.types import ( + AgentCard, + Artifact, + DataPart, + FilePart, + FileWithUri, + Message, + Part, + Task, + TaskState, + TaskStatus, + TextPart, +) from a2a.types import Role as A2ARole from agent_framework import ( AgentRunResponse, @@ -515,3 +527,30 @@ def test_auth_interceptor_parameter() -> None: # Verify the agent was created successfully assert agent.name == "test-agent" assert agent.client is not None + + +def test_transport_negotiation_both_fail() -> None: + """Test that RuntimeError is raised when both primary and fallback transport negotiation fail.""" + # Create a mock agent card + mock_agent_card = MagicMock(spec=AgentCard) + mock_agent_card.url = "http://test-agent.example.com" + + # Mock the factory to simulate both primary and fallback failures + mock_factory = MagicMock() + + # Both calls to factory.create() fail + primary_error = Exception("no compatible transports found") + fallback_error = Exception("fallback also failed") + mock_factory.create.side_effect = [primary_error, fallback_error] + + with ( + patch("agent_framework_a2a._agent.ClientFactory", return_value=mock_factory), + patch("agent_framework_a2a._agent.minimal_agent_card"), + patch("agent_framework_a2a._agent.httpx.AsyncClient"), + raises(RuntimeError, match="A2A transport negotiation failed"), + ): + # Attempt to create A2AAgent - should raise RuntimeError + A2AAgent( + name="test-agent", + agent_card=mock_agent_card, + )