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)]
423#[serde(rename_all = "snake_case")]
424pub enum KnowledgeItemStatus {
425 Working,
426 Promoted,
427 ApprovedDefault,
428 Deprecated,
429}
430
431impl std::fmt::Display for KnowledgeItemStatus {
432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433 match self {
434 Self::Working => write!(f, "working"),
435 Self::Promoted => write!(f, "promoted"),
436 Self::ApprovedDefault => write!(f, "approved_default"),
437 Self::Deprecated => write!(f, "deprecated"),
438 }
439 }
440}
441
442impl std::str::FromStr for KnowledgeItemStatus {
443 type Err = String;
444 fn from_str(s: &str) -> Result<Self, Self::Err> {
445 match s {
446 "working" => Ok(Self::Working),
447 "promoted" => Ok(Self::Promoted),
448 "approved_default" => Ok(Self::ApprovedDefault),
449 "deprecated" => Ok(Self::Deprecated),
450 other => Err(format!("unknown knowledge item status: {}", other)),
451 }
452 }
453}
454
455impl Default for KnowledgeItemStatus {
456 fn default() -> Self {
457 Self::Working
458 }
459}
460
461impl KnowledgeItemStatus {
462 pub fn as_trust_level(self) -> Option<KnowledgeTrustLevel> {
463 match self {
464 Self::Working => Some(KnowledgeTrustLevel::Working),
465 Self::Promoted => Some(KnowledgeTrustLevel::Promoted),
466 Self::ApprovedDefault => Some(KnowledgeTrustLevel::ApprovedDefault),
467 Self::Deprecated => None,
468 }
469 }
470
471 pub fn is_active(self) -> bool {
472 !matches!(self, Self::Deprecated)
473 }
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct KnowledgeSpaceRecord {
479 pub id: String,
480 pub scope: KnowledgeScope,
481 pub project_id: Option<String>,
482 pub namespace: Option<String>,
483 pub title: Option<String>,
484 pub description: Option<String>,
485 pub trust_level: KnowledgeTrustLevel,
486 pub metadata: Option<serde_json::Value>,
487 pub created_at_ms: u64,
488 pub updated_at_ms: u64,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct KnowledgeItemRecord {
494 pub id: String,
495 pub space_id: String,
496 pub coverage_key: String,
497 pub dedupe_key: String,
498 pub item_type: String,
499 pub title: String,
500 pub summary: Option<String>,
501 pub payload: serde_json::Value,
502 pub trust_level: KnowledgeTrustLevel,
503 pub status: KnowledgeItemStatus,
504 pub run_id: Option<String>,
505 pub artifact_refs: Vec<String>,
506 pub source_memory_ids: Vec<String>,
507 pub freshness_expires_at_ms: Option<u64>,
508 pub metadata: Option<serde_json::Value>,
509 pub created_at_ms: u64,
510 pub updated_at_ms: u64,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct KnowledgeCoverageRecord {
516 pub coverage_key: String,
517 pub space_id: String,
518 pub latest_item_id: Option<String>,
519 pub latest_dedupe_key: Option<String>,
520 pub last_seen_at_ms: u64,
521 pub last_promoted_at_ms: Option<u64>,
522 pub freshness_expires_at_ms: Option<u64>,
523 pub metadata: Option<serde_json::Value>,
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct KnowledgePromotionRequest {
529 pub item_id: String,
530 pub target_status: KnowledgeItemStatus,
531 pub promoted_at_ms: u64,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub freshness_expires_at_ms: Option<u64>,
534 #[serde(default, skip_serializing_if = "Option::is_none")]
535 pub reviewer_id: Option<String>,
536 #[serde(default, skip_serializing_if = "Option::is_none")]
537 pub approval_id: Option<String>,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub reason: Option<String>,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct KnowledgePromotionResult {
545 pub previous_status: KnowledgeItemStatus,
546 pub previous_trust_level: KnowledgeTrustLevel,
547 pub promoted: bool,
548 pub item: KnowledgeItemRecord,
549 pub coverage: KnowledgeCoverageRecord,
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(rename_all = "lowercase")]
554pub enum NodeType {
555 Directory,
556 File,
557}
558
559impl std::fmt::Display for NodeType {
560 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
561 match self {
562 NodeType::Directory => write!(f, "directory"),
563 NodeType::File => write!(f, "file"),
564 }
565 }
566}
567
568impl std::str::FromStr for NodeType {
569 type Err = String;
570 fn from_str(s: &str) -> Result<Self, Self::Err> {
571 match s.to_lowercase().as_str() {
572 "directory" => Ok(NodeType::Directory),
573 "file" => Ok(NodeType::File),
574 _ => Err(format!("unknown node type: {}", s)),
575 }
576 }
577}
578
579#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
580#[serde(rename_all = "UPPERCASE")]
581pub enum LayerType {
582 L0,
583 L1,
584 L2,
585}
586
587impl std::fmt::Display for LayerType {
588 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589 match self {
590 LayerType::L0 => write!(f, "L0"),
591 LayerType::L1 => write!(f, "L1"),
592 LayerType::L2 => write!(f, "L2"),
593 }
594 }
595}
596
597impl std::str::FromStr for LayerType {
598 type Err = String;
599 fn from_str(s: &str) -> Result<Self, Self::Err> {
600 match s.to_uppercase().as_str() {
601 "L0" | "L0_ABSTRACT" => Ok(LayerType::L0),
602 "L1" | "L1_OVERVIEW" => Ok(LayerType::L1),
603 "L2" | "L2_DETAIL" => Ok(LayerType::L2),
604 _ => Err(format!("unknown layer type: {}", s)),
605 }
606 }
607}
608
609impl LayerType {
610 pub fn default_tokens(&self) -> usize {
611 match self {
612 LayerType::L0 => 100,
613 LayerType::L1 => 2000,
614 LayerType::L2 => 4000,
615 }
616 }
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct RetrievalStep {
621 pub step_type: String,
622 pub description: String,
623 pub layer_accessed: Option<LayerType>,
624 pub nodes_evaluated: usize,
625 pub scores: std::collections::HashMap<String, f64>,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct NodeVisit {
630 pub uri: String,
631 pub node_type: NodeType,
632 pub score: f64,
633 pub depth: usize,
634 pub layer_loaded: Option<LayerType>,
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct RetrievalTrajectory {
639 pub id: String,
640 pub query: String,
641 pub root_uri: String,
642 pub steps: Vec<RetrievalStep>,
643 pub visited_nodes: Vec<NodeVisit>,
644 pub total_duration_ms: u64,
645}
646
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct RetrievalResult {
649 pub node_id: String,
650 pub uri: String,
651 pub content: String,
652 pub layer_type: LayerType,
653 pub score: f64,
654 pub trajectory: RetrievalTrajectory,
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize)]
658pub struct MemoryNode {
659 pub id: String,
660 pub uri: String,
661 pub parent_uri: Option<String>,
662 pub node_type: NodeType,
663 pub created_at: DateTime<Utc>,
664 pub updated_at: DateTime<Utc>,
665 pub metadata: Option<serde_json::Value>,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize)]
669pub struct MemoryLayer {
670 pub id: String,
671 pub node_id: String,
672 pub layer_type: LayerType,
673 pub content: String,
674 pub token_count: i64,
675 pub embedding_id: Option<String>,
676 pub created_at: DateTime<Utc>,
677 pub source_chunk_id: Option<String>,
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize)]
681pub struct TreeNode {
682 pub node: MemoryNode,
683 pub children: Vec<TreeNode>,
684 pub layer_summary: Option<LayerSummary>,
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct LayerSummary {
689 pub l0_preview: Option<String>,
690 pub l1_preview: Option<String>,
691 pub has_l2: bool,
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize)]
695pub struct DirectoryListing {
696 pub uri: String,
697 pub nodes: Vec<MemoryNode>,
698 pub total_children: usize,
699 pub directories: Vec<MemoryNode>,
700 pub files: Vec<MemoryNode>,
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize)]
704pub struct DistilledFact {
705 pub id: String,
706 pub distillation_id: String,
707 pub content: String,
708 pub category: FactCategory,
709 pub importance_score: f64,
710 pub source_message_ids: Vec<String>,
711 pub contradicts_fact_id: Option<String>,
712}
713
714#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
715#[serde(rename_all = "snake_case")]
716pub enum FactCategory {
717 UserPreference,
718 TaskOutcome,
719 Learning,
720 Fact,
721}
722
723impl std::fmt::Display for FactCategory {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 match self {
726 FactCategory::UserPreference => write!(f, "user_preference"),
727 FactCategory::TaskOutcome => write!(f, "task_outcome"),
728 FactCategory::Learning => write!(f, "learning"),
729 FactCategory::Fact => write!(f, "fact"),
730 }
731 }
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize)]
735pub struct DistillationReport {
736 pub distillation_id: String,
737 pub session_id: String,
738 pub distilled_at: DateTime<Utc>,
739 pub facts_extracted: usize,
740 pub importance_threshold: f64,
741 pub user_memory_updated: bool,
742 pub agent_memory_updated: bool,
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize)]
746pub struct SessionDistillation {
747 pub id: String,
748 pub session_id: String,
749 pub distilled_at: DateTime<Utc>,
750 pub input_token_count: i64,
751 pub output_memory_count: usize,
752 pub key_facts_extracted: usize,
753 pub importance_threshold: f64,
754}