kiromi_ai_memory/
memory.rs1use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8use ulid::Ulid;
9
10use crate::content::{Content, ContentHash};
11use crate::partition::PartitionPath;
12
13#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(into = "String", try_from = "String")]
16pub struct MemoryId(Ulid);
17
18impl MemoryId {
19 #[must_use]
25 pub fn generate() -> Self {
26 if std::env::var_os("KIROMI_AI_TEST_DETERMINISTIC_ULID").is_some() {
27 use std::sync::atomic::{AtomicU64, Ordering};
28 static COUNTER: AtomicU64 = AtomicU64::new(1);
29 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
30 return MemoryId(Ulid::from_parts(n, u128::from(n)));
33 }
34 MemoryId(Ulid::new())
35 }
36
37 #[must_use]
39 pub const fn from_ulid(u: Ulid) -> Self {
40 MemoryId(u)
41 }
42
43 #[must_use]
45 pub const fn as_ulid(&self) -> Ulid {
46 self.0
47 }
48}
49
50impl fmt::Display for MemoryId {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 self.0.fmt(f)
53 }
54}
55
56impl FromStr for MemoryId {
57 type Err = ulid::DecodeError;
58 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
59 s.parse::<Ulid>().map(MemoryId)
60 }
61}
62
63impl From<MemoryId> for String {
64 fn from(id: MemoryId) -> String {
65 id.0.to_string()
66 }
67}
68
69impl TryFrom<String> for MemoryId {
70 type Error = ulid::DecodeError;
71 fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
72 s.parse()
73 }
74}
75
76#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
84#[non_exhaustive]
85pub enum MemoryKind {
86 #[default]
88 Episodic,
89 Semantic,
91 Procedural,
93 Archival,
95 Working,
97}
98
99impl MemoryKind {
100 #[must_use]
102 pub const fn as_persisted_str(self) -> &'static str {
103 match self {
104 MemoryKind::Episodic => "episodic",
105 MemoryKind::Semantic => "semantic",
106 MemoryKind::Procedural => "procedural",
107 MemoryKind::Archival => "archival",
108 MemoryKind::Working => "working",
109 }
110 }
111
112 #[must_use]
114 pub fn from_persisted(s: &str) -> Option<Self> {
115 match s {
116 "episodic" => Some(MemoryKind::Episodic),
117 "semantic" => Some(MemoryKind::Semantic),
118 "procedural" => Some(MemoryKind::Procedural),
119 "archival" => Some(MemoryKind::Archival),
120 "working" => Some(MemoryKind::Working),
121 _ => None,
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
129pub struct MemoryRef {
130 pub id: MemoryId,
132 pub partition: PartitionPath,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138#[non_exhaustive]
139pub struct MemoryRecord {
140 pub r#ref: MemoryRef,
142 pub content: Content,
144 pub hash: ContentHash,
146 pub created_at_ms: i64,
148 pub updated_at_ms: i64,
150 pub tombstoned: bool,
152 pub valid_from_ms: Option<i64>,
155 pub valid_until_ms: Option<i64>,
158 pub kind: Option<MemoryKind>,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
167#[non_exhaustive]
168pub struct MemoryRecordWire<'a> {
169 pub id: String,
171 pub partition: &'a str,
173 pub kind: &'static str,
175 pub body: &'a str,
177 pub bytes: usize,
179 pub created_at_ms: i64,
181 pub updated_at_ms: i64,
183 pub tombstoned: bool,
185}
186
187impl MemoryRecord {
188 #[must_use]
193 pub fn wire(&self) -> MemoryRecordWire<'_> {
194 MemoryRecordWire {
195 id: self.r#ref.id.to_string(),
196 partition: self.r#ref.partition.as_str(),
197 kind: self.content.kind().extension(),
198 body: self.content.as_str(),
199 bytes: self.content.byte_len(),
200 created_at_ms: self.created_at_ms,
201 updated_at_ms: self.updated_at_ms,
202 tombstoned: self.tombstoned,
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn id_roundtrips_through_string() {
213 let id = MemoryId::generate();
214 let s = id.to_string();
215 let back: MemoryId = s.parse().unwrap();
216 assert_eq!(id, back);
217 }
218
219 #[test]
220 fn id_roundtrips_through_serde() {
221 let id = MemoryId::generate();
222 let j = serde_json::to_string(&id).unwrap();
223 let back: MemoryId = serde_json::from_str(&j).unwrap();
224 assert_eq!(id, back);
225 }
226}