Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6517592
feat(sessions): Add session title management functionality
zhangp365 Nov 25, 2025
c737bba
refactor(sessions): format method signatures for the last commit.
zhangp365 Nov 25, 2025
6c96278
feat(sessions): Update session title handling to use FastAPI Body for…
zhangp365 Nov 25, 2025
2a81c3a
Merge branch 'google:main' into main
zhangp365 Nov 26, 2025
f7853c6
Merge branch 'google:main' into main
zhangp365 Nov 27, 2025
075085c
Merge remote-tracking branch 'upstream/main'
zhangp365 Nov 29, 2025
6cfc057
fix a bug in the PerAgentDatabaseSessionService class
zhangp365 Nov 30, 2025
dc98833
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 4, 2025
71a0e5d
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 4, 2025
1022f73
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 4, 2025
04f77ae
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 6, 2025
d7a722f
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 9, 2025
befd3c9
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 10, 2025
f64608c
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 11, 2025
649daaf
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 11, 2025
f8d77d6
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 11, 2025
bfe19b5
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 12, 2025
d224489
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 13, 2025
f70a2d8
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 16, 2025
880edcc
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 17, 2025
07f6061
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 17, 2025
81e5cf1
Merge remote-tracking branch 'upstream/main'
zhangp365 Dec 18, 2025
eafc126
Merge branch 'main' into main
zhangp365 Dec 18, 2025
2e9ab59
Merge branch 'main' into main
zhangp365 Dec 19, 2025
c76c9c2
Merge branch 'main' into main
zhangp365 Dec 20, 2025
3efab01
Merge branch 'main' into main
zhangp365 Dec 23, 2025
c7b8bd3
Merge branch 'main' into main
zhangp365 Dec 24, 2025
fc9d4ff
Merge branch 'main' into main
zhangp365 Dec 28, 2025
447b24b
Merge branch 'main' into main
zhangp365 Jan 1, 2026
0ae2796
Merge branch 'main' into main
zhangp365 Jan 3, 2026
b60e9e4
Merge remote-tracking branch 'upstream/main'
zhangp365 Jan 6, 2026
f4ea24e
Merge branch 'main' into main
zhangp365 Jan 7, 2026
c044877
Merge branch 'main' into main
zhangp365 Jan 8, 2026
6e10f74
Merge branch 'main' into main
zhangp365 Jan 9, 2026
853e9a0
Merge branch 'main' into main
zhangp365 Jan 11, 2026
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
15 changes: 15 additions & 0 deletions src/google/adk/cli/adk_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,21 @@ async def create_session(

return session

@app.patch(
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/title",
response_model_exclude_none=True,
)
async def update_session_title(
app_name: str,
user_id: str,
session_id: str,
title: Optional[str] = None,
) -> dict[str, str]:
await self.session_service.update_session_title(
app_name=app_name, user_id=user_id, session_id=session_id, title=title
)
return {"status": "success"}

@app.delete("/apps/{app_name}/users/{user_id}/sessions/{session_id}")
async def delete_session(
app_name: str, user_id: str, session_id: str
Expand Down
19 changes: 19 additions & 0 deletions src/google/adk/sessions/base_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ async def create_session(
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
title: Optional[str] = None,
) -> Session:
"""Creates a new session.

Expand Down Expand Up @@ -102,6 +103,24 @@ async def delete_session(
) -> None:
"""Deletes a session."""

@abc.abstractmethod
async def update_session_title(
self,
*,
app_name: str,
user_id: str,
session_id: str,
title: Optional[str],
) -> None:
"""Updates the title of a session.

Args:
app_name: The name of the app.
user_id: The id of the user.
session_id: The id of the session.
title: The new title for the session. If None, clears the title.
"""

async def append_event(self, session: Session, event: Event) -> Event:
"""Appends an event to a session object."""
if event.partial:
Expand Down
29 changes: 29 additions & 0 deletions src/google/adk/sessions/database_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ class StorageSession(Base):
MutableDict.as_mutable(DynamicJSON), default={}
)

title: Mapped[Optional[str]] = mapped_column(
String(DEFAULT_MAX_VARCHAR_LENGTH), nullable=True
)

create_time: Mapped[datetime] = mapped_column(
PreciseTimestamp, default=func.now()
)
Expand Down Expand Up @@ -215,6 +219,7 @@ def to_session(
state=state,
events=events,
last_update_time=self.update_timestamp_tz,
title=self.title,
)


Expand Down Expand Up @@ -497,6 +502,7 @@ async def create_session(
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
title: Optional[str] = None,
) -> Session:
# 1. Populate states.
# 2. Build storage session object
Expand Down Expand Up @@ -546,6 +552,7 @@ async def create_session(
user_id=user_id,
id=session_id,
state=session_state,
title=title,
)
sql_session.add(storage_session)
await sql_session.commit()
Expand Down Expand Up @@ -672,6 +679,28 @@ async def delete_session(
await sql_session.execute(stmt)
await sql_session.commit()

@override
async def update_session_title(
self,
*,
app_name: str,
user_id: str,
session_id: str,
title: Optional[str],
) -> None:
await self._ensure_tables_created()
async with self.database_session_factory() as sql_session:
storage_session = await sql_session.get(
StorageSession, (app_name, user_id, session_id)
)
if storage_session is None:
raise ValueError(
f"Session not found: app_name={app_name}, user_id={user_id},"
f" session_id={session_id}"
)
storage_session.title = title
await sql_session.commit()

@override
async def append_event(self, session: Session, event: Event) -> Event:
await self._ensure_tables_created()
Expand Down
26 changes: 26 additions & 0 deletions src/google/adk/sessions/in_memory_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@ async def create_session(
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
title: Optional[str] = None,
) -> Session:
return self._create_session_impl(
app_name=app_name,
user_id=user_id,
state=state,
session_id=session_id,
title=title,
)

def create_session_sync(
Expand All @@ -73,13 +75,15 @@ def create_session_sync(
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
title: Optional[str] = None,
) -> Session:
logger.warning('Deprecated. Please migrate to the async method.')
return self._create_session_impl(
app_name=app_name,
user_id=user_id,
state=state,
session_id=session_id,
title=title,
)

def _create_session_impl(
Expand All @@ -89,6 +93,7 @@ def _create_session_impl(
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
title: Optional[str] = None,
) -> Session:
if session_id and self._get_session_impl(
app_name=app_name, user_id=user_id, session_id=session_id
Expand Down Expand Up @@ -116,6 +121,7 @@ def _create_session_impl(
id=session_id,
state=session_state or {},
last_update_time=time.time(),
title=title,
)

if app_name not in self.sessions:
Expand Down Expand Up @@ -286,6 +292,26 @@ def _delete_session_impl(

self.sessions[app_name][user_id].pop(session_id)

@override
async def update_session_title(
self,
*,
app_name: str,
user_id: str,
session_id: str,
title: Optional[str],
) -> None:
if (
app_name not in self.sessions
or user_id not in self.sessions[app_name]
or session_id not in self.sessions[app_name][user_id]
):
raise ValueError(
f'Session not found: app_name={app_name}, user_id={user_id},'
f' session_id={session_id}'
)
self.sessions[app_name][user_id][session_id].title = title
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The check for session existence is a bit verbose. It can be written more concisely and safely using .get() with default values. This makes the code more readable and less prone to errors.

    session = self.sessions.get(app_name, {}).get(user_id, {}).get(session_id)
    if session is None:
      raise ValueError(
          f'Session not found: app_name={app_name}, user_id={user_id},'
          f' session_id={session_id}'
      )
    session.title = title


@override
async def append_event(self, session: Session, event: Event) -> Event:
if event.partial:
Expand Down
30 changes: 27 additions & 3 deletions src/google/adk/sessions/migrate_from_sqlalchemy_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,40 @@ def migrate(source_db_url: str, dest_db_path: str):

# Migrate sessions
logger.info("Migrating sessions...")
sessions = source_session.query(dss.StorageSession).all()
rows = source_session.query(
dss.StorageSession.app_name,
dss.StorageSession.user_id,
dss.StorageSession.id,
dss.StorageSession.state,
dss.StorageSession.create_time,
dss.StorageSession.update_time,
).all()
sessions = [
type(
"StorageSession",
(),
{
"app_name": row[0],
"user_id": row[1],
"id": row[2],
"state": row[3],
"create_time": row[4],
"update_time": row[5],
"title": None,
},
)()
for row in rows
]
for item in sessions:
dest_cursor.execute(
"INSERT INTO sessions (app_name, user_id, id, state, create_time,"
" update_time) VALUES (?, ?, ?, ?, ?, ?)",
"INSERT INTO sessions (app_name, user_id, id, state, title,"
" create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?)",
(
item.app_name,
item.user_id,
item.id,
json.dumps(item.state),
item.title,
item.create_time.replace(tzinfo=timezone.utc).timestamp(),
item.update_time.replace(tzinfo=timezone.utc).timestamp(),
),
Expand Down
3 changes: 3 additions & 0 deletions src/google/adk/sessions/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

from typing import Any
from typing import Optional

from pydantic import alias_generators
from pydantic import BaseModel
Expand Down Expand Up @@ -48,3 +49,5 @@ class Session(BaseModel):
call/response, etc."""
last_update_time: float = 0.0
"""The last update time of the session."""
title: Optional[str] = None
"""The title of the session."""
43 changes: 38 additions & 5 deletions src/google/adk/sessions/sqlite_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
user_id TEXT NOT NULL,
id TEXT NOT NULL,
state TEXT NOT NULL,
title TEXT,
create_time REAL NOT NULL,
update_time REAL NOT NULL,
PRIMARY KEY (app_name, user_id, id)
Expand Down Expand Up @@ -121,6 +122,7 @@ async def create_session(
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
title: Optional[str] = None,
) -> Session:
if session_id:
session_id = session_id.strip()
Expand Down Expand Up @@ -160,14 +162,15 @@ async def create_session(
# Store the session
await db.execute(
"""
INSERT INTO sessions (app_name, user_id, id, state, create_time, update_time)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO sessions (app_name, user_id, id, state, title, create_time, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
app_name,
user_id,
session_id,
json.dumps(session_state),
title,
now,
now,
),
Expand All @@ -185,6 +188,7 @@ async def create_session(
state=merged_state,
events=[],
last_update_time=now,
title=title,
)

@override
Expand All @@ -198,14 +202,15 @@ async def get_session(
) -> Optional[Session]:
async with self._get_db_connection() as db:
async with db.execute(
"SELECT state, update_time FROM sessions WHERE app_name=? AND"
"SELECT state, title, update_time FROM sessions WHERE app_name=? AND"
" user_id=? AND id=?",
(app_name, user_id, session_id),
) as cursor:
session_row = await cursor.fetchone()
if session_row is None:
return None
session_state = json.loads(session_row["state"])
title = session_row["title"]
last_update_time = session_row["update_time"]

# Build events query
Expand Down Expand Up @@ -248,6 +253,7 @@ async def get_session(
state=merged_state,
events=events,
last_update_time=last_update_time,
title=title,
)

@override
Expand All @@ -259,13 +265,13 @@ async def list_sessions(
# Fetch sessions
if user_id:
session_rows = await db.execute_fetchall(
"SELECT id, user_id, state, update_time FROM sessions WHERE"
"SELECT id, user_id, state, title, update_time FROM sessions WHERE"
" app_name=? AND user_id=?",
(app_name, user_id),
)
else:
session_rows = await db.execute_fetchall(
"SELECT id, user_id, state, update_time FROM sessions WHERE"
"SELECT id, user_id, state, title, update_time FROM sessions WHERE"
" app_name=?",
(app_name,),
)
Expand All @@ -291,6 +297,7 @@ async def list_sessions(
for row in session_rows:
session_user_id = row["user_id"]
session_state = json.loads(row["state"])
title = row["title"]
user_state = user_states_map.get(session_user_id, {})
merged_state = _merge_state(app_state, user_state, session_state)
sessions_list.append(
Expand All @@ -301,10 +308,36 @@ async def list_sessions(
state=merged_state,
events=[],
last_update_time=row["update_time"],
title=title,
)
)
return ListSessionsResponse(sessions=sessions_list)

@override
async def update_session_title(
self,
*,
app_name: str,
user_id: str,
session_id: str,
title: Optional[str],
) -> None:
async with self._get_db_connection() as db:
async with db.execute(
"SELECT 1 FROM sessions WHERE app_name=? AND user_id=? AND id=?",
(app_name, user_id, session_id),
) as cursor:
if not await cursor.fetchone():
raise ValueError(
f"Session not found: app_name={app_name}, user_id={user_id},"
f" session_id={session_id}"
)
await db.execute(
"UPDATE sessions SET title=? WHERE app_name=? AND user_id=? AND id=?",
(title, app_name, user_id, session_id),
)
await db.commit()
Comment on lines 357 to 375
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 implementation uses two separate database queries (SELECT then UPDATE) to update the session title. This can be optimized into a single, more efficient, and atomic UPDATE operation. You can check the cursor.rowcount after the UPDATE to see if any rows were affected. If rowcount is 0, it means the session was not found, and you can raise the ValueError.

  async def update_session_title(
      self,
      *,
      app_name: str,
      user_id: str,
      session_id: str,
      title: Optional[str],
  ) -> None:
    async with self._get_db_connection() as db:
      cursor = await db.execute(
          "UPDATE sessions SET title=? WHERE app_name=? AND user_id=? AND id=?",
          (title, app_name, user_id, session_id),
      )
      if cursor.rowcount == 0:
        raise ValueError(
            f"Session not found: app_name={app_name}, user_id={user_id},"
            f" session_id={session_id}"
        )
      await db.commit()


@override
async def delete_session(
self, *, app_name: str, user_id: str, session_id: str
Expand Down
Loading