Skip to content

Commit d27a83a

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix/jsonrpc-get-card-return-value
2 parents bf5acf1 + 7e121e0 commit d27a83a

File tree

18 files changed

+855
-58
lines changed

18 files changed

+855
-58
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## [0.3.17](https://github.com/a2aproject/a2a-python/compare/v0.3.16...v0.3.17) (2025-11-24)
4+
5+
6+
### Features
7+
8+
* **client:** allow specifying `history_length` via call-site `MessageSendConfiguration` in `BaseClient.send_message` ([53bbf7a](https://github.com/a2aproject/a2a-python/commit/53bbf7ae3ad58fb0c10b14da05cf07c0a7bd9651))
9+
10+
## [0.3.16](https://github.com/a2aproject/a2a-python/compare/v0.3.15...v0.3.16) (2025-11-21)
11+
12+
13+
### Bug Fixes
14+
15+
* 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))
16+
17+
## [0.3.15](https://github.com/a2aproject/a2a-python/compare/v0.3.14...v0.3.15) (2025-11-19)
18+
19+
20+
### Features
21+
22+
* Add client-side extension support ([#525](https://github.com/a2aproject/a2a-python/issues/525)) ([9a92bd2](https://github.com/a2aproject/a2a-python/commit/9a92bd238e7560b195165ac5f78742981760525e))
23+
* **rest, jsonrpc:** Add client-side extension support ([9a92bd2](https://github.com/a2aproject/a2a-python/commit/9a92bd238e7560b195165ac5f78742981760525e))
24+
325
## [0.3.14](https://github.com/a2aproject/a2a-python/compare/v0.3.13...v0.3.14) (2025-11-17)
426

527

src/a2a/client/base_client.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ async def send_message(
4747
self,
4848
request: Message,
4949
*,
50+
configuration: MessageSendConfiguration | None = None,
5051
context: ClientCallContext | None = None,
5152
request_metadata: dict[str, Any] | None = None,
53+
extensions: list[str] | None = None,
5254
) -> AsyncIterator[ClientEvent | Message]:
5355
"""Sends a message to the agent.
5456
@@ -58,13 +60,15 @@ async def send_message(
5860
5961
Args:
6062
request: The message to send to the agent.
63+
configuration: Optional per-call overrides for message sending behavior.
6164
context: The client call context.
6265
request_metadata: Extensions Metadata attached to the request.
66+
extensions: List of extensions to be activated.
6367
6468
Yields:
6569
An async iterator of `ClientEvent` or a final `Message` response.
6670
"""
67-
config = MessageSendConfiguration(
71+
base_config = MessageSendConfiguration(
6872
accepted_output_modes=self._config.accepted_output_modes,
6973
blocking=not self._config.polling,
7074
push_notification_config=(
@@ -73,13 +77,22 @@ async def send_message(
7377
else None
7478
),
7579
)
80+
if configuration is not None:
81+
update_data = configuration.model_dump(
82+
exclude_unset=True,
83+
by_alias=False,
84+
)
85+
config = base_config.model_copy(update=update_data)
86+
else:
87+
config = base_config
88+
7689
params = MessageSendParams(
7790
message=request, configuration=config, metadata=request_metadata
7891
)
7992

8093
if not self._config.streaming or not self._card.capabilities.streaming:
8194
response = await self._transport.send_message(
82-
params, context=context
95+
params, context=context, extensions=extensions
8396
)
8497
result = (
8598
(response, None) if isinstance(response, Task) else response
@@ -89,7 +102,9 @@ async def send_message(
89102
return
90103

91104
tracker = ClientTaskManager()
92-
stream = self._transport.send_message_streaming(params, context=context)
105+
stream = self._transport.send_message_streaming(
106+
params, context=context, extensions=extensions
107+
)
93108

94109
first_event = await anext(stream)
95110
# The response from a server may be either exactly one Message or a
@@ -126,74 +141,91 @@ async def get_task(
126141
request: TaskQueryParams,
127142
*,
128143
context: ClientCallContext | None = None,
144+
extensions: list[str] | None = None,
129145
) -> Task:
130146
"""Retrieves the current state and history of a specific task.
131147
132148
Args:
133149
request: The `TaskQueryParams` object specifying the task ID.
134150
context: The client call context.
151+
extensions: List of extensions to be activated.
135152
136153
Returns:
137154
A `Task` object representing the current state of the task.
138155
"""
139-
return await self._transport.get_task(request, context=context)
156+
return await self._transport.get_task(
157+
request, context=context, extensions=extensions
158+
)
140159

141160
async def cancel_task(
142161
self,
143162
request: TaskIdParams,
144163
*,
145164
context: ClientCallContext | None = None,
165+
extensions: list[str] | None = None,
146166
) -> Task:
147167
"""Requests the agent to cancel a specific task.
148168
149169
Args:
150170
request: The `TaskIdParams` object specifying the task ID.
151171
context: The client call context.
172+
extensions: List of extensions to be activated.
152173
153174
Returns:
154175
A `Task` object containing the updated task status.
155176
"""
156-
return await self._transport.cancel_task(request, context=context)
177+
return await self._transport.cancel_task(
178+
request, context=context, extensions=extensions
179+
)
157180

158181
async def set_task_callback(
159182
self,
160183
request: TaskPushNotificationConfig,
161184
*,
162185
context: ClientCallContext | None = None,
186+
extensions: list[str] | None = None,
163187
) -> TaskPushNotificationConfig:
164188
"""Sets or updates the push notification configuration for a specific task.
165189
166190
Args:
167191
request: The `TaskPushNotificationConfig` object with the new configuration.
168192
context: The client call context.
193+
extensions: List of extensions to be activated.
169194
170195
Returns:
171196
The created or updated `TaskPushNotificationConfig` object.
172197
"""
173-
return await self._transport.set_task_callback(request, context=context)
198+
return await self._transport.set_task_callback(
199+
request, context=context, extensions=extensions
200+
)
174201

175202
async def get_task_callback(
176203
self,
177204
request: GetTaskPushNotificationConfigParams,
178205
*,
179206
context: ClientCallContext | None = None,
207+
extensions: list[str] | None = None,
180208
) -> TaskPushNotificationConfig:
181209
"""Retrieves the push notification configuration for a specific task.
182210
183211
Args:
184212
request: The `GetTaskPushNotificationConfigParams` object specifying the task.
185213
context: The client call context.
214+
extensions: List of extensions to be activated.
186215
187216
Returns:
188217
A `TaskPushNotificationConfig` object containing the configuration.
189218
"""
190-
return await self._transport.get_task_callback(request, context=context)
219+
return await self._transport.get_task_callback(
220+
request, context=context, extensions=extensions
221+
)
191222

192223
async def resubscribe(
193224
self,
194225
request: TaskIdParams,
195226
*,
196227
context: ClientCallContext | None = None,
228+
extensions: list[str] | None = None,
197229
) -> AsyncIterator[ClientEvent]:
198230
"""Resubscribes to a task's event stream.
199231
@@ -202,6 +234,7 @@ async def resubscribe(
202234
Args:
203235
request: Parameters to identify the task to resubscribe to.
204236
context: The client call context.
237+
extensions: List of extensions to be activated.
205238
206239
Yields:
207240
An async iterator of `ClientEvent` objects.
@@ -219,12 +252,15 @@ async def resubscribe(
219252
# we should never see Message updates, despite the typing of the service
220253
# definition indicating it may be possible.
221254
async for event in self._transport.resubscribe(
222-
request, context=context
255+
request, context=context, extensions=extensions
223256
):
224257
yield await self._process_response(tracker, event)
225258

226259
async def get_card(
227-
self, *, context: ClientCallContext | None = None
260+
self,
261+
*,
262+
context: ClientCallContext | None = None,
263+
extensions: list[str] | None = None,
228264
) -> AgentCard:
229265
"""Retrieves the agent's card.
230266
@@ -233,11 +269,14 @@ async def get_card(
233269
234270
Args:
235271
context: The client call context.
272+
extensions: List of extensions to be activated.
236273
237274
Returns:
238275
The `AgentCard` for the agent.
239276
"""
240-
card = await self._transport.get_card(context=context)
277+
card = await self._transport.get_card(
278+
context=context, extensions=extensions
279+
)
241280
self._card = card
242281
return card
243282

src/a2a/client/client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class ClientConfig:
6767
)
6868
"""Push notification callbacks to use for every request."""
6969

70+
extensions: list[str] = dataclasses.field(default_factory=list)
71+
"""A list of extension URIs the client supports."""
72+
7073

7174
UpdateEvent = TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None
7275
# Alias for emitted events from client
@@ -111,6 +114,7 @@ async def send_message(
111114
*,
112115
context: ClientCallContext | None = None,
113116
request_metadata: dict[str, Any] | None = None,
117+
extensions: list[str] | None = None,
114118
) -> AsyncIterator[ClientEvent | Message]:
115119
"""Sends a message to the server.
116120
@@ -129,6 +133,7 @@ async def get_task(
129133
request: TaskQueryParams,
130134
*,
131135
context: ClientCallContext | None = None,
136+
extensions: list[str] | None = None,
132137
) -> Task:
133138
"""Retrieves the current state and history of a specific task."""
134139

@@ -138,6 +143,7 @@ async def cancel_task(
138143
request: TaskIdParams,
139144
*,
140145
context: ClientCallContext | None = None,
146+
extensions: list[str] | None = None,
141147
) -> Task:
142148
"""Requests the agent to cancel a specific task."""
143149

@@ -147,6 +153,7 @@ async def set_task_callback(
147153
request: TaskPushNotificationConfig,
148154
*,
149155
context: ClientCallContext | None = None,
156+
extensions: list[str] | None = None,
150157
) -> TaskPushNotificationConfig:
151158
"""Sets or updates the push notification configuration for a specific task."""
152159

@@ -156,6 +163,7 @@ async def get_task_callback(
156163
request: GetTaskPushNotificationConfigParams,
157164
*,
158165
context: ClientCallContext | None = None,
166+
extensions: list[str] | None = None,
159167
) -> TaskPushNotificationConfig:
160168
"""Retrieves the push notification configuration for a specific task."""
161169

@@ -165,14 +173,18 @@ async def resubscribe(
165173
request: TaskIdParams,
166174
*,
167175
context: ClientCallContext | None = None,
176+
extensions: list[str] | None = None,
168177
) -> AsyncIterator[ClientEvent]:
169178
"""Resubscribes to a task's event stream."""
170179
return
171180
yield
172181

173182
@abstractmethod
174183
async def get_card(
175-
self, *, context: ClientCallContext | None = None
184+
self,
185+
*,
186+
context: ClientCallContext | None = None,
187+
extensions: list[str] | None = None,
176188
) -> AgentCard:
177189
"""Retrieves the agent's card."""
178190

src/a2a/client/client_factory.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def _register_defaults(
8080
card,
8181
url,
8282
interceptors,
83+
config.extensions or None,
8384
),
8485
)
8586
if TransportProtocol.http_json in supported:
@@ -90,6 +91,7 @@ def _register_defaults(
9091
card,
9192
url,
9293
interceptors,
94+
config.extensions or None,
9395
),
9496
)
9597
if TransportProtocol.grpc in supported:
@@ -113,6 +115,7 @@ async def connect( # noqa: PLR0913
113115
relative_card_path: str | None = None,
114116
resolver_http_kwargs: dict[str, Any] | None = None,
115117
extra_transports: dict[str, TransportProducer] | None = None,
118+
extensions: list[str] | None = None,
116119
) -> Client:
117120
"""Convenience method for constructing a client.
118121
@@ -142,6 +145,7 @@ async def connect( # noqa: PLR0913
142145
A2AAgentCardResolver.get_agent_card as the http_kwargs parameter.
143146
extra_transports: Additional transport protocols to enable when
144147
constructing the client.
148+
extensions: List of extensions to be activated.
145149
146150
Returns:
147151
A `Client` object.
@@ -166,7 +170,7 @@ async def connect( # noqa: PLR0913
166170
factory = cls(client_config)
167171
for label, generator in (extra_transports or {}).items():
168172
factory.register(label, generator)
169-
return factory.create(card, consumers, interceptors)
173+
return factory.create(card, consumers, interceptors, extensions)
170174

171175
def register(self, label: str, generator: TransportProducer) -> None:
172176
"""Register a new transport producer for a given transport label."""
@@ -177,6 +181,7 @@ def create(
177181
card: AgentCard,
178182
consumers: list[Consumer] | None = None,
179183
interceptors: list[ClientCallInterceptor] | None = None,
184+
extensions: list[str] | None = None,
180185
) -> Client:
181186
"""Create a new `Client` for the provided `AgentCard`.
182187
@@ -186,6 +191,7 @@ def create(
186191
interceptors: A list of interceptors to use for each request. These
187192
are used for things like attaching credentials or http headers
188193
to all outbound requests.
194+
extensions: List of extensions to be activated.
189195
190196
Returns:
191197
A `Client` object.
@@ -226,12 +232,21 @@ def create(
226232
if consumers:
227233
all_consumers.extend(consumers)
228234

235+
all_extensions = self._config.extensions.copy()
236+
if extensions:
237+
all_extensions.extend(extensions)
238+
self._config.extensions = all_extensions
239+
229240
transport = self._registry[transport_protocol](
230241
card, transport_url, self._config, interceptors or []
231242
)
232243

233244
return BaseClient(
234-
card, self._config, transport, all_consumers, interceptors or []
245+
card,
246+
self._config,
247+
transport,
248+
all_consumers,
249+
interceptors or [],
235250
)
236251

237252

0 commit comments

Comments
 (0)