Skip to main content

zeph_memory/
types.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Memory tier classification for the AOI four-layer architecture.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum MemoryTier {
8    /// Current conversation window. Virtual tier — not stored in the DB.
9    Working,
10    /// Session-bound messages. Default tier for all persisted messages.
11    Episodic,
12    /// Cross-session distilled facts. Promoted from Episodic when a fact
13    /// appears in `promotion_min_sessions`+ distinct sessions.
14    Semantic,
15    /// Long-lived user attributes (preferences, domain knowledge, working style).
16    /// Extracted from conversation history and injected into context (#2461).
17    Persona,
18}
19
20impl MemoryTier {
21    #[must_use]
22    pub fn as_str(self) -> &'static str {
23        match self {
24            Self::Working => "working",
25            Self::Episodic => "episodic",
26            Self::Semantic => "semantic",
27            Self::Persona => "persona",
28        }
29    }
30}
31
32impl std::fmt::Display for MemoryTier {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.write_str(self.as_str())
35    }
36}
37
38impl std::str::FromStr for MemoryTier {
39    type Err = String;
40    fn from_str(s: &str) -> Result<Self, Self::Err> {
41        match s {
42            "working" => Ok(Self::Working),
43            "episodic" => Ok(Self::Episodic),
44            "semantic" => Ok(Self::Semantic),
45            "persona" => Ok(Self::Persona),
46            other => Err(format!("unknown memory tier: {other}")),
47        }
48    }
49}
50
51/// Strongly typed wrapper for conversation row IDs.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
53#[sqlx(transparent)]
54pub struct ConversationId(pub i64);
55
56/// Strongly typed wrapper for message row IDs.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
58#[sqlx(transparent)]
59pub struct MessageId(pub i64);
60
61/// Strongly typed wrapper for `mem_scene` row IDs.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
63#[sqlx(transparent)]
64pub struct MemSceneId(pub i64);
65
66impl std::fmt::Display for MemSceneId {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        write!(f, "{}", self.0)
69    }
70}
71
72impl std::fmt::Display for ConversationId {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        write!(f, "{}", self.0)
75    }
76}
77
78impl std::fmt::Display for MessageId {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}", self.0)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn memory_tier_round_trip() {
90        for tier in [
91            MemoryTier::Working,
92            MemoryTier::Episodic,
93            MemoryTier::Semantic,
94            MemoryTier::Persona,
95        ] {
96            let s = tier.as_str();
97            let parsed: MemoryTier = s.parse().expect("should parse");
98            assert_eq!(parsed, tier);
99            assert_eq!(format!("{tier}"), s);
100        }
101    }
102
103    #[test]
104    fn memory_tier_unknown_string_errors() {
105        assert!("unknown".parse::<MemoryTier>().is_err());
106    }
107
108    #[test]
109    fn memory_tier_serde_round_trip() {
110        let json = serde_json::to_string(&MemoryTier::Semantic).unwrap();
111        assert_eq!(json, "\"semantic\"");
112        let parsed: MemoryTier = serde_json::from_str(&json).unwrap();
113        assert_eq!(parsed, MemoryTier::Semantic);
114    }
115
116    #[test]
117    fn conversation_id_display() {
118        let id = ConversationId(42);
119        assert_eq!(format!("{id}"), "42");
120    }
121
122    #[test]
123    fn message_id_display() {
124        let id = MessageId(7);
125        assert_eq!(format!("{id}"), "7");
126    }
127
128    #[test]
129    fn conversation_id_eq() {
130        assert_eq!(ConversationId(1), ConversationId(1));
131        assert_ne!(ConversationId(1), ConversationId(2));
132    }
133
134    #[test]
135    fn message_id_copy() {
136        let id = MessageId(5);
137        let copied = id;
138        assert_eq!(id, copied);
139    }
140}