From 41f4456c1565f5b2fbd4bb23d3bcbf7828ebdd84 Mon Sep 17 00:00:00 2001 From: tadaki Date: Sat, 11 Oct 2025 14:57:18 +0900 Subject: [PATCH 1/6] test(client): add tests for call-site MessageSendConfiguration merge behavior in BaseClient.send_message --- src/a2a/client/base_client.py | 10 +++- tests/client/test_base_client.py | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/a2a/client/base_client.py b/src/a2a/client/base_client.py index f4a8d03d..cae6cdd9 100644 --- a/src/a2a/client/base_client.py +++ b/src/a2a/client/base_client.py @@ -46,6 +46,7 @@ async def send_message( self, request: Message, *, + configuration: MessageSendConfiguration | None = None, context: ClientCallContext | None = None, ) -> AsyncIterator[ClientEvent | Message]: """Sends a message to the agent. @@ -56,12 +57,13 @@ async def send_message( Args: request: The message to send to the agent. + configuration: Optional per-call overrides for message sending behavior. context: The client call context. Yields: An async iterator of `ClientEvent` or a final `Message` response. """ - config = MessageSendConfiguration( + base_config = MessageSendConfiguration( accepted_output_modes=self._config.accepted_output_modes, blocking=not self._config.polling, push_notification_config=( @@ -70,6 +72,12 @@ async def send_message( else None ), ) + if configuration is not None: + overrides = configuration.model_dump(exclude_unset=True, exclude_none=True) + config = base_config.model_copy(update=overrides) + else: + config = base_config + params = MessageSendParams(message=request, configuration=config) if not self._config.streaming or not self._card.capabilities.streaming: diff --git a/tests/client/test_base_client.py b/tests/client/test_base_client.py index c1251f1c..9ffeb09d 100644 --- a/tests/client/test_base_client.py +++ b/tests/client/test_base_client.py @@ -8,6 +8,7 @@ from a2a.types import ( AgentCapabilities, AgentCard, + MessageSendConfiguration, Message, Part, Role, @@ -115,3 +116,82 @@ async def test_send_message_non_streaming_agent_capability_false( assert not mock_transport.send_message_streaming.called assert len(events) == 1 assert events[0][0].id == 'task-789' + + +@pytest.mark.asyncio +async def test_send_message_uses_callsite_configuration_partial_override_non_streaming( + base_client: BaseClient, mock_transport: MagicMock, sample_message: Message +): + base_client._config.streaming = False + mock_transport.send_message.return_value = Task( + id='task-cfg-ns-1', + context_id='ctx-cfg-ns-1', + status=TaskStatus(state=TaskState.completed), + ) + + cfg = MessageSendConfiguration(history_length=2) + events = [ev async for ev in base_client.send_message(sample_message, configuration=cfg)] + + mock_transport.send_message.assert_called_once() + assert not mock_transport.send_message_streaming.called + assert len(events) == 1 and events[0][0].id == 'task-cfg-ns-1' + + params = mock_transport.send_message.await_args.args[0] + assert params.configuration.history_length == 2 + assert params.configuration.blocking == (not base_client._config.polling) + assert params.configuration.accepted_output_modes == base_client._config.accepted_output_modes + + +@pytest.mark.asyncio +async def test_send_message_ignores_none_fields_in_callsite_configuration_non_streaming( + base_client: BaseClient, mock_transport: MagicMock, sample_message: Message +): + base_client._config.streaming = False + mock_transport.send_message.return_value = Task( + id='task-cfg-ns-2', + context_id='ctx-cfg-ns-2', + status=TaskStatus(state=TaskState.completed), + ) + + cfg = MessageSendConfiguration(history_length=None, blocking=None) + events = [ev async for ev in base_client.send_message(sample_message, configuration=cfg)] + + mock_transport.send_message.assert_called_once() + assert len(events) == 1 and events[0][0].id == 'task-cfg-ns-2' + + params = mock_transport.send_message.await_args.args[0] + assert params.configuration.history_length is None + assert params.configuration.blocking == (not base_client._config.polling) + assert params.configuration.accepted_output_modes == base_client._config.accepted_output_modes + + +@pytest.mark.asyncio +async def test_send_message_uses_callsite_configuration_partial_override_streaming( + base_client: BaseClient, mock_transport: MagicMock, sample_message: Message +): + base_client._config.streaming = True + base_client._card.capabilities.streaming = True + + async def create_stream(*args, **kwargs): + yield Task( + id='task-cfg-s-1', + context_id='ctx-cfg-s-1', + status=TaskStatus(state=TaskState.completed), + ) + + mock_transport.send_message_streaming.return_value = create_stream() + + cfg = MessageSendConfiguration(history_length=0) + events = [ev async for ev in base_client.send_message(sample_message, configuration=cfg)] + + mock_transport.send_message_streaming.assert_called_once() + assert not mock_transport.send_message.called + assert len(events) == 1 + first = events[0][0] if isinstance(events[0], tuple) else events[0] + assert first.id == 'task-cfg-s-1' + + params = mock_transport.send_message_streaming.call_args.args[0] + assert params.configuration.history_length == 0 + assert params.configuration.blocking == (not base_client._config.polling) + assert params.configuration.accepted_output_modes == base_client._config.accepted_output_modes + From cab68ed3144bbc81b459e8f5a88fd31432ab7755 Mon Sep 17 00:00:00 2001 From: tadaki Date: Sat, 11 Oct 2025 15:43:07 +0900 Subject: [PATCH 2/6] style(client): apply linter formatting to BaseClient and related tests --- src/a2a/client/base_client.py | 4 +++- tests/client/test_base_client.py | 39 +++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/a2a/client/base_client.py b/src/a2a/client/base_client.py index cae6cdd9..0e061247 100644 --- a/src/a2a/client/base_client.py +++ b/src/a2a/client/base_client.py @@ -73,7 +73,9 @@ async def send_message( ), ) if configuration is not None: - overrides = configuration.model_dump(exclude_unset=True, exclude_none=True) + overrides = configuration.model_dump( + exclude_unset=True, exclude_none=True + ) config = base_config.model_copy(update=overrides) else: config = base_config diff --git a/tests/client/test_base_client.py b/tests/client/test_base_client.py index 9ffeb09d..6def36c9 100644 --- a/tests/client/test_base_client.py +++ b/tests/client/test_base_client.py @@ -8,8 +8,8 @@ from a2a.types import ( AgentCapabilities, AgentCard, - MessageSendConfiguration, Message, + MessageSendConfiguration, Part, Role, Task, @@ -130,7 +130,12 @@ async def test_send_message_uses_callsite_configuration_partial_override_non_str ) cfg = MessageSendConfiguration(history_length=2) - events = [ev async for ev in base_client.send_message(sample_message, configuration=cfg)] + events = [ + ev + async for ev in base_client.send_message( + sample_message, configuration=cfg + ) + ] mock_transport.send_message.assert_called_once() assert not mock_transport.send_message_streaming.called @@ -139,7 +144,10 @@ async def test_send_message_uses_callsite_configuration_partial_override_non_str params = mock_transport.send_message.await_args.args[0] assert params.configuration.history_length == 2 assert params.configuration.blocking == (not base_client._config.polling) - assert params.configuration.accepted_output_modes == base_client._config.accepted_output_modes + assert ( + params.configuration.accepted_output_modes + == base_client._config.accepted_output_modes + ) @pytest.mark.asyncio @@ -154,7 +162,12 @@ async def test_send_message_ignores_none_fields_in_callsite_configuration_non_st ) cfg = MessageSendConfiguration(history_length=None, blocking=None) - events = [ev async for ev in base_client.send_message(sample_message, configuration=cfg)] + events = [ + ev + async for ev in base_client.send_message( + sample_message, configuration=cfg + ) + ] mock_transport.send_message.assert_called_once() assert len(events) == 1 and events[0][0].id == 'task-cfg-ns-2' @@ -162,7 +175,10 @@ async def test_send_message_ignores_none_fields_in_callsite_configuration_non_st params = mock_transport.send_message.await_args.args[0] assert params.configuration.history_length is None assert params.configuration.blocking == (not base_client._config.polling) - assert params.configuration.accepted_output_modes == base_client._config.accepted_output_modes + assert ( + params.configuration.accepted_output_modes + == base_client._config.accepted_output_modes + ) @pytest.mark.asyncio @@ -182,7 +198,12 @@ async def create_stream(*args, **kwargs): mock_transport.send_message_streaming.return_value = create_stream() cfg = MessageSendConfiguration(history_length=0) - events = [ev async for ev in base_client.send_message(sample_message, configuration=cfg)] + events = [ + ev + async for ev in base_client.send_message( + sample_message, configuration=cfg + ) + ] mock_transport.send_message_streaming.assert_called_once() assert not mock_transport.send_message.called @@ -193,5 +214,7 @@ async def create_stream(*args, **kwargs): params = mock_transport.send_message_streaming.call_args.args[0] assert params.configuration.history_length == 0 assert params.configuration.blocking == (not base_client._config.polling) - assert params.configuration.accepted_output_modes == base_client._config.accepted_output_modes - + assert ( + params.configuration.accepted_output_modes + == base_client._config.accepted_output_modes + ) From 234e23cb00742108006eb82459a04db13b0a26b3 Mon Sep 17 00:00:00 2001 From: tadaki Date: Sat, 11 Oct 2025 15:50:54 +0900 Subject: [PATCH 3/6] fix(client): merge call-site MessageSendConfiguration using field names (by_alias=False) --- src/a2a/client/base_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/a2a/client/base_client.py b/src/a2a/client/base_client.py index 0e061247..66a89ab6 100644 --- a/src/a2a/client/base_client.py +++ b/src/a2a/client/base_client.py @@ -74,7 +74,9 @@ async def send_message( ) if configuration is not None: overrides = configuration.model_dump( - exclude_unset=True, exclude_none=True + exclude_unset=True, + exclude_none=True, + by_alias=False, ) config = base_config.model_copy(update=overrides) else: From bb135cb2178d01ec357f584b2a875a6ed54de236 Mon Sep 17 00:00:00 2001 From: tadaki Date: Sat, 11 Oct 2025 16:16:23 +0900 Subject: [PATCH 4/6] test(client): improve readability in BaseClient send_message tests --- tests/client/test_base_client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/client/test_base_client.py b/tests/client/test_base_client.py index 6def36c9..eb95fe8f 100644 --- a/tests/client/test_base_client.py +++ b/tests/client/test_base_client.py @@ -139,7 +139,9 @@ async def test_send_message_uses_callsite_configuration_partial_override_non_str mock_transport.send_message.assert_called_once() assert not mock_transport.send_message_streaming.called - assert len(events) == 1 and events[0][0].id == 'task-cfg-ns-1' + assert len(events) == 1 + task, _ = events[0] + assert task.id == 'task-cfg-ns-1' params = mock_transport.send_message.await_args.args[0] assert params.configuration.history_length == 2 @@ -170,7 +172,9 @@ async def test_send_message_ignores_none_fields_in_callsite_configuration_non_st ] mock_transport.send_message.assert_called_once() - assert len(events) == 1 and events[0][0].id == 'task-cfg-ns-2' + assert len(events) == 1 + task, _ = events[0] + assert task.id == 'task-cfg-ns-2' params = mock_transport.send_message.await_args.args[0] assert params.configuration.history_length is None @@ -208,8 +212,8 @@ async def create_stream(*args, **kwargs): mock_transport.send_message_streaming.assert_called_once() assert not mock_transport.send_message.called assert len(events) == 1 - first = events[0][0] if isinstance(events[0], tuple) else events[0] - assert first.id == 'task-cfg-s-1' + task, _ = events[0] + assert task.id == 'task-cfg-s-1' params = mock_transport.send_message_streaming.call_args.args[0] assert params.configuration.history_length == 0 From 6afdbcde47d3d4cb7bfc4e1e72706828644a7430 Mon Sep 17 00:00:00 2001 From: tadaki Date: Sat, 11 Oct 2025 16:41:21 +0900 Subject: [PATCH 5/6] test: improve naming and readability in BaseClient send_message configuration tests --- tests/client/test_base_client.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/client/test_base_client.py b/tests/client/test_base_client.py index eb95fe8f..352b7e9e 100644 --- a/tests/client/test_base_client.py +++ b/tests/client/test_base_client.py @@ -119,7 +119,7 @@ async def test_send_message_non_streaming_agent_capability_false( @pytest.mark.asyncio -async def test_send_message_uses_callsite_configuration_partial_override_non_streaming( +async def test_send_message_callsite_config_overrides_history_length_non_streaming( base_client: BaseClient, mock_transport: MagicMock, sample_message: Message ): base_client._config.streaming = False @@ -131,8 +131,7 @@ async def test_send_message_uses_callsite_configuration_partial_override_non_str cfg = MessageSendConfiguration(history_length=2) events = [ - ev - async for ev in base_client.send_message( + event async for event in base_client.send_message( sample_message, configuration=cfg ) ] @@ -165,8 +164,7 @@ async def test_send_message_ignores_none_fields_in_callsite_configuration_non_st cfg = MessageSendConfiguration(history_length=None, blocking=None) events = [ - ev - async for ev in base_client.send_message( + event async for event in base_client.send_message( sample_message, configuration=cfg ) ] @@ -186,7 +184,7 @@ async def test_send_message_ignores_none_fields_in_callsite_configuration_non_st @pytest.mark.asyncio -async def test_send_message_uses_callsite_configuration_partial_override_streaming( +async def test_send_message_callsite_config_overrides_history_length_streaming( base_client: BaseClient, mock_transport: MagicMock, sample_message: Message ): base_client._config.streaming = True @@ -203,8 +201,7 @@ async def create_stream(*args, **kwargs): cfg = MessageSendConfiguration(history_length=0) events = [ - ev - async for ev in base_client.send_message( + event async for event in base_client.send_message( sample_message, configuration=cfg ) ] From c84c4552330169bac2ec7a4d42469602a6b5332d Mon Sep 17 00:00:00 2001 From: tadaki Date: Sat, 11 Oct 2025 16:47:20 +0900 Subject: [PATCH 6/6] chore(test): apply formatting and minor lint fixes in send_message configuration tests --- tests/client/test_base_client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/client/test_base_client.py b/tests/client/test_base_client.py index 352b7e9e..9bfea3f6 100644 --- a/tests/client/test_base_client.py +++ b/tests/client/test_base_client.py @@ -131,7 +131,8 @@ async def test_send_message_callsite_config_overrides_history_length_non_streami cfg = MessageSendConfiguration(history_length=2) events = [ - event async for event in base_client.send_message( + event + async for event in base_client.send_message( sample_message, configuration=cfg ) ] @@ -164,7 +165,8 @@ async def test_send_message_ignores_none_fields_in_callsite_configuration_non_st cfg = MessageSendConfiguration(history_length=None, blocking=None) events = [ - event async for event in base_client.send_message( + event + async for event in base_client.send_message( sample_message, configuration=cfg ) ] @@ -201,7 +203,8 @@ async def create_stream(*args, **kwargs): cfg = MessageSendConfiguration(history_length=0) events = [ - event async for event in base_client.send_message( + event + async for event in base_client.send_message( sample_message, configuration=cfg ) ]