Skip to content

EventCompaction deserialized as dict instead of Pydantic model in DatabaseSessionService #3633

@fhuzjan

Description

@fhuzjan

Bug Report: EventCompaction becomes dict after DatabaseSessionService deserialization

Description

When using DatabaseSessionService, nested Pydantic models in EventActions (specifically EventCompaction) are incorrectly deserialized as dictionaries instead of proper Pydantic model instances. This causes AttributeError when trying to access attributes on the compaction object.

Environment

  • google-adk version: 1.18.0
  • Python version: 3.11
  • Session service: DatabaseSessionService
  • Database: PostgreSQL
  • OS: MacOS 15.6.1

Steps to Reproduce

  1. Create an Event with EventCompaction:
from google.adk.events import Event
from google.adk.events.event_actions import EventActions, EventCompaction
from google.genai.types import Content, Part

# Create a compaction event
compaction = EventCompaction(
    start_timestamp=1.0,
    end_timestamp=2.0,
    compacted_content=Content(parts=[Part(text="Summary")], role='model')
)

actions = EventActions(compaction=compaction)

event = Event(
    author='user',
    actions=actions,
    invocation_id=Event.new_id(),
)
  1. Append the event to a session using DatabaseSessionService:
await session_service.append_event(session, event)
  1. Later, retrieve events from the session and try to access compaction attributes:
for event in session.events:
    if event.actions and event.actions.compaction:
        # This will fail with AttributeError
        end_ts = event.actions.compaction.end_timestamp

Expected Behavior

event.actions.compaction should be an instance of EventCompaction with accessible attributes like start_timestamp, end_timestamp, and compacted_content.

Actual Behavior

event.actions.compaction becomes a plain Python dict, causing:

AttributeError: 'dict' object has no attribute 'end_timestamp'

Root Cause

The bug is in database_session_service.py in the StorageEvent.to_event() method:

File: google/adk/sessions/database_session_service.py
Line: 348

def to_event(self) -> Event:
    return Event(
        id=self.id,
        invocation_id=self.invocation_id,
        author=self.author,
        branch=self.branch,
        actions=EventActions().model_copy(update=self.actions.model_dump()),  # BUG HERE
        # ... other fields ...
    )

Why This Fails

  1. self.actions is unpickled from the database, returning an EventActions object
  2. .model_dump() converts the EventActions to a dictionary, which also converts nested Pydantic models (like EventCompaction) to plain dicts
  3. EventActions().model_copy(update=...) creates a new EventActions and updates it with the dict values
  4. Critical issue: Pydantic's model_copy() does NOT validate or reconstruct nested Pydantic models—it just assigns the dictionary values directly
  5. Result: event.actions.compaction remains as a dict instead of being reconstructed as an EventCompaction object

Proof of Concept

from pydantic import BaseModel
from typing import Optional

class Inner(BaseModel):
    value: str

class Outer(BaseModel):
    inner: Optional[Inner] = None

# Create proper object
obj = Outer(inner=Inner(value='test'))

# Simulate what database_session_service.py does
dumped = obj.model_dump()  # {'inner': {'value': 'test'}}
reconstructed = Outer().model_copy(update=dumped)

print(type(reconstructed.inner))  # <class 'dict'> ❌
print(isinstance(reconstructed.inner, dict))  # True ❌

# What it SHOULD do
validated = Outer.model_validate(dumped)
print(type(validated.inner))  # <class '__main__.Inner'> ✓
print(isinstance(validated.inner, Inner))  # True ✓

Proposed Fix

Replace line 348 in database_session_service.py:

Current (buggy) code:

actions=EventActions().model_copy(update=self.actions.model_dump()),

Fixed code (Option 1 - Simple):

actions=EventActions.model_validate(self.actions if isinstance(self.actions, dict) else self.actions.model_dump()),

Fixed code (Option 2 - More explicit):

# Handle both pickled objects and raw dicts
actions_data = self.actions if isinstance(self.actions, dict) else self.actions
actions=EventActions.model_validate(actions_data) if actions_data else None,

The key insight is that model_validate() properly reconstructs nested Pydantic models, while model_copy() does not.

Impact

  • Severity: High - breaks any code that uses EventCompaction
  • Scope: Only affects DatabaseSessionService; InMemorySessionService works correctly
  • Affected features:
    • Event compaction/summarization
    • Any custom code reading event.actions.compaction from persisted sessions
    • ADK's own compaction logic in google/adk/apps/compaction.py:117

Workaround

Until this is fixed, users must manually reconstruct EventCompaction objects:

from google.adk.events.event_actions import EventCompaction

for event in session.events:
    if event.actions and event.actions.compaction:
        compaction = event.actions.compaction

        # Workaround: reconstruct if it's a dict
        if isinstance(compaction, dict):
            compaction = EventCompaction.model_validate(compaction)

        # Now safe to access attributes
        end_ts = compaction.end_timestamp

Additional Context

This issue likely affects ANY nested Pydantic models in EventActions, not just EventCompaction. The same pattern should be reviewed for:

  • Any other nested models in EventActions
  • Similar serialization/deserialization patterns elsewhere in the codebase

Related Code Locations

  • google/adk/sessions/database_session_service.py:348 - Root cause
  • google/adk/apps/compaction.py:117 - Code that expects EventCompaction to be an object
  • google/adk/events/event_actions.py:30-98 - EventCompaction and EventActions definitions

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions