Overview
The Memory trait defines the standard interface for agent memory and state persistence in the MoFA framework. It provides capabilities for storing, retrieving, and searching memory items, as well as managing conversation history.
All memory implementations (in-memory, database-backed, vector stores) implement this trait to provide a unified API for agents.
Trait Definition
#[async_trait]
pub trait Memory: Send + Sync {
async fn store(&mut self, key: &str, value: MemoryValue) -> AgentResult<()>;
async fn retrieve(&self, key: &str) -> AgentResult<Option<MemoryValue>>;
async fn remove(&mut self, key: &str) -> AgentResult<bool>;
async fn contains(&self, key: &str) -> AgentResult<bool>;
async fn search(&self, query: &str, limit: usize) -> AgentResult<Vec<MemoryItem>>;
async fn clear(&mut self) -> AgentResult<()>;
async fn get_history(&self, session_id: &str) -> AgentResult<Vec<Message>>;
async fn add_to_history(&mut self, session_id: &str, message: Message) -> AgentResult<()>;
async fn clear_history(&mut self, session_id: &str) -> AgentResult<()>;
async fn stats(&self) -> AgentResult<MemoryStats>;
fn memory_type(&self) -> &str;
}
Core Methods
Storage Operations
store
async fn(&mut self, key: &str, value: MemoryValue) -> AgentResult<()>
required
Stores a memory item with the given key and value.Parameters:
key: Unique identifier for the memory item
value: The memory value to store (text, embedding, structured data, or binary)
Example:memory.store("user_preference", MemoryValue::text("Dark mode")).await?;
memory.store("user_data", MemoryValue::structured(json!({
"name": "Alice",
"age": 30
}))).await?;
retrieve
async fn(&self, key: &str) -> AgentResult<Option<MemoryValue>>
required
Retrieves a memory item by key.Parameters:Returns:
Some(MemoryValue) if found, None if not found
Example:if let Some(value) = memory.retrieve("user_preference").await? {
println!("Preference: {}", value.as_text().unwrap());
}
remove
async fn(&mut self, key: &str) -> AgentResult<bool>
required
Removes a memory item by key.Parameters:Returns:
true if the item was removed, false if it didn’t exist
Example:let removed = memory.remove("old_data").await?;
contains
async fn(&self, key: &str) -> AgentResult<bool>
default:"retrieve().is_some()"
Checks if a memory item exists.Parameters:Returns:
true if the item exists, false otherwise
clear
async fn(&mut self) -> AgentResult<()>
required
Clears all memory items (does not clear conversation history).Example:
Semantic Search
search
async fn(&self, query: &str, limit: usize) -> AgentResult<Vec<MemoryItem>>
required
Performs semantic search over stored memories.Parameters:
query: The search query string
limit: Maximum number of results to return
Returns:
- Vector of
MemoryItem sorted by relevance (highest score first)
Example:let results = memory.search("user preferences", 5).await?;
for item in results {
println!("Found: {} (score: {})", item.key, item.score);
}
Conversation History
get_history
async fn(&self, session_id: &str) -> AgentResult<Vec<Message>>
required
Retrieves conversation history for a session.Parameters:
session_id: The conversation session identifier
Returns:
- Vector of messages in chronological order
Example:let history = memory.get_history("session_123").await?;
for msg in history {
println!("{}: {}", msg.role, msg.content);
}
add_to_history
async fn(&mut self, session_id: &str, message: Message) -> AgentResult<()>
required
Adds a message to conversation history.Parameters:
session_id: The conversation session identifier
message: The message to add
Example:let message = Message::user("Hello, assistant!");
memory.add_to_history("session_123", message).await?;
clear_history
async fn(&mut self, session_id: &str) -> AgentResult<()>
required
Clears conversation history for a session.Parameters:
session_id: The conversation session identifier
Example:memory.clear_history("session_123").await?;
stats
async fn(&self) -> AgentResult<MemoryStats>
default:"MemoryStats::default()"
Returns statistics about memory usage.Returns:
MemoryStats containing counts and memory usage information
Example:let stats = memory.stats().await?;
println!("Total items: {}", stats.total_items);
println!("Total sessions: {}", stats.total_sessions);
memory_type
fn() -> &str
default:"memory"
Returns the type name of this memory implementation.Example:println!("Using: {}", memory.memory_type());
Core Types
MemoryValue
#[non_exhaustive]
pub enum MemoryValue {
Text(String),
Embedding(Vec<f32>),
Structured(serde_json::Value),
Binary(Vec<u8>),
TextWithEmbedding { text: String, embedding: Vec<f32> },
}
Represents different types of memory values.
Constructors
// Create text value
MemoryValue::text("Hello world")
// Create embedding value
MemoryValue::embedding(vec![0.1, 0.2, 0.3])
// Create structured value
MemoryValue::structured(serde_json::json!({ "key": "value" }))
// Create text with embedding
MemoryValue::text_with_embedding("Hello", vec![0.1, 0.2])
Helper Methods
// Get text content
if let Some(text) = value.as_text() {
println!("Text: {}", text);
}
// Get embedding vector
if let Some(embedding) = value.as_embedding() {
println!("Dimension: {}", embedding.len());
}
// Get structured data
if let Some(json) = value.as_structured() {
println!("Data: {}", json);
}
MemoryItem
pub struct MemoryItem {
pub key: String,
pub value: MemoryValue,
pub score: f32,
pub metadata: HashMap<String, String>,
pub created_at: u64,
pub last_accessed: u64,
}
Represents a memory item with metadata (typically used in search results).
Unique identifier for the memory item
Similarity score (0.0 - 1.0) for search results
metadata
HashMap<String, String>
required
Additional metadata attached to the item
Unix timestamp (milliseconds) when item was created
Unix timestamp (milliseconds) of last access
Builder Methods
MemoryItem::new("key", MemoryValue::text("value"))
.with_score(0.95)
.with_metadata("source", "user_input")
.with_metadata("category", "preference")
Message
pub struct Message {
pub role: MessageRole,
pub content: String,
pub timestamp: u64,
pub metadata: HashMap<String, serde_json::Value>,
}
Represents a conversation message.
The role of the message sender (System, User, Assistant, Tool)
Unix timestamp (milliseconds) when message was created
metadata
HashMap<String, serde_json::Value>
required
Additional metadata attached to the message
Constructors
// Create system message
Message::system("You are a helpful assistant")
// Create user message
Message::user("What is Rust?")
// Create assistant message
Message::assistant("Rust is a programming language...")
// Create tool message
Message::tool("calculator", "{\"result\": 42}")
// Add metadata
Message::user("Hello").with_metadata("lang", json!("en"))
MessageRole
#[non_exhaustive]
pub enum MessageRole {
System,
User,
Assistant,
Tool,
}
Represents the role of a message sender.
MemoryStats
pub struct MemoryStats {
pub total_items: usize,
pub total_sessions: usize,
pub total_messages: usize,
pub memory_bytes: usize,
}
Statistics about memory usage.
Total number of stored memory items
Total number of conversation sessions
Total number of messages across all sessions
Approximate memory usage in bytes
Implementing a Custom Memory
Basic In-Memory Implementation
use mofa_kernel::agent::components::memory::*;
use async_trait::async_trait;
use std::collections::HashMap;
pub struct SimpleMemory {
data: HashMap<String, MemoryItem>,
history: HashMap<String, Vec<Message>>,
}
impl SimpleMemory {
pub fn new() -> Self {
Self {
data: HashMap::new(),
history: HashMap::new(),
}
}
}
#[async_trait]
impl Memory for SimpleMemory {
async fn store(&mut self, key: &str, value: MemoryValue) -> AgentResult<()> {
let item = MemoryItem::new(key, value);
self.data.insert(key.to_string(), item);
Ok(())
}
async fn retrieve(&self, key: &str) -> AgentResult<Option<MemoryValue>> {
Ok(self.data.get(key).map(|item| item.value.clone()))
}
async fn remove(&mut self, key: &str) -> AgentResult<bool> {
Ok(self.data.remove(key).is_some())
}
async fn search(&self, query: &str, limit: usize) -> AgentResult<Vec<MemoryItem>> {
let query_lower = query.to_lowercase();
let mut results: Vec<MemoryItem> = self
.data
.values()
.filter(|item| {
if let Some(text) = item.value.as_text() {
text.to_lowercase().contains(&query_lower)
} else {
false
}
})
.cloned()
.collect();
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
results.truncate(limit);
Ok(results)
}
async fn clear(&mut self) -> AgentResult<()> {
self.data.clear();
Ok(())
}
async fn get_history(&self, session_id: &str) -> AgentResult<Vec<Message>> {
Ok(self.history.get(session_id).cloned().unwrap_or_default())
}
async fn add_to_history(&mut self, session_id: &str, message: Message) -> AgentResult<()> {
self.history
.entry(session_id.to_string())
.or_insert_with(Vec::new)
.push(message);
Ok(())
}
async fn clear_history(&mut self, session_id: &str) -> AgentResult<()> {
self.history.remove(session_id);
Ok(())
}
async fn stats(&self) -> AgentResult<MemoryStats> {
Ok(MemoryStats {
total_items: self.data.len(),
total_sessions: self.history.len(),
total_messages: self.history.values().map(|v| v.len()).sum(),
memory_bytes: 0, // Approximate if needed
})
}
fn memory_type(&self) -> &str {
"simple_memory"
}
}
Usage Examples
Basic Storage and Retrieval
use mofa_foundation::agent::components::memory::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut memory = InMemoryStorage::new();
// Store text
memory.store("greeting", MemoryValue::text("Hello, world!")).await?;
// Store structured data
memory.store(
"user_profile",
MemoryValue::structured(serde_json::json!({
"name": "Alice",
"role": "developer",
"preferences": {
"theme": "dark",
"language": "rust"
}
}))
).await?;
// Retrieve
if let Some(value) = memory.retrieve("greeting").await? {
println!("Greeting: {}", value.as_text().unwrap());
}
// Check existence
if memory.contains("user_profile").await? {
println!("Profile exists");
}
Ok(())
}
Conversation History
use mofa_kernel::agent::components::memory::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut memory = InMemoryStorage::new();
let session_id = "user_session_123";
// Add messages
memory.add_to_history(
session_id,
Message::system("You are a helpful assistant")
).await?;
memory.add_to_history(
session_id,
Message::user("What is Rust?")
).await?;
memory.add_to_history(
session_id,
Message::assistant("Rust is a systems programming language...")
).await?;
// Retrieve history
let history = memory.get_history(session_id).await?;
for msg in history {
println!("{}: {}", msg.role, msg.content);
}
// Clear when done
memory.clear_history(session_id).await?;
Ok(())
}
Semantic Search
use mofa_foundation::agent::components::memory::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut memory = InMemoryStorage::new();
// Store multiple items
memory.store("fact1", MemoryValue::text("Rust is fast")).await?;
memory.store("fact2", MemoryValue::text("Rust is safe")).await?;
memory.store("fact3", MemoryValue::text("Python is dynamic")).await?;
// Search
let results = memory.search("Rust", 10).await?;
for item in results {
println!("Found: {} - {} (score: {})",
item.key,
item.value.as_text().unwrap(),
item.score
);
}
Ok(())
}
Working with Embeddings
use mofa_kernel::agent::components::memory::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut memory = InMemoryStorage::new();
// Store text with embedding (from LLM)
let text = "The capital of France is Paris";
let embedding = vec![0.1, 0.2, 0.3, 0.4]; // From embedding model
memory.store(
"fact_france",
MemoryValue::text_with_embedding(text, embedding)
).await?;
// Retrieve and access both
if let Some(value) = memory.retrieve("fact_france").await? {
if let Some(text) = value.as_text() {
println!("Text: {}", text);
}
if let Some(emb) = value.as_embedding() {
println!("Embedding dimension: {}", emb.len());
}
}
Ok(())
}
Memory Statistics
use mofa_foundation::agent::components::memory::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut memory = InMemoryStorage::new();
// Store some data
memory.store("key1", MemoryValue::text("value1")).await?;
memory.store("key2", MemoryValue::text("value2")).await?;
memory.add_to_history("session1", Message::user("Hello")).await?;
memory.add_to_history("session1", Message::assistant("Hi")).await?;
// Get stats
let stats = memory.stats().await?;
println!("Items: {}", stats.total_items);
println!("Sessions: {}", stats.total_sessions);
println!("Messages: {}", stats.total_messages);
println!("Memory: {} bytes", stats.memory_bytes);
Ok(())
}
Built-in Implementations
MoFA provides several memory implementations:
- InMemoryStorage: Simple HashMap-based storage (mofa-foundation)
- PostgresMemory: PostgreSQL-backed persistent storage (when
persistence-postgres feature enabled)
- MySQLMemory: MySQL-backed persistent storage (when
persistence-mysql feature enabled)
- SQLiteMemory: SQLite-backed persistent storage (when
persistence-sqlite feature enabled)
Best Practices
- Session Management: Use descriptive session IDs like
user_{id}_{timestamp}
- Memory Cleanup: Periodically clear old sessions to prevent unbounded growth
- Embedding Storage: Store embeddings alongside text for semantic search
- Metadata Usage: Add relevant metadata to items for filtering and categorization
- Error Handling: Handle
AgentResult errors appropriately
- Thread Safety: Ensure implementations are
Send + Sync for concurrent access
- Search Limits: Use reasonable limits for search to prevent performance issues
- History Pruning: Implement conversation history pruning for long sessions