Skip to main content

engram/
types.rs

1//! Core types for Engram
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Deserializer, Serialize};
5use std::collections::HashMap;
6
7/// Unique identifier for a memory
8pub type MemoryId = i64;
9
10/// A memory entry in the database
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Memory {
13    /// Unique identifier
14    pub id: MemoryId,
15    /// Main content of the memory
16    pub content: String,
17    /// Memory type (e.g., "note", "todo", "issue", "decision")
18    #[serde(rename = "type")]
19    pub memory_type: MemoryType,
20    /// Tags for categorization
21    #[serde(default)]
22    pub tags: Vec<String>,
23    /// Arbitrary metadata as JSON
24    #[serde(default)]
25    pub metadata: HashMap<String, serde_json::Value>,
26    /// Importance score (0.0 - 1.0)
27    #[serde(default = "default_importance")]
28    pub importance: f32,
29    /// Number of times accessed
30    #[serde(default)]
31    pub access_count: i32,
32    /// When the memory was created
33    pub created_at: DateTime<Utc>,
34    /// When the memory was last updated
35    pub updated_at: DateTime<Utc>,
36    /// When the memory was last accessed
37    pub last_accessed_at: Option<DateTime<Utc>>,
38    /// Owner ID for multi-user support
39    pub owner_id: Option<String>,
40    /// Visibility level
41    #[serde(default)]
42    pub visibility: Visibility,
43    /// Memory scope for isolation (user/session/agent/global)
44    #[serde(default)]
45    pub scope: MemoryScope,
46    /// Workspace for project-based isolation (normalized: lowercase, [a-z0-9_-], max 64 chars)
47    #[serde(default = "default_workspace")]
48    pub workspace: String,
49    /// Memory tier for tiered storage (permanent vs daily)
50    #[serde(default)]
51    pub tier: MemoryTier,
52    /// Current version number
53    #[serde(default = "default_version")]
54    pub version: i32,
55    /// Whether embedding is computed
56    #[serde(default)]
57    pub has_embedding: bool,
58    /// When the memory expires (None = never for permanent, required for daily)
59    pub expires_at: Option<DateTime<Utc>>,
60    /// Content hash for deduplication (SHA256 of normalized content)
61    pub content_hash: Option<String>,
62    // Phase 1 - Cognitive memory fields (ENG-33)
63    /// Timestamp when the event occurred (for Episodic memories)
64    pub event_time: Option<DateTime<Utc>>,
65    /// Duration of the event in seconds (for Episodic memories)
66    pub event_duration_seconds: Option<i64>,
67    /// Pattern that triggers this procedure (for Procedural memories)
68    pub trigger_pattern: Option<String>,
69    /// Number of times this procedure succeeded (for Procedural memories)
70    #[serde(default)]
71    pub procedure_success_count: i32,
72    /// Number of times this procedure failed (for Procedural memories)
73    #[serde(default)]
74    pub procedure_failure_count: i32,
75    /// ID of the memory this is a summary of (for Summary memories)
76    pub summary_of_id: Option<MemoryId>,
77    // Phase 5 - Lifecycle management (ENG-37)
78    /// Lifecycle state for memory management (active, stale, archived)
79    #[serde(default)]
80    pub lifecycle_state: LifecycleState,
81    /// URL or local path to the primary media asset (for Image/Audio/Video memories)
82    /// Format: local:///path/to/file or https://... or s3://...
83    pub media_url: Option<String>,
84}
85
86/// Lifecycle state for memory management (Phase 5 - ENG-37)
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
88#[serde(rename_all = "lowercase")]
89pub enum LifecycleState {
90    /// Normal state - included in search/list by default
91    #[default]
92    Active,
93    /// Not accessed recently - included in search/list by default
94    Stale,
95    /// Compressed/summarized - EXCLUDED from search/list by default
96    Archived,
97}
98
99impl std::fmt::Display for LifecycleState {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            LifecycleState::Active => write!(f, "active"),
103            LifecycleState::Stale => write!(f, "stale"),
104            LifecycleState::Archived => write!(f, "archived"),
105        }
106    }
107}
108
109impl std::str::FromStr for LifecycleState {
110    type Err = String;
111
112    fn from_str(s: &str) -> Result<Self, Self::Err> {
113        match s.to_lowercase().as_str() {
114            "active" => Ok(LifecycleState::Active),
115            "stale" => Ok(LifecycleState::Stale),
116            "archived" => Ok(LifecycleState::Archived),
117            _ => Err(format!("Unknown lifecycle state: {}", s)),
118        }
119    }
120}
121
122fn default_workspace() -> String {
123    "default".to_string()
124}
125
126/// Reserved workspace names that cannot be used
127pub const RESERVED_WORKSPACES: &[&str] = &["_system", "_archive"];
128
129/// Maximum workspace name length
130pub const MAX_WORKSPACE_LENGTH: usize = 64;
131
132/// Workspace validation error
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum WorkspaceError {
135    Empty,
136    TooLong,
137    InvalidChars,
138    Reserved,
139}
140
141impl std::fmt::Display for WorkspaceError {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            WorkspaceError::Empty => write!(f, "Workspace name cannot be empty"),
145            WorkspaceError::TooLong => write!(f, "Workspace name exceeds {} characters", MAX_WORKSPACE_LENGTH),
146            WorkspaceError::InvalidChars => write!(f, "Workspace name can only contain lowercase letters, numbers, hyphens, and underscores"),
147            WorkspaceError::Reserved => write!(f, "Workspace name is reserved"),
148        }
149    }
150}
151
152impl std::error::Error for WorkspaceError {}
153
154/// Normalize and validate a workspace name
155///
156/// Rules:
157/// - Trim whitespace and convert to lowercase
158/// - Only allow [a-z0-9_-] characters
159/// - Max 64 characters
160/// - Cannot start with underscore (reserved for system workspaces)
161/// - "default" is allowed (it's the default workspace)
162pub fn normalize_workspace(s: &str) -> Result<String, WorkspaceError> {
163    let normalized = s.trim().to_lowercase();
164
165    if normalized.is_empty() {
166        return Err(WorkspaceError::Empty);
167    }
168
169    if normalized.len() > MAX_WORKSPACE_LENGTH {
170        return Err(WorkspaceError::TooLong);
171    }
172
173    if !normalized
174        .chars()
175        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
176    {
177        return Err(WorkspaceError::InvalidChars);
178    }
179
180    if normalized.starts_with('_') || RESERVED_WORKSPACES.contains(&normalized.as_str()) {
181        return Err(WorkspaceError::Reserved);
182    }
183
184    Ok(normalized)
185}
186
187/// Statistics for a workspace
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct WorkspaceStats {
190    /// Workspace name
191    pub workspace: String,
192    /// Total number of memories
193    pub memory_count: i64,
194    /// Number of permanent memories
195    pub permanent_count: i64,
196    /// Number of daily (ephemeral) memories
197    pub daily_count: i64,
198    /// Timestamp of first memory
199    pub first_memory_at: Option<DateTime<Utc>>,
200    /// Timestamp of last memory
201    pub last_memory_at: Option<DateTime<Utc>>,
202    /// Top tags in this workspace (tag, count)
203    #[serde(default)]
204    pub top_tags: Vec<(String, i64)>,
205    /// Average importance score
206    pub avg_importance: Option<f32>,
207}
208
209fn default_importance() -> f32 {
210    0.5
211}
212
213fn default_version() -> i32 {
214    1
215}
216
217/// Memory type classification
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
219#[serde(rename_all = "lowercase")]
220pub enum MemoryType {
221    #[default]
222    Note,
223    Todo,
224    Issue,
225    Decision,
226    Preference,
227    Learning,
228    Context,
229    Credential,
230    Custom,
231    /// Session transcript chunk (for conversation indexing)
232    /// Default tier: Daily with 7-day TTL
233    TranscriptChunk,
234    // Cognitive memory types (Phase 1 - ENG-33)
235    /// Events with temporal context (e.g., "User deployed v2.0 on Jan 15")
236    /// Tracks when things happened and how long they took
237    Episodic,
238    /// Learned patterns and workflows (e.g., "When user asks about auth, check JWT first")
239    /// Tracks success/failure counts for pattern effectiveness
240    Procedural,
241    /// Compressed summaries of other memories
242    /// References the original via summary_of_id
243    Summary,
244    /// Conversation state snapshots for session resumption
245    /// Replaces Context type for checkpoint-specific use
246    Checkpoint,
247    // Multimodal memory types
248    /// Image memory with optional media_url pointing to the asset
249    Image,
250    /// Audio memory with optional media_url pointing to the asset
251    Audio,
252    /// Video memory with optional media_url pointing to the asset
253    Video,
254}
255
256/// Memory tier for tiered storage (permanent vs ephemeral)
257///
258/// Tiers control memory lifetime:
259/// - `Permanent`: Never expires, for important knowledge and decisions
260/// - `Daily`: Auto-expires after TTL, for session context and scratch notes
261///
262/// Invariants enforced at write-time:
263/// - Permanent tier: expires_at MUST be NULL
264/// - Daily tier: expires_at MUST be set (defaults to created_at + 24h)
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
266#[serde(rename_all = "lowercase")]
267pub enum MemoryTier {
268    /// Never expires (default)
269    #[default]
270    Permanent,
271    /// Auto-expires after configurable TTL (default: 24 hours)
272    Daily,
273}
274
275impl MemoryTier {
276    pub fn as_str(&self) -> &'static str {
277        match self {
278            MemoryTier::Permanent => "permanent",
279            MemoryTier::Daily => "daily",
280        }
281    }
282
283    /// Default TTL in seconds for daily tier
284    pub fn default_ttl_seconds(&self) -> Option<i64> {
285        match self {
286            MemoryTier::Permanent => None,
287            MemoryTier::Daily => Some(24 * 60 * 60), // 24 hours
288        }
289    }
290}
291
292impl std::str::FromStr for MemoryTier {
293    type Err = String;
294
295    fn from_str(s: &str) -> Result<Self, Self::Err> {
296        match s.to_lowercase().as_str() {
297            "permanent" => Ok(MemoryTier::Permanent),
298            "daily" => Ok(MemoryTier::Daily),
299            _ => Err(format!("Unknown memory tier: {}", s)),
300        }
301    }
302}
303
304impl MemoryType {
305    pub fn as_str(&self) -> &'static str {
306        match self {
307            MemoryType::Note => "note",
308            MemoryType::Todo => "todo",
309            MemoryType::Issue => "issue",
310            MemoryType::Decision => "decision",
311            MemoryType::Preference => "preference",
312            MemoryType::Learning => "learning",
313            MemoryType::Context => "context",
314            MemoryType::Credential => "credential",
315            MemoryType::Custom => "custom",
316            MemoryType::TranscriptChunk => "transcript_chunk",
317            MemoryType::Episodic => "episodic",
318            MemoryType::Procedural => "procedural",
319            MemoryType::Summary => "summary",
320            MemoryType::Checkpoint => "checkpoint",
321            MemoryType::Image => "image",
322            MemoryType::Audio => "audio",
323            MemoryType::Video => "video",
324        }
325    }
326
327    /// Returns true if this type should be excluded from default search
328    pub fn excluded_from_default_search(&self) -> bool {
329        matches!(self, MemoryType::TranscriptChunk)
330    }
331
332    /// Returns true if this type is a multimodal (media) memory
333    pub fn is_multimodal(&self) -> bool {
334        matches!(self, MemoryType::Image | MemoryType::Audio | MemoryType::Video)
335    }
336}
337
338impl std::str::FromStr for MemoryType {
339    type Err = String;
340
341    fn from_str(s: &str) -> Result<Self, Self::Err> {
342        match s.to_lowercase().as_str() {
343            "note" => Ok(MemoryType::Note),
344            "todo" => Ok(MemoryType::Todo),
345            "issue" => Ok(MemoryType::Issue),
346            "decision" => Ok(MemoryType::Decision),
347            "preference" => Ok(MemoryType::Preference),
348            "learning" => Ok(MemoryType::Learning),
349            "context" => Ok(MemoryType::Context),
350            "credential" => Ok(MemoryType::Credential),
351            "custom" => Ok(MemoryType::Custom),
352            "transcript_chunk" => Ok(MemoryType::TranscriptChunk),
353            "episodic" => Ok(MemoryType::Episodic),
354            "procedural" => Ok(MemoryType::Procedural),
355            "summary" => Ok(MemoryType::Summary),
356            "checkpoint" => Ok(MemoryType::Checkpoint),
357            "image" => Ok(MemoryType::Image),
358            "audio" => Ok(MemoryType::Audio),
359            "video" => Ok(MemoryType::Video),
360            _ => Err(format!("Unknown memory type: {}", s)),
361        }
362    }
363}
364
365/// Visibility levels for memories
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
367#[serde(rename_all = "lowercase")]
368pub enum Visibility {
369    #[default]
370    Private,
371    Shared,
372    Public,
373}
374
375/// Memory scope for isolating memories by user, session, agent, or global
376///
377/// This enables multi-tenant memory management where:
378/// - `User`: Memories belong to a specific user across all sessions
379/// - `Session`: Memories are temporary and bound to a conversation session
380/// - `Agent`: Memories belong to a specific AI agent instance
381/// - `Global`: Memories are shared across all scopes (system-wide)
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
383#[serde(rename_all = "lowercase")]
384pub enum MemoryScope {
385    /// User-scoped memory, persists across sessions
386    User { user_id: String },
387    /// Session-scoped memory, temporary for one conversation
388    Session { session_id: String },
389    /// Agent-scoped memory, belongs to a specific agent instance
390    Agent { agent_id: String },
391    /// Global scope, accessible by all (default for backward compatibility)
392    #[default]
393    Global,
394}
395
396impl MemoryScope {
397    /// Create a user-scoped memory scope
398    pub fn user(user_id: impl Into<String>) -> Self {
399        MemoryScope::User {
400            user_id: user_id.into(),
401        }
402    }
403
404    /// Create a session-scoped memory scope
405    pub fn session(session_id: impl Into<String>) -> Self {
406        MemoryScope::Session {
407            session_id: session_id.into(),
408        }
409    }
410
411    /// Create an agent-scoped memory scope
412    pub fn agent(agent_id: impl Into<String>) -> Self {
413        MemoryScope::Agent {
414            agent_id: agent_id.into(),
415        }
416    }
417
418    /// Get the scope type as a string
419    pub fn scope_type(&self) -> &'static str {
420        match self {
421            MemoryScope::User { .. } => "user",
422            MemoryScope::Session { .. } => "session",
423            MemoryScope::Agent { .. } => "agent",
424            MemoryScope::Global => "global",
425        }
426    }
427
428    /// Get the scope ID (user_id, session_id, agent_id, or None for global)
429    pub fn scope_id(&self) -> Option<&str> {
430        match self {
431            MemoryScope::User { user_id } => Some(user_id.as_str()),
432            MemoryScope::Session { session_id } => Some(session_id.as_str()),
433            MemoryScope::Agent { agent_id } => Some(agent_id.as_str()),
434            MemoryScope::Global => None,
435        }
436    }
437
438    /// Check if this scope matches or is accessible from another scope
439    /// Global scope can access everything, specific scopes can only access their own
440    pub fn can_access(&self, other: &MemoryScope) -> bool {
441        match (self, other) {
442            // Global can access everything
443            (MemoryScope::Global, _) => true,
444            // Same scope type and ID
445            (MemoryScope::User { user_id: a }, MemoryScope::User { user_id: b }) => a == b,
446            (MemoryScope::Session { session_id: a }, MemoryScope::Session { session_id: b }) => {
447                a == b
448            }
449            (MemoryScope::Agent { agent_id: a }, MemoryScope::Agent { agent_id: b }) => a == b,
450            // Anyone can access global memories
451            (_, MemoryScope::Global) => true,
452            // Different scope types cannot access each other
453            _ => false,
454        }
455    }
456}
457
458/// Cross-reference (relation) between memories
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct CrossReference {
461    /// Source memory ID
462    pub from_id: MemoryId,
463    /// Target memory ID
464    pub to_id: MemoryId,
465    /// Type of relationship
466    pub edge_type: EdgeType,
467    /// Similarity/relevance score (0.0 - 1.0)
468    pub score: f32,
469    /// Confidence level (decays over time)
470    #[serde(default = "default_confidence")]
471    pub confidence: f32,
472    /// User-adjustable importance
473    #[serde(default = "default_strength")]
474    pub strength: f32,
475    /// How the relation was created
476    #[serde(default)]
477    pub source: RelationSource,
478    /// Context explaining why the relation exists
479    pub source_context: Option<String>,
480    /// When the relation was created
481    pub created_at: DateTime<Utc>,
482    /// When the relation became valid
483    pub valid_from: DateTime<Utc>,
484    /// When the relation stopped being valid (None = still valid)
485    pub valid_to: Option<DateTime<Utc>>,
486    /// Exempt from confidence decay
487    #[serde(default)]
488    pub pinned: bool,
489    /// Additional metadata
490    #[serde(default)]
491    pub metadata: HashMap<String, serde_json::Value>,
492}
493
494fn default_confidence() -> f32 {
495    1.0
496}
497
498fn default_strength() -> f32 {
499    1.0
500}
501
502/// Types of edges/relationships between memories
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
504#[serde(rename_all = "snake_case")]
505pub enum EdgeType {
506    #[default]
507    RelatedTo,
508    Supersedes,
509    Contradicts,
510    Implements,
511    Extends,
512    References,
513    DependsOn,
514    Blocks,
515    FollowsUp,
516}
517
518impl EdgeType {
519    pub fn as_str(&self) -> &'static str {
520        match self {
521            EdgeType::RelatedTo => "related_to",
522            EdgeType::Supersedes => "supersedes",
523            EdgeType::Contradicts => "contradicts",
524            EdgeType::Implements => "implements",
525            EdgeType::Extends => "extends",
526            EdgeType::References => "references",
527            EdgeType::DependsOn => "depends_on",
528            EdgeType::Blocks => "blocks",
529            EdgeType::FollowsUp => "follows_up",
530        }
531    }
532
533    pub fn all() -> &'static [EdgeType] {
534        &[
535            EdgeType::RelatedTo,
536            EdgeType::Supersedes,
537            EdgeType::Contradicts,
538            EdgeType::Implements,
539            EdgeType::Extends,
540            EdgeType::References,
541            EdgeType::DependsOn,
542            EdgeType::Blocks,
543            EdgeType::FollowsUp,
544        ]
545    }
546}
547
548impl std::str::FromStr for EdgeType {
549    type Err = String;
550
551    fn from_str(s: &str) -> Result<Self, Self::Err> {
552        match s.to_lowercase().as_str() {
553            "related_to" | "related" => Ok(EdgeType::RelatedTo),
554            "supersedes" => Ok(EdgeType::Supersedes),
555            "contradicts" => Ok(EdgeType::Contradicts),
556            "implements" => Ok(EdgeType::Implements),
557            "extends" => Ok(EdgeType::Extends),
558            "references" => Ok(EdgeType::References),
559            "depends_on" => Ok(EdgeType::DependsOn),
560            "blocks" => Ok(EdgeType::Blocks),
561            "follows_up" => Ok(EdgeType::FollowsUp),
562            _ => Err(format!("Unknown edge type: {}", s)),
563        }
564    }
565}
566
567/// How a relation was created
568#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
569#[serde(rename_all = "lowercase")]
570pub enum RelationSource {
571    #[default]
572    Auto,
573    Manual,
574    Llm,
575}
576
577/// Search result with metadata
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct SearchResult {
580    /// The matched memory
581    pub memory: Memory,
582    /// Overall relevance score
583    pub score: f32,
584    /// How the result matched
585    pub match_info: MatchInfo,
586}
587
588/// Information about how a search result matched
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct MatchInfo {
591    /// Which search strategy was used
592    pub strategy: SearchStrategy,
593    /// Terms that matched (for keyword search)
594    #[serde(default)]
595    pub matched_terms: Vec<String>,
596    /// Highlighted snippets
597    #[serde(default)]
598    pub highlights: Vec<String>,
599    /// Semantic similarity score (if used)
600    pub semantic_score: Option<f32>,
601    /// Keyword/BM25 score (if used)
602    pub keyword_score: Option<f32>,
603}
604
605/// Search strategy used
606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
607#[serde(rename_all = "snake_case")]
608pub enum SearchStrategy {
609    #[serde(alias = "keyword")]
610    KeywordOnly,
611    #[serde(alias = "semantic")]
612    SemanticOnly,
613    #[default]
614    Hybrid,
615}
616
617impl SearchStrategy {
618    pub fn parse_str(s: &str) -> Option<Self> {
619        match s.to_lowercase().as_str() {
620            "keyword" | "keyword_only" => Some(SearchStrategy::KeywordOnly),
621            "semantic" | "semantic_only" => Some(SearchStrategy::SemanticOnly),
622            "hybrid" => Some(SearchStrategy::Hybrid),
623            _ => None,
624        }
625    }
626}
627
628fn deserialize_search_strategy_opt<'de, D>(
629    deserializer: D,
630) -> Result<Option<SearchStrategy>, D::Error>
631where
632    D: Deserializer<'de>,
633{
634    let opt = Option::<String>::deserialize(deserializer)?;
635    match opt.as_deref() {
636        None => Ok(None),
637        Some("auto") => Ok(None),
638        Some(other) => SearchStrategy::parse_str(other).map(Some).ok_or_else(|| {
639            <D::Error as serde::de::Error>::custom(format!("Invalid search strategy: {}", other))
640        }),
641    }
642}
643
644/// Memory version for history tracking
645#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct MemoryVersion {
647    /// Version number (1, 2, 3, ...)
648    pub version: i32,
649    /// Content at this version
650    pub content: String,
651    /// Tags at this version
652    pub tags: Vec<String>,
653    /// Metadata at this version
654    pub metadata: HashMap<String, serde_json::Value>,
655    /// When this version was created
656    pub created_at: DateTime<Utc>,
657    /// Who created this version
658    pub created_by: Option<String>,
659    /// Summary of changes
660    pub change_summary: Option<String>,
661}
662
663/// Statistics about the memory store
664#[derive(Debug, Clone, Serialize, Deserialize, Default)]
665pub struct StorageStats {
666    pub total_memories: i64,
667    pub total_tags: i64,
668    pub total_crossrefs: i64,
669    pub total_versions: i64,
670    pub total_identities: i64,
671    pub total_entities: i64,
672    pub db_size_bytes: i64,
673    pub memories_with_embeddings: i64,
674    pub memories_pending_embedding: i64,
675    pub last_sync: Option<DateTime<Utc>>,
676    pub sync_pending: bool,
677    pub storage_mode: String,
678    pub schema_version: i32,
679    pub workspaces: HashMap<String, i64>,
680    pub type_counts: HashMap<String, i64>,
681    pub tier_counts: HashMap<String, i64>,
682}
683
684/// Configuration for the storage engine
685#[derive(Debug, Clone, Serialize, Deserialize)]
686pub struct StorageConfig {
687    /// Path to SQLite database
688    pub db_path: String,
689    /// Storage mode (local or cloud-safe)
690    #[serde(default)]
691    pub storage_mode: StorageMode,
692    /// Cloud storage URI (s3://bucket/path)
693    pub cloud_uri: Option<String>,
694    /// Enable encryption for cloud storage
695    #[serde(default)]
696    pub encrypt_cloud: bool,
697    /// Confidence decay half-life in days
698    #[serde(default = "default_half_life")]
699    pub confidence_half_life_days: f32,
700    /// Auto-sync after writes
701    #[serde(default = "default_true")]
702    pub auto_sync: bool,
703    /// Sync debounce delay in milliseconds
704    #[serde(default = "default_sync_debounce")]
705    pub sync_debounce_ms: u64,
706}
707
708fn default_half_life() -> f32 {
709    30.0
710}
711
712fn default_true() -> bool {
713    true
714}
715
716fn default_sync_debounce() -> u64 {
717    5000
718}
719
720/// Storage mode for SQLite
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
722#[serde(rename_all = "kebab-case")]
723pub enum StorageMode {
724    #[default]
725    Local,
726    CloudSafe,
727}
728
729/// Embedding model configuration
730#[derive(Debug, Clone, Serialize, Deserialize)]
731pub struct EmbeddingConfig {
732    /// Model to use: "openai", "local", "tfidf"
733    pub model: String,
734    /// OpenAI API key (for openai model)
735    pub api_key: Option<String>,
736    /// OpenAI-compatible API base URL (for OpenRouter, Azure, etc.)
737    /// Default: https://api.openai.com/v1
738    pub base_url: Option<String>,
739    /// Embedding model name override (e.g., "text-embedding-3-small", "openai/text-embedding-3-small")
740    pub embedding_model: Option<String>,
741    /// Local model path (for local model)
742    pub model_path: Option<String>,
743    /// Embedding dimensions (must match model output)
744    /// Default: 384 for TF-IDF, 1536 for text-embedding-3-small
745    pub dimensions: usize,
746    /// Batch size for async queue
747    #[serde(default = "default_batch_size")]
748    pub batch_size: usize,
749}
750
751fn default_batch_size() -> usize {
752    100
753}
754
755impl Default for EmbeddingConfig {
756    fn default() -> Self {
757        Self {
758            model: "tfidf".to_string(),
759            api_key: None,
760            base_url: None,
761            embedding_model: None,
762            model_path: None,
763            dimensions: 384,
764            batch_size: 100,
765        }
766    }
767}
768
769/// Deduplication mode when creating memories
770#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
771#[serde(rename_all = "lowercase")]
772pub enum DedupMode {
773    /// Return error if duplicate found
774    Reject,
775    /// Merge with existing memory (update metadata, tags)
776    Merge,
777    /// Silently skip creation, return existing memory
778    Skip,
779    /// Allow duplicate creation (default, current behavior)
780    #[default]
781    Allow,
782}
783
784/// Input for creating a new memory
785#[derive(Debug, Clone, Serialize, Deserialize, Default)]
786pub struct CreateMemoryInput {
787    pub content: String,
788    #[serde(default, alias = "type")]
789    pub memory_type: MemoryType,
790    #[serde(default)]
791    pub tags: Vec<String>,
792    #[serde(default)]
793    pub metadata: HashMap<String, serde_json::Value>,
794    pub importance: Option<f32>,
795    /// Memory scope for isolation (user/session/agent/global)
796    #[serde(default)]
797    pub scope: MemoryScope,
798    /// Workspace for project-based isolation (will be normalized)
799    pub workspace: Option<String>,
800    /// Memory tier (permanent or daily)
801    #[serde(default)]
802    pub tier: MemoryTier,
803    /// Defer embedding computation to background queue
804    #[serde(default)]
805    pub defer_embedding: bool,
806    /// Time-to-live in seconds (None = use tier default, Some(0) = never expires)
807    /// For daily tier: defaults to 24 hours if not specified
808    /// For permanent tier: must be None (enforced at write-time)
809    pub ttl_seconds: Option<i64>,
810    /// Deduplication mode (default: allow)
811    #[serde(default)]
812    pub dedup_mode: DedupMode,
813    /// Similarity threshold for semantic deduplication (0.0-1.0, default: 0.95)
814    pub dedup_threshold: Option<f32>,
815    // Phase 1 - Cognitive memory fields (ENG-33)
816    /// Timestamp when the event occurred (for Episodic memories)
817    pub event_time: Option<DateTime<Utc>>,
818    /// Duration of the event in seconds (for Episodic memories)
819    pub event_duration_seconds: Option<i64>,
820    /// Pattern that triggers this procedure (for Procedural memories)
821    pub trigger_pattern: Option<String>,
822    /// ID of the memory this is a summary of (for Summary memories)
823    pub summary_of_id: Option<MemoryId>,
824    /// URL or local path to the primary media asset (for Image/Audio/Video memories)
825    pub media_url: Option<String>,
826}
827
828/// Input for updating a memory
829#[derive(Debug, Clone, Serialize, Deserialize)]
830pub struct UpdateMemoryInput {
831    pub content: Option<String>,
832    #[serde(alias = "type")]
833    pub memory_type: Option<MemoryType>,
834    pub tags: Option<Vec<String>>,
835    pub metadata: Option<HashMap<String, serde_json::Value>>,
836    pub importance: Option<f32>,
837    /// Memory scope for isolation (user/session/agent/global)
838    pub scope: Option<MemoryScope>,
839    /// Time-to-live in seconds (None = no change, Some(0) = remove expiration, Some(n) = set to n seconds from now)
840    pub ttl_seconds: Option<i64>,
841    // Phase 1 - Cognitive memory fields (ENG-33)
842    /// Timestamp when the event occurred (for Episodic memories)
843    /// Use Some(None) to clear the value
844    pub event_time: Option<Option<DateTime<Utc>>>,
845    /// Pattern that triggers this procedure (for Procedural memories)
846    /// Use Some(None) to clear the value
847    pub trigger_pattern: Option<Option<String>>,
848    /// URL or local path to the primary media asset (for Image/Audio/Video memories)
849    /// Use Some(None) to clear, Some(Some(url)) to set
850    pub media_url: Option<Option<String>>,
851}
852
853/// Input for creating a cross-reference
854#[derive(Debug, Clone, Serialize, Deserialize)]
855pub struct CreateCrossRefInput {
856    pub from_id: MemoryId,
857    pub to_id: MemoryId,
858    #[serde(default)]
859    pub edge_type: EdgeType,
860    pub strength: Option<f32>,
861    pub source_context: Option<String>,
862    #[serde(default)]
863    pub pinned: bool,
864}
865
866/// Options for listing memories
867#[derive(Debug, Clone, Default, Serialize, Deserialize)]
868pub struct ListOptions {
869    pub limit: Option<i64>,
870    pub offset: Option<i64>,
871    pub tags: Option<Vec<String>>,
872    #[serde(alias = "type")]
873    pub memory_type: Option<MemoryType>,
874    pub sort_by: Option<SortField>,
875    pub sort_order: Option<SortOrder>,
876    /// Legacy metadata filter (simple key-value equality)
877    /// Deprecated: Use `filter` for advanced queries
878    pub metadata_filter: Option<HashMap<String, serde_json::Value>>,
879    /// Filter by memory scope
880    pub scope: Option<MemoryScope>,
881    /// Filter by workspace (single workspace)
882    pub workspace: Option<String>,
883    /// Filter by multiple workspaces (OR logic)
884    pub workspaces: Option<Vec<String>>,
885    /// Filter by memory tier
886    pub tier: Option<MemoryTier>,
887    /// Advanced filter expression with AND/OR/comparison operators (RML-932)
888    /// Example: {"AND": [{"metadata.project": {"eq": "engram"}}, {"importance": {"gte": 0.5}}]}
889    pub filter: Option<serde_json::Value>,
890    // Phase 5 - Lifecycle management (ENG-37)
891    /// Include archived memories in results (default: false)
892    #[serde(default)]
893    pub include_archived: bool,
894}
895
896/// Fields to sort by
897#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
898#[serde(rename_all = "snake_case")]
899pub enum SortField {
900    #[default]
901    CreatedAt,
902    UpdatedAt,
903    LastAccessedAt,
904    Importance,
905    AccessCount,
906}
907
908/// Sort order
909#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
910#[serde(rename_all = "lowercase")]
911pub enum SortOrder {
912    Asc,
913    #[default]
914    Desc,
915}
916
917/// Options for search operations
918#[derive(Debug, Clone, Default, Serialize, Deserialize)]
919pub struct SearchOptions {
920    pub limit: Option<i64>,
921    pub min_score: Option<f32>,
922    pub tags: Option<Vec<String>>,
923    #[serde(alias = "type")]
924    pub memory_type: Option<MemoryType>,
925    /// Force a specific search strategy
926    #[serde(default, deserialize_with = "deserialize_search_strategy_opt")]
927    pub strategy: Option<SearchStrategy>,
928    /// Include match explanations
929    #[serde(default)]
930    pub explain: bool,
931    /// Filter by memory scope
932    pub scope: Option<MemoryScope>,
933    /// Filter by workspace (single workspace)
934    pub workspace: Option<String>,
935    /// Filter by multiple workspaces (OR logic)
936    pub workspaces: Option<Vec<String>>,
937    /// Filter by memory tier
938    pub tier: Option<MemoryTier>,
939    /// Include transcript chunks in search (default: false)
940    /// By default, transcript_chunk memories are excluded from search
941    #[serde(default)]
942    pub include_transcripts: bool,
943    /// Advanced filter expression with AND/OR/comparison operators (RML-932)
944    /// Takes precedence over `tags` and `memory_type` if specified
945    pub filter: Option<serde_json::Value>,
946    // Phase 5 - Lifecycle management (ENG-37)
947    /// Include archived memories in search results (default: false)
948    #[serde(default)]
949    pub include_archived: bool,
950    /// Filter by hierarchical scope path (prefix search).
951    ///
952    /// When set, only memories whose `scope_path` starts with (or equals) this value
953    /// are returned. For example, `"global/org:acme"` will match memories at
954    /// `"global/org:acme"`, `"global/org:acme/user:alice"`, etc.
955    pub scope_path: Option<String>,
956}
957
958/// Sync status information
959#[derive(Debug, Clone, Serialize, Deserialize)]
960pub struct SyncStatus {
961    pub pending_changes: i64,
962    pub last_sync: Option<DateTime<Utc>>,
963    pub last_error: Option<String>,
964    pub is_syncing: bool,
965}
966
967/// Embedding queue status
968#[derive(Debug, Clone, Serialize, Deserialize)]
969pub struct EmbeddingStatus {
970    pub memory_id: MemoryId,
971    pub status: EmbeddingState,
972    pub queued_at: Option<DateTime<Utc>>,
973    pub completed_at: Option<DateTime<Utc>>,
974    pub error: Option<String>,
975}
976
977/// State of embedding computation
978#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
979#[serde(rename_all = "lowercase")]
980pub enum EmbeddingState {
981    Pending,
982    Processing,
983    Complete,
984    Failed,
985}