Skip to content

Conversation

@lwangverizon
Copy link
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • Closes: N/A
  • Related: N/A

2. Or, if no issue exists, describe the change:

Problem:

In e-commerce and customer service scenarios, agentic chatbots commonly need to handle both non-signed-in (guest) and signed-in (authenticated) user states. A typical user journey looks like:

  1. Guest browsing: User interacts with the chatbot while browsing products, asking questions, adding items to cart — tracked by a temporary key (e.g., session_id, cookie_id, or device_id)
  2. User signs in: User decides to log in or create an account
  3. Context transition needed: The conversation history from the guest session must be transferred to the authenticated user's account (e.g., customer_id, account_number)

Without this capability, the agent loses all prior context when the user authenticates, forcing them to repeat their questions and preferences. This creates a poor user experience and reduces conversion rates.

Current limitation: ADK session services do not provide a way to clone or transfer session history from one user identity to another.

Solution:

Add a new clone_session method to BaseSessionService that enables applications to seamlessly transfer conversation context when users transition between identity states.

Primary use case - Guest to Authenticated transition:

# User was browsing as guest, now signs in
cloned_session = await session_service.clone_session(
    app_name="ecommerce_assistant",
    src_user_id="guest_cookie_abc123",      # Temporary tracking key
    src_session_id="browsing_session",
    new_user_id="customer_12345",           # Authenticated account ID
    new_session_id="authenticated_session",
)
# Agent now has full context of the guest's browsing history

The method is implemented across all session service backends:

  • InMemorySessionService
  • DatabaseSessionService (SQLAlchemy)
  • SqliteSessionService (aiosqlite)
  • VertexAiSessionService

Additional supported scenarios:

  • Account migration: Transfer history when users merge accounts
  • Multi-session consolidation: Merge fragmented sessions into unified history
  • Context backup: Clone sessions before experimental interactions

Key features:

  • Automatic event deduplication by event ID (first occurrence wins)
  • State merging from source session(s)
  • Auto-generated UUID4 session IDs when new_session_id is not provided
  • Proper error handling (ValueError for missing sources, AlreadyExistsError for duplicate IDs)

API signature:

async def clone_session(
    self,
    *,
    app_name: str,
    src_user_id: str,
    src_session_id: Optional[str] = None,
    new_user_id: Optional[str] = None,
    new_session_id: Optional[str] = None,
) -> Session:

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added 10 new test cases that run across all 3 testable session services (InMemory, Database, SQLite) for a total of 30 new tests:

Test Description
test_clone_session_basic Basic single session clone
test_clone_session_with_different_user_id Clone to a different user (guest → authenticated)
test_clone_session_with_custom_session_id User-provided destination session ID
test_clone_session_with_existing_id_raises_error AlreadyExistsError when destination exists
test_clone_session_preserves_event_content Full event data (content, actions, metadata) preserved
test_clone_session_does_not_affect_source Source session isolation verified
test_clone_all_user_sessions Multi-session merge when src_session_id omitted
test_clone_session_no_source_raises_error ValueError when source session not found
test_clone_all_sessions_no_sessions_raises_error ValueError when user has no sessions
test_clone_session_deduplicates_events Automatic deduplication by event ID

pytest results:

$ pytest tests/unittests/sessions/test_session_service.py -v
============================== 78 passed in 4.12s ==============================

Manual End-to-End (E2E) Tests:

from google.adk.sessions import InMemorySessionService
from google.adk.events import Event

async def e2e_guest_to_authenticated():
    """Simulate e-commerce guest → signed-in transition."""
    service = InMemorySessionService()
    
    # 1. Guest browsing session (tracked by cookie)
    guest_session = await service.create_session(
        app_name="ecommerce_bot",
        user_id="cookie_xyz789",  # Temporary guest identifier
        state={"cart_items": ["SKU-001", "SKU-002"]},
    )
    await service.append_event(guest_session, Event(
        author="user", invocation_id="inv1",
    ))
    await service.append_event(guest_session, Event(
        author="model", invocation_id="inv2",
    ))
    
    # 2. User signs in → Transfer context to authenticated identity
    auth_session = await service.clone_session(
        app_name="ecommerce_bot",
        src_user_id="cookie_xyz789",
        src_session_id=guest_session.id,
        new_user_id="customer_12345",  # Authenticated account
    )
    
    # 3. Verify context transferred
    assert auth_session.user_id == "customer_12345"
    assert len(auth_session.events) == 2  # Full conversation history
    assert auth_session.state["cart_items"] == ["SKU-001", "SKU-002"]
    print("✅ Guest → Authenticated transition successful")

import asyncio
asyncio.run(e2e_guest_to_authenticated())

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Files changed:

File Change
src/google/adk/sessions/base_session_service.py Added abstract clone_session method with comprehensive docstring
src/google/adk/sessions/in_memory_session_service.py Implemented clone_session
src/google/adk/sessions/database_session_service.py Implemented clone_session
src/google/adk/sessions/sqlite_session_service.py Implemented clone_session
src/google/adk/sessions/vertex_ai_session_service.py Implemented clone_session (note: new_session_id not supported due to Vertex AI API limitation)
tests/unittests/sessions/test_session_service.py Added 10 test cases (×3 services = 30 tests)

Design decisions:

  1. Event deduplication is always enabled - Prevents database integrity errors and ensures clean merged history
  2. First occurrence wins - When deduplicating, the first event encountered is kept (maintains chronological integrity)
  3. State merging - When merging multiple sessions, states are merged with later sessions overwriting earlier values for conflicting keys
  4. Vertex AI limitation - new_session_id parameter raises ValueError for VertexAiSessionService since the backend auto-generates session IDs

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @lwangverizon, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the ADK session services by adding a clone_session method, enabling seamless transfer and merging of conversation contexts. This is particularly valuable for maintaining user experience in applications where user identity changes, such as transitioning from a guest browsing session to an authenticated user, by ensuring all prior interaction history and state are preserved in the new session.

Highlights

  • New clone_session Method: Introduces a new clone_session method to BaseSessionService for transferring conversation context between sessions.
  • Flexible Cloning Modes: Supports two primary modes: cloning a single specified session or merging all sessions associated with a given source user ID into a new session.
  • Automatic Event Deduplication: Events are automatically deduplicated by their ID during cloning, ensuring that only the first occurrence of each event is preserved in the new session.
  • State Merging: Session states are intelligently merged from all source sessions, with later sessions overwriting conflicting keys to maintain data integrity.
  • Robust Error Handling: Includes comprehensive error handling for scenarios such as missing source sessions (ValueError) and attempts to clone to an already existing session ID (AlreadyExistsError).
  • Broad Backend Support: The clone_session functionality has been implemented across all supported session service backends: InMemorySessionService, DatabaseSessionService, SqliteSessionService, and VertexAiSessionService.
  • Vertex AI Specifics: The VertexAiSessionService implementation includes a specific limitation where providing a new_session_id will raise a ValueError, as the Vertex AI backend auto-generates session IDs.
  • Extensive Testing: Ten new unit test cases have been added, which are executed across the three testable session services (InMemory, Database, SQLite), resulting in a total of 30 new tests to validate the cloning functionality.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@adk-bot adk-bot added the services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc label Jan 17, 2026
@lwangverizon lwangverizon marked this pull request as ready for review January 17, 2026 21:09
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable clone_session feature across all session service backends, complete with a comprehensive set of unit tests. The implementation is well-structured and addresses a key use case for transitioning user states. My review highlights several opportunities for improvement, primarily focusing on performance and correctness. I've identified a recurring N+1 query pattern that could be optimized, as well as redundant data fetching that can be avoided. Additionally, there are a couple of potential correctness issues in the VertexAiSessionService related to missing deep copies that could lead to unexpected state mutations. Addressing these points will enhance the robustness and efficiency of the new feature.

# Merge states from all source sessions
merged_state = {}
for session in source_sessions:
merged_state.update(session.state)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The session state is being merged using a shallow copy (.update()). This is inconsistent with the other SessionService implementations, which use copy.deepcopy(). If session.state contains mutable objects (e.g., lists, dicts), changes in the cloned session could unintentionally alter the state of the source sessions. To prevent these side effects and ensure correctness, you should use a deep copy.

Note: You will need to add import copy at the top of the file.

Suggested change
merged_state.update(session.state)
merged_state.update(copy.deepcopy(session.state))

Comment on lines 322 to 323
for event in all_events:
await self.append_event(new_session, event)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The event object from a source session is passed directly to self.append_event(). The base implementation of append_event can mutate this object (e.g., by calling _trim_temp_delta_state). This creates an unintended side effect, modifying the original event from the source session. To prevent this, you should pass a deep copy of the event.

Note: This also requires import copy at the top of the file.

Suggested change
for event in all_events:
await self.append_event(new_session, event)
for event in all_events:
await self.append_event(new_session, copy.deepcopy(event))

Comment on lines 501 to 505
return await self.get_session(
app_name=new_session.app_name,
user_id=new_session.user_id,
session_id=new_session.id,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This final get_session call is redundant and inefficient as it performs an extra database query to fetch data you already have. The new_session object returned by create_session contains the correct state, and you have the deduplicated all_events list. You can construct the final Session object in memory.

    # Return the new session with events
    new_session.events = all_events
    return new_session

Comment on lines 342 to 349
for sess in list_response.sessions:
full_session = self._get_session_impl(
app_name=app_name,
user_id=src_user_id,
session_id=sess.id,
)
if full_session:
source_sessions.append(full_session)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop exhibits an N+1-like pattern. You call _list_sessions_impl and then loop to call _get_session_impl for each session. While this is an in-memory operation, it's less efficient than necessary and follows a pattern that is problematic in database-backed services. You could optimize this by directly accessing the sessions for the user from self.sessions and collecting the events without repeated get_session calls.

Comment on lines 383 to 387
return self._get_session_impl(
app_name=new_session.app_name,
user_id=new_session.user_id,
session_id=new_session.id,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This final call to _get_session_impl is unnecessary. You have already created the new_session object and populated the storage_session with the correct events and latest_update_time. Instead of re-fetching, you can update the new_session object you already have in memory and return it directly.

    new_session.events = all_events
    new_session.last_update_time = latest_update_time
    return new_session

Comment on lines 395 to 402
for sess in list_response.sessions:
full_session = await self.get_session(
app_name=app_name,
user_id=src_user_id,
session_id=sess.id,
)
if full_session:
source_sessions.append(full_session)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop introduces an N+1 query problem. You call list_sessions once, and then get_session is called inside a loop for every session found. This can lead to a large number of database queries and impact performance, especially for users with many sessions.

You could optimize this by:

  1. Getting all session objects from list_sessions.
  2. Collecting all session_ids.
  3. Fetching all events for those session IDs in a single query using WHERE session_id IN (...).
  4. Mapping the events back to their respective sessions in memory.

Comment on lines 449 to 453
return await self.get_session(
app_name=new_session.app_name,
user_id=new_session.user_id,
session_id=new_session.id,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This get_session call to fetch the newly created session is redundant. You have all the necessary information to construct the Session object in memory and return it directly. The new_session object returned from create_session already contains the correct state, and you have the all_events list. You can simply assign the events to the session and return it.

    # Return the new session with events
    new_session.events = all_events
    return new_session

Comment on lines 290 to 297
for sess in list_response.sessions:
full_session = await self.get_session(
app_name=app_name,
user_id=src_user_id,
session_id=sess.id,
)
if full_session:
source_sessions.append(full_session)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop creates an N+1 query problem. list_sessions is called once, and then get_session is called for each session found. This results in multiple API calls to Vertex AI, which can be inefficient for users with many sessions. If the API supports batch retrieval of sessions with their events, that would be a more performant approach.

Comment on lines 326 to 330
return await self.get_session(
app_name=new_session.app_name,
user_id=new_session.user_id,
session_id=new_session.id,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This final get_session call is redundant. You already have all the necessary information to construct the final Session object in memory. The new_session object from create_session has the correct state, and you have the all_events list. You can assign the events to the session and return it directly, avoiding an unnecessary API call.

    # Return the new session with events
    new_session.events = all_events
    return new_session

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

Labels

services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants