1use std::path::Path;
2use std::sync::Arc;
3use tokio::sync::Mutex;
4
5use crate::error::{Error, Result};
6use crate::model::acl::{Acl, Permission};
7use crate::model::agent_profile::AgentProfile;
8use crate::model::checkpoint::Checkpoint;
9use crate::model::delegation::{Delegation, DelegationScope};
10use crate::model::embedding_baseline::EmbeddingBaseline;
11use crate::model::event::AgentEvent;
12use crate::model::memory::MemoryRecord;
13use crate::model::relation::Relation;
14use crate::storage::{MemoryFilter, StorageBackend};
15use uuid::Uuid;
16
17pub struct DuckDbStorage {
18 conn: Arc<Mutex<duckdb::Connection>>,
19}
20
21impl DuckDbStorage {
22 pub fn open(path: &Path) -> Result<Self> {
23 let conn = duckdb::Connection::open(path)?;
24 super::migrations::run_migrations(&conn)?;
25 Ok(Self {
26 conn: Arc::new(Mutex::new(conn)),
27 })
28 }
29
30 pub fn open_in_memory() -> Result<Self> {
31 let conn = duckdb::Connection::open_in_memory()?;
32 super::migrations::run_migrations(&conn)?;
33 Ok(Self {
34 conn: Arc::new(Mutex::new(conn)),
35 })
36 }
37}
38
39fn serialize_embedding(embedding: &Option<Vec<f32>>) -> Option<Vec<u8>> {
40 embedding
41 .as_ref()
42 .map(|v| v.iter().flat_map(|f| f.to_le_bytes()).collect())
43}
44
45fn deserialize_embedding(blob: Option<Vec<u8>>) -> Option<Vec<f32>> {
46 blob.map(|bytes| {
47 bytes
48 .chunks_exact(4)
49 .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
50 .collect()
51 })
52}
53
54fn row_to_memory(row: &duckdb::Row<'_>) -> duckdb::Result<MemoryRecord> {
55 let id_str: String = row.get(0)?;
56 let tags_json: Option<String> = row.get(6)?;
57 let metadata_json: Option<String> = row.get(7)?;
58 let embedding_blob: Option<Vec<u8>> = row.get(8)?;
59 let content_hash: Vec<u8> = row.get(9)?;
60 let prev_hash: Option<Vec<u8>> = row.get(10)?;
61
62 let memory_type_str: String = row.get(3)?;
63 let scope_str: String = row.get(4)?;
64 let source_type_str: String = row.get(11)?;
65 let consolidation_state_str: String = row.get(13)?;
66
67 Ok(MemoryRecord {
68 id: Uuid::parse_str(&id_str)
69 .map_err(|e| duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e)))?,
70 agent_id: row.get(1)?,
71 content: row.get(2)?,
72 memory_type: memory_type_str.parse()
73 .map_err(|e: Error| duckdb::Error::FromSqlConversionFailure(3, duckdb::types::Type::Text, e.to_string().into()))?,
74 scope: scope_str.parse()
75 .map_err(|e: Error| duckdb::Error::FromSqlConversionFailure(4, duckdb::types::Type::Text, e.to_string().into()))?,
76 importance: row.get(5)?,
77 tags: match tags_json {
78 Some(ref s) => serde_json::from_str(s).unwrap_or_else(|e| {
79 tracing::warn!(id = %id_str, error = %e, raw = %s, "corrupted tags JSON, defaulting to empty");
80 vec![]
81 }),
82 None => vec![],
83 },
84 metadata: match metadata_json {
85 Some(ref s) => serde_json::from_str(s).unwrap_or_else(|e| {
86 tracing::warn!(id = %id_str, error = %e, "corrupted metadata JSON, defaulting to empty");
87 serde_json::Value::Object(serde_json::Map::new())
88 }),
89 None => serde_json::Value::Object(serde_json::Map::new()),
90 },
91 embedding: deserialize_embedding(embedding_blob),
92 content_hash,
93 prev_hash,
94 source_type: source_type_str.parse()
95 .map_err(|e: Error| duckdb::Error::FromSqlConversionFailure(11, duckdb::types::Type::Text, e.to_string().into()))?,
96 source_id: row.get(12)?,
97 consolidation_state: consolidation_state_str.parse()
98 .map_err(|e: Error| duckdb::Error::FromSqlConversionFailure(13, duckdb::types::Type::Text, e.to_string().into()))?,
99 access_count: u64::try_from(row.get::<_, i64>(14)?).unwrap_or(0),
100 org_id: row.get(15)?,
101 thread_id: row.get(16)?,
102 created_at: row.get(17)?,
103 updated_at: row.get(18)?,
104 last_accessed_at: row.get(19)?,
105 expires_at: row.get(20)?,
106 deleted_at: row.get(21)?,
107 decay_rate: row.get(22)?,
108 created_by: row.get(23)?,
109 version: u32::try_from(row.get::<_, i32>(24)?).unwrap_or(1),
110 prev_version_id: match row.get::<_, Option<String>>(25)? {
111 Some(s) => Uuid::parse_str(&s).map_err(|e| {
112 tracing::warn!(memory_id = %id_str, error = %e, "corrupted prev_version_id UUID");
113 e
114 }).ok(),
115 None => None,
116 },
117 quarantined: row.get::<_, bool>(26)?,
118 quarantine_reason: row.get(27)?,
119 decay_function: row.get(28).unwrap_or(None),
120 })
121}
122
123#[async_trait::async_trait]
124impl StorageBackend for DuckDbStorage {
125 fn backend_name(&self) -> &'static str {
126 "duckdb"
127 }
128
129 async fn insert_memory(&self, record: &MemoryRecord) -> Result<()> {
130 let conn = self.conn.lock().await;
131 let tags_json = serde_json::to_string(&record.tags)?;
132 let metadata_json = serde_json::to_string(&record.metadata)?;
133 let embedding_blob = serialize_embedding(&record.embedding);
134
135 conn.execute(
136 "INSERT INTO memories (id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
137 duckdb::params![
138 record.id.to_string(),
139 record.agent_id,
140 record.content,
141 record.memory_type.to_string(),
142 record.scope.to_string(),
143 record.importance,
144 tags_json,
145 metadata_json,
146 embedding_blob,
147 record.content_hash,
148 record.prev_hash,
149 record.source_type.to_string(),
150 record.source_id,
151 record.consolidation_state.to_string(),
152 record.access_count as i64,
153 record.org_id,
154 record.thread_id,
155 record.created_at,
156 record.updated_at,
157 record.last_accessed_at,
158 record.expires_at,
159 record.deleted_at,
160 record.decay_rate,
161 record.created_by,
162 record.version as i32,
163 record.prev_version_id.map(|id| id.to_string()),
164 record.quarantined,
165 record.quarantine_reason,
166 record.decay_function,
167 ],
168 )?;
169 Ok(())
170 }
171
172 async fn get_memory(&self, id: Uuid) -> Result<Option<MemoryRecord>> {
173 let conn = self.conn.lock().await;
174 let mut stmt = conn.prepare(
175 "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories WHERE id = ?",
176 )?;
177 let result = stmt.query_row([id.to_string()], row_to_memory);
178 match result {
179 Ok(record) => Ok(Some(record)),
180 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
181 Err(e) => Err(Error::Storage(e.to_string())),
182 }
183 }
184
185 async fn update_memory(&self, record: &MemoryRecord) -> Result<()> {
186 let conn = self.conn.lock().await;
187 let tags_json = serde_json::to_string(&record.tags)?;
188 let metadata_json = serde_json::to_string(&record.metadata)?;
189 let embedding_blob = serialize_embedding(&record.embedding);
190
191 let affected = conn.execute(
192 "UPDATE memories SET agent_id=?, content=?, memory_type=?, scope=?, importance=?, tags=?, metadata=?, embedding=?, content_hash=?, prev_hash=?, source_type=?, source_id=?, consolidation_state=?, access_count=?, org_id=?, thread_id=?, updated_at=?, last_accessed_at=?, expires_at=?, deleted_at=?, decay_rate=?, created_by=?, version=?, prev_version_id=?, quarantined=?, quarantine_reason=?, decay_function=? WHERE id=?",
193 duckdb::params![
194 record.agent_id,
195 record.content,
196 record.memory_type.to_string(),
197 record.scope.to_string(),
198 record.importance,
199 tags_json,
200 metadata_json,
201 embedding_blob,
202 record.content_hash,
203 record.prev_hash,
204 record.source_type.to_string(),
205 record.source_id,
206 record.consolidation_state.to_string(),
207 record.access_count as i64,
208 record.org_id,
209 record.thread_id,
210 record.updated_at,
211 record.last_accessed_at,
212 record.expires_at,
213 record.deleted_at,
214 record.decay_rate,
215 record.created_by,
216 record.version as i32,
217 record.prev_version_id.map(|id| id.to_string()),
218 record.quarantined,
219 record.quarantine_reason,
220 record.decay_function,
221 record.id.to_string(),
222 ],
223 )?;
224 if affected == 0 {
225 return Err(Error::NotFound(format!("memory {} not found", record.id)));
226 }
227 Ok(())
228 }
229
230 async fn soft_delete_memory(&self, id: Uuid) -> Result<()> {
231 let conn = self.conn.lock().await;
232 let now = chrono::Utc::now().to_rfc3339();
233 let affected = conn.execute(
234 "UPDATE memories SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL",
235 duckdb::params![now, now, id.to_string()],
236 )?;
237 if affected == 0 {
238 return Err(Error::NotFound(format!(
239 "memory {id} not found or already deleted"
240 )));
241 }
242 Ok(())
243 }
244
245 async fn hard_delete_memory(&self, id: Uuid) -> Result<()> {
246 let conn = self.conn.lock().await;
247 let affected = conn.execute(
248 "DELETE FROM memories WHERE id = ?",
249 duckdb::params![id.to_string()],
250 )?;
251 if affected == 0 {
252 return Err(Error::NotFound(format!("memory {id} not found")));
253 }
254 conn.execute(
256 "DELETE FROM acls WHERE memory_id = ?",
257 duckdb::params![id.to_string()],
258 )?;
259 Ok(())
260 }
261
262 async fn list_memories(
263 &self,
264 filter: &MemoryFilter,
265 limit: usize,
266 offset: usize,
267 ) -> Result<Vec<MemoryRecord>> {
268 let conn = self.conn.lock().await;
269 let mut conditions = Vec::new();
270 let mut params: Vec<Box<dyn duckdb::ToSql>> = Vec::new();
271
272 if !filter.include_deleted {
273 conditions.push("deleted_at IS NULL".to_string());
274 }
275
276 if let Some(ref agent_id) = filter.agent_id {
277 conditions.push(format!("agent_id = ${}", params.len() + 1));
278 params.push(Box::new(agent_id.clone()));
279 }
280
281 if let Some(memory_type) = filter.memory_type {
282 conditions.push(format!("memory_type = ${}", params.len() + 1));
283 params.push(Box::new(memory_type.to_string()));
284 }
285
286 if let Some(scope) = filter.scope {
287 conditions.push(format!("scope = ${}", params.len() + 1));
288 params.push(Box::new(scope.to_string()));
289 }
290
291 if let Some(min_importance) = filter.min_importance {
292 conditions.push(format!("importance >= ${}", params.len() + 1));
293 params.push(Box::new(min_importance));
294 }
295
296 if let Some(ref org_id) = filter.org_id {
297 conditions.push(format!("org_id = ${}", params.len() + 1));
298 params.push(Box::new(org_id.clone()));
299 }
300
301 if let Some(ref thread_id) = filter.thread_id {
302 conditions.push(format!("thread_id = ${}", params.len() + 1));
303 params.push(Box::new(thread_id.clone()));
304 }
305
306 let where_clause = if conditions.is_empty() {
307 String::new()
308 } else {
309 format!("WHERE {}", conditions.join(" AND "))
310 };
311
312 let sql = format!(
313 "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories {where_clause} ORDER BY created_at DESC LIMIT {limit} OFFSET {offset}"
314 );
315
316 let mut stmt = conn.prepare(&sql)?;
317 let param_refs: Vec<&dyn duckdb::ToSql> = params.iter().map(|p| p.as_ref()).collect();
318 let rows = stmt.query_map(param_refs.as_slice(), row_to_memory)?;
319
320 let mut results = Vec::new();
321 for row in rows {
322 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
323 }
324 Ok(results)
325 }
326
327 async fn touch_memory(&self, id: Uuid) -> Result<()> {
328 let conn = self.conn.lock().await;
329 let now = chrono::Utc::now().to_rfc3339();
330 conn.execute(
331 "UPDATE memories SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?",
332 duckdb::params![now, id.to_string()],
333 )?;
334 Ok(())
335 }
336
337 async fn insert_acl(&self, acl: &Acl) -> Result<()> {
338 let conn = self.conn.lock().await;
339 conn.execute(
340 "INSERT INTO acls (id, memory_id, principal_type, principal_id, permission, granted_by, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
341 duckdb::params![
342 acl.id.to_string(),
343 acl.memory_id.to_string(),
344 acl.principal_type.to_string(),
345 acl.principal_id,
346 acl.permission.to_string(),
347 acl.granted_by,
348 acl.created_at,
349 acl.expires_at,
350 ],
351 )?;
352 Ok(())
353 }
354
355 async fn check_permission(
356 &self,
357 memory_id: Uuid,
358 principal_id: &str,
359 required: Permission,
360 ) -> Result<bool> {
361 let acl_result = {
363 let conn = self.conn.lock().await;
364
365 let mut stmt = conn.prepare("SELECT agent_id FROM memories WHERE id = ?")?;
367 let owner_result =
368 stmt.query_row([memory_id.to_string()], |row| row.get::<_, String>(0));
369 match owner_result {
370 Ok(owner) if owner == principal_id => return Ok(true),
371 Err(duckdb::Error::QueryReturnedNoRows) => {
372 return Err(Error::NotFound(format!("memory {memory_id} not found")));
373 }
374 _ => {}
375 }
376
377 let now = chrono::Utc::now().to_rfc3339();
379 let mut stmt = conn.prepare(
380 "SELECT permission FROM acls WHERE memory_id = ? AND principal_id = ? AND (expires_at IS NULL OR expires_at > ?)",
381 )?;
382 let rows = stmt.query_map(
383 duckdb::params![memory_id.to_string(), principal_id, now.clone()],
384 |row| row.get::<_, String>(0),
385 )?;
386
387 let mut perms: Vec<String> = Vec::new();
388 for row in rows {
389 perms.push(row.map_err(|e| Error::Storage(e.to_string()))?);
390 }
391
392 let mut stmt = conn.prepare(
394 "SELECT permission FROM acls WHERE memory_id = ? AND principal_type = 'public' AND (expires_at IS NULL OR expires_at > ?)",
395 )?;
396 let rows = stmt.query_map(duckdb::params![memory_id.to_string(), now], |row| {
397 row.get::<_, String>(0)
398 })?;
399
400 for row in rows {
401 perms.push(row.map_err(|e| Error::Storage(e.to_string()))?);
402 }
403
404 perms
405 }; for perm_str in &acl_result {
408 if let Ok(perm) = perm_str.parse::<Permission>()
409 && perm.satisfies(required)
410 {
411 return Ok(true);
412 }
413 }
414
415 if self
417 .check_delegation(principal_id, memory_id, required)
418 .await?
419 {
420 return Ok(true);
421 }
422
423 Ok(false)
424 }
425
426 async fn insert_relation(&self, relation: &Relation) -> Result<()> {
427 let conn = self.conn.lock().await;
428 conn.execute(
429 "INSERT INTO relations (id, source_id, target_id, relation_type, weight, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
430 duckdb::params![
431 relation.id.to_string(),
432 relation.source_id.to_string(),
433 relation.target_id.to_string(),
434 relation.relation_type,
435 relation.weight,
436 serde_json::to_string(&relation.metadata)?,
437 relation.created_at,
438 ],
439 )?;
440 Ok(())
441 }
442
443 async fn get_relations_from(&self, source_id: Uuid) -> Result<Vec<Relation>> {
444 let conn = self.conn.lock().await;
445 let mut stmt = conn.prepare(
446 "SELECT id, source_id, target_id, relation_type, weight, metadata, created_at FROM relations WHERE source_id = ?",
447 )?;
448 let rows = stmt.query_map([source_id.to_string()], row_to_relation)?;
449 let mut results = Vec::new();
450 for row in rows {
451 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
452 }
453 Ok(results)
454 }
455
456 async fn get_relations_to(&self, target_id: Uuid) -> Result<Vec<Relation>> {
457 let conn = self.conn.lock().await;
458 let mut stmt = conn.prepare(
459 "SELECT id, source_id, target_id, relation_type, weight, metadata, created_at FROM relations WHERE target_id = ?",
460 )?;
461 let rows = stmt.query_map([target_id.to_string()], row_to_relation)?;
462 let mut results = Vec::new();
463 for row in rows {
464 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
465 }
466 Ok(results)
467 }
468
469 async fn delete_relation(&self, id: Uuid) -> Result<()> {
470 let conn = self.conn.lock().await;
471 let affected = conn.execute(
472 "DELETE FROM relations WHERE id = ?",
473 duckdb::params![id.to_string()],
474 )?;
475 if affected == 0 {
476 return Err(Error::NotFound(format!("relation {id} not found")));
477 }
478 Ok(())
479 }
480
481 async fn get_latest_memory_hash(
482 &self,
483 agent_id: &str,
484 thread_id: Option<&str>,
485 ) -> Result<Option<Vec<u8>>> {
486 let conn = self.conn.lock().await;
487 let (sql, result) = if let Some(tid) = thread_id {
488 let mut stmt = conn.prepare(
489 "SELECT content_hash FROM memories WHERE agent_id = ? AND thread_id = ? AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1",
490 )?;
491 let r = stmt.query_row(duckdb::params![agent_id, tid], |row| {
492 row.get::<_, Vec<u8>>(0)
493 });
494 ((), r)
495 } else {
496 let mut stmt = conn.prepare(
497 "SELECT content_hash FROM memories WHERE agent_id = ? AND thread_id IS NULL AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1",
498 )?;
499 let r = stmt.query_row(duckdb::params![agent_id], |row| row.get::<_, Vec<u8>>(0));
500 ((), r)
501 };
502 let _ = sql;
503 match result {
504 Ok(hash) => Ok(Some(hash)),
505 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
506 Err(e) => Err(Error::Storage(e.to_string())),
507 }
508 }
509
510 async fn get_latest_event_hash(
511 &self,
512 agent_id: &str,
513 thread_id: Option<&str>,
514 ) -> Result<Option<Vec<u8>>> {
515 let conn = self.conn.lock().await;
516 let result = if let Some(tid) = thread_id {
517 let mut stmt = conn.prepare(
518 "SELECT content_hash FROM agent_events WHERE agent_id = ? AND thread_id = ? ORDER BY timestamp DESC LIMIT 1",
519 )?;
520 stmt.query_row(duckdb::params![agent_id, tid], |row| {
521 row.get::<_, Vec<u8>>(0)
522 })
523 } else {
524 let mut stmt = conn.prepare(
525 "SELECT content_hash FROM agent_events WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 1",
526 )?;
527 stmt.query_row(duckdb::params![agent_id], |row| row.get::<_, Vec<u8>>(0))
528 };
529 match result {
530 Ok(hash) => Ok(Some(hash)),
531 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
532 Err(e) => Err(Error::Storage(e.to_string())),
533 }
534 }
535
536 async fn get_sync_watermark(&self, key: &str) -> Result<Option<String>> {
537 let conn = self.conn.lock().await;
538 let mut stmt = conn.prepare("SELECT value FROM sync_metadata WHERE key = ?")?;
539 let result = stmt.query_row(duckdb::params![key], |row| row.get::<_, String>(0));
540 match result {
541 Ok(value) => Ok(Some(value)),
542 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
543 Err(e) => Err(Error::Storage(e.to_string())),
544 }
545 }
546
547 async fn set_sync_watermark(&self, key: &str, value: &str) -> Result<()> {
548 let conn = self.conn.lock().await;
549 let now = chrono::Utc::now().to_rfc3339();
550 let affected = conn.execute(
552 "UPDATE sync_metadata SET value = ?, updated_at = ? WHERE key = ?",
553 duckdb::params![value, now, key],
554 )?;
555 if affected == 0 {
556 conn.execute(
557 "INSERT INTO sync_metadata (key, value, updated_at) VALUES (?, ?, ?)",
558 duckdb::params![key, value, now],
559 )?;
560 }
561 Ok(())
562 }
563
564 async fn list_accessible_memory_ids(&self, agent_id: &str, limit: usize) -> Result<Vec<Uuid>> {
565 let conn = self.conn.lock().await;
566 let now = chrono::Utc::now().to_rfc3339();
567 let mut stmt = conn.prepare(
568 "SELECT id FROM memories WHERE (agent_id = ? OR scope = 'public' OR id IN (SELECT memory_id FROM acls WHERE principal_id = ? AND (expires_at IS NULL OR expires_at > ?))) AND deleted_at IS NULL LIMIT ?",
569 )?;
570 let rows = stmt.query_map(
571 duckdb::params![agent_id, agent_id, now, limit as i64],
572 |row| row.get::<_, String>(0),
573 )?;
574 let mut ids = Vec::new();
575 for row in rows {
576 let id_str = row.map_err(|e| Error::Storage(e.to_string()))?;
577 ids.push(Uuid::parse_str(&id_str).map_err(|e| Error::Storage(e.to_string()))?);
578 }
579 Ok(ids)
580 }
581
582 async fn insert_event(&self, event: &AgentEvent) -> Result<()> {
583 let conn = self.conn.lock().await;
584 let payload_json = serde_json::to_string(&event.payload)?;
585 let embedding_blob = serialize_embedding(&event.embedding);
586 conn.execute(
587 "INSERT INTO agent_events (id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
588 duckdb::params![
589 event.id.to_string(),
590 event.agent_id,
591 event.thread_id,
592 event.run_id,
593 event.parent_event_id.map(|id| id.to_string()),
594 event.event_type.to_string(),
595 payload_json,
596 event.trace_id,
597 event.span_id,
598 event.model,
599 event.tokens_input,
600 event.tokens_output,
601 event.latency_ms,
602 event.cost_usd,
603 event.timestamp,
604 event.logical_clock,
605 event.content_hash,
606 event.prev_hash,
607 embedding_blob,
608 ],
609 )?;
610 Ok(())
611 }
612
613 async fn list_events(
614 &self,
615 agent_id: &str,
616 limit: usize,
617 offset: usize,
618 ) -> Result<Vec<AgentEvent>> {
619 let conn = self.conn.lock().await;
620 let mut stmt = conn.prepare(
621 "SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding FROM agent_events WHERE agent_id = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
622 )?;
623 let rows = stmt.query_map(
624 duckdb::params![agent_id, limit as i64, offset as i64],
625 row_to_event,
626 )?;
627 let mut results = Vec::new();
628 for row in rows {
629 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
630 }
631 Ok(results)
632 }
633
634 async fn get_events_by_thread(&self, thread_id: &str, limit: usize) -> Result<Vec<AgentEvent>> {
635 let conn = self.conn.lock().await;
636 let mut stmt = conn.prepare(
637 "SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding FROM agent_events WHERE thread_id = ? ORDER BY timestamp ASC LIMIT ?",
638 )?;
639 let rows = stmt.query_map(duckdb::params![thread_id, limit as i64], row_to_event)?;
640 let mut results = Vec::new();
641 for row in rows {
642 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
643 }
644 Ok(results)
645 }
646
647 async fn get_event(&self, id: Uuid) -> Result<Option<AgentEvent>> {
648 let conn = self.conn.lock().await;
649 let mut stmt = conn.prepare(
650 "SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding FROM agent_events WHERE id = ?",
651 )?;
652 let result = stmt.query_row([id.to_string()], row_to_event);
653 match result {
654 Ok(event) => Ok(Some(event)),
655 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
656 Err(e) => Err(Error::Storage(e.to_string())),
657 }
658 }
659
660 async fn list_child_events(
661 &self,
662 parent_event_id: Uuid,
663 limit: usize,
664 ) -> Result<Vec<AgentEvent>> {
665 let conn = self.conn.lock().await;
666 let mut stmt = conn.prepare(
667 "SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding FROM agent_events WHERE parent_event_id = ? ORDER BY timestamp ASC LIMIT ?",
668 )?;
669 let rows = stmt.query_map(
670 duckdb::params![parent_event_id.to_string(), limit as i64],
671 row_to_event,
672 )?;
673 let mut results = Vec::new();
674 for row in rows {
675 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
676 }
677 Ok(results)
678 }
679
680 async fn list_memories_by_agent_ordered(
681 &self,
682 agent_id: &str,
683 thread_id: Option<&str>,
684 limit: usize,
685 ) -> Result<Vec<MemoryRecord>> {
686 let conn = self.conn.lock().await;
687 let (result,) = if let Some(tid) = thread_id {
688 let mut stmt = conn.prepare(
689 "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories WHERE agent_id = ? AND thread_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT ?",
690 )?;
691 let rows =
692 stmt.query_map(duckdb::params![agent_id, tid, limit as i64], row_to_memory)?;
693 let mut results = Vec::new();
694 for row in rows {
695 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
696 }
697 (results,)
698 } else {
699 let mut stmt = conn.prepare(
700 "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories WHERE agent_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT ?",
701 )?;
702 let rows = stmt.query_map(duckdb::params![agent_id, limit as i64], row_to_memory)?;
703 let mut results = Vec::new();
704 for row in rows {
705 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
706 }
707 (results,)
708 };
709 Ok(result)
710 }
711
712 async fn list_memories_since(
713 &self,
714 updated_after: &str,
715 limit: usize,
716 ) -> Result<Vec<MemoryRecord>> {
717 let conn = self.conn.lock().await;
718 let mut stmt = conn.prepare(
719 "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories WHERE updated_at > ? ORDER BY updated_at ASC LIMIT ?",
720 )?;
721 let rows = stmt.query_map(duckdb::params![updated_after, limit as i64], row_to_memory)?;
722 let mut results = Vec::new();
723 for row in rows {
724 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
725 }
726 Ok(results)
727 }
728
729 async fn upsert_memory(&self, record: &MemoryRecord) -> Result<()> {
730 match self.update_memory(record).await {
732 Ok(()) => Ok(()),
733 Err(Error::NotFound(_)) => self.insert_memory(record).await,
734 Err(e) => Err(e),
735 }
736 }
737
738 async fn cleanup_expired(&self) -> Result<usize> {
739 let conn = self.conn.lock().await;
740 let now = chrono::Utc::now().to_rfc3339();
741 let affected = conn.execute(
742 "UPDATE memories SET deleted_at = ? WHERE expires_at IS NOT NULL AND expires_at < ? AND deleted_at IS NULL",
743 duckdb::params![now.clone(), now],
744 )?;
745 Ok(affected)
746 }
747
748 async fn insert_delegation(&self, d: &Delegation) -> Result<()> {
749 let conn = self.conn.lock().await;
750 let scope_type = d.scope.to_string();
751 let scope_value = match &d.scope {
752 DelegationScope::AllMemories => serde_json::Value::Null,
753 DelegationScope::ByTag(tags) => serde_json::json!(tags),
754 DelegationScope::ByMemoryId(ids) => {
755 serde_json::json!(ids.iter().map(|id| id.to_string()).collect::<Vec<_>>())
756 }
757 };
758 let scope_value_json = serde_json::to_string(&scope_value)?;
759
760 conn.execute(
761 "INSERT INTO delegations (id, delegator_id, delegate_id, permission, scope_type, scope_value, max_depth, current_depth, parent_delegation_id, created_at, expires_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
762 duckdb::params![
763 d.id.to_string(),
764 d.delegator_id,
765 d.delegate_id,
766 d.permission.to_string(),
767 scope_type,
768 scope_value_json,
769 d.max_depth as i32,
770 d.current_depth as i32,
771 d.parent_delegation_id.map(|id| id.to_string()),
772 d.created_at,
773 d.expires_at,
774 d.revoked_at,
775 ],
776 )?;
777 Ok(())
778 }
779
780 async fn list_delegations_for(&self, delegate_id: &str) -> Result<Vec<Delegation>> {
781 let conn = self.conn.lock().await;
782 let now = chrono::Utc::now().to_rfc3339();
783 let mut stmt = conn.prepare(
784 "SELECT id, delegator_id, delegate_id, permission, scope_type, scope_value, max_depth, current_depth, parent_delegation_id, created_at, expires_at, revoked_at FROM delegations WHERE delegate_id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)",
785 )?;
786 let rows = stmt.query_map(duckdb::params![delegate_id, now], row_to_delegation)?;
787 let mut results = Vec::new();
788 for row in rows {
789 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
790 }
791 Ok(results)
792 }
793
794 async fn revoke_delegation(&self, id: Uuid) -> Result<()> {
795 let conn = self.conn.lock().await;
796 let now = chrono::Utc::now().to_rfc3339();
797 let affected = conn.execute(
798 "UPDATE delegations SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL",
799 duckdb::params![now, id.to_string()],
800 )?;
801 if affected == 0 {
802 return Err(Error::NotFound(format!(
803 "delegation {id} not found or already revoked"
804 )));
805 }
806 Ok(())
807 }
808
809 async fn check_delegation(
810 &self,
811 delegate_id: &str,
812 memory_id: Uuid,
813 required: Permission,
814 ) -> Result<bool> {
815 let delegations = self.list_delegations_for(delegate_id).await?;
816 let memory = match self.get_memory(memory_id).await? {
818 Some(m) => m,
819 None => return Ok(false),
820 };
821
822 for d in &delegations {
823 if !d.permission.satisfies(required) {
824 continue;
825 }
826 match &d.scope {
827 DelegationScope::AllMemories => return Ok(true),
828 DelegationScope::ByMemoryId(ids) => {
829 if ids.contains(&memory_id) {
830 return Ok(true);
831 }
832 }
833 DelegationScope::ByTag(tags) => {
834 if tags.iter().any(|t| memory.tags.contains(t)) {
835 return Ok(true);
836 }
837 }
838 }
839 }
840 Ok(false)
841 }
842
843 async fn insert_or_update_agent_profile(&self, profile: &AgentProfile) -> Result<()> {
844 let conn = self.conn.lock().await;
845 let affected = conn.execute(
847 "UPDATE agent_profiles SET avg_importance = ?, avg_content_length = ?, total_memories = ?, last_updated = ? WHERE agent_id = ?",
848 duckdb::params![
849 profile.avg_importance,
850 profile.avg_content_length,
851 profile.total_memories as i64,
852 profile.last_updated,
853 profile.agent_id,
854 ],
855 )?;
856 if affected == 0 {
857 conn.execute(
858 "INSERT INTO agent_profiles (agent_id, avg_importance, avg_content_length, total_memories, last_updated) VALUES (?, ?, ?, ?, ?)",
859 duckdb::params![
860 profile.agent_id,
861 profile.avg_importance,
862 profile.avg_content_length,
863 profile.total_memories as i64,
864 profile.last_updated,
865 ],
866 )?;
867 }
868 Ok(())
869 }
870
871 async fn get_agent_profile(&self, agent_id: &str) -> Result<Option<AgentProfile>> {
872 let conn = self.conn.lock().await;
873 let mut stmt = conn.prepare(
874 "SELECT agent_id, avg_importance, avg_content_length, total_memories, last_updated FROM agent_profiles WHERE agent_id = ?",
875 )?;
876 let result = stmt.query_row([agent_id], |row| {
877 Ok(AgentProfile {
878 agent_id: row.get(0)?,
879 avg_importance: row.get(1)?,
880 avg_content_length: row.get(2)?,
881 total_memories: row.get::<_, i64>(3)? as u64,
882 last_updated: row.get(4)?,
883 })
884 });
885 match result {
886 Ok(profile) => Ok(Some(profile)),
887 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
888 Err(e) => Err(Error::Storage(e.to_string())),
889 }
890 }
891
892 async fn insert_or_update_embedding_baseline(
893 &self,
894 baseline: &EmbeddingBaseline,
895 ) -> Result<()> {
896 let conn = self.conn.lock().await;
897 let mu_json = serde_json::to_string(&baseline.mu)?;
898 let cov_json = serde_json::to_string(&baseline.cov_diag)?;
899 let affected = conn.execute(
900 "UPDATE embedding_baseline SET mu = ?, cov_diag = ?, n = ?, updated_at = ? WHERE agent_id = ?",
901 duckdb::params![
902 mu_json,
903 cov_json,
904 baseline.n as i64,
905 baseline.updated_at,
906 baseline.agent_id,
907 ],
908 )?;
909 if affected == 0 {
910 let mu_json = serde_json::to_string(&baseline.mu)?;
911 let cov_json = serde_json::to_string(&baseline.cov_diag)?;
912 conn.execute(
913 "INSERT INTO embedding_baseline (agent_id, mu, cov_diag, n, updated_at) VALUES (?, ?, ?, ?, ?)",
914 duckdb::params![
915 baseline.agent_id,
916 mu_json,
917 cov_json,
918 baseline.n as i64,
919 baseline.updated_at,
920 ],
921 )?;
922 }
923 Ok(())
924 }
925
926 async fn get_embedding_baseline(&self, agent_id: &str) -> Result<Option<EmbeddingBaseline>> {
927 let conn = self.conn.lock().await;
928 let mut stmt = conn.prepare(
929 "SELECT agent_id, mu, cov_diag, n, updated_at FROM embedding_baseline WHERE agent_id = ?",
930 )?;
931 let result: duckdb::Result<(String, String, String, i64, String)> =
932 stmt.query_row([agent_id], |row| {
933 Ok((
934 row.get(0)?,
935 row.get(1)?,
936 row.get(2)?,
937 row.get(3)?,
938 row.get(4)?,
939 ))
940 });
941 match result {
942 Ok((agent_id, mu_json, cov_json, n, updated_at)) => {
943 let mu: Vec<f32> = serde_json::from_str(&mu_json)?;
944 let cov_diag: Vec<f32> = serde_json::from_str(&cov_json)?;
945 Ok(Some(EmbeddingBaseline {
946 agent_id,
947 mu,
948 cov_diag,
949 n: n as u64,
950 updated_at,
951 }))
952 }
953 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
954 Err(e) => Err(Error::Storage(e.to_string())),
955 }
956 }
957
958 async fn insert_checkpoint(&self, cp: &Checkpoint) -> Result<()> {
959 let conn = self.conn.lock().await;
960 let state_snapshot_json = serde_json::to_string(&cp.state_snapshot)?;
961 let state_diff_json = cp
962 .state_diff
963 .as_ref()
964 .map(serde_json::to_string)
965 .transpose()?;
966 let memory_refs_json = serde_json::to_string(
967 &cp.memory_refs
968 .iter()
969 .map(|id| id.to_string())
970 .collect::<Vec<_>>(),
971 )?;
972 let metadata_json = serde_json::to_string(&cp.metadata)?;
973
974 conn.execute(
975 "INSERT INTO checkpoints (id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
976 duckdb::params![
977 cp.id.to_string(),
978 cp.thread_id,
979 cp.agent_id,
980 cp.parent_id.map(|id| id.to_string()),
981 cp.branch_name,
982 state_snapshot_json,
983 state_diff_json,
984 memory_refs_json,
985 cp.event_cursor.map(|id| id.to_string()),
986 cp.label,
987 cp.created_at,
988 metadata_json,
989 ],
990 )?;
991 Ok(())
992 }
993
994 async fn get_checkpoint(&self, id: Uuid) -> Result<Option<Checkpoint>> {
995 let conn = self.conn.lock().await;
996 let mut stmt = conn.prepare(
997 "SELECT id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata FROM checkpoints WHERE id = ?",
998 )?;
999 let result = stmt.query_row([id.to_string()], row_to_checkpoint);
1000 match result {
1001 Ok(cp) => Ok(Some(cp)),
1002 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
1003 Err(e) => Err(Error::Storage(e.to_string())),
1004 }
1005 }
1006
1007 async fn list_checkpoints(
1008 &self,
1009 thread_id: &str,
1010 branch: Option<&str>,
1011 limit: usize,
1012 ) -> Result<Vec<Checkpoint>> {
1013 let conn = self.conn.lock().await;
1014 let (sql, rows_result) = if let Some(branch_name) = branch {
1015 let mut stmt = conn.prepare(
1016 "SELECT id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata FROM checkpoints WHERE thread_id = ? AND branch_name = ? ORDER BY created_at DESC LIMIT ?",
1017 )?;
1018 let rows = stmt.query_map(
1019 duckdb::params![thread_id, branch_name, limit as i64],
1020 row_to_checkpoint,
1021 )?;
1022 let mut results = Vec::new();
1023 for row in rows {
1024 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
1025 }
1026 ((), Ok(results))
1027 } else {
1028 let mut stmt = conn.prepare(
1029 "SELECT id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata FROM checkpoints WHERE thread_id = ? ORDER BY created_at DESC LIMIT ?",
1030 )?;
1031 let rows =
1032 stmt.query_map(duckdb::params![thread_id, limit as i64], row_to_checkpoint)?;
1033 let mut results = Vec::new();
1034 for row in rows {
1035 results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
1036 }
1037 ((), Ok(results))
1038 };
1039 let _ = sql;
1040 rows_result
1041 }
1042
1043 async fn get_latest_checkpoint(
1044 &self,
1045 thread_id: &str,
1046 branch: &str,
1047 ) -> Result<Option<Checkpoint>> {
1048 let conn = self.conn.lock().await;
1049 let mut stmt = conn.prepare(
1050 "SELECT id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata FROM checkpoints WHERE thread_id = ? AND branch_name = ? ORDER BY created_at DESC LIMIT 1",
1051 )?;
1052 let result = stmt.query_row(duckdb::params![thread_id, branch], row_to_checkpoint);
1053 match result {
1054 Ok(cp) => Ok(Some(cp)),
1055 Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
1056 Err(e) => Err(Error::Storage(e.to_string())),
1057 }
1058 }
1059}
1060
1061fn row_to_event(row: &duckdb::Row<'_>) -> duckdb::Result<AgentEvent> {
1062 let id_str: String = row.get(0)?;
1063 let parent_id_str: Option<String> = row.get(4)?;
1064 let payload_json: Option<String> = row.get(6)?;
1065 let event_type_str: String = row.get(5)?;
1066 let content_hash: Vec<u8> = row.get(16)?;
1067 let prev_hash: Option<Vec<u8>> = row.get(17)?;
1068 let embedding_blob: Option<Vec<u8>> = row.get(18).unwrap_or(None);
1069
1070 Ok(AgentEvent {
1071 id: Uuid::parse_str(&id_str).map_err(|e| {
1072 duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e))
1073 })?,
1074 agent_id: row.get(1)?,
1075 thread_id: row.get(2)?,
1076 run_id: row.get(3)?,
1077 parent_event_id: parent_id_str.and_then(|s| Uuid::parse_str(&s).ok()),
1078 event_type: event_type_str.parse().map_err(|e: Error| {
1079 duckdb::Error::FromSqlConversionFailure(
1080 5,
1081 duckdb::types::Type::Text,
1082 e.to_string().into(),
1083 )
1084 })?,
1085 payload: payload_json
1086 .and_then(|s| serde_json::from_str(&s).ok())
1087 .unwrap_or(serde_json::Value::Null),
1088 trace_id: row.get(7)?,
1089 span_id: row.get(8)?,
1090 model: row.get(9)?,
1091 tokens_input: row.get(10)?,
1092 tokens_output: row.get(11)?,
1093 latency_ms: row.get(12)?,
1094 cost_usd: row.get(13)?,
1095 timestamp: row.get(14)?,
1096 logical_clock: row.get(15)?,
1097 content_hash,
1098 prev_hash,
1099 embedding: deserialize_embedding(embedding_blob),
1100 })
1101}
1102
1103fn row_to_checkpoint(row: &duckdb::Row<'_>) -> duckdb::Result<Checkpoint> {
1104 let id_str: String = row.get(0)?;
1105 let parent_id_str: Option<String> = row.get(3)?;
1106 let state_snapshot_json: Option<String> = row.get(5)?;
1107 let state_diff_json: Option<String> = row.get(6)?;
1108 let memory_refs_json: Option<String> = row.get(7)?;
1109 let event_cursor_str: Option<String> = row.get(8)?;
1110 let metadata_json: Option<String> = row.get(11)?;
1111
1112 Ok(Checkpoint {
1113 id: Uuid::parse_str(&id_str).map_err(|e| {
1114 duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e))
1115 })?,
1116 thread_id: row.get(1)?,
1117 agent_id: row.get(2)?,
1118 parent_id: parent_id_str.and_then(|s| Uuid::parse_str(&s).ok()),
1119 branch_name: row.get(4)?,
1120 state_snapshot: state_snapshot_json
1121 .and_then(|s| serde_json::from_str(&s).ok())
1122 .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
1123 state_diff: state_diff_json.and_then(|s| serde_json::from_str(&s).ok()),
1124 memory_refs: memory_refs_json
1125 .and_then(|s| serde_json::from_str::<Vec<String>>(&s).ok())
1126 .map(|v| {
1127 v.into_iter()
1128 .filter_map(|s| Uuid::parse_str(&s).ok())
1129 .collect()
1130 })
1131 .unwrap_or_default(),
1132 event_cursor: event_cursor_str.and_then(|s| Uuid::parse_str(&s).ok()),
1133 label: row.get(9)?,
1134 created_at: row.get(10)?,
1135 metadata: metadata_json
1136 .and_then(|s| serde_json::from_str(&s).ok())
1137 .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
1138 })
1139}
1140
1141fn row_to_delegation(row: &duckdb::Row<'_>) -> duckdb::Result<Delegation> {
1142 let id_str: String = row.get(0)?;
1143 let scope_type: String = row.get(4)?;
1144 let scope_value_json: Option<String> = row.get(5)?;
1145 let parent_id_str: Option<String> = row.get(8)?;
1146
1147 let scope = match scope_type.as_str() {
1148 "by_tag" => {
1149 let tags: Vec<String> = scope_value_json
1150 .and_then(|s| serde_json::from_str(&s).ok())
1151 .unwrap_or_default();
1152 DelegationScope::ByTag(tags)
1153 }
1154 "by_memory_id" => {
1155 let ids: Vec<String> = scope_value_json
1156 .and_then(|s| serde_json::from_str(&s).ok())
1157 .unwrap_or_default();
1158 let uuids = ids
1159 .into_iter()
1160 .filter_map(|s| Uuid::parse_str(&s).ok())
1161 .collect();
1162 DelegationScope::ByMemoryId(uuids)
1163 }
1164 _ => DelegationScope::AllMemories,
1165 };
1166
1167 let permission_str: String = row.get(3)?;
1168
1169 Ok(Delegation {
1170 id: Uuid::parse_str(&id_str).map_err(|e| {
1171 duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e))
1172 })?,
1173 delegator_id: row.get(1)?,
1174 delegate_id: row.get(2)?,
1175 permission: permission_str.parse().map_err(|e: Error| {
1176 duckdb::Error::FromSqlConversionFailure(
1177 3,
1178 duckdb::types::Type::Text,
1179 e.to_string().into(),
1180 )
1181 })?,
1182 scope,
1183 max_depth: row.get::<_, i32>(6)? as u32,
1184 current_depth: row.get::<_, i32>(7)? as u32,
1185 parent_delegation_id: parent_id_str.and_then(|s| Uuid::parse_str(&s).ok()),
1186 created_at: row.get(9)?,
1187 expires_at: row.get(10)?,
1188 revoked_at: row.get(11)?,
1189 })
1190}
1191
1192fn row_to_relation(row: &duckdb::Row<'_>) -> duckdb::Result<Relation> {
1193 let id_str: String = row.get(0)?;
1194 let source_str: String = row.get(1)?;
1195 let target_str: String = row.get(2)?;
1196 let metadata_json: Option<String> = row.get(5)?;
1197
1198 Ok(Relation {
1199 id: Uuid::parse_str(&id_str).map_err(|e| {
1200 duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e))
1201 })?,
1202 source_id: Uuid::parse_str(&source_str).map_err(|e| {
1203 duckdb::Error::FromSqlConversionFailure(1, duckdb::types::Type::Text, Box::new(e))
1204 })?,
1205 target_id: Uuid::parse_str(&target_str).map_err(|e| {
1206 duckdb::Error::FromSqlConversionFailure(2, duckdb::types::Type::Text, Box::new(e))
1207 })?,
1208 relation_type: row.get(3)?,
1209 weight: row.get(4)?,
1210 metadata: metadata_json
1211 .and_then(|s| serde_json::from_str(&s).ok())
1212 .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
1213 created_at: row.get(6)?,
1214 })
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219 use super::*;
1220 use crate::hash::compute_content_hash;
1221 use crate::model::acl::PrincipalType;
1222 use crate::model::checkpoint::Checkpoint;
1223 use crate::model::event::{AgentEvent, EventType};
1224 use crate::model::memory::{ConsolidationState, MemoryType, Scope, SourceType};
1225
1226 fn make_record(agent_id: &str) -> MemoryRecord {
1227 let now = chrono::Utc::now().to_rfc3339();
1228 let content = "test memory content";
1229 MemoryRecord {
1230 id: Uuid::now_v7(),
1231 agent_id: agent_id.to_string(),
1232 content: content.to_string(),
1233 memory_type: MemoryType::Semantic,
1234 scope: Scope::Private,
1235 importance: 0.7,
1236 tags: vec!["test".to_string()],
1237 metadata: serde_json::json!({"key": "value"}),
1238 embedding: Some(vec![0.1, 0.2, 0.3]),
1239 content_hash: compute_content_hash(content, agent_id, &now),
1240 prev_hash: None,
1241 source_type: SourceType::Agent,
1242 source_id: None,
1243 consolidation_state: ConsolidationState::Raw,
1244 access_count: 0,
1245 org_id: None,
1246 thread_id: None,
1247 created_at: now.clone(),
1248 updated_at: now,
1249 last_accessed_at: None,
1250 expires_at: None,
1251 deleted_at: None,
1252 decay_rate: None,
1253 created_by: None,
1254 version: 1,
1255 prev_version_id: None,
1256 quarantined: false,
1257 quarantine_reason: None,
1258 decay_function: None,
1259 }
1260 }
1261
1262 #[tokio::test]
1263 async fn test_insert_and_get() {
1264 let storage = DuckDbStorage::open_in_memory().unwrap();
1265 let record = make_record("agent-1");
1266 storage.insert_memory(&record).await.unwrap();
1267
1268 let fetched = storage.get_memory(record.id).await.unwrap().unwrap();
1269 assert_eq!(fetched.id, record.id);
1270 assert_eq!(fetched.content, record.content);
1271 assert_eq!(fetched.agent_id, record.agent_id);
1272 assert_eq!(fetched.memory_type, record.memory_type);
1273 assert_eq!(fetched.tags, record.tags);
1274 assert_eq!(fetched.embedding, record.embedding);
1275 }
1276
1277 #[tokio::test]
1278 async fn test_get_nonexistent() {
1279 let storage = DuckDbStorage::open_in_memory().unwrap();
1280 let result = storage.get_memory(Uuid::now_v7()).await.unwrap();
1281 assert!(result.is_none());
1282 }
1283
1284 #[tokio::test]
1285 async fn test_soft_delete() {
1286 let storage = DuckDbStorage::open_in_memory().unwrap();
1287 let record = make_record("agent-1");
1288 storage.insert_memory(&record).await.unwrap();
1289
1290 storage.soft_delete_memory(record.id).await.unwrap();
1291
1292 let fetched = storage.get_memory(record.id).await.unwrap().unwrap();
1294 assert!(fetched.deleted_at.is_some());
1295
1296 let filter = MemoryFilter::default();
1298 let list = storage.list_memories(&filter, 100, 0).await.unwrap();
1299 assert!(list.is_empty());
1300
1301 let filter_with_deleted = MemoryFilter {
1303 include_deleted: true,
1304 ..Default::default()
1305 };
1306 let list = storage
1307 .list_memories(&filter_with_deleted, 100, 0)
1308 .await
1309 .unwrap();
1310 assert_eq!(list.len(), 1);
1311 }
1312
1313 #[tokio::test]
1314 async fn test_hard_delete() {
1315 let storage = DuckDbStorage::open_in_memory().unwrap();
1316 let record = make_record("agent-1");
1317 storage.insert_memory(&record).await.unwrap();
1318
1319 storage.hard_delete_memory(record.id).await.unwrap();
1320
1321 let result = storage.get_memory(record.id).await.unwrap();
1322 assert!(result.is_none());
1323 }
1324
1325 #[tokio::test]
1326 async fn test_list_with_filters() {
1327 let storage = DuckDbStorage::open_in_memory().unwrap();
1328
1329 let mut r1 = make_record("agent-1");
1330 r1.memory_type = MemoryType::Episodic;
1331 storage.insert_memory(&r1).await.unwrap();
1332
1333 let mut r2 = make_record("agent-1");
1334 r2.memory_type = MemoryType::Semantic;
1335 storage.insert_memory(&r2).await.unwrap();
1336
1337 let mut r3 = make_record("agent-2");
1338 r3.memory_type = MemoryType::Semantic;
1339 storage.insert_memory(&r3).await.unwrap();
1340
1341 let filter = MemoryFilter {
1343 agent_id: Some("agent-1".to_string()),
1344 ..Default::default()
1345 };
1346 let list = storage.list_memories(&filter, 100, 0).await.unwrap();
1347 assert_eq!(list.len(), 2);
1348
1349 let filter = MemoryFilter {
1351 memory_type: Some(MemoryType::Semantic),
1352 ..Default::default()
1353 };
1354 let list = storage.list_memories(&filter, 100, 0).await.unwrap();
1355 assert_eq!(list.len(), 2);
1356
1357 let filter = MemoryFilter {
1359 agent_id: Some("agent-1".to_string()),
1360 memory_type: Some(MemoryType::Episodic),
1361 ..Default::default()
1362 };
1363 let list = storage.list_memories(&filter, 100, 0).await.unwrap();
1364 assert_eq!(list.len(), 1);
1365 }
1366
1367 #[tokio::test]
1368 async fn test_touch_memory() {
1369 let storage = DuckDbStorage::open_in_memory().unwrap();
1370 let record = make_record("agent-1");
1371 storage.insert_memory(&record).await.unwrap();
1372
1373 storage.touch_memory(record.id).await.unwrap();
1374 storage.touch_memory(record.id).await.unwrap();
1375
1376 let fetched = storage.get_memory(record.id).await.unwrap().unwrap();
1377 assert_eq!(fetched.access_count, 2);
1378 assert!(fetched.last_accessed_at.is_some());
1379 }
1380
1381 #[tokio::test]
1382 async fn test_acl_and_permission_check() {
1383 let storage = DuckDbStorage::open_in_memory().unwrap();
1384 let record = make_record("agent-1");
1385 storage.insert_memory(&record).await.unwrap();
1386
1387 assert!(
1389 storage
1390 .check_permission(record.id, "agent-1", Permission::Admin)
1391 .await
1392 .unwrap()
1393 );
1394
1395 assert!(
1397 !storage
1398 .check_permission(record.id, "agent-2", Permission::Read)
1399 .await
1400 .unwrap()
1401 );
1402
1403 let acl = Acl {
1405 id: Uuid::now_v7(),
1406 memory_id: record.id,
1407 principal_type: PrincipalType::Agent,
1408 principal_id: "agent-2".to_string(),
1409 permission: Permission::Read,
1410 granted_by: "agent-1".to_string(),
1411 created_at: chrono::Utc::now().to_rfc3339(),
1412 expires_at: None,
1413 };
1414 storage.insert_acl(&acl).await.unwrap();
1415
1416 assert!(
1418 storage
1419 .check_permission(record.id, "agent-2", Permission::Read)
1420 .await
1421 .unwrap()
1422 );
1423 assert!(
1425 !storage
1426 .check_permission(record.id, "agent-2", Permission::Write)
1427 .await
1428 .unwrap()
1429 );
1430 }
1431
1432 #[tokio::test]
1433 async fn test_event_insert_and_list() {
1434 let storage = DuckDbStorage::open_in_memory().unwrap();
1435 let now = chrono::Utc::now().to_rfc3339();
1436 let event = AgentEvent {
1437 id: Uuid::now_v7(),
1438 agent_id: "agent-1".to_string(),
1439 thread_id: Some("thread-1".to_string()),
1440 run_id: None,
1441 parent_event_id: None,
1442 event_type: EventType::MemoryWrite,
1443 payload: serde_json::json!({"memory_id": "abc"}),
1444 trace_id: None,
1445 span_id: None,
1446 model: None,
1447 tokens_input: None,
1448 tokens_output: None,
1449 latency_ms: None,
1450 cost_usd: None,
1451 timestamp: now.clone(),
1452 logical_clock: 1,
1453 content_hash: vec![1, 2, 3],
1454 prev_hash: None,
1455 embedding: None,
1456 };
1457
1458 storage.insert_event(&event).await.unwrap();
1459
1460 let events = storage.list_events("agent-1", 10, 0).await.unwrap();
1461 assert_eq!(events.len(), 1);
1462 assert_eq!(events[0].id, event.id);
1463 assert_eq!(events[0].event_type, EventType::MemoryWrite);
1464 assert_eq!(events[0].agent_id, "agent-1");
1465
1466 let fetched = storage.get_event(event.id).await.unwrap().unwrap();
1468 assert_eq!(fetched.id, event.id);
1469 assert_eq!(fetched.content_hash, vec![1, 2, 3]);
1470 }
1471
1472 #[tokio::test]
1473 async fn test_events_by_thread() {
1474 let storage = DuckDbStorage::open_in_memory().unwrap();
1475 let now = chrono::Utc::now().to_rfc3339();
1476
1477 for i in 0..3 {
1478 let event = AgentEvent {
1479 id: Uuid::now_v7(),
1480 agent_id: "agent-1".to_string(),
1481 thread_id: Some("thread-A".to_string()),
1482 run_id: None,
1483 parent_event_id: None,
1484 event_type: EventType::MemoryWrite,
1485 payload: serde_json::json!({"i": i}),
1486 trace_id: None,
1487 span_id: None,
1488 model: None,
1489 tokens_input: None,
1490 tokens_output: None,
1491 latency_ms: None,
1492 cost_usd: None,
1493 timestamp: now.clone(),
1494 logical_clock: i,
1495 content_hash: vec![i as u8],
1496 prev_hash: None,
1497 embedding: None,
1498 };
1499 storage.insert_event(&event).await.unwrap();
1500 }
1501
1502 let event = AgentEvent {
1504 id: Uuid::now_v7(),
1505 agent_id: "agent-1".to_string(),
1506 thread_id: Some("thread-B".to_string()),
1507 run_id: None,
1508 parent_event_id: None,
1509 event_type: EventType::MemoryRead,
1510 payload: serde_json::json!({}),
1511 trace_id: None,
1512 span_id: None,
1513 model: None,
1514 tokens_input: None,
1515 tokens_output: None,
1516 latency_ms: None,
1517 cost_usd: None,
1518 timestamp: now.clone(),
1519 logical_clock: 0,
1520 content_hash: vec![99],
1521 prev_hash: None,
1522 embedding: None,
1523 };
1524 storage.insert_event(&event).await.unwrap();
1525
1526 let thread_a = storage.get_events_by_thread("thread-A", 10).await.unwrap();
1527 assert_eq!(thread_a.len(), 3);
1528
1529 let thread_b = storage.get_events_by_thread("thread-B", 10).await.unwrap();
1530 assert_eq!(thread_b.len(), 1);
1531 assert_eq!(thread_b[0].event_type, EventType::MemoryRead);
1532 }
1533
1534 #[tokio::test]
1535 async fn test_checkpoint_insert_and_get() {
1536 let storage = DuckDbStorage::open_in_memory().unwrap();
1537 let mem_id = Uuid::now_v7();
1538 let cp = Checkpoint {
1539 id: Uuid::now_v7(),
1540 thread_id: "thread-1".to_string(),
1541 agent_id: "agent-1".to_string(),
1542 parent_id: None,
1543 branch_name: "main".to_string(),
1544 state_snapshot: serde_json::json!({"step": 1}),
1545 state_diff: None,
1546 memory_refs: vec![mem_id],
1547 event_cursor: None,
1548 label: Some("initial".to_string()),
1549 created_at: chrono::Utc::now().to_rfc3339(),
1550 metadata: serde_json::json!({}),
1551 };
1552
1553 storage.insert_checkpoint(&cp).await.unwrap();
1554
1555 let fetched = storage.get_checkpoint(cp.id).await.unwrap().unwrap();
1556 assert_eq!(fetched.id, cp.id);
1557 assert_eq!(fetched.thread_id, "thread-1");
1558 assert_eq!(fetched.branch_name, "main");
1559 assert_eq!(fetched.memory_refs, vec![mem_id]);
1560 assert_eq!(fetched.label, Some("initial".to_string()));
1561 }
1562
1563 #[tokio::test]
1564 async fn test_checkpoint_list_and_latest() {
1565 let storage = DuckDbStorage::open_in_memory().unwrap();
1566
1567 let cp1 = Checkpoint {
1568 id: Uuid::now_v7(),
1569 thread_id: "thread-1".to_string(),
1570 agent_id: "agent-1".to_string(),
1571 parent_id: None,
1572 branch_name: "main".to_string(),
1573 state_snapshot: serde_json::json!({"step": 1}),
1574 state_diff: None,
1575 memory_refs: vec![],
1576 event_cursor: None,
1577 label: Some("first".to_string()),
1578 created_at: "2025-01-01T00:00:00Z".to_string(),
1579 metadata: serde_json::json!({}),
1580 };
1581 storage.insert_checkpoint(&cp1).await.unwrap();
1582
1583 let cp2 = Checkpoint {
1584 id: Uuid::now_v7(),
1585 thread_id: "thread-1".to_string(),
1586 agent_id: "agent-1".to_string(),
1587 parent_id: Some(cp1.id),
1588 branch_name: "main".to_string(),
1589 state_snapshot: serde_json::json!({"step": 2}),
1590 state_diff: Some(serde_json::json!({"step": [1, 2]})),
1591 memory_refs: vec![],
1592 event_cursor: None,
1593 label: Some("second".to_string()),
1594 created_at: "2025-01-02T00:00:00Z".to_string(),
1595 metadata: serde_json::json!({}),
1596 };
1597 storage.insert_checkpoint(&cp2).await.unwrap();
1598
1599 let cp3 = Checkpoint {
1600 id: Uuid::now_v7(),
1601 thread_id: "thread-1".to_string(),
1602 agent_id: "agent-1".to_string(),
1603 parent_id: Some(cp1.id),
1604 branch_name: "experiment".to_string(),
1605 state_snapshot: serde_json::json!({"step": "alt"}),
1606 state_diff: None,
1607 memory_refs: vec![],
1608 event_cursor: None,
1609 label: None,
1610 created_at: "2025-01-03T00:00:00Z".to_string(),
1611 metadata: serde_json::json!({}),
1612 };
1613 storage.insert_checkpoint(&cp3).await.unwrap();
1614
1615 let all = storage
1617 .list_checkpoints("thread-1", None, 10)
1618 .await
1619 .unwrap();
1620 assert_eq!(all.len(), 3);
1621
1622 let main_cps = storage
1624 .list_checkpoints("thread-1", Some("main"), 10)
1625 .await
1626 .unwrap();
1627 assert_eq!(main_cps.len(), 2);
1628
1629 let exp_cps = storage
1630 .list_checkpoints("thread-1", Some("experiment"), 10)
1631 .await
1632 .unwrap();
1633 assert_eq!(exp_cps.len(), 1);
1634
1635 let latest = storage
1637 .get_latest_checkpoint("thread-1", "main")
1638 .await
1639 .unwrap()
1640 .unwrap();
1641 assert_eq!(latest.id, cp2.id);
1642
1643 let latest_exp = storage
1645 .get_latest_checkpoint("thread-1", "experiment")
1646 .await
1647 .unwrap()
1648 .unwrap();
1649 assert_eq!(latest_exp.id, cp3.id);
1650
1651 let none = storage
1653 .get_latest_checkpoint("thread-1", "nonexistent")
1654 .await
1655 .unwrap();
1656 assert!(none.is_none());
1657 }
1658}