Memory
Long-term semantic memory for AI agents using adk-memory.
Overview
The memory system provides persistent, searchable storage for agent conversations. Unlike session state (which is ephemeral), memory persists across sessions and enables agents to recall relevant context from past interactions.
Installation
[dependencies]
adk-memory = "2.0.0"
Core Concepts
MemoryEntry
A single memory record with content, author, and timestamp:
use adk_memory::MemoryEntry;
use adk_core::Content;
use chrono::Utc;
let entry = MemoryEntry {
content: Content::new("user").with_text("I prefer dark mode"),
author: "user".to_string(),
timestamp: Utc::now(),
};
MemoryService Trait
The core trait for memory backends:
#[async_trait]
pub trait MemoryService: Send + Sync {
/// Store session memories for a user
async fn add_session(
&self,
app_name: &str,
user_id: &str,
session_id: &str,
entries: Vec<MemoryEntry>,
) -> Result<()>;
/// Search memories by query
async fn search(&self, req: SearchRequest) -> Result<SearchResponse>;
}
SearchRequest
Query parameters for memory search:
use adk_memory::SearchRequest;
let request = SearchRequest {
query: "user preferences".to_string(),
user_id: "user-123".to_string(),
app_name: "my_app".to_string(),
limit: None,
min_score: None,
project_id: None, // None = global only, Some("id") = global + project
};
InMemoryMemoryService
Simple in-memory implementation for development and testing:
use adk_memory::{InMemoryMemoryService, MemoryService, MemoryEntry, SearchRequest};
use adk_core::Content;
use chrono::Utc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let memory = InMemoryMemoryService::new();
// Store memories from a session
let entries = vec![
MemoryEntry {
content: Content::new("user").with_text("I like Rust programming"),
author: "user".to_string(),
timestamp: Utc::now(),
},
MemoryEntry {
content: Content::new("assistant").with_text("Rust is great for systems programming"),
author: "assistant".to_string(),
timestamp: Utc::now(),
},
];
memory.add_session("my_app", "user-123", "session-1", entries).await?;
// Search memories
let request = SearchRequest {
query: "Rust".to_string(),
user_id: "user-123".to_string(),
app_name: "my_app".to_string(),
limit: None,
min_score: None,
project_id: None,
};
let response = memory.search(request).await?;
println!("Found {} memories", response.memories.len());
Ok(())
}
Memory Isolation
Memories are isolated by:
- app_name: Different applications have separate memory spaces
- user_id: Each user's memories are private
- project_id (optional): Entries can be scoped to a project within a user
// User A's memories
memory.add_session("app", "user-a", "sess-1", entries_a).await?;
// User B's memories (separate)
memory.add_session("app", "user-b", "sess-1", entries_b).await?;
// Search only returns user-a's memories
let request = SearchRequest {
query: "topic".to_string(),
user_id: "user-a".to_string(),
app_name: "app".to_string(),
limit: None,
min_score: None,
project_id: None, // None = global entries only
};
Project-Scoped Memory
Memories can be scoped to a project within a user. The isolation key becomes (app_name, user_id, project_id?):
- Global entries (
project_id = None): visible in all project contexts and in global-only searches. - Project entries (
project_id = Some(id)): visible only when searching within that specific project. - Project search (
project_id = Some(id)): returns global entries + entries for that project. - Global search (
project_id = None): returns only global entries.
Storing project-scoped entries
use adk_memory::{InMemoryMemoryService, MemoryService, MemoryEntry};
use adk_core::Content;
use chrono::Utc;
let service = InMemoryMemoryService::new();
let entry = MemoryEntry {
content: Content::new("user").with_text("Project uses microservices"),
author: "user".to_string(),
timestamp: Utc::now(),
};
// Global entry (no project scope)
service.add_session("app", "user-1", "sess-1", vec![entry.clone()]).await?;
// Project-scoped entry
service.add_session_to_project("app", "user-1", "sess-2", "my-project", vec![entry.clone()]).await?;
// Single entry to a project
service.add_entry_to_project("app", "user-1", "my-project", entry).await?;
Searching with project scope
use adk_memory::SearchRequest;
// Global-only search β returns only global entries
let global = service.search(SearchRequest {
query: "microservices".into(),
user_id: "user-1".into(),
app_name: "app".into(),
limit: None,
min_score: None,
project_id: None,
}).await?;
// Project search β returns global + project entries
let project = service.search(SearchRequest {
query: "microservices".into(),
user_id: "user-1".into(),
app_name: "app".into(),
limit: None,
min_score: None,
project_id: Some("my-project".into()),
}).await?;
Project-scoped deletion
// Delete entries matching a query within a project only
service.delete_entries_in_project("app", "user-1", "my-project", "microservices").await?;
// Delete ALL entries for a project
service.delete_project("app", "user-1", "my-project").await?;
// Global delete β only removes global entries, project entries are unaffected
service.delete_entries("app", "user-1", "microservices").await?;
// GDPR delete_user β removes everything (global + all projects)
service.delete_user("app", "user-1").await?;
MemoryServiceAdapter with project scope
The MemoryServiceAdapter bridges MemoryService to adk_core::Memory. Use with_project_id() to scope all operations:
use adk_memory::{InMemoryMemoryService, MemoryServiceAdapter};
use adk_core::Memory;
use std::sync::Arc;
let service = Arc::new(InMemoryMemoryService::new());
// Adapter without project β operates on global entries
let global_adapter = MemoryServiceAdapter::new(service.clone(), "app", "user-1");
// Adapter with project β all search/add/delete operations scoped to the project
let project_adapter = MemoryServiceAdapter::new(service.clone(), "app", "user-1")
.with_project_id("my-project");
// Core Memory trait also supports ad-hoc project access
global_adapter.search_in_project("query", "other-project").await?;
global_adapter.add_to_project(entry, "other-project").await?;
Project ID validation
Project identifiers are validated on all write operations:
- Must not be empty
- Must not exceed 256 characters
use adk_memory::validate_project_id;
validate_project_id("my-project")?; // Ok
validate_project_id("")?; // Err: must not be empty
validate_project_id(&"x".repeat(257))?; // Err: exceeds 256 chars
Search semantics matrix
SearchRequest.project_id | Returns Global Entries | Returns Project Entries |
|---|---|---|
None | β matching query | β none |
Some("A") | β matching query | β only project "A" entries matching query |
Delete semantics matrix
| Operation | Scope |
|---|---|
delete_entries (no project) | Global entries matching query only |
delete_entries_in_project("A") | Project "A" entries matching query only |
delete_project("A") | All entries for project "A" |
delete_user | All entries (global + all projects) |
Search Behavior
The InMemoryMemoryService uses word-based matching:
- Query is tokenized into words (lowercase)
- Each memory's content is tokenized
- Memories with any matching words are returned
// Query: "rust programming"
// Matches memories containing "rust" OR "programming"
Custom Memory Backend
Implement MemoryService for custom storage (e.g., vector database):
use adk_memory::{MemoryService, MemoryEntry, SearchRequest, SearchResponse};
use adk_core::Result;
use async_trait::async_trait;
pub struct VectorMemoryService {
// Your vector DB client
}
#[async_trait]
impl MemoryService for VectorMemoryService {
async fn add_session(
&self,
app_name: &str,
user_id: &str,
session_id: &str,
entries: Vec<MemoryEntry>,
) -> Result<()> {
// 1. Generate embeddings for each entry
// 2. Store in vector database with metadata
Ok(())
}
async fn search(&self, req: SearchRequest) -> Result<SearchResponse> {
// 1. Generate embedding for query
// 2. Perform similarity search
// 3. Return top-k results
Ok(SearchResponse { memories: vec![] })
}
}
Integration with Agents
Memory integrates with LlmAgentBuilder:
use adk_agent::LlmAgentBuilder;
use adk_memory::InMemoryMemoryService;
use std::sync::Arc;
let memory = Arc::new(InMemoryMemoryService::new());
let agent = LlmAgentBuilder::new("assistant")
.model(model)
.instruction("You are a helpful assistant with memory.")
.memory(memory)
.build()?;
When memory is configured:
- Before each turn, relevant memories are searched
- Matching memories are injected into the context
- After each session, conversation is stored as memories
Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Agent Request β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Memory Search β
β β
β SearchRequest { query, user_id, app_name, project_id } β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β MemoryService β β
β β βββββββββββ ββββββββββββ ββββββββββ ββββββββββββ β β
β β βInMemory β β SQLite β βPostgresβ β Redis β β β
β β β(dev) β β β βpgvectorβ β β β β
β β βββββββββββ ββββββββββββ ββββββββββ ββββββββββββ β β
β β βββββββββββ ββββββββββββ β β
β β βMongoDB β β Neo4j β β β
β β βββββββββββ ββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β SearchResponse { memories: Vec<MemoryEntry> } β
β (filtered by project scope) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Context Injection β
β β
β Relevant memories added to agent context β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Agent Execution β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Memory Storage β
β β
β Session conversation stored for future recall β
β (global or project-scoped) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Best Practices
| Practice | Description |
|---|---|
| Use vector DB in production | InMemory is for dev/test only |
| Scope by user | Always include user_id for privacy |
| Limit results | Cap returned memories to avoid context overflow |
| Clean old memories | Implement TTL or archival for stale data |
| Embed strategically | Store summaries, not raw conversations |
Comparison with Sessions
| Feature | Session State | Memory |
|---|---|---|
| Persistence | Session lifetime | Permanent |
| Scope | Single session | Cross-session |
| Search | Key-value lookup | Semantic search |
| Use case | Current context | Long-term recall |
Previous: β Guardrails | Next: Studio β