From fc31d03e8c6acb68660f6d1924262e16933c5d50 Mon Sep 17 00:00:00 2001 From: Iva Sokolaj <102302011+sokoliva@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:53:53 +0100 Subject: [PATCH 1/4] fix: Ensure metadata propagation for `Task` `ToProto` and `FromProto` conversion (#557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `metadata` dictionary is now correctly propagated in both the `ToProto` (types.Task to a2a_pb2.Task) and `FromProto` (a2a_pb2.Task to types.Task) conversion utilities. Release-As:0.3.16 Fixes #541 🦕 --- src/a2a/utils/proto_utils.py | 2 ++ tests/utils/test_proto_utils.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index d077d62b..8bf01eea 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -203,6 +203,7 @@ def task(cls, task: types.Task) -> a2a_pb2.Task: if task.history else None ), + metadata=cls.metadata(task.metadata), ) @classmethod @@ -660,6 +661,7 @@ def task(cls, task: a2a_pb2.Task) -> types.Task: status=cls.task_status(task.status), artifacts=[cls.artifact(a) for a in task.artifacts], history=[cls.message(h) for h in task.history], + metadata=cls.metadata(task.metadata), ) @classmethod diff --git a/tests/utils/test_proto_utils.py b/tests/utils/test_proto_utils.py index da54f833..33be1f3f 100644 --- a/tests/utils/test_proto_utils.py +++ b/tests/utils/test_proto_utils.py @@ -52,6 +52,7 @@ def sample_task(sample_message: types.Message) -> types.Task: ], ) ], + metadata={'source': 'test'}, ) @@ -508,3 +509,30 @@ def test_large_integer_roundtrip_with_utilities(self): assert final_result['nested']['another_large'] == 12345678901234567890 assert isinstance(final_result['nested']['another_large'], int) assert final_result['nested']['normal'] == 'text' + + def test_task_conversion_roundtrip( + self, sample_task: types.Task, sample_message: types.Message + ): + """Test conversion of Task to proto and back.""" + proto_task = proto_utils.ToProto.task(sample_task) + assert isinstance(proto_task, a2a_pb2.Task) + + roundtrip_task = proto_utils.FromProto.task(proto_task) + assert roundtrip_task.id == 'task-1' + assert roundtrip_task.context_id == 'ctx-1' + assert roundtrip_task.status == types.TaskStatus( + state=types.TaskState.working, message=sample_message + ) + assert roundtrip_task.history == [sample_message] + assert roundtrip_task.artifacts == [ + types.Artifact( + artifact_id='art-1', + description='', + metadata={}, + name='', + parts=[ + types.Part(root=types.TextPart(text='Artifact content')) + ], + ) + ] + assert roundtrip_task.metadata == {'source': 'test'} From dbc73e84020d9ca6dae2eca5bcf5820580ca2edf Mon Sep 17 00:00:00 2001 From: "Agent2Agent (A2A) Bot" Date: Fri, 21 Nov 2025 07:34:03 -0600 Subject: [PATCH 2/4] chore(main): release 0.3.16 (#562) :robot: I have created a release *beep* *boop* --- ## [0.3.16](https://github.com/a2aproject/a2a-python/compare/v0.3.15...v0.3.16) (2025-11-21) ### Bug Fixes * Ensure metadata propagation for `Task` `ToProto` and `FromProto` conversion ([#557](https://github.com/a2aproject/a2a-python/issues/557)) ([fc31d03](https://github.com/a2aproject/a2a-python/commit/fc31d03e8c6acb68660f6d1924262e16933c5d50)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e6048d..026e8fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ - # Changelog +# Changelog + +## [0.3.16](https://github.com/a2aproject/a2a-python/compare/v0.3.15...v0.3.16) (2025-11-21) + + +### Bug Fixes + +* Ensure metadata propagation for `Task` `ToProto` and `FromProto` conversion ([#557](https://github.com/a2aproject/a2a-python/issues/557)) ([fc31d03](https://github.com/a2aproject/a2a-python/commit/fc31d03e8c6acb68660f6d1924262e16933c5d50)) ## [0.3.15](https://github.com/a2aproject/a2a-python/compare/v0.3.14...v0.3.15) (2025-11-19) From 53bbf7ae3ad58fb0c10b14da05cf07c0a7bd9651 Mon Sep 17 00:00:00 2001 From: Tadaki Asechi <127199356+TadakiAsechi@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:41:28 +0900 Subject: [PATCH 3/4] feat(client): allow specifying `history_length` via call-site `MessageSendConfiguration` in `BaseClient.send_message` (#500) ## Description This PR implements the client-side enhancement described in #499. It allows callers to specify `history_length` (and other message configuration options) directly via the optional configuration parameter in `BaseClient.send_message`, complementing the server-side support added in #497. ### Summary of Changes - Added optional argument `configuration: MessageSendConfiguration | None` to `BaseClient.send_message`. - When provided, merges call-site configuration with `ClientConfig` defaults. - Allows partial overrides (e.g., setting `historyLength` only). - Updated docstrings to reflect the new parameter behavior. - Added unit tests covering both non-streaming and streaming scenarios. ### Motivation Previously, `BaseClient.send_message` built `MessageSendConfiguration` internally from `ClientConfig`, but `ClientConfig` does not include `history_length`. This prevented clients from specifying it on a per-call basis, unlike `get_task`, which already supports `history_length` via `TaskQueryParams`. After this change, client behavior becomes consistent across both methods. ### Related Issues Fixes #499 Complements #497 (server-side historyLength support) Release-As: 0.3.17 --------- Co-authored-by: tadaki Co-authored-by: Holt Skinner <13262395+holtskinner@users.noreply.github.com> Co-authored-by: TadakiAsechi Co-authored-by: TadakiAsechi --- src/a2a/client/base_client.py | 13 +++++- tests/client/test_base_client.py | 76 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/a2a/client/base_client.py b/src/a2a/client/base_client.py index 5719bc1b..fac7ecad 100644 --- a/src/a2a/client/base_client.py +++ b/src/a2a/client/base_client.py @@ -47,6 +47,7 @@ async def send_message( self, request: Message, *, + configuration: MessageSendConfiguration | None = None, context: ClientCallContext | None = None, request_metadata: dict[str, Any] | None = None, extensions: list[str] | None = None, @@ -59,6 +60,7 @@ 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. request_metadata: Extensions Metadata attached to the request. extensions: List of extensions to be activated. @@ -66,7 +68,7 @@ async def send_message( 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=( @@ -75,6 +77,15 @@ async def send_message( else None ), ) + if configuration is not None: + update_data = configuration.model_dump( + exclude_unset=True, + by_alias=False, + ) + config = base_config.model_copy(update=update_data) + else: + config = base_config + params = MessageSendParams( message=request, configuration=config, metadata=request_metadata ) diff --git a/tests/client/test_base_client.py b/tests/client/test_base_client.py index f5ab2543..7aa47902 100644 --- a/tests/client/test_base_client.py +++ b/tests/client/test_base_client.py @@ -9,6 +9,7 @@ AgentCapabilities, AgentCard, Message, + MessageSendConfiguration, Part, Role, Task, @@ -125,3 +126,78 @@ 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_callsite_config_overrides_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, + blocking=False, + accepted_output_modes=['application/json'], + ) + events = [ + event + async for event 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 + task, _ = events[0] + assert task.id == 'task-cfg-ns-1' + + params = mock_transport.send_message.call_args[0][0] + assert params.configuration.history_length == 2 + assert params.configuration.blocking is False + assert params.configuration.accepted_output_modes == ['application/json'] + + +@pytest.mark.asyncio +async def test_send_message_callsite_config_overrides_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, + blocking=True, + accepted_output_modes=['text/plain'], + ) + events = [ + event + async for event 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 + task, _ = events[0] + assert task.id == 'task-cfg-s-1' + + params = mock_transport.send_message_streaming.call_args[0][0] + assert params.configuration.history_length == 0 + assert params.configuration.blocking is True + assert params.configuration.accepted_output_modes == ['text/plain'] From 7e121e0f14f61254a1f136d6f25efb81b94c58e7 Mon Sep 17 00:00:00 2001 From: "Agent2Agent (A2A) Bot" Date: Mon, 24 Nov 2025 06:36:54 -0600 Subject: [PATCH 4/4] chore(main): release 0.3.17 (#565) :robot: I have created a release *beep* *boop* --- ## [0.3.17](https://github.com/a2aproject/a2a-python/compare/v0.3.16...v0.3.17) (2025-11-24) ### Features * **client:** allow specifying `history_length` via call-site `MessageSendConfiguration` in `BaseClient.send_message` ([53bbf7a](https://github.com/a2aproject/a2a-python/commit/53bbf7ae3ad58fb0c10b14da05cf07c0a7bd9651)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 026e8fd1..66dfd677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.3.17](https://github.com/a2aproject/a2a-python/compare/v0.3.16...v0.3.17) (2025-11-24) + + +### Features + +* **client:** allow specifying `history_length` via call-site `MessageSendConfiguration` in `BaseClient.send_message` ([53bbf7a](https://github.com/a2aproject/a2a-python/commit/53bbf7ae3ad58fb0c10b14da05cf07c0a7bd9651)) + ## [0.3.16](https://github.com/a2aproject/a2a-python/compare/v0.3.15...v0.3.16) (2025-11-21)