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/// Request to search memory
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct SearchMemoryRequest {
235    pub query: String,
236    pub tier: Option<MemoryTier>,
237    pub project_id: Option<String>,
238    pub session_id: Option<String>,
239    pub limit: Option<i64>,
240}
241
242/// Embedding backend health surfaced to UI/events.
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct EmbeddingHealth {
245    /// "ok" when embeddings are available, "degraded_disabled" otherwise.
246    pub status: String,
247    /// Optional reason when degraded.
248    pub reason: Option<String>,
249}
250
251/// Memory error types
252#[derive(Error, Debug)]
253pub enum MemoryError {
254    #[error("Database error: {0}")]
255    Database(#[from] rusqlite::Error),
256
257    #[error("IO error: {0}")]
258    Io(#[from] std::io::Error),
259
260    #[error("Serialization error: {0}")]
261    Serialization(#[from] serde_json::Error),
262
263    #[error("Embedding error: {0}")]
264    Embedding(String),
265
266    #[error("Chunking error: {0}")]
267    Chunking(String),
268
269    #[error("Invalid configuration: {0}")]
270    InvalidConfig(String),
271
272    #[error("Not found: {0}")]
273    NotFound(String),
274
275    #[error("Tokenization error: {0}")]
276    Tokenization(String),
277
278    #[error("Lock error: {0}")]
279    Lock(String),
280}
281
282impl From<String> for MemoryError {
283    fn from(err: String) -> Self {
284        MemoryError::InvalidConfig(err)
285    }
286}
287
288impl From<&str> for MemoryError {
289    fn from(err: &str) -> Self {
290        MemoryError::InvalidConfig(err.to_string())
291    }
292}
293
294// Implement serialization for Tauri commands
295impl serde::Serialize for MemoryError {
296    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
297    where
298        S: serde::Serializer,
299    {
300        serializer.serialize_str(&self.to_string())
301    }
302}
303
304pub type MemoryResult<T> = Result<T, MemoryError>;
305
306/// Cleanup log entry for audit trail
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct CleanupLogEntry {
309    pub id: String,
310    pub cleanup_type: String,
311    pub tier: MemoryTier,
312    pub project_id: Option<String>,
313    pub session_id: Option<String>,
314    pub chunks_deleted: i64,
315    pub bytes_reclaimed: i64,
316    pub created_at: DateTime<Utc>,
317}
318
319/// Default embedding dimension for all-MiniLM-L6-v2
320pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
321
322/// Default embedding model name
323pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
324
325/// Maximum content length for a single chunk (in characters)
326pub const MAX_CHUNK_LENGTH: usize = 4000;
327
328/// Minimum content length for a chunk (in characters)
329pub const MIN_CHUNK_LENGTH: usize = 50;