1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use tandem_orchestrator::{KnowledgeScope, KnowledgeTrustLevel};
7use thiserror::Error;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum MemoryTier {
13 Session,
15 Project,
17 Global,
19}
20
21impl MemoryTier {
22 pub fn table_prefix(&self) -> &'static str {
24 match self {
25 MemoryTier::Session => "session",
26 MemoryTier::Project => "project",
27 MemoryTier::Global => "global",
28 }
29 }
30}
31
32impl std::fmt::Display for MemoryTier {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 MemoryTier::Session => write!(f, "session"),
36 MemoryTier::Project => write!(f, "project"),
37 MemoryTier::Global => write!(f, "global"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct MemoryChunk {
45 pub id: String,
46 pub content: String,
47 pub tier: MemoryTier,
48 pub session_id: Option<String>,
49 pub project_id: Option<String>,
50 pub source: String, pub source_path: Option<String>,
53 pub source_mtime: Option<i64>,
54 pub source_size: Option<i64>,
55 pub source_hash: Option<String>,
56 pub created_at: DateTime<Utc>,
57 pub token_count: i64,
58 pub metadata: Option<serde_json::Value>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct MemorySearchResult {
64 pub chunk: MemoryChunk,
65 pub similarity: f64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct MemoryConfig {
71 pub max_chunks: i64,
73 pub chunk_size: i64,
75 pub retrieval_k: i64,
77 pub auto_cleanup: bool,
79 pub session_retention_days: i64,
81 pub token_budget: i64,
83 pub chunk_overlap: i64,
85}
86
87impl Default for MemoryConfig {
88 fn default() -> Self {
89 Self {
90 max_chunks: 10_000,
91 chunk_size: 512,
92 retrieval_k: 5,
93 auto_cleanup: true,
94 session_retention_days: 30,
95 token_budget: 5000,
96 chunk_overlap: 64,
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct MemoryStats {
104 pub total_chunks: i64,
106 pub session_chunks: i64,
108 pub project_chunks: i64,
110 pub global_chunks: i64,
112 pub total_bytes: i64,
114 pub session_bytes: i64,
116 pub project_bytes: i64,
118 pub global_bytes: i64,
120 pub file_size: i64,
122 pub last_cleanup: Option<DateTime<Utc>>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct MemoryContext {
129 pub current_session: Vec<MemoryChunk>,
131 pub relevant_history: Vec<MemoryChunk>,
133 pub project_facts: Vec<MemoryChunk>,
135 pub total_tokens: i64,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct MemoryRetrievalMeta {
142 pub used: bool,
143 pub chunks_total: usize,
144 pub session_chunks: usize,
145 pub history_chunks: usize,
146 pub project_fact_chunks: usize,
147 pub score_min: Option<f64>,
148 pub score_max: Option<f64>,
149}
150
151impl MemoryContext {
152 pub fn format_for_injection(&self) -> String {
154 let mut parts = Vec::new();
155
156 if !self.current_session.is_empty() {
157 parts.push("<current_session>".to_string());
158 for chunk in &self.current_session {
159 parts.push(format!("- {}", chunk.content));
160 }
161 parts.push("</current_session>".to_string());
162 }
163
164 if !self.relevant_history.is_empty() {
165 parts.push("<relevant_history>".to_string());
166 for chunk in &self.relevant_history {
167 parts.push(format!("- {}", chunk.content));
168 }
169 parts.push("</relevant_history>".to_string());
170 }
171
172 if !self.project_facts.is_empty() {
173 parts.push("<project_facts>".to_string());
174 for chunk in &self.project_facts {
175 parts.push(format!("- {}", chunk.content));
176 }
177 parts.push("</project_facts>".to_string());
178 }
179
180 if parts.is_empty() {
181 String::new()
182 } else {
183 format!("<memory_context>\n{}\n</memory_context>", parts.join("\n"))
184 }
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct StoreMessageRequest {
191 pub content: String,
192 pub tier: MemoryTier,
193 pub session_id: Option<String>,
194 pub project_id: Option<String>,
195 pub source: String,
196 pub source_path: Option<String>,
198 pub source_mtime: Option<i64>,
199 pub source_size: Option<i64>,
200 pub source_hash: Option<String>,
201 pub metadata: Option<serde_json::Value>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct ProjectMemoryStats {
207 pub project_id: String,
208 pub project_chunks: i64,
210 pub project_bytes: i64,
211 pub file_index_chunks: i64,
213 pub file_index_bytes: i64,
214 pub indexed_files: i64,
216 pub last_indexed_at: Option<DateTime<Utc>>,
218 pub last_total_files: Option<i64>,
220 pub last_processed_files: Option<i64>,
221 pub last_indexed_files: Option<i64>,
222 pub last_skipped_files: Option<i64>,
223 pub last_errors: Option<i64>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ClearFileIndexResult {
228 pub chunks_deleted: i64,
229 pub bytes_estimated: i64,
230 pub did_vacuum: bool,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(rename_all = "snake_case")]
235pub enum MemoryImportFormat {
236 Directory,
237 Openclaw,
238}
239
240impl std::fmt::Display for MemoryImportFormat {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 match self {
243 MemoryImportFormat::Directory => write!(f, "directory"),
244 MemoryImportFormat::Openclaw => write!(f, "openclaw"),
245 }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct MemoryImportRequest {
251 pub root_path: String,
252 pub format: MemoryImportFormat,
253 pub tier: MemoryTier,
254 pub session_id: Option<String>,
255 pub project_id: Option<String>,
256 pub sync_deletes: bool,
257}
258
259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
260pub struct MemoryImportStats {
261 pub discovered_files: usize,
262 pub files_processed: usize,
263 pub indexed_files: usize,
264 pub skipped_files: usize,
265 pub deleted_files: usize,
266 pub chunks_created: usize,
267 pub errors: usize,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct MemoryImportProgress {
272 pub files_processed: usize,
273 pub total_files: usize,
274 pub indexed_files: usize,
275 pub skipped_files: usize,
276 pub deleted_files: usize,
277 pub errors: usize,
278 pub chunks_created: usize,
279 pub current_file: String,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct SearchMemoryRequest {
285 pub query: String,
286 pub tier: Option<MemoryTier>,
287 pub project_id: Option<String>,
288 pub session_id: Option<String>,
289 pub limit: Option<i64>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct EmbeddingHealth {
295 pub status: String,
297 pub reason: Option<String>,
299}
300
301#[derive(Error, Debug)]
303pub enum MemoryError {
304 #[error("Database error: {0}")]
305 Database(#[from] rusqlite::Error),
306
307 #[error("IO error: {0}")]
308 Io(#[from] std::io::Error),
309
310 #[error("Serialization error: {0}")]
311 Serialization(#[from] serde_json::Error),
312
313 #[error("Embedding error: {0}")]
314 Embedding(String),
315
316 #[error("Chunking error: {0}")]
317 Chunking(String),
318
319 #[error("Invalid configuration: {0}")]
320 InvalidConfig(String),
321
322 #[error("Not found: {0}")]
323 NotFound(String),
324
325 #[error("Tokenization error: {0}")]
326 Tokenization(String),
327
328 #[error("Lock error: {0}")]
329 Lock(String),
330}
331
332impl From<String> for MemoryError {
333 fn from(err: String) -> Self {
334 MemoryError::InvalidConfig(err)
335 }
336}
337
338impl From<&str> for MemoryError {
339 fn from(err: &str) -> Self {
340 MemoryError::InvalidConfig(err.to_string())
341 }
342}
343
344impl serde::Serialize for MemoryError {
346 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
347 where
348 S: serde::Serializer,
349 {
350 serializer.serialize_str(&self.to_string())
351 }
352}
353
354pub type MemoryResult<T> = Result<T, MemoryError>;
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct CleanupLogEntry {
359 pub id: String,
360 pub cleanup_type: String,
361 pub tier: MemoryTier,
362 pub project_id: Option<String>,
363 pub session_id: Option<String>,
364 pub chunks_deleted: i64,
365 pub bytes_reclaimed: i64,
366 pub created_at: DateTime<Utc>,
367}
368
369pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
371
372pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
374
375pub const MAX_CHUNK_LENGTH: usize = 4000;
377
378pub const MIN_CHUNK_LENGTH: usize = 50;
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct GlobalMemoryRecord {
384 pub id: String,
385 pub user_id: String,
386 pub source_type: String,
387 pub content: String,
388 pub content_hash: String,
389 pub run_id: String,
390 pub session_id: Option<String>,
391 pub message_id: Option<String>,
392 pub tool_name: Option<String>,
393 pub project_tag: Option<String>,
394 pub channel_tag: Option<String>,
395 pub host_tag: Option<String>,
396 pub metadata: Option<serde_json::Value>,
397 pub provenance: Option<serde_json::Value>,
398 pub redaction_status: String,
399 pub redaction_count: u32,
400 pub visibility: String,
401 pub demoted: bool,
402 pub score_boost: f64,
403 pub created_at_ms: u64,
404 pub updated_at_ms: u64,
405 pub expires_at_ms: Option<u64>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct GlobalMemoryWriteResult {
410 pub id: String,
411 pub stored: bool,
412 pub deduped: bool,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct GlobalMemorySearchHit {
417 pub record: GlobalMemoryRecord,
418 pub score: f64,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
423#[serde(rename_all = "snake_case")]
424pub enum KnowledgeItemStatus {
425 #[default]
426 Working,
427 Promoted,
428 ApprovedDefault,
429 Deprecated,
430}
431
432impl std::fmt::Display for KnowledgeItemStatus {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 match self {
435 Self::Working => write!(f, "working"),
436 Self::Promoted => write!(f, "promoted"),
437 Self::ApprovedDefault => write!(f, "approved_default"),
438 Self::Deprecated => write!(f, "deprecated"),
439 }
440 }
441}
442
443impl std::str::FromStr for KnowledgeItemStatus {
444 type Err = String;
445 fn from_str(s: &str) -> Result<Self, Self::Err> {
446 match s {
447 "working" => Ok(Self::Working),
448 "promoted" => Ok(Self::Promoted),
449 "approved_default" => Ok(Self::ApprovedDefault),
450 "deprecated" => Ok(Self::Deprecated),
451 other => Err(format!("unknown knowledge item status: {}", other)),
452 }
453 }
454}
455
456impl KnowledgeItemStatus {
457 pub fn as_trust_level(self) -> Option<KnowledgeTrustLevel> {
458 match self {
459 Self::Working => Some(KnowledgeTrustLevel::Working),
460 Self::Promoted => Some(KnowledgeTrustLevel::Promoted),
461 Self::ApprovedDefault => Some(KnowledgeTrustLevel::ApprovedDefault),
462 Self::Deprecated => None,
463 }
464 }
465
466 pub fn is_active(self) -> bool {
467 !matches!(self, Self::Deprecated)
468 }
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct KnowledgeSpaceRecord {
474 pub id: String,
475 pub scope: KnowledgeScope,
476 pub project_id: Option<String>,
477 pub namespace: Option<String>,
478 pub title: Option<String>,
479 pub description: Option<String>,
480 pub trust_level: KnowledgeTrustLevel,
481 pub metadata: Option<serde_json::Value>,
482 pub created_at_ms: u64,
483 pub updated_at_ms: u64,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct KnowledgeItemRecord {
489 pub id: String,
490 pub space_id: String,
491 pub coverage_key: String,
492 pub dedupe_key: String,
493 pub item_type: String,
494 pub title: String,
495 pub summary: Option<String>,
496 pub payload: serde_json::Value,
497 pub trust_level: KnowledgeTrustLevel,
498 pub status: KnowledgeItemStatus,
499 pub run_id: Option<String>,
500 pub artifact_refs: Vec<String>,
501 pub source_memory_ids: Vec<String>,
502 pub freshness_expires_at_ms: Option<u64>,
503 pub metadata: Option<serde_json::Value>,
504 pub created_at_ms: u64,
505 pub updated_at_ms: u64,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct KnowledgeCoverageRecord {
511 pub coverage_key: String,
512 pub space_id: String,
513 pub latest_item_id: Option<String>,
514 pub latest_dedupe_key: Option<String>,
515 pub last_seen_at_ms: u64,
516 pub last_promoted_at_ms: Option<u64>,
517 pub freshness_expires_at_ms: Option<u64>,
518 pub metadata: Option<serde_json::Value>,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct KnowledgePromotionRequest {
524 pub item_id: String,
525 pub target_status: KnowledgeItemStatus,
526 pub promoted_at_ms: u64,
527 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub freshness_expires_at_ms: Option<u64>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub reviewer_id: Option<String>,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
532 pub approval_id: Option<String>,
533 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub reason: Option<String>,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct KnowledgePromotionResult {
540 pub previous_status: KnowledgeItemStatus,
541 pub previous_trust_level: KnowledgeTrustLevel,
542 pub promoted: bool,
543 pub item: KnowledgeItemRecord,
544 pub coverage: KnowledgeCoverageRecord,
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
548#[serde(rename_all = "lowercase")]
549pub enum NodeType {
550 Directory,
551 File,
552}
553
554impl std::fmt::Display for NodeType {
555 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
556 match self {
557 NodeType::Directory => write!(f, "directory"),
558 NodeType::File => write!(f, "file"),
559 }
560 }
561}
562
563impl std::str::FromStr for NodeType {
564 type Err = String;
565 fn from_str(s: &str) -> Result<Self, Self::Err> {
566 match s.to_lowercase().as_str() {
567 "directory" => Ok(NodeType::Directory),
568 "file" => Ok(NodeType::File),
569 _ => Err(format!("unknown node type: {}", s)),
570 }
571 }
572}
573
574#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
575#[serde(rename_all = "UPPERCASE")]
576pub enum LayerType {
577 L0,
578 L1,
579 L2,
580}
581
582impl std::fmt::Display for LayerType {
583 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584 match self {
585 LayerType::L0 => write!(f, "L0"),
586 LayerType::L1 => write!(f, "L1"),
587 LayerType::L2 => write!(f, "L2"),
588 }
589 }
590}
591
592impl std::str::FromStr for LayerType {
593 type Err = String;
594 fn from_str(s: &str) -> Result<Self, Self::Err> {
595 match s.to_uppercase().as_str() {
596 "L0" | "L0_ABSTRACT" => Ok(LayerType::L0),
597 "L1" | "L1_OVERVIEW" => Ok(LayerType::L1),
598 "L2" | "L2_DETAIL" => Ok(LayerType::L2),
599 _ => Err(format!("unknown layer type: {}", s)),
600 }
601 }
602}
603
604impl LayerType {
605 pub fn default_tokens(&self) -> usize {
606 match self {
607 LayerType::L0 => 100,
608 LayerType::L1 => 2000,
609 LayerType::L2 => 4000,
610 }
611 }
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct RetrievalStep {
616 pub step_type: String,
617 pub description: String,
618 pub layer_accessed: Option<LayerType>,
619 pub nodes_evaluated: usize,
620 pub scores: std::collections::HashMap<String, f64>,
621}
622
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct NodeVisit {
625 pub uri: String,
626 pub node_type: NodeType,
627 pub score: f64,
628 pub depth: usize,
629 pub layer_loaded: Option<LayerType>,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct RetrievalTrajectory {
634 pub id: String,
635 pub query: String,
636 pub root_uri: String,
637 pub steps: Vec<RetrievalStep>,
638 pub visited_nodes: Vec<NodeVisit>,
639 pub total_duration_ms: u64,
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize)]
643pub struct RetrievalResult {
644 pub node_id: String,
645 pub uri: String,
646 pub content: String,
647 pub layer_type: LayerType,
648 pub score: f64,
649 pub trajectory: RetrievalTrajectory,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct MemoryNode {
654 pub id: String,
655 pub uri: String,
656 pub parent_uri: Option<String>,
657 pub node_type: NodeType,
658 pub created_at: DateTime<Utc>,
659 pub updated_at: DateTime<Utc>,
660 pub metadata: Option<serde_json::Value>,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct MemoryLayer {
665 pub id: String,
666 pub node_id: String,
667 pub layer_type: LayerType,
668 pub content: String,
669 pub token_count: i64,
670 pub embedding_id: Option<String>,
671 pub created_at: DateTime<Utc>,
672 pub source_chunk_id: Option<String>,
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct TreeNode {
677 pub node: MemoryNode,
678 pub children: Vec<TreeNode>,
679 pub layer_summary: Option<LayerSummary>,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct LayerSummary {
684 pub l0_preview: Option<String>,
685 pub l1_preview: Option<String>,
686 pub has_l2: bool,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct DirectoryListing {
691 pub uri: String,
692 pub nodes: Vec<MemoryNode>,
693 pub total_children: usize,
694 pub directories: Vec<MemoryNode>,
695 pub files: Vec<MemoryNode>,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct DistilledFact {
700 pub id: String,
701 pub distillation_id: String,
702 pub content: String,
703 pub category: FactCategory,
704 pub importance_score: f64,
705 pub source_message_ids: Vec<String>,
706 pub contradicts_fact_id: Option<String>,
707}
708
709#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
710#[serde(rename_all = "snake_case")]
711pub enum FactCategory {
712 UserPreference,
713 TaskOutcome,
714 Learning,
715 Fact,
716}
717
718impl std::fmt::Display for FactCategory {
719 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
720 match self {
721 FactCategory::UserPreference => write!(f, "user_preference"),
722 FactCategory::TaskOutcome => write!(f, "task_outcome"),
723 FactCategory::Learning => write!(f, "learning"),
724 FactCategory::Fact => write!(f, "fact"),
725 }
726 }
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct DistillationReport {
731 pub distillation_id: String,
732 pub session_id: String,
733 pub distilled_at: DateTime<Utc>,
734 pub facts_extracted: usize,
735 pub importance_threshold: f64,
736 pub user_memory_updated: bool,
737 pub agent_memory_updated: bool,
738 #[serde(default)]
739 pub stored_count: usize,
740 #[serde(default)]
741 pub deduped_count: usize,
742 #[serde(default)]
743 pub memory_ids: Vec<String>,
744 #[serde(default)]
745 pub candidate_ids: Vec<String>,
746 #[serde(default)]
747 pub status: String,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub struct SessionDistillation {
752 pub id: String,
753 pub session_id: String,
754 pub distilled_at: DateTime<Utc>,
755 pub input_token_count: i64,
756 pub output_memory_count: usize,
757 pub key_facts_extracted: usize,
758 pub importance_threshold: f64,
759}