Skip to content

Commit 0f4c807

Browse files
seanzhougooglecopybara-github
authored andcommitted
feat(a2a): add lifespan parameter to to_a2a()
Support Starlette lifespan protocol in to_a2a() so users can run async startup/shutdown logic (e.g. initializing DB connections, loading prompt registries) without resorting to module-level globals. The lifespan parameter accepts a standard Starlette async context manager. Internally, a composed lifespan runs the A2A route setup first, then delegates to the user's lifespan if provided. This also replaces the deprecated add_event_handler("startup", ...) pattern with the modern Starlette(lifespan=...) constructor. Closes #4701 Co-authored-by: Xiang (Sean) Zhou <seanzhougoogle@google.com> PiperOrigin-RevId: 886892363
1 parent 1f9f0fe commit 0f4c807

2 files changed

Lines changed: 212 additions & 73 deletions

File tree

src/google/adk/a2a/utils/agent_to_a2a.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515
from __future__ import annotations
1616

17+
from contextlib import asynccontextmanager
1718
import logging
19+
from typing import AsyncIterator
20+
from typing import Callable
1821
from typing import Optional
1922
from typing import Union
2023

@@ -82,6 +85,7 @@ def to_a2a(
8285
agent_card: Optional[Union[AgentCard, str]] = None,
8386
push_config_store: Optional[PushNotificationConfigStore] = None,
8487
runner: Optional[Runner] = None,
88+
lifespan: Optional[Callable[[Starlette], AsyncIterator[None]]] = None,
8589
) -> Starlette:
8690
"""Convert an ADK agent to a A2A Starlette application.
8791
@@ -98,6 +102,11 @@ def to_a2a(
98102
config RPC methods are supported.
99103
runner: Optional pre-built Runner object. If not provided, a default
100104
runner will be created using in-memory services.
105+
lifespan: Optional async context manager for Starlette lifespan
106+
events. Use this to run startup/shutdown logic (e.g. initializing
107+
database connections or loading resources). The context manager
108+
receives the Starlette app instance and can set state on
109+
``app.state``.
101110
102111
Returns:
103112
A Starlette application that can be run with uvicorn
@@ -109,6 +118,15 @@ def to_a2a(
109118
110119
# Or with custom agent card:
111120
app = to_a2a(agent, agent_card=my_custom_agent_card)
121+
122+
# Or with lifespan:
123+
@asynccontextmanager
124+
async def lifespan(app):
125+
app.state.db = await init_db()
126+
yield
127+
await app.state.db.close()
128+
129+
app = to_a2a(agent, lifespan=lifespan)
112130
"""
113131
# Set up ADK logging to ensure logs are visible when using uvicorn directly
114132
adk_logger = logging.getLogger("google_adk")
@@ -151,11 +169,8 @@ async def create_runner() -> Runner:
151169
rpc_url=rpc_url,
152170
)
153171

154-
# Create a Starlette app that will be configured during startup
155-
app = Starlette()
156-
157-
# Add startup handler to build the agent card and configure A2A routes
158-
async def setup_a2a():
172+
# Build the agent card and configure A2A routes
173+
async def setup_a2a(app: Starlette):
159174
# Use provided agent card or build one asynchronously
160175
if provided_agent_card is not None:
161176
final_agent_card = provided_agent_card
@@ -173,7 +188,19 @@ async def setup_a2a():
173188
app,
174189
)
175190

176-
# Store the setup function to be called during startup
177-
app.add_event_handler("startup", setup_a2a)
191+
# Compose a lifespan that runs A2A setup and the user's lifespan
192+
@asynccontextmanager
193+
async def _combined_lifespan(
194+
app: Starlette,
195+
) -> AsyncIterator[None]:
196+
await setup_a2a(app)
197+
if lifespan:
198+
async with lifespan(app):
199+
yield
200+
else:
201+
yield
202+
203+
# Create a Starlette app with the composed lifespan
204+
app = Starlette(lifespan=_combined_lifespan)
178205

179206
return app

0 commit comments

Comments
 (0)