1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::core::{
11 CanonicalMemory, CanonicalPredicate, DiscardReason, EntityRef, EvidenceCounters, Explicitness,
12 FactFingerprint, MemoryId, MemoryKind, MemorySource, MemoryStatus, MemoryValue, MutationIntent,
13 PrivacyMetadata, ProposedPersistence, RetrievalMetadata, SensitivityClass, SessionId,
14 TemporalMetadata, TemporalScope, TurnId, UserId, normalize_token,
15};
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct ProposedMemory {
20 pub fingerprint: FactFingerprint,
22 pub subject: EntityRef,
24 pub predicate: CanonicalPredicate,
26 pub value: MemoryValue,
28 pub statement: String,
30 pub evidence_summary: String,
32 pub kind: MemoryKind,
34 pub temporal_scope: TemporalScope,
36 pub explicitness: Explicitness,
38 pub confidence: f32,
40 pub evidence: EvidenceCounters,
42 pub persistence: ProposedPersistence,
44 pub expected_expiry: Option<DateTime<Utc>>,
46 pub mutation_intent: Option<MutationIntent>,
48 pub sensitivity: SensitivityClass,
50 pub qualifier: Option<String>,
52 pub session_id: SessionId,
54 pub turn_id: TurnId,
56 pub tags: Vec<String>,
58}
59
60impl ProposedMemory {
61 pub fn into_canonical(
63 self,
64 owner: &UserId,
65 id: MemoryId,
66 status: MemoryStatus,
67 now: DateTime<Utc>,
68 ) -> CanonicalMemory {
69 let mut temporal = TemporalMetadata::created_at(now);
70 temporal.expires_at =
71 crate::core::resolve_expiry(self.kind, self.temporal_scope, self.expected_expiry, now);
72
73 CanonicalMemory {
74 id,
75 owner: owner.clone(),
76 kind: self.kind,
77 predicate: self.predicate.clone(),
78 status,
79 confidence: self.confidence,
80 subject: self.subject.clone(),
81 value: self.value,
82 statement: self.statement,
83 evidence_summary: self.evidence_summary,
84 source: MemorySource::from_explicitness(
85 self.explicitness,
86 self.session_id,
87 self.turn_id,
88 ),
89 temporal,
90 retrieval: RetrievalMetadata {
91 subject: normalize_token(&self.subject.display),
92 tags: self.tags,
93 aliases: Vec::new(),
94 entities: self.subject.surface_forms(),
95 location: None,
96 },
97 evidence: self.evidence,
98 privacy: PrivacyMetadata {
99 deletable: true,
100 exportable: true,
101 sensitivity: self.sensitivity,
102 },
103 temporal_scope: self.temporal_scope,
104 supersedes: Vec::new(),
105 superseded_by: None,
106 qualifier: self.qualifier,
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq)]
113pub enum MemorySelector {
114 ById(MemoryId),
116 BySubjectPredicate(String),
118 ByTopic(String),
120}
121
122impl MemorySelector {
123 pub fn matches(&self, memory: &CanonicalMemory) -> bool {
125 self.matches_fields(
126 Some(&memory.id),
127 memory.fingerprint().subject_predicate(),
128 &memory.statement,
129 &memory.retrieval.tags,
130 )
131 }
132
133 pub(crate) fn matches_proposal(&self, proposal: &ProposedMemory) -> bool {
134 self.matches_fields(
135 None,
136 proposal.fingerprint.subject_predicate(),
137 &proposal.statement,
138 &proposal.tags,
139 )
140 }
141
142 fn matches_fields(
143 &self,
144 id: Option<&MemoryId>,
145 subject_predicate: &str,
146 statement: &str,
147 tags: &[String],
148 ) -> bool {
149 match self {
150 Self::ById(expected) => id == Some(expected),
151 Self::BySubjectPredicate(prefix) => subject_predicate == prefix,
152 Self::ByTopic(topic) => {
153 let needle = normalize_token(topic);
158 !needle.is_empty()
159 && (contains_word_sequence(&normalize_token(statement), &needle)
160 || tags.iter().any(|t| normalize_token(t) == needle))
161 }
162 }
163 }
164}
165
166fn contains_word_sequence(haystack: &str, needle: &str) -> bool {
170 let hay: Vec<&str> = haystack.split_whitespace().collect();
171 let ned: Vec<&str> = needle.split_whitespace().collect();
172 if ned.is_empty() || ned.len() > hay.len() {
173 return false;
174 }
175 hay.windows(ned.len()).any(|w| w == ned.as_slice())
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum ResolutionKind {
182 Create,
184 Reinforce,
186 Refine,
188 Supersede,
190 Coexist,
192 Stage,
194 Delete,
196 Discard,
198}
199
200impl ResolutionKind {
201 pub fn label(self) -> &'static str {
203 match self {
204 Self::Create => "create",
205 Self::Reinforce => "reinforce",
206 Self::Refine => "refine",
207 Self::Supersede => "supersede",
208 Self::Coexist => "coexist",
209 Self::Stage => "stage",
210 Self::Delete => "delete",
211 Self::Discard => "discard",
212 }
213 }
214
215 pub fn is_write(self) -> bool {
217 !matches!(self, Self::Discard)
218 }
219}
220
221#[derive(Debug, Clone, PartialEq)]
223pub struct ResolvedMutation {
224 pub kind: ResolutionKind,
226 pub fingerprint: FactFingerprint,
228 pub writes: Vec<CanonicalMemory>,
230 pub deletes: Vec<MemoryId>,
232 pub discard_reason: Option<DiscardReason>,
234}
235
236impl ResolvedMutation {
237 pub fn discard(fingerprint: FactFingerprint, reason: DiscardReason) -> Self {
239 Self {
240 kind: ResolutionKind::Discard,
241 fingerprint,
242 writes: Vec::new(),
243 deletes: Vec::new(),
244 discard_reason: Some(reason),
245 }
246 }
247
248 pub fn write(
250 kind: ResolutionKind,
251 fingerprint: FactFingerprint,
252 memory: CanonicalMemory,
253 ) -> Self {
254 Self {
255 kind,
256 fingerprint,
257 writes: vec![memory],
258 deletes: Vec::new(),
259 discard_reason: None,
260 }
261 }
262
263 pub fn is_empty(&self) -> bool {
265 self.writes.is_empty() && self.deletes.is_empty()
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn memory(id: &str, statement: &str, tags: &[&str]) -> CanonicalMemory {
274 let now = Utc::now();
275 CanonicalMemory {
276 id: MemoryId::new(id),
277 owner: UserId::new("usr_1"),
278 kind: MemoryKind::Preference,
279 predicate: CanonicalPredicate::new("dietary_identity"),
280 status: MemoryStatus::Active,
281 confidence: 0.9,
282 subject: EntityRef::user(),
283 value: MemoryValue::Text(statement.into()),
284 statement: statement.into(),
285 evidence_summary: "stated".into(),
286 source: MemorySource::from_explicitness(
287 Explicitness::ExplicitStatement,
288 SessionId::new("ses_1"),
289 TurnId(1),
290 ),
291 temporal: TemporalMetadata::created_at(now),
292 retrieval: RetrievalMetadata {
293 subject: "user".into(),
294 tags: tags.iter().map(|t| (*t).to_string()).collect(),
295 ..Default::default()
296 },
297 evidence: EvidenceCounters::first(),
298 privacy: PrivacyMetadata::default(),
299 temporal_scope: TemporalScope::Persistent,
300 supersedes: Vec::new(),
301 superseded_by: None,
302 qualifier: None,
303 }
304 }
305
306 #[test]
307 fn selectors_target_what_they_name_and_nothing_else() {
308 let record = memory("mem_a", "The user is pescatarian.", &["diet"]);
309
310 assert!(MemorySelector::ById(MemoryId::new("mem_a")).matches(&record));
311 assert!(!MemorySelector::ById(MemoryId::new("mem_b")).matches(&record));
312
313 assert!(
314 MemorySelector::BySubjectPredicate("user|dietary_identity".into()).matches(&record)
315 );
316 assert!(!MemorySelector::BySubjectPredicate("user|coffee_order".into()).matches(&record));
317
318 assert!(MemorySelector::ByTopic("pescatarian".into()).matches(&record));
319 assert!(MemorySelector::ByTopic("diet".into()).matches(&record));
320 assert!(!MemorySelector::ByTopic("cycling".into()).matches(&record));
321 }
322
323 #[test]
324 fn a_topic_matches_whole_words_only() {
325 let cart = memory("mem_cart", "The user left items in the cart.", &[]);
327 assert!(!MemorySelector::ByTopic("art".into()).matches(&cart));
328 assert!(MemorySelector::ByTopic("cart".into()).matches(&cart));
329
330 let dinner = memory("mem_d", "The user enjoyed the quiet dinner in Bandra.", &[]);
332 assert!(MemorySelector::ByTopic("quiet dinner".into()).matches(&dinner));
333 assert!(!MemorySelector::ByTopic("dinner quiet".into()).matches(&dinner));
334 }
335
336 #[test]
337 fn an_empty_topic_selector_matches_nothing() {
338 let record = memory("mem_a", "The user is pescatarian.", &[]);
339 assert!(!MemorySelector::ByTopic(" ".into()).matches(&record));
340 }
341
342 #[test]
343 fn every_resolution_but_discard_writes_something() {
344 for kind in [
345 ResolutionKind::Create,
346 ResolutionKind::Reinforce,
347 ResolutionKind::Refine,
348 ResolutionKind::Supersede,
349 ResolutionKind::Coexist,
350 ResolutionKind::Stage,
351 ResolutionKind::Delete,
352 ] {
353 assert!(kind.is_write(), "{} should write", kind.label());
354 }
355 assert!(!ResolutionKind::Discard.is_write());
356 }
357}