Skip to content

[Bug] custom_metadata not persisted by DatabaseSessionService, breaks RemoteA2aAgent context_id reuse across requests #6055

@Gautam0610

Description

@Gautam0610

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:

  1. Sub-agents lose all session continuity across requests
  2. Token usage tracking breaks — each request starts accumulating
    from zero in a new context
  3. Multi-turn conversations with sub-agents don't work correctly
  4. Works fine with InMemorySessionService (no DB round-trip)
    but breaks with DatabaseSessionService

Reproduction

  1. Set up an orchestrator using DatabaseSessionService with a
    RemoteA2aAgent sub-agent
  2. Send two sequential requests in the same session
  3. 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).

Metadata

Metadata

Assignees

Labels

services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions