Skip to main content

batuta/agent/memory/
mod.rs

1//! Memory substrate — agent persistent state.
2//!
3//! Defines the `MemorySubstrate` trait for storing and recalling
4//! agent memories. Phase 1 provides `InMemorySubstrate` (ephemeral,
5//! substring matching). Phase 2 adds `TruenoMemory` (durable,
6//! semantic similarity via trueno-rag vector search).
7//!
8//! See: arXiv:2512.13564 (memory survey), arXiv:2602.19320 (taxonomy).
9
10pub mod in_memory;
11#[cfg(feature = "rag")]
12pub mod trueno;
13
14pub use in_memory::InMemorySubstrate;
15#[cfg(feature = "rag")]
16pub use trueno::TruenoMemory;
17
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20
21use crate::agent::result::AgentError;
22
23/// Unique identifier for a stored memory fragment.
24pub type MemoryId = String;
25
26/// Filter for memory recall queries.
27#[derive(Debug, Clone, Default)]
28pub struct MemoryFilter {
29    /// Filter by agent ID.
30    pub agent_id: Option<String>,
31    /// Filter by memory source type.
32    pub source: Option<MemorySource>,
33    /// Filter memories created after this time.
34    pub since: Option<chrono::DateTime<chrono::Utc>>,
35}
36
37/// Source of a memory fragment.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub enum MemorySource {
40    /// From agent conversation.
41    Conversation,
42    /// From tool execution result.
43    ToolResult,
44    /// System-injected memory.
45    System,
46    /// User-provided memory.
47    User,
48}
49
50/// A recalled memory fragment with relevance score.
51#[derive(Debug, Clone)]
52pub struct MemoryFragment {
53    /// Unique identifier.
54    pub id: MemoryId,
55    /// Memory content text.
56    pub content: String,
57    /// How the memory was created.
58    pub source: MemorySource,
59    /// Relevance score (0.0-1.0, higher = more relevant).
60    pub relevance_score: f32,
61    /// When the memory was created.
62    pub created_at: chrono::DateTime<chrono::Utc>,
63}
64
65/// Unified structured + semantic memory store.
66#[async_trait]
67pub trait MemorySubstrate: Send + Sync {
68    /// Store a memory fragment.
69    async fn remember(
70        &self,
71        agent_id: &str,
72        content: &str,
73        source: MemorySource,
74        embedding: Option<&[f32]>,
75    ) -> Result<MemoryId, AgentError>;
76
77    /// Recall relevant memories.
78    async fn recall(
79        &self,
80        query: &str,
81        limit: usize,
82        filter: Option<MemoryFilter>,
83        query_embedding: Option<&[f32]>,
84    ) -> Result<Vec<MemoryFragment>, AgentError>;
85
86    /// Store structured key-value data.
87    async fn set(
88        &self,
89        agent_id: &str,
90        key: &str,
91        value: serde_json::Value,
92    ) -> Result<(), AgentError>;
93
94    /// Retrieve structured key-value data.
95    async fn get(&self, agent_id: &str, key: &str)
96        -> Result<Option<serde_json::Value>, AgentError>;
97
98    /// Delete a memory fragment.
99    async fn forget(&self, id: MemoryId) -> Result<(), AgentError>;
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_memory_source_serialization() {
108        let sources = vec![
109            MemorySource::Conversation,
110            MemorySource::ToolResult,
111            MemorySource::System,
112            MemorySource::User,
113        ];
114        for source in &sources {
115            let json = serde_json::to_string(source).expect("serialize failed");
116            let back: MemorySource = serde_json::from_str(&json).expect("deserialize failed");
117            assert_eq!(*source, back);
118        }
119    }
120
121    #[test]
122    fn test_memory_filter_default() {
123        let filter = MemoryFilter::default();
124        assert!(filter.agent_id.is_none());
125        assert!(filter.source.is_none());
126        assert!(filter.since.is_none());
127    }
128}