batuta/agent/memory/
trueno.rs1use async_trait::async_trait;
13use std::sync::Mutex;
14
15use super::{MemoryFilter, MemoryFragment, MemoryId, MemorySource, MemorySubstrate};
16use crate::agent::result::AgentError;
17
18pub struct TruenoMemory {
24 index: trueno_rag::sqlite::SqliteIndex,
26 next_id: Mutex<u64>,
28}
29
30impl TruenoMemory {
31 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, AgentError> {
35 let index = trueno_rag::sqlite::SqliteIndex::open(path)
36 .map_err(|e| AgentError::Memory(format!("open failed: {e}")))?;
37
38 let next_id = index
40 .get_metadata("memory_next_id")
41 .ok()
42 .flatten()
43 .and_then(|s| s.parse::<u64>().ok())
44 .unwrap_or(1);
45
46 Ok(Self { index, next_id: Mutex::new(next_id) })
47 }
48
49 pub fn open_in_memory() -> Result<Self, AgentError> {
51 let index = trueno_rag::sqlite::SqliteIndex::open_in_memory()
52 .map_err(|e| AgentError::Memory(format!("in-memory open failed: {e}")))?;
53 Ok(Self { index, next_id: Mutex::new(1) })
54 }
55
56 fn gen_id(&self) -> Result<String, AgentError> {
58 let mut id = self.next_id.lock().map_err(|e| AgentError::Memory(format!("lock: {e}")))?;
59 let current = *id;
60 *id += 1;
61
62 let _ = self.index.set_metadata("memory_next_id", &id.to_string());
64
65 Ok(format!("trueno-{current}"))
66 }
67
68 fn doc_id(agent_id: &str, memory_id: &str) -> String {
70 format!("{agent_id}:{memory_id}")
71 }
72
73 fn kv_key(agent_id: &str, key: &str) -> String {
75 format!("kv:{agent_id}:{key}")
76 }
77
78 pub fn fragment_count(&self) -> Result<usize, AgentError> {
80 self.index.chunk_count().map_err(|e| AgentError::Memory(format!("chunk count: {e}")))
81 }
82}
83
84#[async_trait]
85impl MemorySubstrate for TruenoMemory {
86 async fn remember(
87 &self,
88 agent_id: &str,
89 content: &str,
90 source: MemorySource,
91 _embedding: Option<&[f32]>,
92 ) -> Result<MemoryId, AgentError> {
93 let memory_id = self.gen_id()?;
94 let doc_id = Self::doc_id(agent_id, &memory_id);
95
96 let source_str = match &source {
98 MemorySource::Conversation => "conversation",
99 MemorySource::ToolResult => "tool_result",
100 MemorySource::System => "system",
101 MemorySource::User => "user",
102 };
103
104 let chunk_id = format!("{doc_id}:0");
106 let chunks = vec![(chunk_id, content.to_string())];
107
108 self.index
109 .insert_document(&doc_id, Some(source_str), Some(agent_id), content, &chunks, None)
110 .map_err(|e| AgentError::Memory(format!("insert failed: {e}")))?;
111
112 Ok(memory_id)
113 }
114
115 async fn recall(
116 &self,
117 query: &str,
118 limit: usize,
119 filter: Option<MemoryFilter>,
120 _query_embedding: Option<&[f32]>,
121 ) -> Result<Vec<MemoryFragment>, AgentError> {
122 if query.trim().is_empty() {
123 return Ok(Vec::new());
124 }
125
126 let search_limit = if filter.is_some() { limit * 4 } else { limit };
128
129 let results = self
130 .index
131 .search_fts(query, search_limit)
132 .map_err(|e| AgentError::Memory(format!("search failed: {e}")))?;
133
134 let max_score = results.iter().map(|r| r.score).fold(0.0_f64, f64::max);
136
137 let mut fragments: Vec<MemoryFragment> = results
138 .into_iter()
139 .filter(|r| {
140 let Some(ref f) = filter else {
141 return true;
142 };
143 if let Some(ref aid) = f.agent_id {
145 if !r.doc_id.starts_with(&format!("{aid}:")) {
146 return false;
147 }
148 }
149 if let Some(ref src) = f.source {
151 let src_str = match src {
152 MemorySource::Conversation => "conversation",
153 MemorySource::ToolResult => "tool_result",
154 MemorySource::System => "system",
155 MemorySource::User => "user",
156 };
157 let _ = src_str;
161 }
162 true
163 })
164 .map(|r| {
165 #[allow(clippy::cast_possible_truncation)]
167 let relevance = if max_score > 0.0 { (r.score / max_score) as f32 } else { 0.0 };
168
169 let memory_id = match r.doc_id.split_once(':') {
171 Some((_, mid)) => mid.to_string(),
172 None => r.doc_id.clone(),
173 };
174
175 MemoryFragment {
176 id: memory_id,
177 content: r.content,
178 source: MemorySource::Conversation, relevance_score: relevance,
180 created_at: chrono::Utc::now(), }
182 })
183 .collect();
184
185 fragments.truncate(limit);
186 Ok(fragments)
187 }
188
189 async fn set(
190 &self,
191 agent_id: &str,
192 key: &str,
193 value: serde_json::Value,
194 ) -> Result<(), AgentError> {
195 let kv_key = Self::kv_key(agent_id, key);
196 let serialized = serde_json::to_string(&value)
197 .map_err(|e| AgentError::Memory(format!("serialize: {e}")))?;
198 self.index
199 .set_metadata(&kv_key, &serialized)
200 .map_err(|e| AgentError::Memory(format!("set_metadata: {e}")))?;
201 Ok(())
202 }
203
204 async fn get(
205 &self,
206 agent_id: &str,
207 key: &str,
208 ) -> Result<Option<serde_json::Value>, AgentError> {
209 let kv_key = Self::kv_key(agent_id, key);
210 let stored = self
211 .index
212 .get_metadata(&kv_key)
213 .map_err(|e| AgentError::Memory(format!("get_metadata: {e}")))?;
214 match stored {
215 Some(s) => {
216 let value = serde_json::from_str(&s)
217 .map_err(|e| AgentError::Memory(format!("deserialize: {e}")))?;
218 Ok(Some(value))
219 }
220 None => Ok(None),
221 }
222 }
223
224 async fn forget(&self, id: MemoryId) -> Result<(), AgentError> {
225 let doc_count = self
233 .index
234 .document_count()
235 .map_err(|e| AgentError::Memory(format!("doc_count: {e}")))?;
236
237 if doc_count > 0 {
240 let _ = self.index.remove_document(&id);
244 }
245
246 Ok(())
247 }
248}
249
250#[cfg(test)]
251#[path = "trueno_tests.rs"]
252mod tests;