-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
Is your feature request related to a specific problem?
When using to_a2a() to expose an ADK agent as a Starlette application, there is no supported way to access the returned Starlette app instance from within agent callbacks or tool functions. The current
documentation shows to_a2a() returning a runnable Starlette app, but documents no mechanism for passing startup-loaded state into the agent's execution
context.
# What the docs show — no lifecycle hooks, no state injection
a2a_app = to_a2a(root_agent, port=8001)In production, agents frequently need resources loaded at startup: prompt registries, database connections, monitoring instrumentation, or configuration objects. Without a supported path from the Starlette app to the agent's execution context, the only option is module-level globals — which don't integrate with Starlette's lifespan protocol and make testing difficult.
Describe the Solution You'd Like
Support a lifespan parameter on to_a2a(), consistent with the standard Starlette lifespan protocol, and expose app.state (or equivalent) within ToolContext / CallbackContext.
Input: A lifespan async context manager passed to to_a2a().
Output: State stored on app.state during startup is accessible inside agent tool and callback functions.
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
app.state.prompts = await load_prompt_registry()
app.state.db = await init_db()
yield
await app.state.db.close()
a2a_app = to_a2a(root_agent, port=8001, lifespan=lifespan)
# Inside a tool or agent callback:
async def my_tool(context: ToolContext) -> str:
prompt = context.app.state.prompts.get("my_prompt")
return prompt.render(...)If threading app through ToolContext is architecturally complex, an alternative is a get_app() accessor on the
ADK runner context, or support for dependency injection at the to_a2a() call site.
Impact on your work
Any production ADK service using to_a2a() that loads external resources at startup hits this gap. We route around it with module-level globals, but this bypasses Starlette's lifecycle management (no clean shutdown, no async initialization, harder to test in isolation).
Willingness to contribute
Yes. Willing to submit a PR targeting google/adk-python. Would want maintainer input first on whether lifespan support belongs in to_a2a() directly or at a lower layer in the A2A runner, and whether ToolContext is the right injection point for app.state.
Describe Alternatives You've Considered
Module-level globals (current workaround): Load state at import time before to_a2a() is called and reference it directly in tool functions. Works in practice but bypasses async initialization, has no shutdown hook, and leaks state between tests.
Post-hoc app.state assignment: Set a2a_app.state.foo = ... after calling to_a2a(). The state is on the app object, but there is still no supported path to read it from inside a tool or callback — the agent has no reference to the app.
Custom Starlette middleware: Can intercept requests and attach to request.state, but ADK tool/callback functions do not receive the raw Starlette request, so this doesn't reach the agent execution context.
Proposed API / Implementation
Minimal addition to to_a2a() signature:
def to_a2a(
...
lifespan: Callable | None = None, # new
) -> Starlette:
...If lifespan is provided, pass it through to the Starlette(lifespan=lifespan) constructor. State set on app.state during lifespan would then be accessible anywhere the app instance is reachable.
The open question is the second half: how app.state reaches ToolContext / CallbackContext. The simplest path may be storing the app reference on the ADK runner and exposing it via context — but this depends on internal architecture.