Skip to main content

tandem_memory/
types.rs

1// Memory Context Types
2// Type definitions and error types for the memory system
3
4use 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/// Memory tier - determines persistence level
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum MemoryTier {
16    /// Ephemeral session memory - cleared when session ends
17    Session,
18    /// Persistent project memory - survives across sessions
19    Project,
20    /// Cross-project global memory - user preferences and patterns
21    Global,
22}
23
24impl MemoryTier {
25    /// Get the table prefix for this tier
26    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/// Tenant partition for vector-backed memory rows.
46#[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/// A memory chunk - unit of storage
70#[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, // e.g., "user_message", "assistant_response", "file_content"
78    // File-derived fields (only set when source == "file")
79    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/// Search result with similarity score
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct MemorySearchResult {
93    pub chunk: MemoryChunk,
94    pub similarity: f64,
95}
96
97/// Optional enterprise access projection applied before memory ranking.
98#[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/// Memory configuration for a project
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct MemoryConfig {
167    /// Maximum chunks to store per project
168    pub max_chunks: i64,
169    /// Chunk size in tokens
170    pub chunk_size: i64,
171    /// Number of chunks to retrieve
172    pub retrieval_k: i64,
173    /// Whether auto-cleanup is enabled
174    pub auto_cleanup: bool,
175    /// Session memory retention in days
176    pub session_retention_days: i64,
177    /// Token budget for memory context injection
178    pub token_budget: i64,
179    /// Overlap between chunks in tokens
180    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/// Memory storage statistics
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct MemoryStats {
200    /// Total number of chunks
201    pub total_chunks: i64,
202    /// Number of session chunks
203    pub session_chunks: i64,
204    /// Number of project chunks
205    pub project_chunks: i64,
206    /// Number of global chunks
207    pub global_chunks: i64,
208    /// Total size in bytes
209    pub total_bytes: i64,
210    /// Session memory size in bytes
211    pub session_bytes: i64,
212    /// Project memory size in bytes
213    pub project_bytes: i64,
214    /// Global memory size in bytes
215    pub global_bytes: i64,
216    /// Database file size in bytes
217    pub file_size: i64,
218    /// Last cleanup timestamp
219    pub last_cleanup: Option<DateTime<Utc>>,
220}
221
222/// Context to inject into messages
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct MemoryContext {
225    /// Recent messages from current session
226    pub current_session: Vec<MemoryChunk>,
227    /// Relevant historical chunks
228    pub relevant_history: Vec<MemoryChunk>,
229    /// Important project facts
230    pub project_facts: Vec<MemoryChunk>,
231    /// Total tokens in context
232    pub total_tokens: i64,
233}
234
235/// Metadata describing how memory retrieval executed for a single query.
236#[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    /// Format the context for injection into a prompt
249    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/// Request to store a message
285#[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    // File-derived fields (only set when source == "file")
293    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/// Project-scoped memory statistics (filtered by project_id)
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct ProjectMemoryStats {
305    pub project_id: String,
306    /// Total chunks stored under this project_id (all sources)
307    pub project_chunks: i64,
308    pub project_bytes: i64,
309    /// Chunks/bytes that came from workspace file indexing (source == "file")
310    pub file_index_chunks: i64,
311    pub file_index_bytes: i64,
312    /// Number of indexed files currently tracked for this project_id
313    pub indexed_files: i64,
314    /// Last time indexing completed for this project_id (if known)
315    pub last_indexed_at: Option<DateTime<Utc>>,
316    /// Last run totals (if known)
317    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/// Request to search memory
452#[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/// Embedding backend health surfaced to UI/events.
462#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct EmbeddingHealth {
464    /// "ok" when embeddings are available, "degraded_disabled" otherwise.
465    pub status: String,
466    /// Optional reason when degraded.
467    pub reason: Option<String>,
468}
469
470/// Memory error types
471#[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
513// Implement serialization for Tauri commands
514impl 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/// Cleanup log entry for audit trail
526#[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
538/// Default embedding dimension for all-MiniLM-L6-v2
539pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
540
541/// Default embedding model name
542pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
543
544/// Maximum content length for a single chunk (in characters)
545pub const MAX_CHUNK_LENGTH: usize = 4000;
546
547/// Minimum content length for a chunk (in characters)
548pub const MIN_CHUNK_LENGTH: usize = 50;
549
550/// Persistent global memory record keyed by user identity.
551#[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/// Status for a reusable knowledge item.
591#[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/// A reusable knowledge space scoped to a project, run, or global namespace.
641#[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/// A reusable knowledge item promoted from run artifacts or validated memory.
656#[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/// Coverage state for a task/topic key within a knowledge space.
678#[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/// Request to promote or retire a knowledge item.
691#[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/// Result of a knowledge item promotion or retirement.
707#[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}