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
18 changes: 17 additions & 1 deletion docs/guides/memory/memory_service/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ holding `memories`, a list of `MemoryEntry`. Each entry carries `content` (a
`custom_metadata`. Memory is scoped by the `(app_name, user_id)` pair, so one
user never sees another's memories.

### Lifecycle & Removal

Memory persists independently of sessions. Deleting a session in a `BaseSessionService`
does not remove what was previously ingested into memory. To purge memories,
`BaseMemoryService` provides explicit lifecycle methods:

* `delete_session_memory(*, app_name, user_id, session_id)` removes all memory
entries associated with a specific session.
* `delete_user_memory(*, app_name, user_id)` removes all memories for a user,
supporting right-to-be-forgotten / GDPR deletion requirements.

Inside an agent callback, `Context` provides matching helpers
`await ctx.delete_session_memory()` and `await ctx.delete_user_memory()`. Services
that do not support deletion raise `NotImplementedError`.

### From inside an agent

`Context` — what tools and callbacks receive — exposes the same operations
Expand All @@ -150,7 +165,8 @@ async def save_to_memory(callback_context: Context) -> None:

Attach that as an `after_agent_callback` and each turn is ingested as it
finishes, rather than at some later point you have to remember to trigger.
`Context` also offers `add_events_to_memory`, `add_memory`, and `search_memory`.
`Context` also offers `add_events_to_memory`, `add_memory`,
`search_memory`, `delete_session_memory`, and `delete_user_memory`.

## The memory tools

Expand Down
39 changes: 39 additions & 0 deletions src/google/adk/agents/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,45 @@ async def search_memory(self, query: str) -> SearchMemoryResponse:
query=query,
)

async def delete_session_memory(
self,
session_id: str | None = None,
) -> None:
"""Removes memory items for the current (or specified) session.

Args:
session_id: Optional session ID to delete. Defaults to the current
session ID.

Raises:
ValueError: If memory service is not available.
"""
if self._invocation_context.memory_service is None:
raise ValueError(
'Cannot delete session memory: memory service is not available.'
)
target_session_id = session_id or self._invocation_context.session.id
await self._invocation_context.memory_service.delete_session_memory(
app_name=self._invocation_context.app_name,
user_id=self._invocation_context.user_id,
session_id=target_session_id,
)

async def delete_user_memory(self) -> None:
"""Removes all memories for the current user.

Raises:
ValueError: If memory service is not available.
"""
if self._invocation_context.memory_service is None:
raise ValueError(
'Cannot delete user memory: memory service is not available.'
)
await self._invocation_context.memory_service.delete_user_memory(
app_name=self._invocation_context.app_name,
user_id=self._invocation_context.user_id,
)

# ============================================================================
# UI Widget methods
# ============================================================================
Expand Down
64 changes: 63 additions & 1 deletion src/google/adk/memory/base_memory_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,19 @@ class BaseMemoryService(ABC):
"""Base class for memory services.

The service provides functionality to ingest conversation history into memory
so that it can be used for user queries.
so that it can be used for user queries, as well as removal APIs to manage
retention and data lifecycles.

### Retention & Lifecycle Management
Session services persist conversational history within an active session, while
memory services persist durable recall across sessions. Deleting a session via
a `BaseSessionService` removes only the session record; memories previously
ingested or synthesized from that session remain in the memory service until
explicitly removed.

Callers can use `delete_session_memory` to remove memories associated with a
specific session, or `delete_user_memory` to purge all memories for a user.
Services that do not support deletion will raise `NotImplementedError`.
"""

@abstractmethod
Expand Down Expand Up @@ -138,3 +150,53 @@ async def search_memory(
Returns:
A SearchMemoryResponse containing the matching memories.
"""

async def delete_session_memory(
self,
*,
app_name: str,
user_id: str,
session_id: str,
) -> None:
"""Removes all memory items associated with a specific session.

This enables retention lifecycle management when a session is closed or
deleted, ensuring that memories ingested from that session (e.g., via
`add_session_to_memory` or `add_events_to_memory`) do not outlive the
intended retention window.

Args:
app_name: The application name for memory scope.
user_id: The user ID for memory scope.
session_id: The session ID whose memories should be removed.

Raises:
NotImplementedError: If the memory service does not support session memory
deletion.
"""
raise NotImplementedError(
"This memory service does not support session memory deletion."
)

async def delete_user_memory(
self,
*,
app_name: str,
user_id: str,
) -> None:
"""Removes all memories associated with a user.

This provides a complete lifecycle removal path for user data (e.g., for
right-to-be-forgotten / GDPR compliance or user account deletion).

Args:
app_name: The application name for memory scope.
user_id: The user ID whose memories should be removed.

Raises:
NotImplementedError: If the memory service does not support user memory
deletion.
"""
raise NotImplementedError(
"This memory service does not support user memory deletion."
)
28 changes: 26 additions & 2 deletions src/google/adk/memory/in_memory_memory_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,6 @@ async def search_memory(
with self._lock:
# Copy the events into a stable snapshot while holding the lock. Iterating
# a live reference outside the lock would race with concurrent writers
# (add_session_to_memory / add_events_to_memory) mutating the same dict
# and lists, raising "dictionary changed size during iteration".
session_event_lists = [
list(events)
for events in self._session_events.get(user_key, {}).values()
Expand Down Expand Up @@ -188,3 +186,29 @@ async def search_memory(
return SearchMemoryResponse(
memories=[memory for _, memory in scored_memories[:_MAX_SEARCH_RESULTS]]
)

@override
async def delete_session_memory(
self,
*,
app_name: str,
user_id: str,
session_id: str,
) -> None:
user_key = _user_key(app_name, user_id)
with self._lock:
if user_key in self._session_events:
self._session_events[user_key].pop(session_id, None)
if not self._session_events[user_key]:
del self._session_events[user_key]

@override
async def delete_user_memory(
self,
*,
app_name: str,
user_id: str,
) -> None:
user_key = _user_key(app_name, user_id)
with self._lock:
self._session_events.pop(user_key, None)
73 changes: 73 additions & 0 deletions tests/unittests/agents/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,79 @@ async def test_add_memory_no_service_raises(self, mock_invocation_context):
]
)

async def test_delete_session_memory_default_current_session(
self, mock_invocation_context
):

memory_service = AsyncMock()
mock_invocation_context.memory_service = memory_service

context = Context(mock_invocation_context)
await context.delete_session_memory()

memory_service.delete_session_memory.assert_called_once_with(
app_name=mock_invocation_context.app_name,
user_id=mock_invocation_context.user_id,
session_id=mock_invocation_context.session.id,
)

async def test_delete_session_memory_explicit_session_id(
self, mock_invocation_context
):

memory_service = AsyncMock()
mock_invocation_context.memory_service = memory_service

context = Context(mock_invocation_context)
await context.delete_session_memory(session_id="custom-session-id")

memory_service.delete_session_memory.assert_called_once_with(
app_name=mock_invocation_context.app_name,
user_id=mock_invocation_context.user_id,
session_id="custom-session-id",
)

async def test_delete_session_memory_no_service_raises(
self, mock_invocation_context
):

mock_invocation_context.memory_service = None

context = Context(mock_invocation_context)
with pytest.raises(
ValueError,
match=(
r"Cannot delete session memory: memory service is not available\."
),
):
await context.delete_session_memory()

async def test_delete_user_memory_success(self, mock_invocation_context):

memory_service = AsyncMock()
mock_invocation_context.memory_service = memory_service

context = Context(mock_invocation_context)
await context.delete_user_memory()

memory_service.delete_user_memory.assert_called_once_with(
app_name=mock_invocation_context.app_name,
user_id=mock_invocation_context.user_id,
)

async def test_delete_user_memory_no_service_raises(
self, mock_invocation_context
):

mock_invocation_context.memory_service = None

context = Context(mock_invocation_context)
with pytest.raises(
ValueError,
match=r"Cannot delete user memory: memory service is not available\.",
):
await context.delete_user_memory()


class TestContextAddUiWidget:
"""Test render_ui_widget method in Context."""
Expand Down
Loading
Loading