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