Skip to content

Commit db57a8f

Browse files
authored
Merge branch 'main' into feat-tp
2 parents 582bcd1 + 9eda056 commit db57a8f

File tree

12 files changed

+109
-30
lines changed

12 files changed

+109
-30
lines changed

.github/actions/spelling/allow.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ lifecycles
3232
linting
3333
oauthoidc
3434
opensource
35+
pyversions
3536
socio
3637
sse
3738
tagwords

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818

1919
strategy:
2020
matrix:
21-
python-version: ["3.13"]
21+
python-version: ["3.10", "3.13"]
2222

2323
steps:
2424
- name: Checkout code

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

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
name: Update A2A Schema from Specification
22

33
on:
4-
schedule:
5-
- cron: "0 0 * * *" # Runs daily at 00:00 UTC
4+
repository_dispatch:
5+
types: [a2a_json_update]
66
workflow_dispatch:
77

88
jobs:
9-
check_and_update:
9+
generate_and_pr:
1010
runs-on: ubuntu-latest
1111
permissions:
1212
contents: write
@@ -19,7 +19,7 @@ jobs:
1919
- name: Set up Python
2020
uses: actions/setup-python@v5
2121
with:
22-
python-version: "3.13"
22+
python-version: "3.10"
2323

2424
- name: Install uv
2525
run: curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -61,20 +61,17 @@ jobs:
6161
--use-standard-collections
6262
echo "Codegen finished."
6363
64-
- name: Create Pull Request if generated file changed
64+
- name: Create Pull Request with Updates
6565
uses: peter-evans/create-pull-request@v6
6666
with:
6767
token: ${{ secrets.A2A_BOT_PAT }}
68-
committer: a2a-bot <[email protected]>
69-
author: a2a-bot <[email protected]>
70-
commit-message: "chore: 🤖 Auto-update A2A schema from specification"
71-
title: "chore: 🤖 Auto-update A2A Schema from Specification"
68+
committer: "a2a-bot <[email protected]>"
69+
author: "a2a-bot <[email protected]>"
70+
commit-message: "chore: 🤖Auto-update A2A types from google/A2A@${{ github.event.client_payload.sha }}"
71+
title: "chore: 🤖 Auto-update A2A types from google/A2A"
7272
body: |
73-
This PR automatically updates the A2A schema types based on the latest specification.
74-
75-
This update was triggered by the scheduled workflow run.
76-
See the workflow run details: [${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
77-
branch: "a2a-schema-update"
73+
This PR updates `src/a2a/types.py` based on the latest `specification/json/a2a.json` from [google/A2A](https://github.com/google/A2A/commit/${{ github.event.client_payload.sha }}).
74+
branch: "auto-update-a2a-types-${{ github.event.client_payload.sha }}"
7875
base: main
7976
labels: |
8077
automated

.ruff.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
line-length = 80 # Google Style Guide §3.2: 80 columns
1010
indent-width = 4 # Google Style Guide §3.4: 4 spaces
1111

12-
target-version = "py313" # Minimum Python version
12+
target-version = "py310" # Minimum Python version
1313

1414
[lint]
1515
ignore = [

CHANGELOG.md

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

3+
## [0.2.4](https://github.com/google/a2a-python/compare/v0.2.3...v0.2.4) (2025-05-22)
4+
5+
6+
### Features
7+
8+
* Update to support python 3.10 ([#85](https://github.com/google/a2a-python/issues/85)) ([fd9c3b5](https://github.com/google/a2a-python/commit/fd9c3b5b0bbef509789a701171d95f690c84750b))
9+
10+
11+
### Bug Fixes
12+
13+
* Throw exception for task_id mismatches ([#70](https://github.com/google/a2a-python/issues/70)) ([a9781b5](https://github.com/google/a2a-python/commit/a9781b589075280bfaaab5742d8b950916c9de74))
14+
315
## [0.2.3](https://github.com/google/a2a-python/compare/v0.2.2...v0.2.3) (2025-05-20)
416

517

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
# A2A Python SDK
22

3-
A Python library that helps run agentic applications as A2AServers following the [Agent2Agent (A2A) Protocol](https://google.github.io/A2A/).
3+
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
4+
![PyPI - Version](https://img.shields.io/pypi/v/a2a-sdk)
5+
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/a2a-sdk)
6+
7+
<!-- markdownlint-disable no-inline-html -->
8+
9+
<html>
10+
<h2 align="center">
11+
<img src="https://raw.githubusercontent.com/google/A2A/refs/heads/main/docs/assets/a2a-logo-black.svg" width="256" alt="A2A Logo"/>
12+
</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>
14+
</html>
15+
16+
<!-- markdownlint-enable no-inline-html -->
417

518
## Installation
619

720
You can install the A2A SDK using either `uv` or `pip`.
821

922
## Prerequisites
1023

11-
- Python 3.13+
24+
- Python 3.10+
1225
- `uv` (optional, but recommended) or `pip`
1326

1427
### Using `uv`

noxfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import nox
2424

2525

26-
DEFAULT_PYTHON_VERSION = '3.13'
26+
DEFAULT_PYTHON_VERSION = '3.10'
2727

2828
CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
2929

@@ -127,7 +127,7 @@ def format(session):
127127
session.run(
128128
'pyupgrade',
129129
'--exit-zero-even-if-changed',
130-
'--py313-plus',
130+
'--py310-plus',
131131
*lint_paths_py,
132132
)
133133
session.run(

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "A2A Python SDK"
55
readme = "README.md"
66
license = { file = "LICENSE" }
77
authors = [{ name = "Google LLC", email = "[email protected]" }]
8-
requires-python = ">=3.13"
8+
requires-python = ">=3.10"
99
keywords = ["A2A", "A2A SDK", "A2A Protocol", "Agent2Agent"]
1010
dependencies = [
1111
"httpx>=0.28.1",
@@ -22,6 +22,9 @@ classifiers = [
2222
"Intended Audience :: Developers",
2323
"Programming Language :: Python",
2424
"Programming Language :: Python :: 3",
25+
"Programming Language :: Python :: 3.10",
26+
"Programming Language :: Python :: 3.11",
27+
"Programming Language :: Python :: 3.12",
2528
"Programming Language :: Python :: 3.13",
2629
"Operating System :: OS Independent",
2730
"Topic :: Software Development :: Libraries :: Python Modules",

src/a2a/server/events/event_consumer.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import logging
3+
import sys
34

45
from collections.abc import AsyncGenerator
56

@@ -15,6 +16,13 @@
1516
from a2a.utils.telemetry import SpanKind, trace_class
1617

1718

19+
# This is an alias to the exception for closed queue
20+
QueueClosed = asyncio.QueueEmpty
21+
22+
# When using python 3.13 or higher, the closed queue signal is QueueShutdown
23+
if sys.version_info >= (3, 13):
24+
QueueClosed = asyncio.QueueShutDown
25+
1826
logger = logging.getLogger(__name__)
1927

2028

@@ -111,13 +119,16 @@ async def consume_all(self) -> AsyncGenerator[Event]:
111119

112120
if is_final_event:
113121
logger.debug('Stopping event consumption in consume_all.')
114-
self.queue.close()
122+
await self.queue.close()
115123
break
116124
except TimeoutError:
117125
# continue polling until there is a final event
118126
continue
119-
except asyncio.QueueShutDown:
120-
break
127+
except QueueClosed:
128+
# Confirm that the queue is closed, e.g. we aren't on
129+
# python 3.12 and get a queue empty error on an open queue
130+
if self.queue.is_closed():
131+
break
121132

122133
def agent_task_callback(self, agent_task: asyncio.Task[None]):
123134
"""Callback to handle exceptions from the agent's execution task.

src/a2a/server/events/event_queue.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import logging
3+
import sys
34

45
from a2a.types import (
56
A2AError,
@@ -39,6 +40,8 @@ def __init__(self) -> None:
3940
"""Initializes the EventQueue."""
4041
self.queue: asyncio.Queue[Event] = asyncio.Queue()
4142
self._children: list[EventQueue] = []
43+
self._is_closed = False
44+
self._lock = asyncio.Lock()
4245
logger.debug('EventQueue initialized.')
4346

4447
def enqueue_event(self, event: Event):
@@ -47,6 +50,9 @@ def enqueue_event(self, event: Event):
4750
Args:
4851
event: The event object to enqueue.
4952
"""
53+
if self._is_closed:
54+
logger.warning('Queue is closed. Event will not be enqueued.')
55+
return
5056
logger.debug(f'Enqueuing event of type: {type(event)}')
5157
self.queue.put_nowait(event)
5258
for child in self._children:
@@ -55,6 +61,20 @@ def enqueue_event(self, event: Event):
5561
async def dequeue_event(self, no_wait: bool = False) -> Event:
5662
"""Dequeues an event from the queue.
5763
64+
This implementation expects that dequeue to raise an exception when
65+
the queue has been closed. In python 3.13+ this is naturally provided
66+
by the QueueShutDown exception generated when the queue has closed and
67+
the user is awaiting the queue.get method. Python<=3.12 this needs to
68+
manage this lifecycle itself. The current implementation can lead to
69+
blocking if the dequeue_event is called before the EventQueue has been
70+
closed but when there are no events on the queue. Two ways to avoid this
71+
are to call this with no_wait = True which won't block, but is the
72+
callers responsibility to retry as appropriate. Alternatively, one can
73+
use a async Task management solution to cancel the get task if the queue
74+
has closed or some other condition is met. The implementation of the
75+
EventConsumer uses an async.wait with a timeout to abort the
76+
dequeue_event call and retry, when it will return with a closed error.
77+
5878
Args:
5979
no_wait: If True, retrieve an event immediately or raise `asyncio.QueueEmpty`.
6080
If False (default), wait until an event is available.
@@ -66,6 +86,11 @@ async def dequeue_event(self, no_wait: bool = False) -> Event:
6686
asyncio.QueueEmpty: If `no_wait` is True and the queue is empty.
6787
asyncio.QueueShutDown: If the queue has been closed and is empty.
6888
"""
89+
async with self._lock:
90+
if self._is_closed and self.queue.empty():
91+
logger.warning('Queue is closed. Event will not be dequeued.')
92+
raise asyncio.QueueEmpty('Queue is closed.')
93+
6994
if no_wait:
7095
logger.debug('Attempting to dequeue event (no_wait=True).')
7196
event = self.queue.get_nowait()
@@ -99,13 +124,30 @@ def tap(self) -> 'EventQueue':
99124
self._children.append(queue)
100125
return queue
101126

102-
def close(self):
127+
async def close(self):
103128
"""Closes the queue for future push events.
104129
105130
Once closed, `dequeue_event` will eventually raise `asyncio.QueueShutDown`
106131
when the queue is empty. Also closes all child queues.
107132
"""
108133
logger.debug('Closing EventQueue.')
109-
self.queue.shutdown()
110-
for child in self._children:
111-
child.close()
134+
async with self._lock:
135+
# If already closed, just return.
136+
if self._is_closed:
137+
return
138+
self._is_closed = True
139+
# If using python 3.13 or higher, use the shutdown method
140+
if sys.version_info >= (3, 13):
141+
self.queue.shutdown()
142+
for child in self._children:
143+
child.close()
144+
# Otherwise, join the queue
145+
else:
146+
tasks = [asyncio.create_task(self.queue.join())]
147+
for child in self._children:
148+
tasks.append(asyncio.create_task(child.close()))
149+
await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
150+
151+
def is_closed(self) -> bool:
152+
"""Checks if the queue is closed."""
153+
return self._is_closed

0 commit comments

Comments
 (0)