Skip to content

Commit b36f175

Browse files
authored
Merge branch 'main' into refactor-apps2
2 parents 8e1df04 + fda4223 commit b36f175

File tree

4 files changed

+83
-20
lines changed

4 files changed

+83
-20
lines changed

CHANGELOG.md

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

3+
## [0.2.11](https://github.com/a2aproject/a2a-python/compare/v0.2.10...v0.2.11) (2025-07-07)
4+
5+
6+
### ⚠ BREAKING CHANGES
7+
8+
* Removes `push_notifier` interface from the SDK and introduces `push_notification_config_store` and `push_notification_sender` for supporting push notifications.
9+
10+
### Features
11+
12+
* Add constants for Well-Known URIs ([#271](https://github.com/a2aproject/a2a-python/issues/271)) ([1c8e12e](https://github.com/a2aproject/a2a-python/commit/1c8e12e448dc7469e508fccdac06818836f5b520))
13+
* Adds support for List and Delete push notification configurations. ([f1b576e](https://github.com/a2aproject/a2a-python/commit/f1b576e061e7a3ab891d8368ade56c7046684c5e))
14+
* Adds support for more than one `push_notification_config` per task. ([f1b576e](https://github.com/a2aproject/a2a-python/commit/f1b576e061e7a3ab891d8368ade56c7046684c5e))
15+
* **server:** Add lock to TaskUpdater to prevent race conditions ([#279](https://github.com/a2aproject/a2a-python/issues/279)) ([1022093](https://github.com/a2aproject/a2a-python/commit/1022093110100da27f040be4b35831bf8b1fe094))
16+
* Support for database backend Task Store ([#259](https://github.com/a2aproject/a2a-python/issues/259)) ([7c46e70](https://github.com/a2aproject/a2a-python/commit/7c46e70b3142f3ec274c492bacbfd6e8f0204b36))
17+
18+
19+
### Code Refactoring
20+
21+
* Removes `push_notifier` interface from the SDK and introduces `push_notification_config_store` and `push_notification_sender` for supporting push notifications. ([f1b576e](https://github.com/a2aproject/a2a-python/commit/f1b576e061e7a3ab891d8368ade56c7046684c5e))
22+
323
## [0.2.10](https://github.com/a2aproject/a2a-python/compare/v0.2.9...v0.2.10) (2025-06-30)
424

525

src/a2a/server/apps/jsonrpc/fastapi_app.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ def add_routes_to_app(
4040
rpc_url: The URL for the A2A JSON-RPC endpoint.
4141
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
4242
"""
43-
route_definitions = self._get_route_definitions(
44-
agent_card_url, rpc_url, extended_agent_card_url
45-
)
43+
app.post(rpc_url)(self._handle_requests)
44+
app.get(agent_card_url)(self._handle_get_agent_card)
4645

47-
for route_def in route_definitions:
48-
app.add_api_route(**route_def)
46+
if self.agent_card.supportsAuthenticatedExtendedCard:
47+
app.get(extended_agent_card_url)(
48+
self._handle_get_authenticated_extended_agent_card
49+
)
4950

5051
def build(
5152
self,

src/a2a/server/tasks/task_updater.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import uuid
23

34
from datetime import datetime, timezone
@@ -33,6 +34,14 @@ def __init__(self, event_queue: EventQueue, task_id: str, context_id: str):
3334
self.event_queue = event_queue
3435
self.task_id = task_id
3536
self.context_id = context_id
37+
self._lock = asyncio.Lock()
38+
self._terminal_state_reached = False
39+
self._terminal_states = {
40+
TaskState.completed,
41+
TaskState.canceled,
42+
TaskState.failed,
43+
TaskState.rejected,
44+
}
3645

3746
async def update_status(
3847
self,
@@ -49,21 +58,26 @@ async def update_status(
4958
final: If True, indicates this is the final status update for the task.
5059
timestamp: Optional ISO 8601 datetime string. Defaults to current time.
5160
"""
52-
current_timestamp = (
53-
timestamp if timestamp else datetime.now(timezone.utc).isoformat()
54-
)
55-
await self.event_queue.enqueue_event(
56-
TaskStatusUpdateEvent(
57-
taskId=self.task_id,
58-
contextId=self.context_id,
59-
final=final,
60-
status=TaskStatus(
61-
state=state,
62-
message=message,
63-
timestamp=current_timestamp,
64-
),
61+
async with self._lock:
62+
if self._terminal_state_reached:
63+
raise RuntimeError(f"Task {self.task_id} is already in a terminal state.")
64+
if state in self._terminal_states:
65+
self._terminal_state_reached = True
66+
final = True
67+
68+
current_timestamp = timestamp if timestamp else datetime.now(timezone.utc).isoformat()
69+
await self.event_queue.enqueue_event(
70+
TaskStatusUpdateEvent(
71+
taskId=self.task_id,
72+
contextId=self.context_id,
73+
final=final,
74+
status=TaskStatus(
75+
state=state,
76+
message=message,
77+
timestamp=current_timestamp,
78+
),
79+
)
6580
)
66-
)
6781

6882
async def add_artifact( # noqa: PLR0913
6983
self,

tests/server/tasks/test_task_updater.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import asyncio
12
import uuid
2-
33
from unittest.mock import AsyncMock, patch
44

55
import pytest
@@ -505,3 +505,31 @@ async def test_cancel_with_message(task_updater, event_queue, sample_message):
505505
assert event.status.state == TaskState.canceled
506506
assert event.final is True
507507
assert event.status.message == sample_message
508+
509+
510+
@pytest.mark.asyncio
511+
async def test_update_status_raises_error_if_terminal_state_reached(task_updater, event_queue):
512+
await task_updater.complete()
513+
event_queue.reset_mock()
514+
with pytest.raises(RuntimeError):
515+
await task_updater.start_work()
516+
event_queue.enqueue_event.assert_not_called()
517+
518+
519+
@pytest.mark.asyncio
520+
async def test_concurrent_updates_race_condition(event_queue):
521+
task_updater = TaskUpdater(
522+
event_queue=event_queue,
523+
task_id="test-task-id",
524+
context_id="test-context-id",
525+
)
526+
tasks = [
527+
task_updater.complete(),
528+
task_updater.failed(),
529+
]
530+
results = await asyncio.gather(*tasks, return_exceptions=True)
531+
successes = [r for r in results if not isinstance(r, Exception)]
532+
failures = [r for r in results if isinstance(r, RuntimeError)]
533+
assert len(successes) == 1
534+
assert len(failures) == 1
535+
assert event_queue.enqueue_event.call_count == 1

0 commit comments

Comments
 (0)