Skip to main content

tandem_memory/
types.rs

1// Memory Context Types
2// Type definitions and error types for the memory system
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8/// Memory tier - determines persistence level
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum MemoryTier {
12    /// Ephemeral session memory - cleared when session ends
13    Session,
14    /// Persistent project memory - survives across sessions
15    Project,
16    /// Cross-project global memory - user preferences and patterns
17    Global,
18}
19
20impl MemoryTier {
21    /// Get the table prefix for this tier
22    pub fn table_prefix(&self) -> &'static str {
23        match self {
24            MemoryTier::Session => "session",
25            MemoryTier::Project => "project",
26            MemoryTier::Global => "global",
27        }
28    }
29}
30
31impl std::fmt::Display for MemoryTier {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            MemoryTier::Session => write!(f, "session"),
35            MemoryTier::Project => write!(f, "project"),
36            MemoryTier::Global => write!(f, "global"),
37        }
38    }
39}
40
41/// A memory chunk - unit of storage
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct MemoryChunk {
44    pub id: String,
45    pub content: String,
46    pub tier: MemoryTier,
47    pub session_id: Option<String>,
48    pub project_id: Option<String>,
49    pub source: String, // e.g., "user_message", "assistant_response", "file_content"
50    // File-derived fields (only set when source == "file")
51    pub source_path: Option<String>,
52    pub source_mtime: Option<i64>,
53    pub source_size: Option<i64>,
54    pub source_hash: Option<String>,
55    pub created_at: DateTime<Utc>,
56    pub token_count: i64,
57    pub metadata: Option<serde_json::Value>,
58}
59
60/// Search result with similarity score
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct MemorySearchResult {
63    pub chunk: MemoryChunk,
64    pub similarity: f64,
65}
66
67/// Memory configuration for a project
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct MemoryConfig {
70    /// Maximum chunks to store per project
71    pub max_chunks: i64,
72    /// Chunk size in tokens
73    pub chunk_size: i64,
74    /// Number of chunks to retrieve
75    pub retrieval_k: i64,
76    /// Whether auto-cleanup is enabled
77    pub auto_cleanup: bool,
78    /// Session memory retention in days
79    pub session_retention_days: i64,
80    /// Token budget for memory context injection
81    pub token_budget: i64,
82    /// Overlap between chunks in tokens
83    pub chunk_overlap: i64,
84}
85
86impl Default for MemoryConfig {
87    fn default() -> Self {
88        Self {
89            max_chunks: 10_000,
90            chunk_size: 512,
91            retrieval_k: 5,
92            auto_cleanup: true,
93            session_retention_days: 30,
94            token_budget: 5000,
95            chunk_overlap: 64,
96        }
97    }
98}
99
100/// Memory storage statistics
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct MemoryStats {
103    /// Total number of chunks
104    pub total_chunks: i64,
105    /// Number of session chunks
106    pub session_chunks: i64,
107    /// Number of project chunks
108    pub project_chunks: i64,
109    /// Number of global chunks
110    pub global_chunks: i64,
111    /// Total size in bytes
112    pub total_bytes: i64,
113    /// Session memory size in bytes
114    pub session_bytes: i64,
115    /// Project memory size in bytes
116    pub project_bytes: i64,
117    /// Global memory size in bytes
118    pub global_bytes: i64,
119    /// Database file size in bytes
120    pub file_size: i64,
121    /// Last cleanup timestamp
122    pub last_cleanup: Option<DateTime<Utc>>,
123}
124
125/// Context to inject into messages
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct MemoryContext {
128    /// Recent messages from current session
129    pub current_session: Vec<MemoryChunk>,
130    /// Relevant historical chunks
131    pub relevant_history: Vec<MemoryChunk>,
132    /// Important project facts
133    pub project_facts: Vec<MemoryChunk>,
134    /// Total tokens in context
135    pub total_tokens: i64,
136}
137
138/// Metadata describing how memory retrieval executed for a single query.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct MemoryRetrievalMeta {
141    pub used: bool,
142    pub chunks_total: usize,
143    pub session_chunks: usize,
144    pub history_chunks: usize,
145    pub project_fact_chunks: usize,
146    pub score_min: Option<f64>,
147    pub score_max: Option<f64>,
148}
149
150impl MemoryContext {
151    /// Format the context for injection into a prompt
152    pub fn format_for_injection(&self) -> String {
153        let mut parts = Vec::new();
154
155        if !self.current_session.is_empty() {
156            parts.push("<current_session>".to_string());
157            for chunk in &self.current_session {
158                parts.push(format!("- {}", chunk.content));
159            }
160            parts.push("</current_session>".to_string());
161        }
162
163        if !self.relevant_history.is_empty() {
164            parts.push("<relevant_history>".to_string());
165            for chunk in &self.relevant_history {
166                parts.push(format!("- {}", chunk.content));
167            }
168            parts.push("</relevant_history>".to_string());
169        }
170
171        if !self.project_facts.is_empty() {
172            parts.push("<project_facts>".to_string());
173            for chunk in &self.project_facts {
174                parts.push(format!("- {}", chunk.content));
175            }
176            parts.push("</project_facts>".to_string());
177        }
178
179        if parts.is_empty() {
180            String::new()
181        } else {
182            format!("<memory_context>\n{}\n</memory_context>", parts.join("\n"))
183        }
184    }
185}
186
187/// Request to store a message
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct StoreMessageRequest {
190    pub content: String,
191    pub tier: MemoryTier,
192    pub session_id: Option<String>,
193    pub project_id: Option<String>,
194    pub source: String,
195    // File-derived fields (only set when source == "file")
196    pub source_path: Option<String>,
197    pub source_mtime: Option<i64>,
198    pub source_size: Option<i64>,
199    pub source_hash: Option<String>,
200    pub metadata: Option<serde_json::Value>,
201}
202
203/// Project-scoped memory statistics (filtered by project_id)
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct ProjectMemoryStats {
206    pub project_id: String,
207    /// Total chunks stored under this project_id (all sources)
208    pub project_chunks: i64,
209    pub project_bytes: i64,
210    /// Chunks/bytes that came from workspace file indexing (source == "file")
211    pub file_index_chunks: i64,
212    pub file_index_bytes: i64,
213    /// Number of indexed files currently tracked for this project_id
214    pub indexed_files: i64,
215    /// Last time indexing completed for this project_id (if known)
216    pub last_indexed_at: Option<DateTime<Utc>>,
217    /// Last run totals (if known)
218    pub last_total_files: Option<i64>,
219    pub last_processed_files: Option<i64>,
220    pub last_indexed_files: Option<i64>,
221    pub last_skipped_files: Option<i64>,
222    pub last_errors: Option<i64>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ClearFileIndexResult {
227    pub chunks_deleted: i64,
228    pub bytes_estimated: i64,
229    pub did_vacuum: bool,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
233#[serde(rename_all = "snake_case")]
234pub enum MemoryImportFormat {
235    Directory,
236    Openclaw,
237}
238
239impl std::fmt::Display for MemoryImportFormat {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        match self {
242            MemoryImportFormat::Directory => write!(f, "directory"),
243            MemoryImportFormat::Openclaw => write!(f, "openclaw"),
244        }
245    }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct MemoryImportRequest {
250    pub root_path: String,
251    pub format: MemoryImportFormat,
252    pub tier: MemoryTier,
253    pub session_id: Option<String>,
254    pub project_id: Option<String>,
255    pub sync_deletes: bool,
256}
257
258#[derive(Debug, Clone, Default, Serialize, Deserialize)]
259pub struct MemoryImportStats {
260    pub discovered_files: usize,
261    pub files_processed: usize,
262    pub indexed_files: usize,
263    pub skipped_files: usize,
264    pub deleted_files: usize,
265    pub chunks_created: usize,
266    pub errors: usize,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct MemoryImportProgress {
271    pub files_processed: usize,
272    pub total_files: usize,
273    pub indexed_files: usize,
274    pub skipped_files: usize,
275    pub deleted_files: usize,
276    pub errors: usize,
277    pub chunks_created: usize,
278    pub current_file: String,
279}
280
281/// Request to search memory
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct SearchMemoryRequest {
284    pub query: String,
285    pub tier: Option<MemoryTier>,
286    pub project_id: Option<String>,
287    pub session_id: Option<String>,
288    pub limit: Option<i64>,
289}
290
291/// Embedding backend health surfaced to UI/events.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct EmbeddingHealth {
294    /// "ok" when embeddings are available, "degraded_disabled" otherwise.
295    pub status: String,
296    /// Optional reason when degraded.
297    pub reason: Option<String>,
298}
299
300/// Memory error types
301#[derive(Error, Debug)]
302pub enum MemoryError {
303    #[error("Database error: {0}")]
304    Database(#[from] rusqlite::Error),
305
306    #[error("IO error: {0}")]
307    Io(#[from] std::io::Error),
308
309    #[error("Serialization error: {0}")]
310    Serialization(#[from] serde_json::Error),
311
312    #[error("Embedding error: {0}")]
313    Embedding(String),
314
315    #[error("Chunking error: {0}")]
316    Chunking(String),
317
318    #[error("Invalid configuration: {0}")]
319    InvalidConfig(String),
320
321    #[error("Not found: {0}")]
322    NotFound(String),
323
324    #[error("Tokenization error: {0}")]
325    Tokenization(String),
326
327    #[error("Lock error: {0}")]
328    Lock(String),
329}
330
331impl From<String> for MemoryError {
332    fn from(err: String) -> Self {
333        MemoryError::InvalidConfig(err)
334    }
335}
336
337impl From<&str> for MemoryError {
338    fn from(err: &str) -> Self {
339        MemoryError::InvalidConfig(err.to_string())
340    }
341}
342
343// Implement serialization for Tauri commands
344impl serde::Serialize for MemoryError {
345    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
346    where
347        S: serde::Serializer,
348    {
349        serializer.serialize_str(&self.to_string())
350    }
351}
352
353pub type MemoryResult<T> = Result<T, MemoryError>;
354
355/// Cleanup log entry for audit trail
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct CleanupLogEntry {
358    pub id: String,
359    pub cleanup_type: String,
360    pub tier: MemoryTier,
361    pub project_id: Option<String>,
362    pub session_id: Option<String>,
363    pub chunks_deleted: i64,
364    pub bytes_reclaimed: i64,
365    pub created_at: DateTime<Utc>,
366}
367
368/// Default embedding dimension for all-MiniLM-L6-v2
369pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
370
371/// Default embedding model name
372pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
373
374/// Maximum content length for a single chunk (in characters)
375pub const MAX_CHUNK_LENGTH: usize = 4000;
376
377/// Minimum content length for a chunk (in characters)
378pub const MIN_CHUNK_LENGTH: usize = 50;
379
380/// Persistent global memory record keyed by user identity.
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct GlobalMemoryRecord {
383    pub id: String,
384    pub user_id: String,
385    pub source_type: String,
386    pub content: String,
387    pub content_hash: String,
388    pub run_id: String,
389    pub session_id: Option<String>,
390    pub message_id: Option<String>,
391    pub tool_name: Option<String>,
392    pub project_tag: Option<String>,
393    pub channel_tag: Option<String>,
394    pub host_tag: Option<String>,
395    pub metadata: Option<serde_json::Value>,
396    pub provenance: Option<serde_json::Value>,
397    pub redaction_status: String,
398    pub redaction_count: u32,
399    pub visibility: String,
400    pub demoted: bool,
401    pub score_boost: f64,
402    pub created_at_ms: u64,
403    pub updated_at_ms: u64,
404    pub expires_at_ms: Option<u64>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct GlobalMemoryWriteResult {
409    pub id: String,
410    pub stored: bool,
411    pub deduped: bool,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct GlobalMemorySearchHit {
416    pub record: GlobalMemoryRecord,
417    pub score: f64,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
421#[serde(rename_all = "lowercase")]
422pub enum NodeType {
423    Directory,
424    File,
425}
426
427impl std::fmt::Display for NodeType {
428    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429        match self {
430            NodeType::Directory => write!(f, "directory"),
431            NodeType::File => write!(f, "file"),
432        }
433    }
434}
435
436impl std::str::FromStr for NodeType {
437    type Err = String;
438    fn from_str(s: &str) -> Result<Self, Self::Err> {
439        match s.to_lowercase().as_str() {
440            "directory" => Ok(NodeType::Directory),
441            "file" => Ok(NodeType::File),
442            _ => Err(format!("unknown node type: {}", s)),
443        }
444    }
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
448#[serde(rename_all = "UPPERCASE")]
449pub enum LayerType {
450    L0,
451    L1,
452    L2,
453}
454
455impl std::fmt::Display for LayerType {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        match self {
458            LayerType::L0 => write!(f, "L0"),
459            LayerType::L1 => write!(f, "L1"),
460            LayerType::L2 => write!(f, "L2"),
461        }
462    }
463}
464
465impl std::str::FromStr for LayerType {
466    type Err = String;
467    fn from_str(s: &str) -> Result<Self, Self::Err> {
468        match s.to_uppercase().as_str() {
469            "L0" | "L0_ABSTRACT" => Ok(LayerType::L0),
470            "L1" | "L1_OVERVIEW" => Ok(LayerType::L1),
471            "L2" | "L2_DETAIL" => Ok(LayerType::L2),
472            _ => Err(format!("unknown layer type: {}", s)),
473        }
474    }
475}
476
477impl LayerType {
478    pub fn default_tokens(&self) -> usize {
479        match self {
480            LayerType::L0 => 100,
481            LayerType::L1 => 2000,
482            LayerType::L2 => 4000,
483        }
484    }
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct RetrievalStep {
489    pub step_type: String,
490    pub description: String,
491    pub layer_accessed: Option<LayerType>,
492    pub nodes_evaluated: usize,
493    pub scores: std::collections::HashMap<String, f64>,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct NodeVisit {
498    pub uri: String,
499    pub node_type: NodeType,
500    pub score: f64,
501    pub depth: usize,
502    pub layer_loaded: Option<LayerType>,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct RetrievalTrajectory {
507    pub id: String,
508    pub query: String,
509    pub root_uri: String,
510    pub steps: Vec<RetrievalStep>,
511    pub visited_nodes: Vec<NodeVisit>,
512    pub total_duration_ms: u64,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct RetrievalResult {
517    pub node_id: String,
518    pub uri: String,
519    pub content: String,
520    pub layer_type: LayerType,
521    pub score: f64,
522    pub trajectory: RetrievalTrajectory,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct MemoryNode {
527    pub id: String,
528    pub uri: String,
529    pub parent_uri: Option<String>,
530    pub node_type: NodeType,
531    pub created_at: DateTime<Utc>,
532    pub updated_at: DateTime<Utc>,
533    pub metadata: Option<serde_json::Value>,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct MemoryLayer {
538    pub id: String,
539    pub node_id: String,
540    pub layer_type: LayerType,
541    pub content: String,
542    pub token_count: i64,
543    pub embedding_id: Option<String>,
544    pub created_at: DateTime<Utc>,
545    pub source_chunk_id: Option<String>,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct TreeNode {
550    pub node: MemoryNode,
551    pub children: Vec<TreeNode>,
552    pub layer_summary: Option<LayerSummary>,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct LayerSummary {
557    pub l0_preview: Option<String>,
558    pub l1_preview: Option<String>,
559    pub has_l2: bool,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct DirectoryListing {
564    pub uri: String,
565    pub nodes: Vec<MemoryNode>,
566    pub total_children: usize,
567    pub directories: Vec<MemoryNode>,
568    pub files: Vec<MemoryNode>,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct DistilledFact {
573    pub id: String,
574    pub distillation_id: String,
575    pub content: String,
576    pub category: FactCategory,
577    pub importance_score: f64,
578    pub source_message_ids: Vec<String>,
579    pub contradicts_fact_id: Option<String>,
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
583#[serde(rename_all = "snake_case")]
584pub enum FactCategory {
585    UserPreference,
586    TaskOutcome,
587    Learning,
588    Fact,
589}
590
591impl std::fmt::Display for FactCategory {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        match self {
594            FactCategory::UserPreference => write!(f, "user_preference"),
595            FactCategory::TaskOutcome => write!(f, "task_outcome"),
596            FactCategory::Learning => write!(f, "learning"),
597            FactCategory::Fact => write!(f, "fact"),
598        }
599    }
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct DistillationReport {
604    pub distillation_id: String,
605    pub session_id: String,
606    pub distilled_at: DateTime<Utc>,
607    pub facts_extracted: usize,
608    pub importance_threshold: f64,
609    pub user_memory_updated: bool,
610    pub agent_memory_updated: bool,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct SessionDistillation {
615    pub id: String,
616    pub session_id: String,
617    pub distilled_at: DateTime<Utc>,
618    pub input_token_count: i64,
619    pub output_memory_count: usize,
620    pub key_facts_extracted: usize,
621    pub importance_threshold: f64,
622}