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//! Core identifier and tier types used throughout `zeph-memory`.
5
6/// Memory tier classification for the AOI four-layer architecture.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum MemoryTier {
10    /// Current conversation window. Virtual tier — not stored in the DB.
11    Working,
12    /// Session-bound messages. Default tier for all persisted messages.
13    Episodic,
14    /// Cross-session distilled facts. Promoted from Episodic when a fact
15    /// appears in `promotion_min_sessions`+ distinct sessions.
16    Semantic,
17    /// Long-lived user attributes (preferences, domain knowledge, working style).
18    /// Extracted from conversation history and injected into context (#2461).
19    Persona,
20}
21
22impl MemoryTier {
23    /// Return the canonical lowercase string representation.
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// use zeph_memory::MemoryTier;
29    ///
30    /// assert_eq!(MemoryTier::Episodic.as_str(), "episodic");
31    /// assert_eq!(MemoryTier::Semantic.as_str(), "semantic");
32    /// ```
33    #[must_use]
34    pub fn as_str(self) -> &'static str {
35        match self {
36            Self::Working => "working",
37            Self::Episodic => "episodic",
38            Self::Semantic => "semantic",
39            Self::Persona => "persona",
40        }
41    }
42}
43
44impl std::fmt::Display for MemoryTier {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.write_str(self.as_str())
47    }
48}
49
50impl std::str::FromStr for MemoryTier {
51    type Err = String;
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        match s {
54            "working" => Ok(Self::Working),
55            "episodic" => Ok(Self::Episodic),
56            "semantic" => Ok(Self::Semantic),
57            "persona" => Ok(Self::Persona),
58            other => Err(format!("unknown memory tier: {other}")),
59        }
60    }
61}
62
63/// Strongly typed wrapper for conversation row IDs.
64///
65/// Wraps the `SQLite` `conversations.id` integer primary key to prevent accidental
66/// confusion with [`MessageId`] or [`MemSceneId`] values.
67///
68/// # Examples
69///
70/// ```
71/// use zeph_memory::ConversationId;
72///
73/// let id = ConversationId(42);
74/// assert_eq!(id.to_string(), "42");
75/// ```
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
77#[sqlx(transparent)]
78pub struct ConversationId(pub i64);
79
80impl std::fmt::Display for ConversationId {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(f, "{}", self.0)
83    }
84}
85
86/// Strongly typed wrapper for message row IDs.
87///
88/// Wraps the `SQLite` `messages.id` integer primary key to prevent confusion
89/// with [`ConversationId`] or [`MemSceneId`] values.
90///
91/// # Examples
92///
93/// ```
94/// use zeph_memory::MessageId;
95///
96/// let id = MessageId(7);
97/// assert_eq!(id.to_string(), "7");
98/// ```
99#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
100#[sqlx(transparent)]
101pub struct MessageId(pub i64);
102
103/// Strongly typed wrapper for `mem_scene` row IDs.
104///
105/// Wraps the `SQLite` `mem_scenes.id` integer primary key. Used by the scene
106/// consolidation subsystem to identify distinct conversational scenes.
107///
108/// # Examples
109///
110/// ```
111/// use zeph_memory::MemSceneId;
112///
113/// let id = MemSceneId(3);
114/// assert_eq!(id.to_string(), "3");
115/// ```
116#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
117#[sqlx(transparent)]
118pub struct MemSceneId(pub i64);
119
120impl std::fmt::Display for MemSceneId {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{}", self.0)
123    }
124}
125
126impl std::fmt::Display for MessageId {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        write!(f, "{}", self.0)
129    }
130}
131
132/// Strongly typed wrapper for `experience_nodes.id` row IDs.
133///
134/// Prevents accidental confusion with [`EntityId`], [`ConversationId`], or [`MessageId`]
135/// at experience-memory API boundaries.
136///
137/// # Examples
138///
139/// ```
140/// use zeph_memory::ExperienceId;
141///
142/// let id = ExperienceId(10);
143/// assert_eq!(id.to_string(), "10");
144/// ```
145#[derive(
146    Debug,
147    Clone,
148    Copy,
149    PartialEq,
150    Eq,
151    PartialOrd,
152    Ord,
153    Hash,
154    sqlx::Type,
155    serde::Serialize,
156    serde::Deserialize,
157)]
158#[sqlx(transparent)]
159pub struct ExperienceId(pub i64);
160
161impl std::fmt::Display for ExperienceId {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        write!(f, "{}", self.0)
164    }
165}
166
167/// Strongly typed wrapper for `graph_entities.id` row IDs.
168///
169/// Prevents confusion with [`ExperienceId`] or other integer IDs at graph-store
170/// API boundaries.
171///
172/// # Examples
173///
174/// ```
175/// use zeph_memory::EntityId;
176///
177/// let id = EntityId(5);
178/// assert_eq!(id.to_string(), "5");
179/// ```
180#[derive(
181    Debug,
182    Clone,
183    Copy,
184    PartialEq,
185    Eq,
186    PartialOrd,
187    Ord,
188    Hash,
189    sqlx::Type,
190    serde::Serialize,
191    serde::Deserialize,
192)]
193#[sqlx(transparent)]
194pub struct EntityId(pub i64);
195
196impl std::fmt::Display for EntityId {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        write!(f, "{}", self.0)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn memory_tier_round_trip() {
208        for tier in [
209            MemoryTier::Working,
210            MemoryTier::Episodic,
211            MemoryTier::Semantic,
212            MemoryTier::Persona,
213        ] {
214            let s = tier.as_str();
215            let parsed: MemoryTier = s.parse().expect("should parse");
216            assert_eq!(parsed, tier);
217            assert_eq!(format!("{tier}"), s);
218        }
219    }
220
221    #[test]
222    fn memory_tier_unknown_string_errors() {
223        assert!("unknown".parse::<MemoryTier>().is_err());
224    }
225
226    #[test]
227    fn memory_tier_serde_round_trip() {
228        let json = serde_json::to_string(&MemoryTier::Semantic).unwrap();
229        assert_eq!(json, "\"semantic\"");
230        let parsed: MemoryTier = serde_json::from_str(&json).unwrap();
231        assert_eq!(parsed, MemoryTier::Semantic);
232    }
233
234    #[test]
235    fn conversation_id_display() {
236        let id = ConversationId(42);
237        assert_eq!(format!("{id}"), "42");
238    }
239
240    #[test]
241    fn message_id_display() {
242        let id = MessageId(7);
243        assert_eq!(format!("{id}"), "7");
244    }
245
246    #[test]
247    fn conversation_id_eq() {
248        assert_eq!(ConversationId(1), ConversationId(1));
249        assert_ne!(ConversationId(1), ConversationId(2));
250    }
251
252    #[test]
253    fn message_id_copy() {
254        let id = MessageId(5);
255        let copied = id;
256        assert_eq!(id, copied);
257    }
258
259    #[test]
260    fn experience_id_display() {
261        let id = ExperienceId(10);
262        assert_eq!(format!("{id}"), "10");
263    }
264
265    #[test]
266    fn entity_id_display() {
267        let id = EntityId(5);
268        assert_eq!(format!("{id}"), "5");
269    }
270
271    #[test]
272    fn experience_id_ord() {
273        assert!(ExperienceId(1) < ExperienceId(2));
274        assert_eq!(ExperienceId(3), ExperienceId(3));
275    }
276
277    #[test]
278    fn entity_id_hash() {
279        use std::collections::HashSet;
280        let mut set = HashSet::new();
281        set.insert(EntityId(1));
282        set.insert(EntityId(2));
283        set.insert(EntityId(1));
284        assert_eq!(set.len(), 2);
285    }
286}