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
80/// Strongly typed wrapper for message row IDs.
81///
82/// Wraps the `SQLite` `messages.id` integer primary key to prevent confusion
83/// with [`ConversationId`] or [`MemSceneId`] values.
84///
85/// # Examples
86///
87/// ```
88/// use zeph_memory::MessageId;
89///
90/// let id = MessageId(7);
91/// assert_eq!(id.to_string(), "7");
92/// ```
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
94#[sqlx(transparent)]
95pub struct MessageId(pub i64);
96
97/// Strongly typed wrapper for `mem_scene` row IDs.
98///
99/// Wraps the `SQLite` `mem_scenes.id` integer primary key. Used by the scene
100/// consolidation subsystem to identify distinct conversational scenes.
101///
102/// # Examples
103///
104/// ```
105/// use zeph_memory::MemSceneId;
106///
107/// let id = MemSceneId(3);
108/// assert_eq!(id.to_string(), "3");
109/// ```
110#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
111#[sqlx(transparent)]
112pub struct MemSceneId(pub i64);
113
114impl std::fmt::Display for MemSceneId {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        write!(f, "{}", self.0)
117    }
118}
119
120impl std::fmt::Display for ConversationId {
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#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn memory_tier_round_trip() {
138        for tier in [
139            MemoryTier::Working,
140            MemoryTier::Episodic,
141            MemoryTier::Semantic,
142            MemoryTier::Persona,
143        ] {
144            let s = tier.as_str();
145            let parsed: MemoryTier = s.parse().expect("should parse");
146            assert_eq!(parsed, tier);
147            assert_eq!(format!("{tier}"), s);
148        }
149    }
150
151    #[test]
152    fn memory_tier_unknown_string_errors() {
153        assert!("unknown".parse::<MemoryTier>().is_err());
154    }
155
156    #[test]
157    fn memory_tier_serde_round_trip() {
158        let json = serde_json::to_string(&MemoryTier::Semantic).unwrap();
159        assert_eq!(json, "\"semantic\"");
160        let parsed: MemoryTier = serde_json::from_str(&json).unwrap();
161        assert_eq!(parsed, MemoryTier::Semantic);
162    }
163
164    #[test]
165    fn conversation_id_display() {
166        let id = ConversationId(42);
167        assert_eq!(format!("{id}"), "42");
168    }
169
170    #[test]
171    fn message_id_display() {
172        let id = MessageId(7);
173        assert_eq!(format!("{id}"), "7");
174    }
175
176    #[test]
177    fn conversation_id_eq() {
178        assert_eq!(ConversationId(1), ConversationId(1));
179        assert_ne!(ConversationId(1), ConversationId(2));
180    }
181
182    #[test]
183    fn message_id_copy() {
184        let id = MessageId(5);
185        let copied = id;
186        assert_eq!(id, copied);
187    }
188}