Skip to content

Commit e9a6063

Browse files
giles17Copilot
andauthored
Python A2A: Expose supported_protocol_bindings as configurable parameter (#6098)
* Expose supported_protocol_bindings as configurable parameter on A2AAgent Add supported_protocol_bindings parameter to A2AAgent.__init__() allowing users to configure which A2A protocol bindings (JSONRPC, GRPC, HTTP+JSON) the client prefers when connecting to remote agents. - Defaults to ["JSONRPC"] matching current behavior - Passes through to ClientConfig for transport negotiation - Replaces 4 hardcoded references with the configurable value Closes #6057 Co-authored-by: Copilot <[email protected]> * Fix empty list falsy trap and add fallback path test coverage - Use 'is not None' check instead of 'or' to preserve explicit empty list - Add test verifying empty list is not silently replaced with defaults - Add test verifying fallback path uses custom bindings Co-authored-by: Copilot <[email protected]> * Document known protocol binding values in docstring Co-authored-by: Copilot <[email protected]> * Use Literal union for protocol binding type hint Provides IDE autocomplete for known values while keeping the type open for custom bindings (Literal is str at runtime). Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent d2f7993 commit e9a6063

2 files changed

Lines changed: 97 additions & 5 deletions

File tree

python/packages/a2a/agent_framework_a2a/_agent.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ def __init__(
176176
http_client: httpx.AsyncClient | None = None,
177177
auth_interceptor: AuthInterceptor | None = None,
178178
timeout: float | httpx.Timeout | None = None,
179+
supported_protocol_bindings: list[Literal["JSONRPC", "GRPC", "HTTP+JSON"] | str] | None = None,
179180
**kwargs: Any,
180181
) -> None:
181182
"""Initialize the A2AAgent.
@@ -193,6 +194,9 @@ def __init__(
193194
timeout: Request timeout configuration. Can be a float (applied to all timeout components),
194195
httpx.Timeout object (for full control), or None (uses 10.0s connect, 60.0s read,
195196
10.0s write, 5.0s pool - optimized for A2A operations).
197+
supported_protocol_bindings: List of protocol bindings to use for transport negotiation.
198+
Known values: "JSONRPC", "GRPC", "HTTP+JSON". Defaults to ["JSONRPC"].
199+
The A2A spec treats this as an open-form string, so custom bindings are also accepted.
196200
kwargs: any additional properties, passed to BaseAgent.
197201
"""
198202
# Default name/description from agent_card when not explicitly provided
@@ -205,6 +209,7 @@ def __init__(
205209
super().__init__(id=id, name=name, description=description, **kwargs)
206210
self._http_client: httpx.AsyncClient | None = http_client
207211
self._timeout_config = self._create_timeout_config(timeout)
212+
bindings = supported_protocol_bindings if supported_protocol_bindings is not None else ["JSONRPC"]
208213
if client is not None:
209214
self.client = client
210215
self._non_streaming_client: Client | None = None
@@ -214,7 +219,7 @@ def __init__(
214219
if url is None:
215220
raise ValueError("Either agent_card or url must be provided")
216221
# Create minimal agent card from URL
217-
agent_card = minimal_agent_card(url, ["JSONRPC"])
222+
agent_card = minimal_agent_card(url, bindings)
218223

219224
# Create or use provided httpx client
220225
if http_client is None:
@@ -229,13 +234,13 @@ def __init__(
229234
streaming_config = ClientConfig(
230235
httpx_client=http_client,
231236
streaming=True,
232-
supported_protocol_bindings=["JSONRPC"],
237+
supported_protocol_bindings=bindings,
233238
)
234239
# Create non-streaming client (single request/response for stream=False)
235240
non_streaming_config = ClientConfig(
236241
httpx_client=http_client,
237242
streaming=False,
238-
supported_protocol_bindings=["JSONRPC"],
243+
supported_protocol_bindings=bindings,
239244
)
240245
streaming_factory = ClientFactory(streaming_config)
241246
non_streaming_factory = ClientFactory(non_streaming_config)
@@ -256,7 +261,7 @@ def __init__(
256261
"Provide a 'url' argument or ensure 'agent_card.supported_interfaces' "
257262
"contains at least one interface with a URL."
258263
) from transport_error
259-
fallback_card = minimal_agent_card(fallback_url, ["JSONRPC"])
264+
fallback_card = minimal_agent_card(fallback_url, bindings)
260265
try:
261266
self.client = streaming_factory.create(fallback_card, interceptors=interceptors) # type: ignore
262267
self._non_streaming_client = non_streaming_factory.create(

python/packages/a2a/tests/test_a2a_agent.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,94 @@ def test_a2a_agent_initialization_with_timeout_parameter() -> None:
703703
assert isinstance(timeout_arg, httpx.Timeout)
704704

705705

706-
# region Continuation Token Tests
706+
def test_a2a_agent_initialization_with_supported_protocol_bindings() -> None:
707+
"""Test A2AAgent initialization with custom supported_protocol_bindings."""
708+
with (
709+
patch("agent_framework_a2a._agent.httpx.AsyncClient") as mock_async_client,
710+
patch("agent_framework_a2a._agent.ClientConfig") as mock_config,
711+
patch("agent_framework_a2a._agent.ClientFactory") as mock_factory,
712+
):
713+
mock_async_client.return_value = MagicMock()
714+
mock_client_instance = MagicMock()
715+
mock_factory.return_value.create.return_value = mock_client_instance
716+
717+
A2AAgent(
718+
name="Test Agent",
719+
url="https://test-agent.example.com",
720+
supported_protocol_bindings=["GRPC", "JSONRPC"],
721+
)
722+
723+
# Verify ClientConfig was called with our custom bindings for both streaming and non-streaming
724+
assert mock_config.call_count == 2
725+
for call in mock_config.call_args_list:
726+
assert call.kwargs["supported_protocol_bindings"] == ["GRPC", "JSONRPC"]
727+
728+
729+
def test_a2a_agent_initialization_defaults_to_jsonrpc() -> None:
730+
"""Test A2AAgent defaults to JSONRPC when supported_protocol_bindings is not provided."""
731+
with (
732+
patch("agent_framework_a2a._agent.httpx.AsyncClient") as mock_async_client,
733+
patch("agent_framework_a2a._agent.ClientConfig") as mock_config,
734+
patch("agent_framework_a2a._agent.ClientFactory") as mock_factory,
735+
):
736+
mock_async_client.return_value = MagicMock()
737+
mock_client_instance = MagicMock()
738+
mock_factory.return_value.create.return_value = mock_client_instance
739+
740+
A2AAgent(name="Test Agent", url="https://test-agent.example.com")
741+
742+
# Verify ClientConfig was called with default JSONRPC bindings
743+
assert mock_config.call_count == 2
744+
for call in mock_config.call_args_list:
745+
assert call.kwargs["supported_protocol_bindings"] == ["JSONRPC"]
746+
747+
748+
def test_a2a_agent_initialization_empty_list_preserved() -> None:
749+
"""Test that an explicit empty list is preserved and not replaced with defaults."""
750+
with (
751+
patch("agent_framework_a2a._agent.httpx.AsyncClient") as mock_async_client,
752+
patch("agent_framework_a2a._agent.ClientConfig") as mock_config,
753+
patch("agent_framework_a2a._agent.ClientFactory") as mock_factory,
754+
):
755+
mock_async_client.return_value = MagicMock()
756+
mock_client_instance = MagicMock()
757+
mock_factory.return_value.create.return_value = mock_client_instance
758+
759+
A2AAgent(
760+
name="Test Agent",
761+
url="https://test-agent.example.com",
762+
supported_protocol_bindings=[],
763+
)
764+
765+
# Verify ClientConfig was called with the explicit empty list, not the default
766+
assert mock_config.call_count == 2
767+
for call in mock_config.call_args_list:
768+
assert call.kwargs["supported_protocol_bindings"] == []
769+
770+
771+
def test_a2a_agent_fallback_uses_custom_bindings() -> None:
772+
"""Test that transport fallback path uses custom bindings."""
773+
mock_agent_card = MagicMock()
774+
mock_agent_card.supported_interfaces = [MagicMock(url="https://fallback.example.com")]
775+
776+
mock_factory = MagicMock()
777+
# First create() call fails (primary streaming), then fallback calls succeed
778+
primary_error = Exception("no compatible transports found")
779+
mock_factory.create.side_effect = [primary_error, MagicMock(), MagicMock()]
780+
781+
with (
782+
patch("agent_framework_a2a._agent.ClientFactory", return_value=mock_factory),
783+
patch("agent_framework_a2a._agent.minimal_agent_card") as mock_minimal_card,
784+
patch("agent_framework_a2a._agent.httpx.AsyncClient"),
785+
):
786+
A2AAgent(
787+
name="test-agent",
788+
agent_card=mock_agent_card,
789+
supported_protocol_bindings=["GRPC", "HTTP+JSON"],
790+
)
791+
792+
# Verify minimal_agent_card was called with the custom bindings
793+
mock_minimal_card.assert_called_once_with("https://fallback.example.com", ["GRPC", "HTTP+JSON"])
707794

708795

709796
async def test_working_task_emits_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None:

0 commit comments

Comments
 (0)