Skip to content

Commit 3d5e34f

Browse files
authored
Merge branch 'main' into update-test-types
2 parents 1514eb4 + e75f27d commit 3d5e34f

File tree

12 files changed

+271
-187
lines changed

12 files changed

+271
-187
lines changed

.github/workflows/security.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Bandit
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
analyze:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
security-events: write
11+
actions: read
12+
contents: read
13+
steps:
14+
- name: Perform Bandit Analysis
15+
uses: PyCQA/bandit-action@v1
16+
with:
17+
severity: medium
18+
confidence: medium
19+
targets: "src/a2a"

.vscode/extensions.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"recommendations": [
3+
"charliermarsh.ruff"
4+
],
5+
"unwantedRecommendations": []
6+
}

.vscode/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
2-
"python.testing.pytestArgs": ["tests"],
2+
"python.testing.pytestArgs": [
3+
"tests"
4+
],
35
"python.testing.unittestEnabled": false,
46
"python.testing.pytestEnabled": true,
57
"editor.formatOnSave": true,
@@ -12,4 +14,10 @@
1214
}
1315
},
1416
"ruff.importStrategy": "fromEnvironment",
17+
"files.insertFinalNewline": true,
18+
"files.trimFinalNewlines": false,
19+
"files.trimTrailingWhitespace": false,
20+
"editor.rulers": [
21+
80
22+
]
1523
}

CHANGELOG.md

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

3+
## [0.2.10](https://github.com/a2aproject/a2a-python/compare/v0.2.9...v0.2.10) (2025-06-30)
4+
5+
6+
### ⚠ BREAKING CHANGES
7+
8+
* Update to A2A Spec Version [0.2.5](https://github.com/a2aproject/A2A/releases/tag/v0.2.5) ([#197](https://github.com/a2aproject/a2a-python/issues/197))
9+
10+
### Features
11+
12+
* Add `append` and `last_chunk` to `add_artifact` method on `TaskUpdater` ([#186](https://github.com/a2aproject/a2a-python/issues/186)) ([8c6560f](https://github.com/a2aproject/a2a-python/commit/8c6560fd403887fab9d774bfcc923a5f6f459364))
13+
* add a2a routes to existing app ([#188](https://github.com/a2aproject/a2a-python/issues/188)) ([32fecc7](https://github.com/a2aproject/a2a-python/commit/32fecc7194a61c2f5be0b8795d5dc17cdbab9040))
14+
* Add middleware to the client SDK ([#171](https://github.com/a2aproject/a2a-python/issues/171)) ([efaabd3](https://github.com/a2aproject/a2a-python/commit/efaabd3b71054142109b553c984da1d6e171db24))
15+
* Add more task state management methods to TaskUpdater ([#208](https://github.com/a2aproject/a2a-python/issues/208)) ([2b3bf6d](https://github.com/a2aproject/a2a-python/commit/2b3bf6d53ac37ed93fc1b1c012d59c19060be000))
16+
* raise error for tasks in terminal states ([#215](https://github.com/a2aproject/a2a-python/issues/215)) ([a0bf13b](https://github.com/a2aproject/a2a-python/commit/a0bf13b208c90b439b4be1952c685e702c4917a0))
17+
18+
### Bug Fixes
19+
20+
* `consume_all` doesn't catch `asyncio.TimeoutError` in python 3.10 ([#216](https://github.com/a2aproject/a2a-python/issues/216)) ([39307f1](https://github.com/a2aproject/a2a-python/commit/39307f15a1bb70eb77aee2211da038f403571242))
21+
* Append metadata and context id when processing TaskStatusUpdateE… ([#238](https://github.com/a2aproject/a2a-python/issues/238)) ([e106020](https://github.com/a2aproject/a2a-python/commit/e10602033fdd4f4e6b61af717ffc242d772545b3))
22+
* Fix reference to `grpc.aio.ServicerContext` ([#237](https://github.com/a2aproject/a2a-python/issues/237)) ([0c1987b](https://github.com/a2aproject/a2a-python/commit/0c1987bb85f3e21089789ee260a0c62ac98b66a5))
23+
* Fixes Short Circuit clause for context ID ([#236](https://github.com/a2aproject/a2a-python/issues/236)) ([a5509e6](https://github.com/a2aproject/a2a-python/commit/a5509e6b37701dfb5c729ccc12531e644a12f8ae))
24+
* Resolve `APIKeySecurityScheme` parsing failed ([#226](https://github.com/a2aproject/a2a-python/issues/226)) ([aa63b98](https://github.com/a2aproject/a2a-python/commit/aa63b982edc2a07fd0df0b01fb9ad18d30b35a79))
25+
* send notifications on message not streaming ([#219](https://github.com/a2aproject/a2a-python/issues/219)) ([91539d6](https://github.com/a2aproject/a2a-python/commit/91539d69e5c757712c73a41ab95f1ec6656ef5cd)), closes [#218](https://github.com/a2aproject/a2a-python/issues/218)
26+
27+
## [0.2.9](https://github.com/a2aproject/a2a-python/compare/v0.2.8...v0.2.9) (2025-06-24)
28+
29+
### Bug Fixes
30+
31+
* Set `protobuf==5.29.5` and `fastapi>=0.115.2` to prevent version conflicts ([#224](https://github.com/a2aproject/a2a-python/issues/224)) ([1412a85](https://github.com/a2aproject/a2a-python/commit/1412a855b4980d8373ed1cea38c326be74069633))
32+
333
## [0.2.8](https://github.com/a2aproject/a2a-python/compare/v0.2.7...v0.2.8) (2025-06-12)
434

535

src/a2a/grpc/a2a_pb2.py

Lines changed: 78 additions & 78 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/a2a/grpc/a2a_pb2.pyi

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,15 @@ class AgentInterface(_message.Message):
202202
def __init__(self, url: _Optional[str] = ..., transport: _Optional[str] = ...) -> None: ...
203203

204204
class AgentCard(_message.Message):
205-
__slots__ = ("name", "description", "url", "preferred_transport", "additional_interfaces", "provider", "version", "documentation_url", "capabilities", "security_schemes", "security", "default_input_modes", "default_output_modes", "skills", "supports_authenticated_extended_card")
205+
__slots__ = ("protocol_version", "name", "description", "url", "preferred_transport", "additional_interfaces", "provider", "version", "documentation_url", "capabilities", "security_schemes", "security", "default_input_modes", "default_output_modes", "skills", "supports_authenticated_extended_card")
206206
class SecuritySchemesEntry(_message.Message):
207207
__slots__ = ("key", "value")
208208
KEY_FIELD_NUMBER: _ClassVar[int]
209209
VALUE_FIELD_NUMBER: _ClassVar[int]
210210
key: str
211211
value: SecurityScheme
212212
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SecurityScheme, _Mapping]] = ...) -> None: ...
213+
PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int]
213214
NAME_FIELD_NUMBER: _ClassVar[int]
214215
DESCRIPTION_FIELD_NUMBER: _ClassVar[int]
215216
URL_FIELD_NUMBER: _ClassVar[int]
@@ -225,6 +226,7 @@ class AgentCard(_message.Message):
225226
DEFAULT_OUTPUT_MODES_FIELD_NUMBER: _ClassVar[int]
226227
SKILLS_FIELD_NUMBER: _ClassVar[int]
227228
SUPPORTS_AUTHENTICATED_EXTENDED_CARD_FIELD_NUMBER: _ClassVar[int]
229+
protocol_version: str
228230
name: str
229231
description: str
230232
url: str
@@ -240,7 +242,7 @@ class AgentCard(_message.Message):
240242
default_output_modes: _containers.RepeatedScalarFieldContainer[str]
241243
skills: _containers.RepeatedCompositeFieldContainer[AgentSkill]
242244
supports_authenticated_extended_card: bool
243-
def __init__(self, name: _Optional[str] = ..., description: _Optional[str] = ..., url: _Optional[str] = ..., preferred_transport: _Optional[str] = ..., additional_interfaces: _Optional[_Iterable[_Union[AgentInterface, _Mapping]]] = ..., provider: _Optional[_Union[AgentProvider, _Mapping]] = ..., version: _Optional[str] = ..., documentation_url: _Optional[str] = ..., capabilities: _Optional[_Union[AgentCapabilities, _Mapping]] = ..., security_schemes: _Optional[_Mapping[str, SecurityScheme]] = ..., security: _Optional[_Iterable[_Union[Security, _Mapping]]] = ..., default_input_modes: _Optional[_Iterable[str]] = ..., default_output_modes: _Optional[_Iterable[str]] = ..., skills: _Optional[_Iterable[_Union[AgentSkill, _Mapping]]] = ..., supports_authenticated_extended_card: bool = ...) -> None: ...
245+
def __init__(self, protocol_version: _Optional[str] = ..., name: _Optional[str] = ..., description: _Optional[str] = ..., url: _Optional[str] = ..., preferred_transport: _Optional[str] = ..., additional_interfaces: _Optional[_Iterable[_Union[AgentInterface, _Mapping]]] = ..., provider: _Optional[_Union[AgentProvider, _Mapping]] = ..., version: _Optional[str] = ..., documentation_url: _Optional[str] = ..., capabilities: _Optional[_Union[AgentCapabilities, _Mapping]] = ..., security_schemes: _Optional[_Mapping[str, SecurityScheme]] = ..., security: _Optional[_Iterable[_Union[Security, _Mapping]]] = ..., default_input_modes: _Optional[_Iterable[str]] = ..., default_output_modes: _Optional[_Iterable[str]] = ..., skills: _Optional[_Iterable[_Union[AgentSkill, _Mapping]]] = ..., supports_authenticated_extended_card: bool = ...) -> None: ...
244246

245247
class AgentProvider(_message.Message):
246248
__slots__ = ("url", "organization")

src/a2a/grpc/a2a_pb2_grpc.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class A2AServiceStub(object):
1414
- Messages are not a standard resource so there is no get/delete/update/list
1515
interface, only a send and stream custom methods.
1616
- Tasks have a get interface and custom cancel and subscribe methods.
17-
- TaskPushNotificationConfig are a resource whose parent is a task.
17+
- TaskPushNotificationConfig are a resource whose parent is a task.
1818
They have get, list and create methods.
1919
- AgentCard is a static resource with only a get method.
2020
fields are not present as they don't comply with AIP rules, and the
@@ -88,7 +88,7 @@ class A2AServiceServicer(object):
8888
- Messages are not a standard resource so there is no get/delete/update/list
8989
interface, only a send and stream custom methods.
9090
- Tasks have a get interface and custom cancel and subscribe methods.
91-
- TaskPushNotificationConfig are a resource whose parent is a task.
91+
- TaskPushNotificationConfig are a resource whose parent is a task.
9292
They have get, list and create methods.
9393
- AgentCard is a static resource with only a get method.
9494
fields are not present as they don't comply with AIP rules, and the
@@ -241,7 +241,7 @@ class A2AService(object):
241241
- Messages are not a standard resource so there is no get/delete/update/list
242242
interface, only a send and stream custom methods.
243243
- Tasks have a get interface and custom cancel and subscribe methods.
244-
- TaskPushNotificationConfig are a resource whose parent is a task.
244+
- TaskPushNotificationConfig are a resource whose parent is a task.
245245
They have get, list and create methods.
246246
- AgentCard is a static resource with only a get method.
247247
fields are not present as they don't comply with AIP rules, and the

src/a2a/server/request_handlers/default_request_handler.py

Lines changed: 79 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
TaskState.rejected,
5656
}
5757

58+
5859
@trace_class(kind=SpanKind.SERVER)
5960
class DefaultRequestHandler(RequestHandler):
6061
"""Default request handler for all incoming requests.
@@ -168,23 +169,25 @@ async def _run_event_stream(
168169
await self.agent_executor.execute(request, queue)
169170
await queue.close()
170171

171-
async def on_message_send(
172+
async def _setup_message_execution(
172173
self,
173174
params: MessageSendParams,
174175
context: ServerCallContext | None = None,
175-
) -> Message | Task:
176-
"""Default handler for 'message/send' interface (non-streaming).
176+
) -> tuple[TaskManager, str, EventQueue, ResultAggregator, asyncio.Task]:
177+
"""Common setup logic for both streaming and non-streaming message handling.
177178
178-
Starts the agent execution for the message and waits for the final
179-
result (Task or Message).
179+
Returns:
180+
A tuple of (task_manager, task_id, queue, result_aggregator, producer_task)
180181
"""
182+
# Create task manager and validate existing task
181183
task_manager = TaskManager(
182184
task_id=params.message.taskId,
183185
context_id=params.message.contextId,
184186
task_store=self.task_store,
185187
initial_message=params.message,
186188
)
187189
task: Task | None = await task_manager.get_task()
190+
188191
if task:
189192
if task.status.state in TERMINAL_TASK_STATES:
190193
raise ServerError(
@@ -206,6 +209,8 @@ async def on_message_send(
206209
await self._push_notifier.set_info(
207210
task.id, params.configuration.pushNotificationConfig
208211
)
212+
213+
# Build request context
209214
request_context = await self._request_context_builder.build(
210215
params=params,
211216
task_id=task.id if task else None,
@@ -222,13 +227,49 @@ async def on_message_send(
222227
result_aggregator = ResultAggregator(task_manager)
223228
# TODO: to manage the non-blocking flows.
224229
producer_task = asyncio.create_task(
225-
self._run_event_stream(
226-
request_context,
227-
queue,
228-
)
230+
self._run_event_stream(request_context, queue)
229231
)
230232
await self._register_producer(task_id, producer_task)
231233

234+
return task_manager, task_id, queue, result_aggregator, producer_task
235+
236+
def _validate_task_id_match(self, task_id: str, event_task_id: str) -> None:
237+
"""Validates that agent-generated task ID matches the expected task ID."""
238+
if task_id != event_task_id:
239+
logger.error(
240+
f'Agent generated task_id={event_task_id} does not match the RequestContext task_id={task_id}.'
241+
)
242+
raise ServerError(
243+
InternalError(message='Task ID mismatch in agent response')
244+
)
245+
246+
async def _send_push_notification_if_needed(
247+
self, task_id: str, result_aggregator: ResultAggregator
248+
) -> None:
249+
"""Sends push notification if configured and task is available."""
250+
if self._push_notifier and task_id:
251+
latest_task = await result_aggregator.current_result
252+
if isinstance(latest_task, Task):
253+
await self._push_notifier.send_notification(latest_task)
254+
255+
async def on_message_send(
256+
self,
257+
params: MessageSendParams,
258+
context: ServerCallContext | None = None,
259+
) -> Message | Task:
260+
"""Default handler for 'message/send' interface (non-streaming).
261+
262+
Starts the agent execution for the message and waits for the final
263+
result (Task or Message).
264+
"""
265+
(
266+
task_manager,
267+
task_id,
268+
queue,
269+
result_aggregator,
270+
producer_task,
271+
) = await self._setup_message_execution(params, context)
272+
232273
consumer = EventConsumer(queue)
233274
producer_task.add_done_callback(consumer.agent_task_callback)
234275

@@ -241,13 +282,13 @@ async def on_message_send(
241282
if not result:
242283
raise ServerError(error=InternalError())
243284

244-
if isinstance(result, Task) and task_id != result.id:
245-
logger.error(
246-
f'Agent generated task_id={result.id} does not match the RequestContext task_id={task_id}.'
247-
)
248-
raise ServerError(
249-
InternalError(message='Task ID mismatch in agent response')
250-
)
285+
if isinstance(result, Task):
286+
self._validate_task_id_match(task_id, result.id)
287+
288+
await self._send_push_notification_if_needed(
289+
task_id, result_aggregator
290+
)
291+
251292
except Exception as e:
252293
logger.error(f'Agent execution failed. Error: {e}')
253294
raise
@@ -272,85 +313,34 @@ async def on_message_send_stream(
272313
Starts the agent execution and yields events as they are produced
273314
by the agent.
274315
"""
275-
task_manager = TaskManager(
276-
task_id=params.message.taskId,
277-
context_id=params.message.contextId,
278-
task_store=self.task_store,
279-
initial_message=params.message,
280-
)
281-
task: Task | None = await task_manager.get_task()
282-
283-
if task:
284-
if task.status.state in TERMINAL_TASK_STATES:
285-
raise ServerError(
286-
error=InvalidParamsError(
287-
message=f'Task {task.id} is in terminal state: {task.status.state}'
288-
)
289-
)
290-
291-
task = task_manager.update_with_message(params.message, task)
292-
if self.should_add_push_info(params):
293-
assert isinstance(self._push_notifier, PushNotifier)
294-
assert isinstance(
295-
params.configuration, MessageSendConfiguration
296-
)
297-
assert isinstance(
298-
params.configuration.pushNotificationConfig,
299-
PushNotificationConfig,
300-
)
301-
await self._push_notifier.set_info(
302-
task.id, params.configuration.pushNotificationConfig
303-
)
304-
else:
305-
queue = EventQueue()
306-
result_aggregator = ResultAggregator(task_manager)
307-
request_context = await self._request_context_builder.build(
308-
params=params,
309-
task_id=task.id if task else None,
310-
context_id=params.message.contextId,
311-
task=task,
312-
context=context,
313-
)
314-
315-
task_id = cast('str', request_context.task_id)
316-
queue = await self._queue_manager.create_or_tap(task_id)
317-
producer_task = asyncio.create_task(
318-
self._run_event_stream(
319-
request_context,
320-
queue,
321-
)
322-
)
323-
await self._register_producer(task_id, producer_task)
316+
(
317+
task_manager,
318+
task_id,
319+
queue,
320+
result_aggregator,
321+
producer_task,
322+
) = await self._setup_message_execution(params, context)
324323

325324
try:
326325
consumer = EventConsumer(queue)
327326
producer_task.add_done_callback(consumer.agent_task_callback)
328327
async for event in result_aggregator.consume_and_emit(consumer):
329328
if isinstance(event, Task):
330-
if task_id != event.id:
331-
logger.error(
332-
f'Agent generated task_id={event.id} does not match the RequestContext task_id={task_id}.'
333-
)
334-
raise ServerError(
335-
InternalError(
336-
message='Task ID mismatch in agent response'
337-
)
338-
)
339-
340-
if (
341-
self._push_notifier
342-
and params.configuration
343-
and params.configuration.pushNotificationConfig
344-
):
345-
await self._push_notifier.set_info(
346-
task_id,
347-
params.configuration.pushNotificationConfig,
348-
)
349-
350-
if self._push_notifier and task_id:
351-
latest_task = await result_aggregator.current_result
352-
if isinstance(latest_task, Task):
353-
await self._push_notifier.send_notification(latest_task)
329+
self._validate_task_id_match(task_id, event.id)
330+
331+
if (
332+
self._push_notifier
333+
and params.configuration
334+
and params.configuration.pushNotificationConfig
335+
):
336+
await self._push_notifier.set_info(
337+
task_id,
338+
params.configuration.pushNotificationConfig,
339+
)
340+
341+
await self._send_push_notification_if_needed(
342+
task_id, result_aggregator
343+
)
354344
yield event
355345
finally:
356346
await self._cleanup_producer(producer_task, task_id)

0 commit comments

Comments
 (0)