Is your feature request related to a problem? Please describe.
In multi-user applications, it is often necessary to inject user-specific context into the system prompt on a per-request basis to personalize responses. A common example of this is injecting long-term memory, such as conversation summaries. However, the current methods for accessing this context within callbacks are brittle and lead to boilerplate code.
- Brittle Context Access: Accessing user context inside callbacks (e.g.,
before_model_callback) requires repeated parsing of context.session.user_id with null checks, which is error-prone.
- Multimodal Input Issues: JSON-wrapping inputs to carry metadata breaks when using audio or other multimodal inputs.
- Unsuitable
RunConfig: The RunConfig object is not a suitable context carrier for arbitrary per-request data due to its schema (extra='forbid').
These issues cause boilerplate code, subtle personalization failures, and slow down development.
Proposed solution
We propose adding a canonical, per-request context channel on the input side of the ADK.
- Add a metadata field to
LlmRequest:
LlmRequest: metadata: dict[str, Any] = Field(default_factory=dict)
- Update the
Runner methods to accept this metadata:
Runner.run_async(..., metadata: dict | None = None)
Runner.run_live(..., metadata: dict | None = None)
- The
Runner will merge the provided metadata into the underlying LlmRequest, allowing callbacks to reliably access per-request context:
llm_request.metadata.get("user_id")
llm_request.metadata.get("memctx_key")
Conceptual API
# In models/llm_request.py
class LlmRequest(BaseModel):
...
metadata: Dict[str, Any] = Field(
default_factory=dict,
description="Per-request context for callbacks/memory"
)
model_config = ConfigDict(extra='ignore') # Keep ignoring unknown fields
# In Runner
async def run_async(self, ..., metadata: dict | None = None, ...):
# If provided, merge into request.metadata
...
async def run_live(self, ..., metadata: dict | None = None, ...):
...
Minimal Reproduction Case
- Today's Problem: A
before_model_callback that needs user_id for memory injection must perform null checks everywhere and fails when a session is missing.
- Example Scenario:
- Define a
before_model_callback that requires a user_id.
- Call an agent with an audio input (where JSON-wrapped metadata is not possible).
- Observation: It's not possible to reliably pass the
user_id into the callback without brittle wiring or misusing RunConfig.
Alternatives Considered
- Parse
context.session.user_id in every callback: This is repetitive and brittle.
- JSON-wrap inputs with metadata: This approach breaks for audio/multimodal inputs and adds unnecessary parsing overhead.
- Subclass
RunConfig: The schema forbids arbitrary fields, and the config object is not designed to be a per-request context carrier.
- Use
LlmResponse.custom_metadata: This is on the output side only and does not solve the input-side context problem.
Backward Compatibility
This is a non-breaking change:
- The new
LlmRequest.metadata field defaults to an empty dictionary ({}).
- The
Runner methods accept an optional metadata parameter; omitting it results in no change to the current behavior.
- Extra fields remain ignored where applicable (
ConfigDict(extra='ignore')).
Security & Privacy
- The request metadata can carry user-scoped keys (e.g.,
user_id, memctx_key).
- Applications should avoid storing sensitive PII in the metadata itself. We recommend using short-TTL keys for any Redis-backed memory contexts.
Performance Impact
- The performance impact is negligible.
- It involves an O(1) dictionary merge with no additional network calls.
- No measurable overhead was observed in our local tests (100+ executions on ADK v1.2.0).
Test Plan (Upstream)
- Unit: Verify that metadata round-trips correctly into
LlmRequest on run_async and run_live.
- Unit: Ensure
before_model_callback can see request.metadata for both normal and streaming flows.
- Unit: Confirm isolation across concurrent runs (no metadata bleed).
- Unit: Check that
metadata defaults to {} when omitted.
- Integration: Test a memory injection callback using
metadata.memctx_key in a full streaming session.
Open Questions
- Should metadata be propagated to all nested flows and tools automatically?
- Should there be any size limits or filtering for the metadata dictionary?
How We Solved It Locally (Workaround)
We have shipped a safe, runtime-only workaround that mirrors this proposal without modifying ADK source code, demonstrating its viability.
ContextVar Carrier (app/adk/memctx.py): Uses current_run_metadata to hold context.
- Web Server Propagation (
app/api/custom_adk_web_server.py): Wraps runner methods to set and reset current_run_metadata from request headers.
LlmRequest Monkey Patch (app/adk/llm_request_patch.py): Ensures request.metadata exists and merges the ContextVar on LlmRequest construction.
- Callback Implementation (
app/agents/common/dynamic_callback.py): Reads llm_request.metadata['memctx_key'], loads data from Redis, and injects it into the system_instruction.
- Memory Count Policy (
app/agents/prompts/prompt_loader.py): Respects a memory_max_items count from context so that backend-selected items are injected as-is.
Environment
We are happy to contribute a PR that aligns with the proposal above.
Is your feature request related to a problem? Please describe.
In multi-user applications, it is often necessary to inject user-specific context into the system prompt on a per-request basis to personalize responses. A common example of this is injecting long-term memory, such as conversation summaries. However, the current methods for accessing this context within callbacks are brittle and lead to boilerplate code.
before_model_callback) requires repeated parsing ofcontext.session.user_idwith null checks, which is error-prone.RunConfig: TheRunConfigobject is not a suitable context carrier for arbitrary per-request data due to its schema (extra='forbid').These issues cause boilerplate code, subtle personalization failures, and slow down development.
Proposed solution
We propose adding a canonical, per-request context channel on the input side of the ADK.
LlmRequest:LlmRequest: metadata: dict[str, Any] = Field(default_factory=dict)Runnermethods to accept this metadata:Runner.run_async(..., metadata: dict | None = None)Runner.run_live(..., metadata: dict | None = None)Runnerwill merge the providedmetadatainto the underlyingLlmRequest, allowing callbacks to reliably access per-request context:llm_request.metadata.get("user_id")llm_request.metadata.get("memctx_key")Conceptual API
Minimal Reproduction Case
before_model_callbackthat needsuser_idfor memory injection must perform null checks everywhere and fails when a session is missing.before_model_callbackthat requires auser_id.user_idinto the callback without brittle wiring or misusingRunConfig.Alternatives Considered
context.session.user_idin every callback: This is repetitive and brittle.RunConfig: The schema forbids arbitrary fields, and the config object is not designed to be a per-request context carrier.LlmResponse.custom_metadata: This is on the output side only and does not solve the input-side context problem.Backward Compatibility
This is a non-breaking change:
LlmRequest.metadatafield defaults to an empty dictionary ({}).Runnermethods accept an optionalmetadataparameter; omitting it results in no change to the current behavior.ConfigDict(extra='ignore')).Security & Privacy
user_id,memctx_key).Performance Impact
Test Plan (Upstream)
LlmRequestonrun_asyncandrun_live.before_model_callbackcan seerequest.metadatafor both normal and streaming flows.metadatadefaults to{}when omitted.metadata.memctx_keyin a full streaming session.Open Questions
How We Solved It Locally (Workaround)
We have shipped a safe, runtime-only workaround that mirrors this proposal without modifying ADK source code, demonstrating its viability.
ContextVarCarrier (app/adk/memctx.py): Usescurrent_run_metadatato hold context.app/api/custom_adk_web_server.py): Wraps runner methods to set and resetcurrent_run_metadatafrom request headers.LlmRequestMonkey Patch (app/adk/llm_request_patch.py): Ensuresrequest.metadataexists and merges theContextVaronLlmRequestconstruction.app/agents/common/dynamic_callback.py): Readsllm_request.metadata['memctx_key'], loads data from Redis, and injects it into thesystem_instruction.app/agents/prompts/prompt_loader.py): Respects amemory_max_itemscount from context so that backend-selected items are injected as-is.Environment
We are happy to contribute a PR that aligns with the proposal above.