Skip to content

VertexAiSessionService.get_session replaces Event.id with the Vertex resource name, discarding the persisted raw_event.id #6530

Description

@ashubham

Description

When a session is loaded via VertexAiSessionService.get_session, each returned Event.id is set to the Vertex event resource name (api_event_obj.name.split('/')[-1], a server-assigned numeric id) instead of the original ADK Event.id that was generated during the run and streamed to the caller.

The original id is still persisted — it's inside the raw_event blob — but _from_api_event deep-copies raw_event and then overwrites the id before validating. So the same logical event ends up with two different ids depending on how you observe it:

Observed via Event.id
live run / streaming (run_async, :query/streamQuery) the ADK Event.id (e.g. a UUID like c452feb0-b9ad-48b1-b863-c7dd2f085378)
get_session (reload) the Vertex resource name (e.g. 658731057016733696)

Where

src/google/adk/sessions/vertex_ai_session_service.py, _from_api_event — the raw_event branch (and the same in the fallback branch):

raw_event_dict = _get_raw_event(api_event_obj)   # contains the original ADK id
if raw_event_dict:
    event_dict = copy.deepcopy(raw_event_dict)
    event_dict.update({
        'id': api_event_obj.name.split('/')[-1],  # <-- overwrites the original id
        'invocation_id': getattr(api_event_obj, 'invocation_id', None),
        'author': getattr(api_event_obj, 'author', None),
    })
    return Event.model_validate(event_dict)

raw_event_dict['id'] holds the original id and is discarded. Confirmed on main and on the released 1.31.1.

Impact

Any feature that correlates a streamed event with its persisted form by Event.id breaks, because the two id-spaces don't reconcile.

Our concrete case: a resumable SSE layer keys a per-event resume cursor on Event.id. After a page reload we render history from get_session and reconnect using the last rendered event's id as the resume cursor. Because the reloaded id (numeric) is in a different id-space than the live stream (UUID), the cursor can't be resolved against the in-flight buffer and dedupe-by-id fails — a mid-turn reload re-renders the already-shown prefix of the in-flight turn as duplicates.

More generally this makes Event.id unusable as a stable cross-observation key, which is surprising given invocation_id, author, and the full content are faithfully round-tripped from raw_event.

Minimal repro (no live backend needed)

The overwrite is visible by driving the conversion function directly:

import datetime
from types import SimpleNamespace
from google.adk.events.event import Event
from google.adk.sessions.vertex_ai_session_service import _from_api_event

original = Event(id="c452feb0-b9ad-48b1-b863-c7dd2f085378",
                 invocation_id="e-1", author="model")
raw_event = original.model_dump(exclude_none=True, mode="json", by_alias=True)
# raw_event["id"] == "c452feb0-..."  (the original id is persisted)

api_event = SimpleNamespace(
    raw_event=raw_event,
    name="projects/p/locations/l/reasoningEngines/r/sessions/s/events/658731057016733696",
    invocation_id="e-1", author="model",
    timestamp=datetime.datetime.now(datetime.timezone.utc),
)
loaded = _from_api_event(api_event)
print(loaded.id)  # -> "658731057016733696"  (original UUID discarded, though it's in raw_event)

Expected

get_session should return each event with its original Event.id (the value already present in raw_event), so ids are stable across streaming and reload. The Vertex resource name is still available separately on api_event_obj.name for callers who need it.

Proposed fix

When raw_event is present, preserve its id rather than overwriting it (fall back to the resource name only when raw_event is absent, for older data):

event_dict = copy.deepcopy(raw_event_dict)
event_dict.update({
    'invocation_id': getattr(api_event_obj, 'invocation_id', None),
    'author': getattr(api_event_obj, 'author', None),
})
event_dict.setdefault('id', api_event_obj.name.split('/')[-1])  # only if raw_event lacked one
return Event.model_validate(event_dict)

Workaround

Stash the original id in custom_metadata on append and restore it on read — custom_metadata is a declared field that round-trips through raw_event and (unlike the top-level id) is not overwritten by _from_api_event. (Related: #6055, where an undeclared custom_metadata attribute was dropped on persistence — a declared field avoids that.)

Environment

  • google-adk 1.31.1 (behavior identical on main)
  • VertexAiSessionService (Vertex AI Agent Engine sessions)

Metadata

Metadata

Labels

request clarification[Status] The maintainer need clarification or more information from the authorservices[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions