diff --git a/src/google/adk/sessions/vertex_ai_session_service.py b/src/google/adk/sessions/vertex_ai_session_service.py index a465044dd2..5bb310a09e 100644 --- a/src/google/adk/sessions/vertex_ai_session_service.py +++ b/src/google/adk/sessions/vertex_ai_session_service.py @@ -556,10 +556,13 @@ def _from_api_event(api_event_obj: vertexai.types.SessionEvent) -> Event: event_dict = copy.deepcopy(raw_event_dict) timestamp_obj = getattr(api_event_obj, 'timestamp', None) event_dict.update({ - 'id': api_event_obj.name.split('/')[-1], 'invocation_id': getattr(api_event_obj, 'invocation_id', None), 'author': getattr(api_event_obj, 'author', None), }) + # Preserve the original ADK event id from raw_event so it stays stable + # across streaming and reload; only fall back to the Vertex resource + # name if raw_event lacks an id (older data). + event_dict.setdefault('id', api_event_obj.name.split('/')[-1]) if timestamp_obj: event_dict['timestamp'] = timestamp_obj.timestamp() return Event.model_validate(event_dict) diff --git a/tests/unittests/sessions/test_vertex_ai_session_service.py b/tests/unittests/sessions/test_vertex_ai_session_service.py index b589d8c368..d286fb8a9a 100644 --- a/tests/unittests/sessions/test_vertex_ai_session_service.py +++ b/tests/unittests/sessions/test_vertex_ai_session_service.py @@ -248,6 +248,24 @@ def _generate_mock_events_for_session_5(num_events): }, }] +MOCK_EVENT_WITH_RAW_EVENT_ID_JSON = [{ + 'name': ( + 'projects/test-project/locations/test-location/' + 'reasoningEngines/123/sessions/override/events/658731057016733696' + ), + 'invocationId': 'e-1', + 'author': 'user_with_override', + 'timestamp': '2024-12-12T12:12:12.123456Z', + 'content': {}, + 'actions': {}, + 'rawEvent': { + 'id': 'c452feb0-b9ad-48b1-b863-c7dd2f085378', + 'invocationId': 'e-1', + 'author': 'user_with_override', + 'content': {}, + }, +}] + MOCK_SESSION_WITH_OVERRIDE_JSON = { 'name': ( 'projects/test-project/locations/test-location/' @@ -888,6 +906,33 @@ async def test_get_session_from_raw_event( assert event.error_message == 'raw_event_error' +@pytest.mark.asyncio +@pytest.mark.usefixtures('mock_get_api_client') +async def test_get_session_preserves_original_event_id_from_raw_event( + mock_api_client_instance: MockAsyncClient, +) -> None: + """get_session should keep the original ADK Event.id persisted in raw_event. + + Regression test for the raw_event id being overwritten by the Vertex event + resource name, which made the same logical event report a different id + depending on whether it was observed via streaming or via a reload. + """ + mock_api_client_instance.session_dict['6'] = MOCK_SESSION_WITH_OVERRIDE_JSON + mock_api_client_instance.event_dict['6'] = ( + copy.deepcopy(MOCK_EVENT_WITH_RAW_EVENT_ID_JSON), + None, + ) + session_service = mock_vertex_ai_session_service() + session = await session_service.get_session( + app_name='123', user_id='user_with_override', session_id='6' + ) + assert session is not None + assert len(session.events) == 1 + assert ( + session.events[0].id == 'c452feb0-b9ad-48b1-b863-c7dd2f085378' + ), 'Event.id should come from raw_event, not the Vertex resource name.' + + @pytest.mark.asyncio @pytest.mark.usefixtures('mock_get_api_client') async def test_get_session_with_many_events(mock_api_client_instance):