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
2 changes: 1 addition & 1 deletion src/google/adk/sessions/in_memory_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ async def append_event(self, session: Session, event: Event) -> Event:

if event.actions and event.actions.state_delta:
state_deltas = _session_util.extract_state_delta(
event.actions.state_delta
_copy_state(event.actions.state_delta)
)
app_state_delta = state_deltas['app']
user_state_delta = state_deltas['user']
Expand Down
27 changes: 25 additions & 2 deletions tests/unittests/sessions/_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,31 @@ async def _make_per_agent_database(


BACKENDS = [
_Backend('in_memory', _make_in_memory),
_Backend('in_memory_light_copy', _make_in_memory_light_copy),
_Backend(
'in_memory',
_make_in_memory,
divergences={
'test_append_event_does_not_retain_caller_event': (
'append stores the event object passed by the caller: events'
' may hold values that cannot be copied, and append must not'
' pay copy costs; reads return deep copies instead'
),
},
),
_Backend(
'in_memory_light_copy',
_make_in_memory_light_copy,
divergences={
'test_append_event_does_not_retain_caller_event': (
'append stores the event object passed by the caller: events'
' may hold values that cannot be copied, and append must not'
' pay copy costs; reads return copies instead'
),
'test_append_event_does_not_retain_caller_state': (
'light copy keeps state values aliased to the caller, by design'
),
},
),
_Backend('database', _make_database),
_Backend('sqlite', _make_sqlite),
_Backend(
Expand Down
91 changes: 91 additions & 0 deletions tests/unittests/sessions/test_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,54 @@ async def test_append_event_complete(session_service):
)


@pytest.mark.asyncio
async def test_append_event_does_not_retain_caller_state(session_service):
"""A completed append preserves the applied state delta.

Mutating the caller's state delta after append_event returns must not
change what the service returns later: storage keeps the caller's snapshot
at append time, not live references into the caller's values.
"""
session = await session_service.create_session(app_name='a', user_id='u')
event = Event(
author='assistant',
content=types.Content(
role='model', parts=[types.Part(text='original answer')]
),
actions=EventActions(state_delta={'cart': {'items': ['coffee']}}),
)
await session_service.append_event(session=session, event=event)
event.actions.state_delta['cart']['items'].append('tea')

stored = await session_service.get_session(
app_name='a', user_id='u', session_id=session.id
)
assert stored.state['cart'] == {'items': ['coffee']}


@pytest.mark.asyncio
async def test_append_event_does_not_retain_caller_event(session_service):
"""A completed append preserves the stored event content.

Mutating the caller's event after append_event returns must not change
what the service returns later.
"""
session = await session_service.create_session(app_name='a', user_id='u')
event = Event(
author='assistant',
content=types.Content(
role='model', parts=[types.Part(text='original answer')]
),
)
await session_service.append_event(session=session, event=event)
event.content.parts[0].text = 'changed after append'

stored = await session_service.get_session(
app_name='a', user_id='u', session_id=session.id
)
assert stored.events[-1].content.parts[0].text == 'original answer'


# pylint: disable=redefined-outer-name
@pytest.mark.asyncio
async def test_append_event_with_requested_tool_confirmations(session_service):
Expand Down Expand Up @@ -3496,6 +3544,49 @@ async def test_in_memory_get_session_events_share_nothing_with_storage():
)


@pytest.mark.asyncio
@pytest.mark.parametrize('light_copy', [False, True])
async def test_in_memory_append_event_state_uses_configured_copy_depth(
light_copy,
):
"""A completed append copies the caller's state delta to the configured depth.

Light copy exists to skip the recursive copy, so under it state values stay
shared with the caller; without it they are deep-copied. The stored event
itself stays aliased to the caller's event in both modes: events may hold
values that cannot be copied, and append must not pay copy costs.
"""
override_feature_enabled(
FeatureName.IN_MEMORY_SESSION_SERVICE_LIGHT_COPY, light_copy
)
try:
service = InMemorySessionService()
session = await service.create_session(
app_name='my_app', user_id='u1', session_id='s1'
)
event = Event(
author='agent',
content=types.Content(
role='model', parts=[types.Part(text='original')]
),
actions=EventActions(state_delta={'cart': {'items': ['coffee']}}),
)
await service.append_event(session, event)
event.actions.state_delta['cart']['items'].append('tea')

stored = service.sessions['my_app']['u1']['s1']
assert stored.events[0] is event
assert (stored.state['cart'] is event.actions.state_delta['cart']) == (
light_copy
)
if not light_copy:
assert stored.state['cart'] == {'items': ['coffee']}
finally:
override_feature_enabled(
FeatureName.IN_MEMORY_SESSION_SERVICE_LIGHT_COPY, False
)


@pytest.mark.asyncio
async def test_in_memory_get_session_keeps_shared_state_values_shared():
"""Two state keys holding one value still hold one value after a read."""
Expand Down
Loading