1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Deserializer, Serialize};
5use std::collections::HashMap;
6
7pub type MemoryId = i64;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Memory {
13 pub id: MemoryId,
15 pub content: String,
17 #[serde(rename = "type")]
19 pub memory_type: MemoryType,
20 #[serde(default)]
22 pub tags: Vec<String>,
23 #[serde(default)]
25 pub metadata: HashMap<String, serde_json::Value>,
26 #[serde(default = "default_importance")]
28 pub importance: f32,
29 #[serde(default)]
31 pub access_count: i32,
32 pub created_at: DateTime<Utc>,
34 pub updated_at: DateTime<Utc>,
36 pub last_accessed_at: Option<DateTime<Utc>>,
38 pub owner_id: Option<String>,
40 #[serde(default)]
42 pub visibility: Visibility,
43 #[serde(default)]
45 pub scope: MemoryScope,
46 #[serde(default = "default_workspace")]
48 pub workspace: String,
49 #[serde(default)]
51 pub tier: MemoryTier,
52 #[serde(default = "default_version")]
54 pub version: i32,
55 #[serde(default)]
57 pub has_embedding: bool,
58 pub expires_at: Option<DateTime<Utc>>,
60 pub content_hash: Option<String>,
62 pub event_time: Option<DateTime<Utc>>,
65 pub event_duration_seconds: Option<i64>,
67 pub trigger_pattern: Option<String>,
69 #[serde(default)]
71 pub procedure_success_count: i32,
72 #[serde(default)]
74 pub procedure_failure_count: i32,
75 pub summary_of_id: Option<MemoryId>,
77 #[serde(default)]
80 pub lifecycle_state: LifecycleState,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
85#[serde(rename_all = "lowercase")]
86pub enum LifecycleState {
87 #[default]
89 Active,
90 Stale,
92 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
123pub const RESERVED_WORKSPACES: &[&str] = &["_system", "_archive"];
125
126pub const MAX_WORKSPACE_LENGTH: usize = 64;
128
129#[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
151pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct WorkspaceStats {
187 pub workspace: String,
189 pub memory_count: i64,
191 pub permanent_count: i64,
193 pub daily_count: i64,
195 pub first_memory_at: Option<DateTime<Utc>>,
197 pub last_memory_at: Option<DateTime<Utc>>,
199 #[serde(default)]
201 pub top_tags: Vec<(String, i64)>,
202 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#[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 TranscriptChunk,
231 Episodic,
235 Procedural,
238 Summary,
241 Checkpoint,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
256#[serde(rename_all = "lowercase")]
257pub enum MemoryTier {
258 #[default]
260 Permanent,
261 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 pub fn default_ttl_seconds(&self) -> Option<i64> {
275 match self {
276 MemoryTier::Permanent => None,
277 MemoryTier::Daily => Some(24 * 60 * 60), }
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 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
362#[serde(rename_all = "lowercase")]
363pub enum MemoryScope {
364 User { user_id: String },
366 Session { session_id: String },
368 Agent { agent_id: String },
370 #[default]
372 Global,
373}
374
375impl MemoryScope {
376 pub fn user(user_id: impl Into<String>) -> Self {
378 MemoryScope::User {
379 user_id: user_id.into(),
380 }
381 }
382
383 pub fn session(session_id: impl Into<String>) -> Self {
385 MemoryScope::Session {
386 session_id: session_id.into(),
387 }
388 }
389
390 pub fn agent(agent_id: impl Into<String>) -> Self {
392 MemoryScope::Agent {
393 agent_id: agent_id.into(),
394 }
395 }
396
397 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 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 pub fn can_access(&self, other: &MemoryScope) -> bool {
420 match (self, other) {
421 (MemoryScope::Global, _) => true,
423 (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 (_, MemoryScope::Global) => true,
431 _ => false,
433 }
434 }
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct CrossReference {
440 pub from_id: MemoryId,
442 pub to_id: MemoryId,
444 pub edge_type: EdgeType,
446 pub score: f32,
448 #[serde(default = "default_confidence")]
450 pub confidence: f32,
451 #[serde(default = "default_strength")]
453 pub strength: f32,
454 #[serde(default)]
456 pub source: RelationSource,
457 pub source_context: Option<String>,
459 pub created_at: DateTime<Utc>,
461 pub valid_from: DateTime<Utc>,
463 pub valid_to: Option<DateTime<Utc>>,
465 #[serde(default)]
467 pub pinned: bool,
468 #[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct SearchResult {
559 pub memory: Memory,
561 pub score: f32,
563 pub match_info: MatchInfo,
565}
566
567#[derive(Debug, Clone, Serialize, Deserialize)]
569pub struct MatchInfo {
570 pub strategy: SearchStrategy,
572 #[serde(default)]
574 pub matched_terms: Vec<String>,
575 #[serde(default)]
577 pub highlights: Vec<String>,
578 pub semantic_score: Option<f32>,
580 pub keyword_score: Option<f32>,
582}
583
584#[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#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct MemoryVersion {
626 pub version: i32,
628 pub content: String,
630 pub tags: Vec<String>,
632 pub metadata: HashMap<String, serde_json::Value>,
634 pub created_at: DateTime<Utc>,
636 pub created_by: Option<String>,
638 pub change_summary: Option<String>,
640}
641
642#[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#[derive(Debug, Clone, Serialize, Deserialize)]
665pub struct StorageConfig {
666 pub db_path: String,
668 #[serde(default)]
670 pub storage_mode: StorageMode,
671 pub cloud_uri: Option<String>,
673 #[serde(default)]
675 pub encrypt_cloud: bool,
676 #[serde(default = "default_half_life")]
678 pub confidence_half_life_days: f32,
679 #[serde(default = "default_true")]
681 pub auto_sync: bool,
682 #[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct EmbeddingConfig {
711 pub model: String,
713 pub api_key: Option<String>,
715 pub base_url: Option<String>,
718 pub embedding_model: Option<String>,
720 pub model_path: Option<String>,
722 pub dimensions: usize,
725 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
750#[serde(rename_all = "lowercase")]
751pub enum DedupMode {
752 Reject,
754 Merge,
756 Skip,
758 #[default]
760 Allow,
761}
762
763#[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 #[serde(default)]
776 pub scope: MemoryScope,
777 pub workspace: Option<String>,
779 #[serde(default)]
781 pub tier: MemoryTier,
782 #[serde(default)]
784 pub defer_embedding: bool,
785 pub ttl_seconds: Option<i64>,
789 #[serde(default)]
791 pub dedup_mode: DedupMode,
792 pub dedup_threshold: Option<f32>,
794 pub event_time: Option<DateTime<Utc>>,
797 pub event_duration_seconds: Option<i64>,
799 pub trigger_pattern: Option<String>,
801 pub summary_of_id: Option<MemoryId>,
803}
804
805#[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 pub scope: Option<MemoryScope>,
816 pub ttl_seconds: Option<i64>,
818 pub event_time: Option<Option<DateTime<Utc>>>,
822 pub trigger_pattern: Option<Option<String>>,
825}
826
827#[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#[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 pub metadata_filter: Option<HashMap<String, serde_json::Value>>,
853 pub scope: Option<MemoryScope>,
855 pub workspace: Option<String>,
857 pub workspaces: Option<Vec<String>>,
859 pub tier: Option<MemoryTier>,
861 pub filter: Option<serde_json::Value>,
864 #[serde(default)]
867 pub include_archived: bool,
868}
869
870#[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#[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#[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 #[serde(default, deserialize_with = "deserialize_search_strategy_opt")]
901 pub strategy: Option<SearchStrategy>,
902 #[serde(default)]
904 pub explain: bool,
905 pub scope: Option<MemoryScope>,
907 pub workspace: Option<String>,
909 pub workspaces: Option<Vec<String>>,
911 pub tier: Option<MemoryTier>,
913 #[serde(default)]
916 pub include_transcripts: bool,
917 pub filter: Option<serde_json::Value>,
920 #[serde(default)]
923 pub include_archived: bool,
924}
925
926#[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#[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#[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}