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