Documentation Index
Fetch the complete documentation index at: https://mintlify.com/dallay/corvus/llms.txt
Use this file to discover all available pages before exploring further.
Memory Trait
TheMemory trait defines the interface for all memory backends in Corvus. Every backend (SQLite, SurrealDB, Markdown) implements this trait.
Source: src/memory/traits.rs:58-111
Trait Definition
use async_trait::async_trait;
#[async_trait]
pub trait Memory: Send + Sync {
/// Backend name
fn name(&self) -> &str;
/// Store a memory entry
async fn store(
&self,
key: &str,
content: &str,
category: MemoryCategory,
session_id: Option<&str>,
) -> anyhow::Result<()>;
/// Recall memories matching a query
async fn recall(
&self,
query: &str,
limit: usize,
session_id: Option<&str>,
) -> anyhow::Result<Vec<MemoryEntry>>;
/// Get a specific memory by key
async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>>;
/// List all memory keys
async fn list(
&self,
category: Option<&MemoryCategory>,
session_id: Option<&str>,
) -> anyhow::Result<Vec<MemoryEntry>>;
/// Remove a memory by key
async fn forget(&self, key: &str) -> anyhow::Result<bool>;
/// Count total memories
async fn count(&self) -> anyhow::Result<usize>;
/// Health check
async fn health_check(&self) -> bool;
/// Validate AI response against memory rules (optional)
async fn validate_response(
&self,
_user_query: &str,
_response: &str,
_session_id: Option<&str>,
) -> anyhow::Result<MemoryValidationResult> {
Ok(MemoryValidationResult::default())
}
}
Core Types
MemoryEntry
Fromsrc/memory/traits.rs:5-14:
pub struct MemoryEntry {
pub id: String,
pub key: String,
pub content: String,
pub category: MemoryCategory,
pub timestamp: String,
pub session_id: Option<String>,
pub score: Option<f64>, // relevance score from search
}
MemoryCategory
Fromsrc/memory/traits.rs:32-54:
pub enum MemoryCategory {
/// Long-term facts, preferences, decisions
Core,
/// Daily session logs
Daily,
/// Conversation context
Conversation,
/// User-defined custom category
Custom(String),
}
impl std::fmt::Display for MemoryCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Core => write!(f, "core"),
Self::Daily => write!(f, "daily"),
Self::Conversation => write!(f, "conversation"),
Self::Custom(name) => write!(f, "{}", name),
}
}
}
MemoryValidationResult
Fromsrc/memory/traits.rs:16-29:
pub struct MemoryValidationResult {
pub valid: bool,
pub violations: Vec<String>,
}
impl Default for MemoryValidationResult {
fn default() -> Self {
Self {
valid: true,
violations: Vec::new(),
}
}
}
Implementing a Memory Backend
Minimal example:use async_trait::async_trait;
use std::collections::HashMap;
use tokio::sync::RwLock;
pub struct InMemoryBackend {
data: Arc<RwLock<HashMap<String, MemoryEntry>>>,
}
impl InMemoryBackend {
pub fn new() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[async_trait]
impl Memory for InMemoryBackend {
fn name(&self) -> &str {
"in-memory"
}
async fn store(
&self,
key: &str,
content: &str,
category: MemoryCategory,
session_id: Option<&str>,
) -> anyhow::Result<()> {
let mut data = self.data.write().await;
let entry = MemoryEntry {
id: uuid::Uuid::new_v4().to_string(),
key: key.to_string(),
content: content.to_string(),
category,
timestamp: chrono::Utc::now().to_rfc3339(),
session_id: session_id.map(|s| s.to_string()),
score: None,
};
data.insert(key.to_string(), entry);
Ok(())
}
async fn recall(
&self,
query: &str,
limit: usize,
session_id: Option<&str>,
) -> anyhow::Result<Vec<MemoryEntry>> {
let data = self.data.read().await;
let mut results: Vec<_> = data.values()
.filter(|e| {
// Filter by session if provided
if let Some(sid) = session_id {
e.session_id.as_deref() == Some(sid)
} else {
true
}
})
.filter(|e| {
// Simple keyword matching
e.content.to_lowercase().contains(&query.to_lowercase())
})
.cloned()
.collect();
// Sort by relevance (placeholder: alphabetical)
results.sort_by(|a, b| a.key.cmp(&b.key));
results.truncate(limit);
Ok(results)
}
async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>> {
let data = self.data.read().await;
Ok(data.get(key).cloned())
}
async fn list(
&self,
category: Option<&MemoryCategory>,
session_id: Option<&str>,
) -> anyhow::Result<Vec<MemoryEntry>> {
let data = self.data.read().await;
let results = data.values()
.filter(|e| {
let cat_match = category.map_or(true, |c| &e.category == c);
let sess_match = session_id.map_or(true, |s| e.session_id.as_deref() == Some(s));
cat_match && sess_match
})
.cloned()
.collect();
Ok(results)
}
async fn forget(&self, key: &str) -> anyhow::Result<bool> {
let mut data = self.data.write().await;
Ok(data.remove(key).is_some())
}
async fn count(&self) -> anyhow::Result<usize> {
let data = self.data.read().await;
Ok(data.len())
}
async fn health_check(&self) -> bool {
true
}
}
Built-in Backends
Fromsrc/memory/mod.rs:
| Backend | File | Features |
|---|---|---|
| SQLite | sqlite.rs | Vector search, FTS5, caching |
| SurrealDB | surreal.rs | Graph queries, native vectors |
| Markdown | markdown.rs | Human-readable, git-friendly |
| None | none.rs | No-op implementation |
Search Implementation: SQLite
Fromsrc/memory/sqlite.rs:200-300:
async fn recall(
&self,
query: &str,
limit: usize,
session_id: Option<&str>,
) -> anyhow::Result<Vec<MemoryEntry>> {
// 1. Embed query
let query_embedding = self.embedder.embed(query).await?;
// 2. Vector search
let vector_results = self.vector_search(&query_embedding, limit * 2, session_id)?;
// 3. Keyword search (FTS5)
let keyword_results = self.keyword_search(query, limit * 2, session_id)?;
// 4. Hybrid merge
let merged = merge_search_results(
vector_results,
keyword_results,
self.vector_weight,
self.keyword_weight,
limit,
);
// 5. Fetch full entries
let entries = self.fetch_entries(&merged)?;
Ok(entries)
}
Registration
Register your backend insrc/memory/mod.rs:
pub fn create_memory_backend(
backend: &str,
workspace_dir: &Path,
config: &MemoryConfig,
) -> anyhow::Result<Arc<dyn Memory>> {
match backend {
"sqlite" => Ok(Arc::new(sqlite::SqliteMemory::with_embedder(
workspace_dir,
create_embedder(config),
config.vector_weight,
config.keyword_weight,
10_000,
None,
)?)),
"surreal" => Ok(Arc::new(surreal::SurrealMemory::new(config)?)),
"markdown" => Ok(Arc::new(markdown::MarkdownMemory::new(workspace_dir))),
"none" => Ok(Arc::new(none::NoopMemory)),
_ => Err(anyhow::anyhow!("Unknown memory backend: {}", backend)),
}
}
Session Scoping
Memories can be scoped to sessions:// Store session-scoped memory
memory.store(
"current_task",
"Implementing feature X",
MemoryCategory::Conversation,
Some("session-abc-123"),
).await?;
// Recall only from this session
let results = memory.recall(
"task status",
10,
Some("session-abc-123"),
).await?;
Response Validation (Advanced)
SurrealDB backend can validate responses against domain rules:impl Memory for SurrealMemory {
async fn validate_response(
&self,
user_query: &str,
response: &str,
session_id: Option<&str>,
) -> anyhow::Result<MemoryValidationResult> {
// Query ontology/rules from graph
let rules = self.fetch_validation_rules(user_query).await?;
let mut violations = Vec::new();
for rule in rules {
if !rule.validate(response) {
violations.push(rule.description);
}
}
Ok(MemoryValidationResult {
valid: violations.is_empty(),
violations,
})
}
}
Best Practices
Implement async operations to avoid blocking the agent loop
Use connection pooling for database backends (SQLite, SurrealDB)
Cache embeddings to avoid redundant API calls (see
src/memory/sqlite.rs:164-186)Never store secrets (API keys, passwords) in memory. Use config or secure storage.