Skip to content

Commit e8bffc6

Browse files
authored
Merge branch '1.0-dev' into owner-optional
2 parents 07655fd + 1c72bcd commit e8bffc6

15 files changed

Lines changed: 444 additions & 362 deletions

.github/workflows/python-publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
run: uv build
2727

2828
- name: Upload distributions
29-
uses: actions/upload-artifact@v6
29+
uses: actions/upload-artifact@v7
3030
with:
3131
name: release-dists
3232
path: dist/
@@ -40,7 +40,7 @@ jobs:
4040

4141
steps:
4242
- name: Retrieve release distributions
43-
uses: actions/download-artifact@v7
43+
uses: actions/download-artifact@v8
4444
with:
4545
name: release-dists
4646
path: dist/

.github/workflows/release-please.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ on:
22
push:
33
branches:
44
- main
5+
- 1.0-dev
56

67
permissions:
78
contents: write
@@ -16,4 +17,6 @@ jobs:
1617
- uses: googleapis/release-please-action@v4
1718
with:
1819
token: ${{ secrets.A2A_BOT_PAT }}
19-
release-type: python
20+
target-branch: ${{ github.ref_name }}
21+
config-file: release-please-config.json
22+
manifest-file: .release-please-manifest.json

.release-please-manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

CHANGELOG.md

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

3+
## [0.3.25](https://github.com/a2aproject/a2a-python/compare/v0.3.24...v0.3.25) (2026-03-10)
4+
5+
6+
### Features
7+
8+
* Implement a vertex based task store ([#752](https://github.com/a2aproject/a2a-python/issues/752)) ([fa14dbf](https://github.com/a2aproject/a2a-python/commit/fa14dbf46b603f288a1f1c474401483bf53950e4))
9+
10+
11+
### Bug Fixes
12+
13+
* return background task from consume_and_break_on_interrupt to prevent GC ([#775](https://github.com/a2aproject/a2a-python/issues/775)) ([a236d4d](https://github.com/a2aproject/a2a-python/commit/a236d4df8dceb2db1e1170e0b57599f3837ebd71))
14+
* use default_factory for mutable field defaults in ServerCallContext ([#744](https://github.com/a2aproject/a2a-python/issues/744)) ([22b25d6](https://github.com/a2aproject/a2a-python/commit/22b25d653e57e2d1453bbc282052e51dbd904ac6))
15+
316
## [0.3.24](https://github.com/a2aproject/a2a-python/compare/v0.3.23...v0.3.24) (2026-02-20)
417

518

release-please-config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"release-type": "python",
3+
"prerelease": true,
4+
"last-release-sha": "697ab8ed094ee053bcaf0c23609f3534b561da05",
5+
"prerelease-type": "alpha",
6+
"packages": {
7+
".": {}
8+
}
9+
}

src/a2a/contrib/__init__.py

Whitespace-only changes.

src/a2a/server/context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ class ServerCallContext(BaseModel):
1919

2020
model_config = ConfigDict(arbitrary_types_allowed=True)
2121

22-
state: State = Field(default={})
23-
user: User = Field(default=UnauthenticatedUser())
22+
state: State = Field(default_factory=dict)
23+
user: User = Field(default_factory=UnauthenticatedUser)
2424
tenant: str = Field(default='')
2525
requested_extensions: set[str] = Field(default_factory=set)
2626
activated_extensions: set[str] = Field(default_factory=set)

src/a2a/server/request_handlers/default_request_handler.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,17 @@ async def push_notification_callback(event: Event) -> None:
351351
(
352352
result,
353353
interrupted_or_non_blocking,
354+
bg_consume_task,
354355
) = await result_aggregator.consume_and_break_on_interrupt(
355356
consumer,
356357
blocking=blocking,
357358
event_callback=push_notification_callback,
358359
)
359360

361+
if bg_consume_task is not None:
362+
bg_consume_task.set_name(f'continue_consuming:{task_id}')
363+
self._track_background_task(bg_consume_task)
364+
360365
except Exception:
361366
logger.exception('Agent execution failed')
362367
producer_task.cancel()

src/a2a/server/tasks/result_aggregator.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ async def consume_and_break_on_interrupt(
9999
consumer: EventConsumer,
100100
blocking: bool = True,
101101
event_callback: Callable[[Event], Awaitable[None]] | None = None,
102-
) -> tuple[Task | Message | None, bool]:
102+
) -> tuple[Task | Message | None, bool, asyncio.Task | None]:
103103
"""Processes the event stream until completion or an interruptible state is encountered.
104104
105105
If `blocking` is False, it returns after the first event that creates a Task or Message.
@@ -119,16 +119,23 @@ async def consume_and_break_on_interrupt(
119119
A tuple containing:
120120
- The current aggregated result (`Task` or `Message`) at the point of completion or interruption.
121121
- A boolean indicating whether the consumption was interrupted (`True`) or completed naturally (`False`).
122+
- The background ``asyncio.Task`` that continues consuming events
123+
after an interruption, or ``None`` when no background work was
124+
spawned. **Callers must hold a strong reference** to this task
125+
(e.g. in a ``set``) to prevent the garbage collector from
126+
collecting it before it finishes — the event loop only keeps
127+
weak references to tasks.
122128
123129
Raises:
124130
BaseException: If the `EventConsumer` raises an exception during consumption.
125131
"""
126132
event_stream = consumer.consume_all()
127133
interrupted = False
134+
bg_task: asyncio.Task | None = None
128135
async for event in event_stream:
129136
if isinstance(event, Message):
130137
self._message = event
131-
return event, False
138+
return event, False, None
132139
await self.task_manager.process(event)
133140

134141
if event_callback:
@@ -161,13 +168,13 @@ async def consume_and_break_on_interrupt(
161168

162169
if should_interrupt:
163170
# Continue consuming the rest of the events in the background.
164-
# TODO: We should track all outstanding tasks to ensure they eventually complete.
165-
asyncio.create_task( # noqa: RUF006
171+
# The caller is responsible for tracking this task to prevent GC.
172+
bg_task = asyncio.create_task(
166173
self._continue_consuming(event_stream, event_callback)
167174
)
168175
interrupted = True
169176
break
170-
return await self.task_manager.get_task(), interrupted
177+
return await self.task_manager.get_task(), interrupted, bg_task
171178

172179
async def _continue_consuming(
173180
self,

tck/sut_agent.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
DefaultRequestHandler,
1616
)
1717
from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore
18+
from a2a.server.tasks.task_store import TaskStore
1819
from a2a.types import (
1920
AgentCapabilities,
2021
AgentCard,
@@ -128,8 +129,8 @@ async def execute(
128129
await event_queue.enqueue_event(final_update)
129130

130131

131-
def main() -> None:
132-
"""Main entrypoint."""
132+
def serve(task_store: TaskStore) -> None:
133+
"""Sets up the A2A service and starts the HTTP server."""
133134
http_port = int(os.environ.get('HTTP_PORT', '41241'))
134135

135136
agent_card = AgentCard(
@@ -168,7 +169,7 @@ def main() -> None:
168169

169170
request_handler = DefaultRequestHandler(
170171
agent_executor=SUTAgentExecutor(),
171-
task_store=InMemoryTaskStore(),
172+
task_store=task_store,
172173
)
173174

174175
server = A2AStarletteApplication(
@@ -182,5 +183,10 @@ def main() -> None:
182183
uvicorn.run(app, host='127.0.0.1', port=http_port, log_level='info')
183184

184185

186+
def main() -> None:
187+
"""Main entrypoint."""
188+
serve(InMemoryTaskStore())
189+
190+
185191
if __name__ == '__main__':
186192
main()

0 commit comments

Comments
 (0)