Skip to content

to_a2a(): support lifespan hooks and app state access from agent callbacks #4701

@thorrester

Description

@thorrester

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.

Metadata

Metadata

Labels

a2a[Component] This issue is related a2a support inside ADK.needs review[Status] The PR/issue is awaiting review from the maintainer

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions