Skip to content

VertexAiSessionService mutates local session before append_event succeeds #6998

Description

@1fanwang

🔴 Required Information

Describe the Bug:

VertexAiSessionService.append_event() updates the caller's Session before the remote append succeeds. If the remote call fails, the exception propagates but the event and its normal state delta remain in memory. Retrying the same event after the service recovers appends it a second time.

Temporary state has different semantics. A temp: value should remain available in the in-memory session during the invocation while being removed from the persisted event. The failure path should preserve that behavior without applying normal state or adding the event until the remote append succeeds.

This reproduces on current main at c506ddf.

Steps to Reproduce:

  1. Run git checkout c506ddf3bc34a6312ffc81899221bfa3f2da3b1d.
  2. Install the gcp extra.
  3. Run the script under "Minimal Reproduction Code."

Expected Behavior:

After the failed append, only temp:scratch is present in the local session and the event list is unchanged. A retry that succeeds applies the normal state delta and appends the event once.

first append: network failure
after failure state: {'existing': 'value', 'temp:scratch': 'ephemeral'}
after failure events: 0
remote delta: {'normal': 'persisted'}
after retry state: {'existing': 'value', 'temp:scratch': 'ephemeral', 'normal': 'persisted'}
after retry events: 1

Observed Behavior:

The failed remote append changes normal state and adds the event. The successful retry adds the same event again.

first append: network failure
after failure state: {'existing': 'value', 'temp:scratch': 'ephemeral', 'normal': 'persisted'}
after failure events: 1
remote delta: {'normal': 'persisted'}
after retry state: {'existing': 'value', 'temp:scratch': 'ephemeral', 'normal': 'persisted'}
after retry events: 2

Environment Details:

  • ADK Library Version: editable checkout of current main
  • Desktop OS: macOS
  • Python Version: 3.12.13

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A. The failure occurs in session persistence without a model call.

🟡 Optional Information

Regression:

Unknown.

Logs:

The expected and observed output are included above.

Screenshots / Video:

N/A.

Additional Context:

DatabaseSessionService, SqliteSessionService, and FirestoreSessionService apply temporary state and remove it from the event before storage, but defer normal in-memory state and event updates until storage succeeds. VertexAiSessionService currently calls BaseSessionService.append_event() before its remote API call.

The smallest fix is to keep the temporary-state handling before the remote call and move the base append until after the remote append succeeds.

Minimal Reproduction Code:

import asyncio
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from types import SimpleNamespace
from unittest import mock

from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.adk.sessions.session import Session
from google.adk.sessions.vertex_ai_session_service import VertexAiSessionService


async def main() -> None:
  append = mock.AsyncMock(side_effect=[RuntimeError('network failure'), None])
  client = SimpleNamespace(
      agent_engines=SimpleNamespace(
          sessions=SimpleNamespace(
              events=SimpleNamespace(append=append),
          )
      )
  )

  @asynccontextmanager
  async def fake_client() -> AsyncIterator[SimpleNamespace]:
    yield client

  service = VertexAiSessionService(project='test', location='us-central1')
  session = Session(
      id='1',
      app_name='123',
      user_id='user',
      state={'existing': 'value'},
  )
  event = Event(
      invocation_id='invocation',
      author='model',
      actions=EventActions(
          state_delta={
              'normal': 'persisted',
              'temp:scratch': 'ephemeral',
          }
      ),
  )

  with mock.patch.object(service, '_get_api_client', fake_client):
    try:
      await service.append_event(session, event)
    except RuntimeError as error:
      print(f'first append: {error}')
    print(f'after failure state: {session.state}')
    print(f'after failure events: {len(session.events)}')
    print(f'remote delta: {event.actions.state_delta}')

    await service.append_event(session, event)
    print(f'after retry state: {session.state}')
    print(f'after retry events: {len(session.events)}')


asyncio.run(main())

How often has this issue occurred?:

  • Always (100%)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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