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_orchestrator::{KnowledgeScope, KnowledgeTrustLevel};
7use thiserror::Error;
8
9/// Memory tier - determines persistence level
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum MemoryTier {
13    /// Ephemeral session memory - cleared when session ends
14    Session,
15    /// Persistent project memory - survives across sessions
16    Project,
17    /// Cross-project global memory - user preferences and patterns
18    Global,
19}
20
21impl MemoryTier {
22    /// Get the table prefix for this tier
23    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/// Tenant partition for vector-backed memory rows.
43#[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/// A memory chunk - unit of storage
67#[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, // e.g., "user_message", "assistant_response", "file_content"
75    // File-derived fields (only set when source == "file")
76    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/// Search result with similarity score
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct MemorySearchResult {
90    pub chunk: MemoryChunk,
91    pub similarity: f64,
92}
93
94/// Memory configuration for a project
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct MemoryConfig {
97    /// Maximum chunks to store per project
98    pub max_chunks: i64,
99    /// Chunk size in tokens
100    pub chunk_size: i64,
101    /// Number of chunks to retrieve
102    pub retrieval_k: i64,
103    /// Whether auto-cleanup is enabled
104    pub auto_cleanup: bool,
105    /// Session memory retention in days
106    pub session_retention_days: i64,
107    /// Token budget for memory context injection
108    pub token_budget: i64,
109    /// Overlap between chunks in tokens
110    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/// Memory storage statistics
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MemoryStats {
130    /// Total number of chunks
131    pub total_chunks: i64,
132    /// Number of session chunks
133    pub session_chunks: i64,
134    /// Number of project chunks
135    pub project_chunks: i64,
136    /// Number of global chunks
137    pub global_chunks: i64,
138    /// Total size in bytes
139    pub total_bytes: i64,
140    /// Session memory size in bytes
141    pub session_bytes: i64,
142    /// Project memory size in bytes
143    pub project_bytes: i64,
144    /// Global memory size in bytes
145    pub global_bytes: i64,
146    /// Database file size in bytes
147    pub file_size: i64,
148    /// Last cleanup timestamp
149    pub last_cleanup: Option<DateTime<Utc>>,
150}
151
152/// Context to inject into messages
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct MemoryContext {
155    /// Recent messages from current session
156    pub current_session: Vec<MemoryChunk>,
157    /// Relevant historical chunks
158    pub relevant_history: Vec<MemoryChunk>,
159    /// Important project facts
160    pub project_facts: Vec<MemoryChunk>,
161    /// Total tokens in context
162    pub total_tokens: i64,
163}
164
165/// Metadata describing how memory retrieval executed for a single query.
166#[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    /// Format the context for injection into a prompt
179    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/// Request to store a message
215#[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    // File-derived fields (only set when source == "file")
223    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/// Project-scoped memory statistics (filtered by project_id)
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct ProjectMemoryStats {
235    pub project_id: String,
236    /// Total chunks stored under this project_id (all sources)
237    pub project_chunks: i64,
238    pub project_bytes: i64,
239    /// Chunks/bytes that came from workspace file indexing (source == "file")
240    pub file_index_chunks: i64,
241    pub file_index_bytes: i64,
242    /// Number of indexed files currently tracked for this project_id
243    pub indexed_files: i64,
244    /// Last time indexing completed for this project_id (if known)
245    pub last_indexed_at: Option<DateTime<Utc>>,
246    /// Last run totals (if known)
247    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/// Request to search memory
313#[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/// Embedding backend health surfaced to UI/events.
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct EmbeddingHealth {
325    /// "ok" when embeddings are available, "degraded_disabled" otherwise.
326    pub status: String,
327    /// Optional reason when degraded.
328    pub reason: Option<String>,
329}
330
331/// Memory error types
332#[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
374// Implement serialization for Tauri commands
375impl 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/// Cleanup log entry for audit trail
387#[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
399/// Default embedding dimension for all-MiniLM-L6-v2
400pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
401
402/// Default embedding model name
403pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
404
405/// Maximum content length for a single chunk (in characters)
406pub const MAX_CHUNK_LENGTH: usize = 4000;
407
408/// Minimum content length for a chunk (in characters)
409pub const MIN_CHUNK_LENGTH: usize = 50;
410
411/// Persistent global memory record keyed by user identity.
412#[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/// Status for a reusable knowledge item.
452#[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/// A reusable knowledge space scoped to a project, run, or global namespace.
502#[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/// A reusable knowledge item promoted from run artifacts or validated memory.
517#[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/// Coverage state for a task/topic key within a knowledge space.
539#[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/// Request to promote or retire a knowledge item.
552#[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/// Result of a knowledge item promotion or retirement.
568#[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}