Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion contributing/samples/gepa/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from tau_bench.types import EnvRunResult
from tau_bench.types import RunConfig
import tau_bench_agent as tau_bench_agent_lib

import utils


Expand Down
1 change: 0 additions & 1 deletion contributing/samples/gepa/run_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from absl import flags
import experiment
from google.genai import types

import utils

_OUTPUT_DIR = flags.DEFINE_string(
Expand Down
2 changes: 1 addition & 1 deletion src/google/adk/sessions/database_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ def to_event(self) -> Event:
branch=self.branch,
# This is needed as previous ADK version pickled actions might not have
# value defined in the current version of the EventActions model.
actions=EventActions().model_copy(update=self.actions.model_dump()),
actions=EventActions.model_validate(self.actions.model_dump()),
timestamp=self.timestamp.timestamp(),
long_running_tool_ids=self.long_running_tool_ids,
partial=self.partial,
Expand Down
170 changes: 170 additions & 0 deletions tests/unittests/sessions/test_database_compaction_deserialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import enum

from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.adk.events.event_actions import EventCompaction
from google.adk.sessions.database_session_service import DatabaseSessionService
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.sessions.sqlite_session_service import SqliteSessionService
from google.genai.types import Content
from google.genai.types import Part
import pytest


class SessionServiceType(enum.Enum):
IN_MEMORY = 'IN_MEMORY'
DATABASE = 'DATABASE'
SQLITE = 'SQLITE'


def get_session_service(
service_type: SessionServiceType = SessionServiceType.IN_MEMORY,
tmp_path=None,
):
"""Creates a session service for testing."""
if service_type == SessionServiceType.DATABASE:
return DatabaseSessionService('sqlite+aiosqlite:///:memory:')
if service_type == SessionServiceType.SQLITE:
return SqliteSessionService(str(tmp_path / 'sqlite.db'))
return InMemorySessionService()


@pytest.mark.asyncio
@pytest.mark.parametrize(
'service_type',
[
SessionServiceType.IN_MEMORY,
SessionServiceType.DATABASE,
SessionServiceType.SQLITE,
],
)
async def test_compaction_survives_database_roundtrip(service_type, tmp_path):
"""Test EventCompaction remains an object after DB save/load.

Reproduces bug where event.actions.compaction becomes dict after loading
from database, causing AttributeError on attribute access.
"""
session_service = get_session_service(service_type, tmp_path)

# Create event with EventCompaction
compaction = EventCompaction(
start_timestamp=1.0,
end_timestamp=2.0,
compacted_content=Content(
role='user', parts=[Part(text='Compacted summary')]
),
)
event = Event(
author='user',
actions=EventActions(compaction=compaction),
invocation_id='test_inv',
)

# Save to database
session = await session_service.create_session(
app_name='test_app', user_id='test_user'
)
await session_service.append_event(session=session, event=event)

# Load from database (simulates app restart)
loaded_session = await session_service.get_session(
app_name='test_app', user_id='test_user', session_id=session.id
)
loaded_event = loaded_session.events[0]

# Critical assertions: compaction should be EventCompaction, not dict
assert isinstance(loaded_event.actions.compaction, EventCompaction)
# These would raise AttributeError if compaction was a dict
assert loaded_event.actions.compaction.start_timestamp == 1.0
assert loaded_event.actions.compaction.end_timestamp == 2.0
assert (
loaded_event.actions.compaction.compacted_content.parts[0].text
== 'Compacted summary'
)


@pytest.mark.asyncio
@pytest.mark.parametrize(
'service_type',
[
SessionServiceType.IN_MEMORY,
SessionServiceType.DATABASE,
SessionServiceType.SQLITE,
],
)
async def test_multiple_events_with_compaction(service_type, tmp_path):
"""Test multiple events with compaction are properly deserialized."""
session_service = get_session_service(service_type, tmp_path)

session = await session_service.create_session(
app_name='test_app', user_id='test_user'
)

# Create and save multiple events
for i in range(3):
event = Event(
author='user',
actions=EventActions(
compaction=EventCompaction(
start_timestamp=float(i),
end_timestamp=float(i + 1),
compacted_content=Content(
role='user', parts=[Part(text=f'Summary {i}')]
),
)
),
invocation_id=f'inv_{i}',
)
await session_service.append_event(session=session, event=event)

# Load and verify all
loaded_session = await session_service.get_session(
app_name='test_app', user_id='test_user', session_id=session.id
)

for i, loaded_event in enumerate(loaded_session.events):
assert isinstance(loaded_event.actions.compaction, EventCompaction)
assert loaded_event.actions.compaction.start_timestamp == float(i)


@pytest.mark.asyncio
@pytest.mark.parametrize(
'service_type',
[
SessionServiceType.IN_MEMORY,
SessionServiceType.DATABASE,
SessionServiceType.SQLITE,
],
)
async def test_event_without_compaction(service_type, tmp_path):
"""Test events without compaction are not affected."""
session_service = get_session_service(service_type, tmp_path)

session = await session_service.create_session(
app_name='test_app', user_id='test_user'
)
event = Event(
author='user', actions=EventActions(), invocation_id='no_compaction'
)
await session_service.append_event(session=session, event=event)

loaded_session = await session_service.get_session(
app_name='test_app', user_id='test_user', session_id=session.id
)
assert loaded_session.events[0].actions.compaction is None