Bug Description
custom_metadata on Event objects is silently dropped when persisted
by DatabaseSessionService. This breaks RemoteA2aAgent which relies
on custom_metadata to reuse the same A2A context_id across requests
from the same orchestrator session.
ADK Version
2.2.0
Root Cause
In schemas/v1.py, StorageEvent.from_event serializes events using:
event_data=event.model_dump(exclude_none=True, mode="json")
custom_metadata is not a declared Pydantic field on Event — it is
set dynamically at runtime by RemoteA2aAgent:
# remote_a2a_agent.py line 567-571
event.custom_metadata = event.custom_metadata or {}
event.custom_metadata[A2A_METADATA_PREFIX + "task_id"] = task.id
event.custom_metadata[A2A_METADATA_PREFIX + "context_id"] = task.context_id
Because it is not a Pydantic field, model_dump() never includes it.
to_event() never restores it. Every DB round-trip silently loses it.
Impact
RemoteA2aAgent._construct_message_parts_from_session reads
context_id from previous events' custom_metadata:
# remote_a2a_agent.py line 456-458
if event.custom_metadata:
metadata = event.custom_metadata
context_id = metadata.get(A2A_METADATA_PREFIX + "context_id")
When custom_metadata is lost, context_id is always None, so
every request to a sub-agent creates a brand new A2A context instead
of reusing the existing one. This means:
- Sub-agents lose all session continuity across requests
- Token usage tracking breaks — each request starts accumulating
from zero in a new context
- Multi-turn conversations with sub-agents don't work correctly
- Works fine with
InMemorySessionService (no DB round-trip)
but breaks with DatabaseSessionService
Reproduction
- Set up an orchestrator using
DatabaseSessionService with a
RemoteA2aAgent sub-agent
- Send two sequential requests in the same session
- Observe logs — every request creates a new
context_id:
Request 1
Task not found. Creating new task (context_id: aaa-111)
Request 2 — should reuse aaa-111, creates new one instead
Task not found. Creating new task (context_id: bbb-222)
With InMemorySessionService, request 2 correctly reuses aaa-111.
Expected Behavior
custom_metadata should survive DB round-trips so RemoteA2aAgent
can reuse context_id across requests in the same session.
Workaround
Monkey-patch StorageEvent.from_event and to_event to manually
preserve custom_metadata in event_data:
@classmethod
def _patched_from_event(cls, session, event):
storage_event = _original_from_event(cls, session, event)
if hasattr(event, 'custom_metadata') and event.custom_metadata:
if storage_event.event_data is None:
storage_event.event_data = {}
storage_event.event_data['custom_metadata'] = event.custom_metadata
return storage_event
def _patched_to_event(self):
event = _original_to_event(self)
if self.event_data and 'custom_metadata' in self.event_data:
event.custom_metadata = self.event_data['custom_metadata']
return event
Suggested Fix
Either:
Option A — Add custom_metadata as a proper Pydantic field on Event:
# events/event.py
custom_metadata: Optional[dict[str, Any]] = None
Option B — Explicitly include it in from_event serialization:
# schemas/v1.py StorageEvent.from_event
event_data = event.model_dump(exclude_none=True, mode="json")
if hasattr(event, 'custom_metadata') and event.custom_metadata:
event_data['custom_metadata'] = event.custom_metadata
return StorageEvent(
...
event_data=event_data,
)
And restore it in to_event:
def to_event(self) -> Event:
event = Event.model_validate({...})
if self.event_data and 'custom_metadata' in self.event_data:
event.custom_metadata = self.event_data['custom_metadata']
return event
Option A is cleaner as it makes custom_metadata a first-class field
that serializes automatically.
Environment
- OS: Ubuntu / Linux
- Python: 3.12
- google-adk: 2.2.0
- sqlalchemy: (run
pip show sqlalchemy | grep Version)
- Database: SQLite (sqlite+aiosqlite)
Minimal Reproduction Code
# orchestrator setup
from google.adk.sessions import DatabaseSessionService
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.tools import agent_tool
session_service = DatabaseSessionService("sqlite+aiosqlite:///test.db")
remote_agent = RemoteA2aAgent(
name="sub_agent",
description="test",
agent_card="http://localhost:8001/.well-known/agent.json",
)
# Send request 1 — sub_agent gets context_id = aaa-111
# Send request 2 in SAME session — sub_agent gets NEW context_id = bbb-222
# Expected: context_id = aaa-111 reused
# Actual: new context_id every request
Additional Context
This regression was introduced in ADK v2. In ADK v1 the A2A session
handling preserved context continuity across requests automatically.
The bug only manifests with DatabaseSessionService —
InMemorySessionService is unaffected because events are never
serialized. The custom_metadata field is also absent from the Event class
definition entirely (events/event.py) — it is only set as a dynamic
attribute by RemoteA2aAgent at runtime, which is why model_dump()
cannot capture it. The fix requires changes in both events/event.py
(declare the field) and schemas/v1.py (serialize/deserialize it).
Bug Description
custom_metadataonEventobjects is silently dropped when persistedby
DatabaseSessionService. This breaksRemoteA2aAgentwhich relieson
custom_metadatato reuse the same A2Acontext_idacross requestsfrom the same orchestrator session.
ADK Version
2.2.0
Root Cause
In
schemas/v1.py,StorageEvent.from_eventserializes events using:custom_metadatais not a declared Pydantic field onEvent— it isset dynamically at runtime by
RemoteA2aAgent:Because it is not a Pydantic field,
model_dump()never includes it.to_event()never restores it. Every DB round-trip silently loses it.Impact
RemoteA2aAgent._construct_message_parts_from_sessionreadscontext_idfrom previous events'custom_metadata:When
custom_metadatais lost,context_idis alwaysNone, soevery request to a sub-agent creates a brand new A2A context instead
of reusing the existing one. This means:
from zero in a new context
InMemorySessionService(no DB round-trip)but breaks with
DatabaseSessionServiceReproduction
DatabaseSessionServicewith aRemoteA2aAgentsub-agentcontext_id:Request 1
Task not found. Creating new task (context_id: aaa-111)
Request 2 — should reuse aaa-111, creates new one instead
Task not found. Creating new task (context_id: bbb-222)
With
InMemorySessionService, request 2 correctly reusesaaa-111.Expected Behavior
custom_metadatashould survive DB round-trips soRemoteA2aAgentcan reuse
context_idacross requests in the same session.Workaround
Monkey-patch
StorageEvent.from_eventandto_eventto manuallypreserve
custom_metadatainevent_data:Suggested Fix
Either:
Option A — Add
custom_metadataas a proper Pydantic field onEvent:Option B — Explicitly include it in
from_eventserialization:And restore it in
to_event:Option A is cleaner as it makes
custom_metadataa first-class fieldthat serializes automatically.
Environment
pip show sqlalchemy | grep Version)Minimal Reproduction Code
Additional Context
This regression was introduced in ADK v2. In ADK v1 the A2A session
handling preserved context continuity across requests automatically.
The bug only manifests with
DatabaseSessionService—InMemorySessionServiceis unaffected because events are neverserialized. The
custom_metadatafield is also absent from theEventclassdefinition entirely (
events/event.py) — it is only set as a dynamicattribute by
RemoteA2aAgentat runtime, which is whymodel_dump()cannot capture it. The fix requires changes in both
events/event.py(declare the field) and
schemas/v1.py(serialize/deserialize it).