Skip to content

feat(memory): add DatabaseMemoryService with SQL backend and agent scratchpad#4736

Open
Raman369AI wants to merge 2 commits intogoogle:mainfrom
Raman369AI:feat/database-memory-service
Open

feat(memory): add DatabaseMemoryService with SQL backend and agent scratchpad#4736
Raman369AI wants to merge 2 commits intogoogle:mainfrom
Raman369AI:feat/database-memory-service

Conversation

@Raman369AI
Copy link

Closes #4735

Summary

  • Adds DatabaseMemoryService, a BaseMemoryService backed by any SQLAlchemy async-compatible database (SQLite, PostgreSQL, MySQL, MariaDB)
  • Adds MemorySearchBackend ABC and KeywordSearchBackend (LIKE/ILIKE, AND-first → OR-fallback tokenization)
  • Adds a scratchpad subsystem (KV store + append-only log) for intermediate agent working memory, exposed as four BaseTool subclasses

Motivation

The existing InMemoryMemoryService is volatile and explicitly test-only. Developers not using Vertex AI have no durable, self-hosted memory option. DatabaseMemoryService fills that gap using the same SQLAlchemy async pattern already established by DatabaseSessionService.

Changes

File Action
src/google/adk/memory/schemas/__init__.py New — package marker
src/google/adk/memory/schemas/memory_schema.py New — ORM tables (adk_memory_entries, adk_scratchpad_kv, adk_scratchpad_log) with a separate DeclarativeBase
src/google/adk/memory/memory_search_backend.py New — MemorySearchBackend ABC + KeywordSearchBackend
src/google/adk/memory/database_memory_service.py New — main service class
src/google/adk/tools/scratchpad_tool.py New — ScratchpadGetTool, ScratchpadSetTool, ScratchpadAppendLogTool, ScratchpadGetLogTool + singleton instances
src/google/adk/memory/__init__.py Modified — exports three new public symbols, guarded by try/except ImportError
tests/unittests/memory/test_database_memory_service.py New — 38 unit tests

Design notes

  • Separate DeclarativeBase from sessions schema — avoids table coupling while allowing the same DB
  • session_id='' sentinel in scratchpad tables — enables user-level (non-session) scope without nullable PK columns
  • Lazy table creation behind asyncio.Lock (double-checked) — no await svc.initialize() call required
  • try/except ImportError in __init__.py — SQLAlchemy + async driver are optional; users without them are unaffected
  • TYPE_CHECKING guard for Event/Session imports — correct pattern with from __future__ import annotations

Test plan

All tests run with sqlite+aiosqlite:///:memory: — no external database required.

uv sync --extra test
pytest tests/unittests/memory/test_database_memory_service.py -v
# 38 passed

pytest tests/unittests/memory/ -v
# 78 passed, 0 failures (existing tests unaffected)

Scenarios covered:

  • add_session_to_memory — filters empty events, persists content/author/timestamp
  • Re-ingest same session is idempotent (no duplicates)
  • add_events_to_memory — delta, skips duplicate event_id
  • add_memory — direct MemoryEntry persist, auto-UUID
  • search_memory — AND match, OR fallback, empty query, no match
  • Scratchpad KV — set/get/overwrite/delete/list, JSON types, session scoping
  • Scratchpad log — append/get, tag filter, limit, session scoping
  • Multi-user isolation — user A results do not leak to user B
  • Custom MemorySearchBackend is honoured
  • All 4 scratchpad tools — happy path and wrong-service ValueError
  • Engine construction errors raise ValueError

Formatting

isort + pyink --config pyproject.toml run on all changed files — no remaining violations.

@google-cla
Copy link

google-cla bot commented Mar 6, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, 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 introduces a robust, persistent memory solution for agents, addressing the current limitation of volatile in-memory storage. By integrating a DatabaseMemoryService with SQLAlchemy, it allows developers to utilize various SQL databases for durable memory storage. Furthermore, it enhances agent capabilities with a dedicated scratchpad subsystem, providing tools for agents to manage their working memory effectively through key-value pairs and an append-only log, thereby improving state management and complex task execution.

Highlights

  • DatabaseMemoryService: Added a new DatabaseMemoryService which provides a durable, SQL-backed memory solution compatible with various SQLAlchemy async-compatible databases (SQLite, PostgreSQL, MySQL, MariaDB).
  • Memory Search Backend: Introduced a MemorySearchBackend abstract base class and a KeywordSearchBackend implementation, enabling keyword-based searching within the stored memory entries.
  • Agent Scratchpad Subsystem: Implemented a scratchpad subsystem, featuring a key-value store and an append-only log, designed for intermediate agent working memory. This includes four new BaseTool subclasses for agent interaction.

🧠 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.

Changelog
  • .gitignore
    • Added AGENT_HANDOFF.md to the ignore list.
  • src/google/adk/memory/init.py
    • Updated to conditionally import and export new DatabaseMemoryService, KeywordSearchBackend, and MemorySearchBackend symbols, handling ImportError if dependencies are not installed.
  • src/google/adk/memory/database_memory_service.py
    • Implemented the DatabaseMemoryService class, providing SQL-backed memory storage, session management, and scratchpad functionalities (KV store and append-only log).
  • src/google/adk/memory/memory_search_backend.py
    • Defined the abstract base class MemorySearchBackend and its concrete implementation KeywordSearchBackend for keyword-based memory searching.
  • src/google/adk/memory/schemas/init.py
    • Created as a package marker for the memory schemas.
  • src/google/adk/memory/schemas/memory_schema.py
    • Defined SQLAlchemy ORM models (StorageMemoryEntry, StorageScratchpadKV, StorageScratchpadLog) for persistent storage of memory entries and agent scratchpad data.
  • src/google/adk/tools/scratchpad_tool.py
    • Introduced four new BaseTool subclasses (ScratchpadGetTool, ScratchpadSetTool, ScratchpadAppendLogTool, ScratchpadGetLogTool) to allow agents to interact with the scratchpad.
  • tests/unittests/memory/test_database_memory_service.py
    • Added comprehensive unit tests covering the functionality of DatabaseMemoryService, including memory persistence, search, scratchpad operations, and tool interactions.
Activity
  • The author confirmed that all 38 new unit tests for DatabaseMemoryService passed successfully.
  • The author verified that existing memory tests (78 total) remained unaffected by the changes.
  • The author ensured code formatting was applied using isort and pyink on all modified files.
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 Mar 6, 2026
@adk-bot
Copy link
Collaborator

adk-bot commented Mar 6, 2026

Response from ADK Triaging Agent

Hello @Raman369AI, thank you for your contribution!

Before we can review your pull request, you'll need to sign our Contributor License Agreement (CLA). It seems the CLA check has failed. You can review and sign the agreement here: https://cla.developers.google.com/

Once the CLA is signed, we can proceed with the review. Thanks!

Adds a durable, RDBMS-backed memory service that works with any
SQLAlchemy-supported database (SQLite, PostgreSQL, MySQL, MariaDB)
as an alternative to the volatile InMemoryMemoryService.

Key additions:
- DatabaseMemoryService: implements BaseMemoryService with lazy table
  creation, idempotent session ingest, and delta event ingestion
- MemorySearchBackend ABC + KeywordSearchBackend: LIKE/ILIKE search
  with AND-first → OR-fallback tokenization strategy
- Scratchpad KV store and append-only log for intermediate agent state
- Four agent-callable BaseTool subclasses: scratchpad_get_tool,
  scratchpad_set_tool, scratchpad_append_log_tool, scratchpad_get_log_tool
- 38 unit tests covering all methods, tool happy-paths, wrong-service
  errors, multi-user isolation, and session scoping
@Raman369AI Raman369AI force-pushed the feat/database-memory-service branch from 5c69b8f to 7e9c8e9 Compare March 6, 2026 04:41
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 DatabaseMemoryService that provides a durable, SQL-backed memory and scratchpad for agents, including a search backend abstraction, SQLAlchemy ORM schemas, and agent-facing tools. While the implementation demonstrates high code quality and strong patterns, a security issue was identified: sensitive database credentials could be leaked in error messages if the database initialization fails. This should be addressed by sanitizing the database URL in exception messages. Additionally, there are a couple of suggestions aimed at enhancing code readability and simplifying logic in a few areas.

Comment on lines +114 to +117
except ArgumentError as exc:
raise ValueError(
f"Invalid database URL format or argument '{db_url}'."
) from exc
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The DatabaseMemoryService.__init__ method catches ArgumentError and ImportError during database engine creation and re-raises them as a ValueError. The error message includes the full db_url, which may contain sensitive credentials such as database passwords. If this exception is logged or displayed to a user, it could lead to the exposure of these credentials.

To remediate this, avoid including the full db_url in the error message. If the URL must be included for debugging purposes, ensure that any passwords or sensitive tokens are sanitized or masked.

Comment on lines +118 to +121
except ImportError as exc:
raise ValueError(
f"Database-related module not found for URL '{db_url}'."
) from exc
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The DatabaseMemoryService.__init__ method catches ArgumentError and ImportError during database engine creation and re-raises them as a ValueError. The error message includes the full db_url, which may contain sensitive credentials such as database passwords. If this exception is logged or displayed to a user, it could lead to the exposure of these credentials.

To remediate this, avoid including the full db_url in the error message. If the URL must be included for debugging purposes, ensure that any passwords or sensitive tokens are sanitized or masked.

"""Return True if the event has no usable text content."""
if not event.content or not event.content.parts:
return True
return not any(part.text for part in event.content.parts if part.text)
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 logic to check for usable text content can be simplified. The if part.text condition within the generator expression is redundant because any() on an iterator of strings will correctly evaluate to False only if all strings are empty.

Suggested change
return not any(part.text for part in event.content.parts if part.text)
return not any(part.text for part in event.content.parts)

Comment on lines +88 to +94
tokens = [
cleaned
for raw in query.split()
if raw.strip()
for cleaned in [re.sub(r"[^\w]", "", raw).lower()]
if cleaned
]
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 list comprehension is quite complex and can be difficult to read due to the nested for and the use of a single-element list to assign a variable. For better maintainability and readability, I suggest refactoring this into a simple for loop.

Note that query.split() (with no arguments) handles splitting by any whitespace and discards empty strings, so the if raw.strip() check becomes unnecessary.

Suggested change
tokens = [
cleaned
for raw in query.split()
if raw.strip()
for cleaned in [re.sub(r"[^\w]", "", raw).lower()]
if cleaned
]
tokens = []
for raw_token in query.split():
# Clean the token by removing non-alphanumeric characters and lowercasing.
cleaned_token = re.sub(r"[^\w]", "", raw_token).lower()
# Add the token only if it's not empty after cleaning.
if cleaned_token:
tokens.append(cleaned_token)

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.

feat(memory): SQL-backed DatabaseMemoryService with scratchpad for durable agent memory

2 participants