Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 17 additions & 11 deletions src/agentic_cli/workflow/adk/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,23 +1092,25 @@ def _job_result_payload(record, result: Any) -> dict:
# Sessions (native — DatabaseSessionService persists events continuously)
# -------------------------------------------------------------------------

async def session_exists(self, session_id: str) -> bool:
"""True if the store holds this session with any events."""
async def session_exists(
self, session_id: str, *, user_id: str | None = None
) -> bool:
"""True if the store holds this session (for the given user) with any events."""
if not self._session_service:
return False
session = await self._session_service.get_session(
app_name=self.app_name,
user_id=self._settings.default_user,
user_id=user_id or self._settings.default_user,
session_id=session_id,
)
return session is not None and bool(getattr(session, "events", None))

async def list_sessions(self) -> list[dict]:
"""List persisted sessions for the current user (most recent first)."""
async def list_sessions(self, *, user_id: str | None = None) -> list[dict]:
"""List persisted sessions for a user (default: settings.default_user)."""
if not self._session_service:
return []
resp = await self._session_service.list_sessions(
app_name=self.app_name, user_id=self._settings.default_user,
app_name=self.app_name, user_id=user_id or self._settings.default_user,
)
sessions = [
{
Expand All @@ -1121,24 +1123,28 @@ async def list_sessions(self) -> list[dict]:
sessions.sort(key=lambda x: x["last_update"] or 0, reverse=True)
return sessions

async def delete_session(self, session_id: str) -> bool:
"""Delete a persisted session from the store."""
async def delete_session(
self, session_id: str, *, user_id: str | None = None
) -> bool:
"""Delete a persisted session (for the given user) from the store."""
if not self._session_service:
return False
await self._session_service.delete_session(
app_name=self.app_name,
user_id=self._settings.default_user,
user_id=user_id or self._settings.default_user,
session_id=session_id,
)
return True

async def recent_messages(self, session_id: str, limit: int = 20) -> list[dict]:
async def recent_messages(
self, session_id: str, limit: int = 20, *, user_id: str | None = None
) -> list[dict]:
"""Recent text messages from the stored session (for fact extraction)."""
if not self._session_service:
return []
session = await self._session_service.get_session(
app_name=self.app_name,
user_id=self._settings.default_user,
user_id=user_id or self._settings.default_user,
session_id=session_id,
)
if session is None or not getattr(session, "events", None):
Expand Down
65 changes: 65 additions & 0 deletions tests/workflow/test_session_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,68 @@ async def test_persists_across_fresh_manager(self, tmp_path: Path):
# A second manager over the same sqlite file sees the session.
mgr2, _ = self._manager(tmp_path)
assert await mgr2.session_exists("sess-z") is True


class TestAdkSessionUserScope:
"""Session APIs accept an explicit user_id, defaulting to settings.default_user.

process() always accepted arbitrary user_id (and job-resume threads
record.user_id), but the query/manage APIs hard-coded default_user —
sessions created for another user were invisible to them.
"""

@pytest.fixture(autouse=True)
def _require_adk(self):
pytest.importorskip("google.adk")

def _manager(self, tmp_path: Path):
from agentic_cli.workflow.adk.manager import GoogleADKWorkflowManager

settings = _settings(tmp_path, session_store="sqlite")
mgr = GoogleADKWorkflowManager.__new__(GoogleADKWorkflowManager)
mgr._settings = settings
mgr._app_name = "test_app"
mgr.session_id = "default_session"
mgr._session_service = mgr._make_session_service()
return mgr, settings

async def _seed_as(self, mgr, user_id: str, sid: str, text: str):
from google.adk.events import Event
from google.genai import types

s = await mgr._session_service.create_session(
app_name=mgr.app_name, user_id=user_id, session_id=sid
)
await mgr._session_service.append_event(
session=s,
event=Event(
author="user",
content=types.Content(
role="user", parts=[types.Part.from_text(text=text)]
),
),
)

async def test_session_apis_scope_to_given_user(self, tmp_path: Path):
mgr, settings = self._manager(tmp_path)
await self._seed_as(mgr, "alice", "sess-a", "alice message")
await self._seed_as(mgr, settings.default_user, "sess-d", "default message")

# Default scope: unchanged behavior, sees only default_user's sessions
assert await mgr.session_exists("sess-d") is True
assert await mgr.session_exists("sess-a") is False

# Explicit user scope reaches alice's session through every API
assert await mgr.session_exists("sess-a", user_id="alice") is True

listed = await mgr.list_sessions(user_id="alice")
assert [s["session_id"] for s in listed] == ["sess-a"]

recent = await mgr.recent_messages("sess-a", user_id="alice")
assert recent and recent[-1]["content"] == "alice message"

assert await mgr.delete_session("sess-a", user_id="alice") is True
assert await mgr.session_exists("sess-a", user_id="alice") is False

# Default user's session untouched by alice-scoped operations
assert await mgr.session_exists("sess-d") is True
Loading