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