1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum MemoryTier {
12 Session,
14 Project,
16 Global,
18}
19
20impl MemoryTier {
21 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#[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, 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#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct MemorySearchResult {
63 pub chunk: MemoryChunk,
64 pub similarity: f64,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct MemoryConfig {
70 pub max_chunks: i64,
72 pub chunk_size: i64,
74 pub retrieval_k: i64,
76 pub auto_cleanup: bool,
78 pub session_retention_days: i64,
80 pub token_budget: i64,
82 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#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct MemoryStats {
103 pub total_chunks: i64,
105 pub session_chunks: i64,
107 pub project_chunks: i64,
109 pub global_chunks: i64,
111 pub total_bytes: i64,
113 pub session_bytes: i64,
115 pub project_bytes: i64,
117 pub global_bytes: i64,
119 pub file_size: i64,
121 pub last_cleanup: Option<DateTime<Utc>>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct MemoryContext {
128 pub current_session: Vec<MemoryChunk>,
130 pub relevant_history: Vec<MemoryChunk>,
132 pub project_facts: Vec<MemoryChunk>,
134 pub total_tokens: i64,
136}
137
138#[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 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#[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct ProjectMemoryStats {
206 pub project_id: String,
207 pub project_chunks: i64,
209 pub project_bytes: i64,
210 pub file_index_chunks: i64,
212 pub file_index_bytes: i64,
213 pub indexed_files: i64,
215 pub last_indexed_at: Option<DateTime<Utc>>,
217 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, 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#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct EmbeddingHealth {
245 pub status: String,
247 pub reason: Option<String>,
249}
250
251#[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
294impl 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#[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
319pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
321
322pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
324
325pub const MAX_CHUNK_LENGTH: usize = 4000;
327
328pub const MIN_CHUNK_LENGTH: usize = 50;