1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use tandem_enterprise_contract::{
7 AccessDecision, AccessPermission, DataClass, ResourceRef, StrictTenantContext,
8};
9use tandem_orchestrator::{KnowledgeScope, KnowledgeTrustLevel};
10use thiserror::Error;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum MemoryTier {
16 Session,
18 Project,
20 Global,
22}
23
24impl MemoryTier {
25 pub fn table_prefix(&self) -> &'static str {
27 match self {
28 MemoryTier::Session => "session",
29 MemoryTier::Project => "project",
30 MemoryTier::Global => "global",
31 }
32 }
33}
34
35impl std::fmt::Display for MemoryTier {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 MemoryTier::Session => write!(f, "session"),
39 MemoryTier::Project => write!(f, "project"),
40 MemoryTier::Global => write!(f, "global"),
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct MemoryTenantScope {
48 pub org_id: String,
49 pub workspace_id: String,
50 pub deployment_id: Option<String>,
51}
52
53impl MemoryTenantScope {
54 pub fn local() -> Self {
55 Self {
56 org_id: "local".to_string(),
57 workspace_id: "local".to_string(),
58 deployment_id: None,
59 }
60 }
61}
62
63impl Default for MemoryTenantScope {
64 fn default() -> Self {
65 Self::local()
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct MemoryChunk {
72 pub id: String,
73 pub content: String,
74 pub tier: MemoryTier,
75 pub session_id: Option<String>,
76 pub project_id: Option<String>,
77 pub source: String, pub source_path: Option<String>,
80 pub source_mtime: Option<i64>,
81 pub source_size: Option<i64>,
82 pub source_hash: Option<String>,
83 #[serde(default)]
84 pub tenant_scope: MemoryTenantScope,
85 pub created_at: DateTime<Utc>,
86 pub token_count: i64,
87 pub metadata: Option<serde_json::Value>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct MemorySearchResult {
93 pub chunk: MemoryChunk,
94 pub similarity: f64,
95}
96
97#[derive(Debug, Clone)]
99pub struct MemoryAccessFilter {
100 pub strict_context: StrictTenantContext,
101 pub now_ms: u64,
102}
103
104impl MemoryAccessFilter {
105 pub fn strict(strict_context: StrictTenantContext, now_ms: u64) -> Self {
106 Self {
107 strict_context,
108 now_ms,
109 }
110 }
111
112 pub fn allows_chunk(&self, chunk: &MemoryChunk) -> bool {
113 let Some(target) = MemorySourceAccessTarget::from_chunk(chunk) else {
114 return true;
115 };
116 self.allows_source_target(&target)
117 }
118
119 pub fn allows_source_target(&self, target: &MemorySourceAccessTarget) -> bool {
120 self.strict_context
121 .evaluate_access(
122 &target.resource_ref,
123 AccessPermission::Read,
124 target.data_class,
125 self.now_ms,
126 )
127 .decision
128 == AccessDecision::Allow
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct MemorySourceAccessTarget {
134 pub resource_ref: ResourceRef,
135 pub data_class: DataClass,
136 pub source_binding_id: Option<String>,
137 pub source_object_id: Option<String>,
138}
139
140impl MemorySourceAccessTarget {
141 pub fn from_chunk(chunk: &MemoryChunk) -> Option<Self> {
142 Self::from_metadata(chunk.metadata.as_ref())
143 }
144
145 pub fn from_metadata(metadata: Option<&serde_json::Value>) -> Option<Self> {
146 let binding = metadata?.get("enterprise_source_binding")?;
147 let resource_ref = serde_json::from_value(binding.get("resource_ref")?.clone()).ok()?;
148 let data_class = serde_json::from_value(binding.get("data_class")?.clone()).ok()?;
149 Some(Self {
150 resource_ref,
151 data_class,
152 source_binding_id: binding
153 .get("binding_id")
154 .and_then(serde_json::Value::as_str)
155 .map(ToOwned::to_owned),
156 source_object_id: binding
157 .get("source_object_id")
158 .and_then(serde_json::Value::as_str)
159 .map(ToOwned::to_owned),
160 })
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct MemoryConfig {
167 pub max_chunks: i64,
169 pub chunk_size: i64,
171 pub retrieval_k: i64,
173 pub auto_cleanup: bool,
175 pub session_retention_days: i64,
177 pub token_budget: i64,
179 pub chunk_overlap: i64,
181}
182
183impl Default for MemoryConfig {
184 fn default() -> Self {
185 Self {
186 max_chunks: 10_000,
187 chunk_size: 512,
188 retrieval_k: 5,
189 auto_cleanup: true,
190 session_retention_days: 30,
191 token_budget: 5000,
192 chunk_overlap: 64,
193 }
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct MemoryStats {
200 pub total_chunks: i64,
202 pub session_chunks: i64,
204 pub project_chunks: i64,
206 pub global_chunks: i64,
208 pub total_bytes: i64,
210 pub session_bytes: i64,
212 pub project_bytes: i64,
214 pub global_bytes: i64,
216 pub file_size: i64,
218 pub last_cleanup: Option<DateTime<Utc>>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct MemoryContext {
225 pub current_session: Vec<MemoryChunk>,
227 pub relevant_history: Vec<MemoryChunk>,
229 pub project_facts: Vec<MemoryChunk>,
231 pub total_tokens: i64,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct MemoryRetrievalMeta {
238 pub used: bool,
239 pub chunks_total: usize,
240 pub session_chunks: usize,
241 pub history_chunks: usize,
242 pub project_fact_chunks: usize,
243 pub score_min: Option<f64>,
244 pub score_max: Option<f64>,
245}
246
247impl MemoryContext {
248 pub fn format_for_injection(&self) -> String {
250 let mut parts = Vec::new();
251
252 if !self.current_session.is_empty() {
253 parts.push("<current_session>".to_string());
254 for chunk in &self.current_session {
255 parts.push(format!("- {}", chunk.content));
256 }
257 parts.push("</current_session>".to_string());
258 }
259
260 if !self.relevant_history.is_empty() {
261 parts.push("<relevant_history>".to_string());
262 for chunk in &self.relevant_history {
263 parts.push(format!("- {}", chunk.content));
264 }
265 parts.push("</relevant_history>".to_string());
266 }
267
268 if !self.project_facts.is_empty() {
269 parts.push("<project_facts>".to_string());
270 for chunk in &self.project_facts {
271 parts.push(format!("- {}", chunk.content));
272 }
273 parts.push("</project_facts>".to_string());
274 }
275
276 if parts.is_empty() {
277 String::new()
278 } else {
279 format!("<memory_context>\n{}\n</memory_context>", parts.join("\n"))
280 }
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct StoreMessageRequest {
287 pub content: String,
288 pub tier: MemoryTier,
289 pub session_id: Option<String>,
290 pub project_id: Option<String>,
291 pub source: String,
292 pub source_path: Option<String>,
294 pub source_mtime: Option<i64>,
295 pub source_size: Option<i64>,
296 pub source_hash: Option<String>,
297 #[serde(default)]
298 pub tenant_scope: MemoryTenantScope,
299 pub metadata: Option<serde_json::Value>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct ProjectMemoryStats {
305 pub project_id: String,
306 pub project_chunks: i64,
308 pub project_bytes: i64,
309 pub file_index_chunks: i64,
311 pub file_index_bytes: i64,
312 pub indexed_files: i64,
314 pub last_indexed_at: Option<DateTime<Utc>>,
316 pub last_total_files: Option<i64>,
318 pub last_processed_files: Option<i64>,
319 pub last_indexed_files: Option<i64>,
320 pub last_skipped_files: Option<i64>,
321 pub last_errors: Option<i64>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ClearFileIndexResult {
326 pub chunks_deleted: i64,
327 pub bytes_estimated: i64,
328 pub did_vacuum: bool,
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
332#[serde(rename_all = "snake_case")]
333pub enum MemoryImportFormat {
334 Directory,
335 Openclaw,
336}
337
338impl std::fmt::Display for MemoryImportFormat {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 match self {
341 MemoryImportFormat::Directory => write!(f, "directory"),
342 MemoryImportFormat::Openclaw => write!(f, "openclaw"),
343 }
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct MemoryImportRequest {
349 pub root_path: String,
350 pub format: MemoryImportFormat,
351 pub tier: MemoryTier,
352 pub session_id: Option<String>,
353 pub project_id: Option<String>,
354 #[serde(default)]
355 pub tenant_scope: MemoryTenantScope,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub source_binding: Option<MemoryImportSourceBinding>,
358 pub sync_deletes: bool,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub import_namespace: Option<String>,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct MemoryImportSourceBinding {
365 pub binding_id: String,
366 pub connector_id: String,
367 pub resource_ref: serde_json::Value,
368 pub data_class: String,
369 #[serde(default)]
370 pub require_review: bool,
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375pub enum SourceObjectLifecycleState {
376 Active,
377 Quarantined,
378 Tombstoned,
379 Deleted,
380 Rescoped,
381}
382
383impl SourceObjectLifecycleState {
384 pub fn as_str(self) -> &'static str {
385 match self {
386 Self::Active => "active",
387 Self::Quarantined => "quarantined",
388 Self::Tombstoned => "tombstoned",
389 Self::Deleted => "deleted",
390 Self::Rescoped => "rescoped",
391 }
392 }
393
394 pub fn parse(value: &str) -> Self {
395 match value.trim().to_ascii_lowercase().as_str() {
396 "quarantined" => Self::Quarantined,
397 "tombstoned" => Self::Tombstoned,
398 "deleted" => Self::Deleted,
399 "rescoped" => Self::Rescoped,
400 _ => Self::Active,
401 }
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
406pub struct SourceObjectLifecycleRecord {
407 pub source_object_id: String,
408 pub tenant_scope: MemoryTenantScope,
409 pub source_binding_id: String,
410 pub connector_id: String,
411 pub state: SourceObjectLifecycleState,
412 pub tier: MemoryTier,
413 pub session_id: Option<String>,
414 pub project_id: Option<String>,
415 pub import_namespace: String,
416 pub indexed_path: String,
417 pub native_object_id: String,
418 pub resource_ref: serde_json::Value,
419 pub data_class: String,
420 pub content_hash: Option<String>,
421 pub source_hash: Option<String>,
422 pub first_seen_at_ms: u64,
423 pub last_seen_at_ms: u64,
424 pub tombstoned_at_ms: Option<u64>,
425 pub metadata: Option<serde_json::Value>,
426}
427
428#[derive(Debug, Clone, Default, Serialize, Deserialize)]
429pub struct MemoryImportStats {
430 pub discovered_files: usize,
431 pub files_processed: usize,
432 pub indexed_files: usize,
433 pub skipped_files: usize,
434 pub deleted_files: usize,
435 pub chunks_created: usize,
436 pub errors: usize,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct MemoryImportProgress {
441 pub files_processed: usize,
442 pub total_files: usize,
443 pub indexed_files: usize,
444 pub skipped_files: usize,
445 pub deleted_files: usize,
446 pub errors: usize,
447 pub chunks_created: usize,
448 pub current_file: String,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct SearchMemoryRequest {
454 pub query: String,
455 pub tier: Option<MemoryTier>,
456 pub project_id: Option<String>,
457 pub session_id: Option<String>,
458 pub limit: Option<i64>,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct EmbeddingHealth {
464 pub status: String,
466 pub reason: Option<String>,
468}
469
470#[derive(Error, Debug)]
472pub enum MemoryError {
473 #[error("Database error: {0}")]
474 Database(#[from] rusqlite::Error),
475
476 #[error("IO error: {0}")]
477 Io(#[from] std::io::Error),
478
479 #[error("Serialization error: {0}")]
480 Serialization(#[from] serde_json::Error),
481
482 #[error("Embedding error: {0}")]
483 Embedding(String),
484
485 #[error("Chunking error: {0}")]
486 Chunking(String),
487
488 #[error("Invalid configuration: {0}")]
489 InvalidConfig(String),
490
491 #[error("Not found: {0}")]
492 NotFound(String),
493
494 #[error("Tokenization error: {0}")]
495 Tokenization(String),
496
497 #[error("Lock error: {0}")]
498 Lock(String),
499}
500
501impl From<String> for MemoryError {
502 fn from(err: String) -> Self {
503 MemoryError::InvalidConfig(err)
504 }
505}
506
507impl From<&str> for MemoryError {
508 fn from(err: &str) -> Self {
509 MemoryError::InvalidConfig(err.to_string())
510 }
511}
512
513impl serde::Serialize for MemoryError {
515 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
516 where
517 S: serde::Serializer,
518 {
519 serializer.serialize_str(&self.to_string())
520 }
521}
522
523pub type MemoryResult<T> = Result<T, MemoryError>;
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct CleanupLogEntry {
528 pub id: String,
529 pub cleanup_type: String,
530 pub tier: MemoryTier,
531 pub project_id: Option<String>,
532 pub session_id: Option<String>,
533 pub chunks_deleted: i64,
534 pub bytes_reclaimed: i64,
535 pub created_at: DateTime<Utc>,
536}
537
538pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
540
541pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
543
544pub const MAX_CHUNK_LENGTH: usize = 4000;
546
547pub const MIN_CHUNK_LENGTH: usize = 50;
549
550#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct GlobalMemoryRecord {
553 pub id: String,
554 pub user_id: String,
555 pub source_type: String,
556 pub content: String,
557 pub content_hash: String,
558 pub run_id: String,
559 pub session_id: Option<String>,
560 pub message_id: Option<String>,
561 pub tool_name: Option<String>,
562 pub project_tag: Option<String>,
563 pub channel_tag: Option<String>,
564 pub host_tag: Option<String>,
565 pub metadata: Option<serde_json::Value>,
566 pub provenance: Option<serde_json::Value>,
567 pub redaction_status: String,
568 pub redaction_count: u32,
569 pub visibility: String,
570 pub demoted: bool,
571 pub score_boost: f64,
572 pub created_at_ms: u64,
573 pub updated_at_ms: u64,
574 pub expires_at_ms: Option<u64>,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
578pub struct GlobalMemoryWriteResult {
579 pub id: String,
580 pub stored: bool,
581 pub deduped: bool,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
585pub struct GlobalMemorySearchHit {
586 pub record: GlobalMemoryRecord,
587 pub score: f64,
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
592#[serde(rename_all = "snake_case")]
593pub enum KnowledgeItemStatus {
594 #[default]
595 Working,
596 Promoted,
597 ApprovedDefault,
598 Deprecated,
599}
600
601impl std::fmt::Display for KnowledgeItemStatus {
602 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603 match self {
604 Self::Working => write!(f, "working"),
605 Self::Promoted => write!(f, "promoted"),
606 Self::ApprovedDefault => write!(f, "approved_default"),
607 Self::Deprecated => write!(f, "deprecated"),
608 }
609 }
610}
611
612impl std::str::FromStr for KnowledgeItemStatus {
613 type Err = String;
614 fn from_str(s: &str) -> Result<Self, Self::Err> {
615 match s {
616 "working" => Ok(Self::Working),
617 "promoted" => Ok(Self::Promoted),
618 "approved_default" => Ok(Self::ApprovedDefault),
619 "deprecated" => Ok(Self::Deprecated),
620 other => Err(format!("unknown knowledge item status: {}", other)),
621 }
622 }
623}
624
625impl KnowledgeItemStatus {
626 pub fn as_trust_level(self) -> Option<KnowledgeTrustLevel> {
627 match self {
628 Self::Working => Some(KnowledgeTrustLevel::Working),
629 Self::Promoted => Some(KnowledgeTrustLevel::Promoted),
630 Self::ApprovedDefault => Some(KnowledgeTrustLevel::ApprovedDefault),
631 Self::Deprecated => None,
632 }
633 }
634
635 pub fn is_active(self) -> bool {
636 !matches!(self, Self::Deprecated)
637 }
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct KnowledgeSpaceRecord {
643 pub id: String,
644 pub scope: KnowledgeScope,
645 pub project_id: Option<String>,
646 pub namespace: Option<String>,
647 pub title: Option<String>,
648 pub description: Option<String>,
649 pub trust_level: KnowledgeTrustLevel,
650 pub metadata: Option<serde_json::Value>,
651 pub created_at_ms: u64,
652 pub updated_at_ms: u64,
653}
654
655#[derive(Debug, Clone, Serialize, Deserialize)]
657pub struct KnowledgeItemRecord {
658 pub id: String,
659 pub space_id: String,
660 pub coverage_key: String,
661 pub dedupe_key: String,
662 pub item_type: String,
663 pub title: String,
664 pub summary: Option<String>,
665 pub payload: serde_json::Value,
666 pub trust_level: KnowledgeTrustLevel,
667 pub status: KnowledgeItemStatus,
668 pub run_id: Option<String>,
669 pub artifact_refs: Vec<String>,
670 pub source_memory_ids: Vec<String>,
671 pub freshness_expires_at_ms: Option<u64>,
672 pub metadata: Option<serde_json::Value>,
673 pub created_at_ms: u64,
674 pub updated_at_ms: u64,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct KnowledgeCoverageRecord {
680 pub coverage_key: String,
681 pub space_id: String,
682 pub latest_item_id: Option<String>,
683 pub latest_dedupe_key: Option<String>,
684 pub last_seen_at_ms: u64,
685 pub last_promoted_at_ms: Option<u64>,
686 pub freshness_expires_at_ms: Option<u64>,
687 pub metadata: Option<serde_json::Value>,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct KnowledgePromotionRequest {
693 pub item_id: String,
694 pub target_status: KnowledgeItemStatus,
695 pub promoted_at_ms: u64,
696 #[serde(default, skip_serializing_if = "Option::is_none")]
697 pub freshness_expires_at_ms: Option<u64>,
698 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub reviewer_id: Option<String>,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
701 pub approval_id: Option<String>,
702 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub reason: Option<String>,
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct KnowledgePromotionResult {
709 pub previous_status: KnowledgeItemStatus,
710 pub previous_trust_level: KnowledgeTrustLevel,
711 pub promoted: bool,
712 pub item: KnowledgeItemRecord,
713 pub coverage: KnowledgeCoverageRecord,
714}
715
716#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
717#[serde(rename_all = "lowercase")]
718pub enum NodeType {
719 Directory,
720 File,
721}
722
723impl std::fmt::Display for NodeType {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 match self {
726 NodeType::Directory => write!(f, "directory"),
727 NodeType::File => write!(f, "file"),
728 }
729 }
730}
731
732impl std::str::FromStr for NodeType {
733 type Err = String;
734 fn from_str(s: &str) -> Result<Self, Self::Err> {
735 match s.to_lowercase().as_str() {
736 "directory" => Ok(NodeType::Directory),
737 "file" => Ok(NodeType::File),
738 _ => Err(format!("unknown node type: {}", s)),
739 }
740 }
741}
742
743#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
744#[serde(rename_all = "UPPERCASE")]
745pub enum LayerType {
746 L0,
747 L1,
748 L2,
749}
750
751impl std::fmt::Display for LayerType {
752 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
753 match self {
754 LayerType::L0 => write!(f, "L0"),
755 LayerType::L1 => write!(f, "L1"),
756 LayerType::L2 => write!(f, "L2"),
757 }
758 }
759}
760
761impl std::str::FromStr for LayerType {
762 type Err = String;
763 fn from_str(s: &str) -> Result<Self, Self::Err> {
764 match s.to_uppercase().as_str() {
765 "L0" | "L0_ABSTRACT" => Ok(LayerType::L0),
766 "L1" | "L1_OVERVIEW" => Ok(LayerType::L1),
767 "L2" | "L2_DETAIL" => Ok(LayerType::L2),
768 _ => Err(format!("unknown layer type: {}", s)),
769 }
770 }
771}
772
773impl LayerType {
774 pub fn default_tokens(&self) -> usize {
775 match self {
776 LayerType::L0 => 100,
777 LayerType::L1 => 2000,
778 LayerType::L2 => 4000,
779 }
780 }
781}
782
783#[derive(Debug, Clone, Serialize, Deserialize)]
784pub struct RetrievalStep {
785 pub step_type: String,
786 pub description: String,
787 pub layer_accessed: Option<LayerType>,
788 pub nodes_evaluated: usize,
789 pub scores: std::collections::HashMap<String, f64>,
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct NodeVisit {
794 pub uri: String,
795 pub node_type: NodeType,
796 pub score: f64,
797 pub depth: usize,
798 pub layer_loaded: Option<LayerType>,
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize)]
802pub struct RetrievalTrajectory {
803 pub id: String,
804 pub query: String,
805 pub root_uri: String,
806 pub steps: Vec<RetrievalStep>,
807 pub visited_nodes: Vec<NodeVisit>,
808 pub total_duration_ms: u64,
809}
810
811#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct RetrievalResult {
813 pub node_id: String,
814 pub uri: String,
815 pub content: String,
816 pub layer_type: LayerType,
817 pub score: f64,
818 pub trajectory: RetrievalTrajectory,
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize)]
822pub struct MemoryNode {
823 pub id: String,
824 pub uri: String,
825 pub parent_uri: Option<String>,
826 pub node_type: NodeType,
827 pub created_at: DateTime<Utc>,
828 pub updated_at: DateTime<Utc>,
829 pub metadata: Option<serde_json::Value>,
830}
831
832#[derive(Debug, Clone, Serialize, Deserialize)]
833pub struct MemoryLayer {
834 pub id: String,
835 pub node_id: String,
836 pub layer_type: LayerType,
837 pub content: String,
838 pub token_count: i64,
839 pub embedding_id: Option<String>,
840 pub created_at: DateTime<Utc>,
841 pub source_chunk_id: Option<String>,
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize)]
845pub struct TreeNode {
846 pub node: MemoryNode,
847 pub children: Vec<TreeNode>,
848 pub layer_summary: Option<LayerSummary>,
849}
850
851#[derive(Debug, Clone, Serialize, Deserialize)]
852pub struct LayerSummary {
853 pub l0_preview: Option<String>,
854 pub l1_preview: Option<String>,
855 pub has_l2: bool,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
859pub struct DirectoryListing {
860 pub uri: String,
861 pub nodes: Vec<MemoryNode>,
862 pub total_children: usize,
863 pub directories: Vec<MemoryNode>,
864 pub files: Vec<MemoryNode>,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize)]
868pub struct DistilledFact {
869 pub id: String,
870 pub distillation_id: String,
871 pub content: String,
872 pub category: FactCategory,
873 pub importance_score: f64,
874 pub source_message_ids: Vec<String>,
875 pub contradicts_fact_id: Option<String>,
876}
877
878#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
879#[serde(rename_all = "snake_case")]
880pub enum FactCategory {
881 UserPreference,
882 TaskOutcome,
883 Learning,
884 Fact,
885}
886
887impl std::fmt::Display for FactCategory {
888 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
889 match self {
890 FactCategory::UserPreference => write!(f, "user_preference"),
891 FactCategory::TaskOutcome => write!(f, "task_outcome"),
892 FactCategory::Learning => write!(f, "learning"),
893 FactCategory::Fact => write!(f, "fact"),
894 }
895 }
896}
897
898#[derive(Debug, Clone, Serialize, Deserialize)]
899pub struct DistillationReport {
900 pub distillation_id: String,
901 pub session_id: String,
902 pub distilled_at: DateTime<Utc>,
903 pub facts_extracted: usize,
904 pub importance_threshold: f64,
905 pub user_memory_updated: bool,
906 pub agent_memory_updated: bool,
907 #[serde(default)]
908 pub stored_count: usize,
909 #[serde(default)]
910 pub deduped_count: usize,
911 #[serde(default)]
912 pub memory_ids: Vec<String>,
913 #[serde(default)]
914 pub candidate_ids: Vec<String>,
915 #[serde(default)]
916 pub status: String,
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize)]
920pub struct SessionDistillation {
921 pub id: String,
922 pub session_id: String,
923 pub distilled_at: DateTime<Utc>,
924 pub input_token_count: i64,
925 pub output_memory_count: usize,
926 pub key_facts_extracted: usize,
927 pub importance_threshold: f64,
928}