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