Skip to content

Commit 88a9f38

Browse files
committed
Merge branch 'main' into sql-support
2 parents b00905a + 5e7d418 commit 88a9f38

File tree

15 files changed

+573
-290
lines changed

15 files changed

+573
-290
lines changed

.github/workflows/update-a2a-types.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ jobs:
6868
token: ${{ secrets.A2A_BOT_PAT }}
6969
committer: "a2a-bot <[email protected]>"
7070
author: "a2a-bot <[email protected]>"
71-
commit-message: "${{github.event.client_payload.message}} 🤖"
72-
title: "${{github.event.client_payload.message}} 🤖"
71+
commit-message: "chore: Update A2A types from specification 🤖"
72+
title: "chore: Update A2A types from specification 🤖"
7373
body: |
7474
This PR updates `src/a2a/types.py` based on the latest `specification/json/a2a.json` from [google-a2a/A2A](https://github.com/google-a2a/A2A/commit/${{ github.event.client_payload.sha }}).
7575
branch: "auto-update-a2a-types-${{ github.event.client_payload.sha }}"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<h2 align="center">
1111
<img src="https://raw.githubusercontent.com/google-a2a/A2A/refs/heads/main/docs/assets/a2a-logo-black.svg" width="256" alt="A2A Logo"/>
1212
</h2>
13-
<h3 align="center">A Python library that helps run agentic applications as A2AServers following the <a href="https://google.github.io/A2A">Agent2Agent (A2A) Protocol</a>.</h3>
13+
<h3 align="center">A Python library that helps run agentic applications as A2AServers following the <a href="https://google-a2a.github.io/A2A">Agent2Agent (A2A) Protocol</a>.</h3>
1414
</html>
1515

1616
<!-- markdownlint-enable no-inline-html -->

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ sql = [
5151
]
5252

5353
[project.urls]
54-
homepage = "https://google.github.io/A2A/"
54+
homepage = "https://google-a2a.github.io/A2A/"
5555
repository = "https://github.com/google-a2a/a2a-python"
5656
changelog = "https://github.com/google-a2a/a2a-python/blob/main/CHANGELOG.md"
57-
documentation = "https://google.github.io/A2A/"
57+
documentation = "https://google-a2a.github.io/A2A/sdk/python/"
5858

5959
[tool.hatch.build.targets.wheel]
6060
packages = ["src/a2a"]

src/a2a/client/client.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
import json
22
import logging
3+
34
from collections.abc import AsyncGenerator
45
from typing import Any
56
from uuid import uuid4
67

78
import httpx
9+
810
from httpx_sse import SSEError, aconnect_sse
911
from pydantic import ValidationError
1012

1113
from a2a.client.errors import A2AClientHTTPError, A2AClientJSONError
12-
from a2a.types import (AgentCard, CancelTaskRequest, CancelTaskResponse,
13-
GetTaskPushNotificationConfigRequest,
14-
GetTaskPushNotificationConfigResponse, GetTaskRequest,
15-
GetTaskResponse, SendMessageRequest,
16-
SendMessageResponse, SendStreamingMessageRequest,
17-
SendStreamingMessageResponse,
18-
SetTaskPushNotificationConfigRequest,
19-
SetTaskPushNotificationConfigResponse)
14+
from a2a.types import (
15+
AgentCard,
16+
CancelTaskRequest,
17+
CancelTaskResponse,
18+
GetTaskPushNotificationConfigRequest,
19+
GetTaskPushNotificationConfigResponse,
20+
GetTaskRequest,
21+
GetTaskResponse,
22+
SendMessageRequest,
23+
SendMessageResponse,
24+
SendStreamingMessageRequest,
25+
SendStreamingMessageResponse,
26+
SetTaskPushNotificationConfigRequest,
27+
SetTaskPushNotificationConfigResponse,
28+
)
2029
from a2a.utils.telemetry import SpanKind, trace_class
2130

31+
2232
logger = logging.getLogger(__name__)
2333

34+
2435
class A2ACardResolver:
2536
"""Agent Card resolver."""
2637

@@ -160,6 +171,7 @@ async def get_client_from_agent_card_url(
160171
agent_card_path: The path to the agent card endpoint, relative to the base URL.
161172
http_kwargs: Optional dictionary of keyword arguments to pass to the
162173
underlying httpx.get request when fetching the agent card.
174+
163175
Returns:
164176
An initialized `A2AClient` instance.
165177
@@ -169,7 +181,9 @@ async def get_client_from_agent_card_url(
169181
"""
170182
agent_card: AgentCard = await A2ACardResolver(
171183
httpx_client, base_url=base_url, agent_card_path=agent_card_path
172-
).get_agent_card(http_kwargs=http_kwargs) # Fetches public card by default
184+
).get_agent_card(
185+
http_kwargs=http_kwargs
186+
) # Fetches public card by default
173187
return A2AClient(httpx_client=httpx_client, agent_card=agent_card)
174188

175189
async def send_message(

src/a2a/server/apps/starlette_app.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,20 @@
4545

4646
logger = logging.getLogger(__name__)
4747

48-
# Register Starlette User as an implementation of a2a.auth.user.User
49-
A2AUser.register(BaseUser)
48+
49+
class StarletteUserProxy(A2AUser):
50+
"""Adapts the Starlette User class to the A2A user representation."""
51+
52+
def __init__(self, user: BaseUser):
53+
self._user = user
54+
55+
@property
56+
def is_authenticated(self):
57+
return self._user.is_authenticated
58+
59+
@property
60+
def user_name(self):
61+
return self._user.display_name
5062

5163

5264
class CallContextBuilder(ABC):
@@ -64,7 +76,7 @@ def build(self, request: Request) -> ServerCallContext:
6476
user = UnauthenticatedUser()
6577
state = {}
6678
with contextlib.suppress(Exception):
67-
user = request.user
79+
user = StarletteUserProxy(request.user)
6880
state['auth'] = request.auth
6981
return ServerCallContext(user=user, state=state)
7082

@@ -139,7 +151,7 @@ def _generate_error_response(
139151
log_level,
140152
f'Request Error (ID: {request_id}): '
141153
f"Code={error_resp.error.code}, Message='{error_resp.error.message}'"
142-
f'{", Data=" + str(error_resp.error.data) if hasattr(error, "data") and error_resp.error.data else ""}',
154+
f'{", Data=" + str(error_resp.error.data) if error_resp.error.data else ""}',
143155
)
144156
return JSONResponse(
145157
error_resp.model_dump(mode='json', exclude_none=True),

src/a2a/server/events/event_queue.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@
1414
logger = logging.getLogger(__name__)
1515

1616

17-
Event = (
18-
Message
19-
| Task
20-
| TaskStatusUpdateEvent
21-
| TaskArtifactUpdateEvent
22-
)
17+
Event = Message | Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent
2318
"""Type alias for events that can be enqueued."""
2419

20+
DEFAULT_MAX_QUEUE_SIZE = 1024
21+
2522

2623
@trace_class(kind=SpanKind.SERVER)
2724
class EventQueue:
@@ -32,27 +29,37 @@ class EventQueue:
3229
to create child queues that receive the same events.
3330
"""
3431

35-
def __init__(self) -> None:
32+
def __init__(self, max_queue_size: int = DEFAULT_MAX_QUEUE_SIZE) -> None:
3633
"""Initializes the EventQueue."""
37-
self.queue: asyncio.Queue[Event] = asyncio.Queue()
34+
# Make sure the `asyncio.Queue` is bounded.
35+
# If it's unbounded (maxsize=0), then `queue.put()` never needs to wait,
36+
# and so the streaming won't work correctly.
37+
if max_queue_size <= 0:
38+
raise ValueError('max_queue_size must be greater than 0')
39+
40+
self.queue: asyncio.Queue[Event] = asyncio.Queue(maxsize=max_queue_size)
3841
self._children: list[EventQueue] = []
3942
self._is_closed = False
4043
self._lock = asyncio.Lock()
4144
logger.debug('EventQueue initialized.')
4245

43-
def enqueue_event(self, event: Event):
46+
async def enqueue_event(self, event: Event):
4447
"""Enqueues an event to this queue and all its children.
4548
4649
Args:
4750
event: The event object to enqueue.
4851
"""
49-
if self._is_closed:
50-
logger.warning('Queue is closed. Event will not be enqueued.')
51-
return
52+
async with self._lock:
53+
if self._is_closed:
54+
logger.warning('Queue is closed. Event will not be enqueued.')
55+
return
56+
5257
logger.debug(f'Enqueuing event of type: {type(event)}')
53-
self.queue.put_nowait(event)
58+
59+
# Make sure to use put instead of put_nowait to avoid blocking the event loop.
60+
await self.queue.put(event)
5461
for child in self._children:
55-
child.enqueue_event(event)
62+
await child.enqueue_event(event)
5663

5764
async def dequeue_event(self, no_wait: bool = False) -> Event:
5865
"""Dequeues an event from the queue.

src/a2a/server/tasks/task_updater.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import uuid
22

3+
from datetime import datetime, timezone
34
from typing import Any
45

56
from a2a.server.events import EventQueue
@@ -33,29 +34,38 @@ def __init__(self, event_queue: EventQueue, task_id: str, context_id: str):
3334
self.task_id = task_id
3435
self.context_id = context_id
3536

36-
def update_status(
37-
self, state: TaskState, message: Message | None = None, final=False
37+
async def update_status(
38+
self,
39+
state: TaskState,
40+
message: Message | None = None,
41+
final=False,
42+
timestamp: str | None = None,
3843
):
3944
"""Updates the status of the task and publishes a `TaskStatusUpdateEvent`.
4045
4146
Args:
4247
state: The new state of the task.
4348
message: An optional message associated with the status update.
4449
final: If True, indicates this is the final status update for the task.
50+
timestamp: Optional ISO 8601 datetime string. Defaults to current time.
4551
"""
46-
self.event_queue.enqueue_event(
52+
current_timestamp = (
53+
timestamp if timestamp else datetime.now(timezone.utc).isoformat()
54+
)
55+
await self.event_queue.enqueue_event(
4756
TaskStatusUpdateEvent(
4857
taskId=self.task_id,
4958
contextId=self.context_id,
5059
final=final,
5160
status=TaskStatus(
5261
state=state,
5362
message=message,
63+
timestamp=current_timestamp,
5464
),
5565
)
5666
)
5767

58-
def add_artifact(
68+
async def add_artifact(
5969
self,
6070
parts: list[Part],
6171
artifact_id: str = str(uuid.uuid4()),
@@ -72,7 +82,7 @@ def add_artifact(
7282
append: Optional boolean indicating if this chunk appends to a previous one.
7383
last_chunk: Optional boolean indicating if this is the last chunk.
7484
"""
75-
self.event_queue.enqueue_event(
85+
await self.event_queue.enqueue_event(
7686
TaskArtifactUpdateEvent(
7787
taskId=self.task_id,
7888
contextId=self.context_id,
@@ -85,32 +95,34 @@ def add_artifact(
8595
)
8696
)
8797

88-
def complete(self, message: Message | None = None):
98+
async def complete(self, message: Message | None = None):
8999
"""Marks the task as completed and publishes a final status update."""
90-
self.update_status(
100+
await self.update_status(
91101
TaskState.completed,
92102
message=message,
93103
final=True,
94104
)
95105

96-
def failed(self, message: Message | None = None):
106+
async def failed(self, message: Message | None = None):
97107
"""Marks the task as failed and publishes a final status update."""
98-
self.update_status(TaskState.failed, message=message, final=True)
99-
100-
def reject(self, message: Message | None = None):
108+
await self.update_status(TaskState.failed, message=message, final=True)
109+
110+
async def reject(self, message: Message | None = None):
101111
"""Marks the task as rejected and publishes a final status update."""
102-
self.update_status(TaskState.rejected, message=message, final=True)
112+
await self.update_status(
113+
TaskState.rejected, message=message, final=True
114+
)
103115

104-
def submit(self, message: Message | None = None):
116+
async def submit(self, message: Message | None = None):
105117
"""Marks the task as submitted and publishes a status update."""
106-
self.update_status(
118+
await self.update_status(
107119
TaskState.submitted,
108120
message=message,
109121
)
110122

111-
def start_work(self, message: Message | None = None):
123+
async def start_work(self, message: Message | None = None):
112124
"""Marks the task as working and publishes a status update."""
113-
self.update_status(
125+
await self.update_status(
114126
TaskState.working,
115127
message=message,
116128
)

src/a2a/types.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,15 @@ class AgentSkill(BaseModel):
100100
"""
101101
The set of interaction modes that the skill supports
102102
(if different than the default).
103-
Supported mime types for input.
103+
Supported media types for input.
104104
"""
105105
name: str
106106
"""
107107
Human readable name of the skill.
108108
"""
109109
outputModes: list[str] | None = None
110110
"""
111-
Supported mime types for output.
111+
Supported media types for output.
112112
"""
113113
tags: list[str]
114114
"""
@@ -583,6 +583,10 @@ class PushNotificationConfig(BaseModel):
583583
"""
584584

585585
authentication: PushNotificationAuthenticationInfo | None = None
586+
id: str | None = None
587+
"""
588+
Push Notification ID - created by server to support multiple callbacks
589+
"""
586590
token: str | None = None
587591
"""
588592
Token unique to this task/session.
@@ -1368,11 +1372,11 @@ class AgentCard(BaseModel):
13681372
defaultInputModes: list[str]
13691373
"""
13701374
The set of interaction modes that the agent supports across all skills. This can be overridden per-skill.
1371-
Supported mime types for input.
1375+
Supported media types for input.
13721376
"""
13731377
defaultOutputModes: list[str]
13741378
"""
1375-
Supported mime types for output.
1379+
Supported media types for output.
13761380
"""
13771381
description: str
13781382
"""

src/a2a/utils/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def append_artifact_to_task(task: Task, event: TaskArtifactUpdateEvent) -> None:
6969

7070
# Find existing artifact by its id
7171
for i, art in enumerate(task.artifacts):
72-
if hasattr(art, 'artifactId') and art.artifactId == artifact_id:
72+
if art.artifactId == artifact_id:
7373
existing_artifact = art
7474
existing_artifact_list_index = i
7575
break

0 commit comments

Comments
 (0)