-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
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 ofcontext.session.user_idwith 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: 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.
- Add a metadata field to
LlmRequest:LlmRequest: metadata: dict[str, Any] = Field(default_factory=dict)
- Update the
Runnermethods to accept this metadata:Runner.run_async(..., metadata: dict | None = None)Runner.run_live(..., metadata: dict | None = None)
- The
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
# 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_callbackthat needsuser_idfor memory injection must perform null checks everywhere and fails when a session is missing. - Example Scenario:
- Define a
before_model_callbackthat requires auser_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_idinto the callback without brittle wiring or misusingRunConfig.
- Define a
Alternatives Considered
- Parse
context.session.user_idin 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.metadatafield defaults to an empty dictionary ({}). - The
Runnermethods accept an optionalmetadataparameter; 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
LlmRequestonrun_asyncandrun_live. - Unit: Ensure
before_model_callbackcan seerequest.metadatafor both normal and streaming flows. - Unit: Confirm isolation across concurrent runs (no metadata bleed).
- Unit: Check that
metadatadefaults to{}when omitted. - Integration: Test a memory injection callback using
metadata.memctx_keyin 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.
ContextVarCarrier (app/adk/memctx.py): Usescurrent_run_metadatato hold context.- Web Server Propagation (
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.- Callback Implementation (
app/agents/common/dynamic_callback.py): Readsllm_request.metadata['memctx_key'], loads data from Redis, and injects it into thesystem_instruction. - Memory Count Policy (
app/agents/prompts/prompt_loader.py): Respects amemory_max_itemscount from context so that backend-selected items are injected as-is.
Environment
- Python 3.12+
- ADK v1.2.0
We are happy to contribute a PR that aligns with the proposal above.