Skip to main content

sim_lib_agent/memory/core/
advanced.rs

1use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol, Value};
2use std::{
3    cmp::Ordering,
4    path::PathBuf,
5    sync::{Arc, Mutex},
6};
7
8use crate::{FILE_WRITE_CAPABILITY, PersonaMemory, VectorMemory, cosine, embed};
9
10use super::super::store::{
11    append_memory_log, flatten_expr_text, load_memory_log, rewrite_memory_log, snapshot_entries,
12    value_to_snapshot_expr,
13};
14use super::types::MemoryBackend;
15
16#[derive(Clone)]
17pub(super) struct VectorEntry {
18    pub(super) expr: Expr,
19    embedding: [f32; crate::embed::EMBED_DIM],
20}
21
22#[derive(Clone, Default)]
23pub(super) struct PersonaState {
24    profile: Vec<(Expr, Expr)>,
25    notes: Vec<Expr>,
26}
27
28fn lock_vector_entries<'a>(
29    entries: &'a Arc<Mutex<Vec<VectorEntry>>>,
30) -> Result<std::sync::MutexGuard<'a, Vec<VectorEntry>>> {
31    entries
32        .lock()
33        .map_err(|_| Error::PoisonedLock("vector memory entries"))
34}
35
36fn lock_persona_state<'a>(
37    state: &'a Arc<Mutex<PersonaState>>,
38) -> Result<std::sync::MutexGuard<'a, PersonaState>> {
39    state
40        .lock()
41        .map_err(|_| Error::PoisonedLock("persona memory state"))
42}
43
44impl VectorMemory {
45    pub(crate) fn open(path: Option<PathBuf>, codecs: Vec<Symbol>) -> Result<Self> {
46        let entries = match &path {
47            Some(path) => load_memory_log(path)?
48                .into_iter()
49                .map(VectorEntry::from_expr)
50                .collect(),
51            None => Vec::new(),
52        };
53        Ok(Self {
54            symbol: Symbol::qualified("memory", "vector"),
55            capabilities: path
56                .as_ref()
57                .map(|_| vec![CapabilityName::new(FILE_WRITE_CAPABILITY)])
58                .unwrap_or_default(),
59            address: sim_lib_server::ServerAddress::Local,
60            codecs,
61            path,
62            entries: Arc::new(Mutex::new(entries)),
63        })
64    }
65}
66
67impl PersonaMemory {
68    pub(crate) fn open(path: Option<PathBuf>, codecs: Vec<Symbol>) -> Result<Self> {
69        let state = match &path {
70            Some(path) => {
71                let entries = load_memory_log(path)?;
72                match entries.as_slice() {
73                    [] => PersonaState::default(),
74                    [snapshot] => persona_state_from_snapshot(snapshot.clone())?,
75                    _ => {
76                        return Err(Error::HostError(format!(
77                            "persona memory {} expected at most one snapshot entry",
78                            path.display()
79                        )));
80                    }
81                }
82            }
83            None => PersonaState::default(),
84        };
85        Ok(Self {
86            symbol: Symbol::qualified("memory", "persona"),
87            capabilities: path
88                .as_ref()
89                .map(|_| vec![CapabilityName::new(FILE_WRITE_CAPABILITY)])
90                .unwrap_or_default(),
91            address: sim_lib_server::ServerAddress::Local,
92            codecs,
93            path,
94            state: Arc::new(Mutex::new(state)),
95        })
96    }
97}
98
99impl VectorEntry {
100    fn from_expr(expr: Expr) -> Self {
101        let embedding = embed(&flatten_expr_text(&expr));
102        Self { expr, embedding }
103    }
104}
105
106impl PersonaState {
107    fn snapshot(&self) -> Expr {
108        Expr::List(vec![Expr::Map(vec![
109            (
110                Expr::Symbol(Symbol::new("state")),
111                Expr::Map(self.profile.clone()),
112            ),
113            (
114                Expr::Symbol(Symbol::new("notes")),
115                Expr::List(self.notes.clone()),
116            ),
117        ])])
118    }
119}
120
121fn persona_state_from_snapshot(snapshot: Expr) -> Result<PersonaState> {
122    let (Expr::List(items) | Expr::Vector(items)) = snapshot else {
123        return Err(Error::TypeMismatch {
124            expected: "persona snapshot list",
125            found: "non-list",
126        });
127    };
128    let Some(entry) = items.first() else {
129        return Ok(PersonaState::default());
130    };
131    let Expr::Map(entries) = entry else {
132        return Err(Error::TypeMismatch {
133            expected: "persona snapshot map",
134            found: "non-map",
135        });
136    };
137    let mut profile = Vec::new();
138    let mut notes = Vec::new();
139    for (key, value) in entries {
140        match key {
141            Expr::Symbol(symbol) if symbol.name.as_ref() == "state" => {
142                let Expr::Map(state_entries) = value else {
143                    return Err(Error::TypeMismatch {
144                        expected: "persona state map",
145                        found: "non-map",
146                    });
147                };
148                profile = state_entries.clone();
149            }
150            Expr::Symbol(symbol) if symbol.name.as_ref() == "notes" => {
151                notes = snapshot_entries(value.clone())?;
152            }
153            _ => {}
154        }
155    }
156    Ok(PersonaState { profile, notes })
157}
158
159impl MemoryBackend for VectorMemory {
160    fn append(&self, cx: &mut Cx, msg: Value) -> Result<()> {
161        if self.path.is_some() {
162            cx.require(&CapabilityName::new(FILE_WRITE_CAPABILITY))?;
163        }
164        let expr = value_to_snapshot_expr(cx, msg)?;
165        let entry = VectorEntry::from_expr(expr.clone());
166        lock_vector_entries(&self.entries)?.push(entry);
167        if let Some(path) = &self.path {
168            append_memory_log(path, &expr)?;
169        }
170        Ok(())
171    }
172
173    fn recent(&self, cx: &mut Cx, count: u32) -> Result<Vec<Value>> {
174        let entries = lock_vector_entries(&self.entries)?;
175        let count = usize::try_from(count).unwrap_or(usize::MAX);
176        entries[entries.len().saturating_sub(count)..]
177            .iter()
178            .map(|entry| crate::expr_to_value(cx, &entry.expr))
179            .collect()
180    }
181
182    fn search(&self, cx: &mut Cx, query: Expr, k: u32) -> Result<Vec<Value>> {
183        let query_embedding = embed(&flatten_expr_text(&query));
184        let mut scored = lock_vector_entries(&self.entries)?
185            .iter()
186            .enumerate()
187            .map(|(index, entry)| {
188                (
189                    index,
190                    cosine(&query_embedding, &entry.embedding),
191                    entry.expr.clone(),
192                )
193            })
194            .collect::<Vec<_>>();
195        scored.sort_by(|left, right| {
196            right
197                .1
198                .partial_cmp(&left.1)
199                .unwrap_or(Ordering::Equal)
200                .then(left.0.cmp(&right.0))
201        });
202        scored
203            .into_iter()
204            .take(usize::try_from(k).unwrap_or(usize::MAX))
205            .map(|(_, _, expr)| crate::expr_to_value(cx, &expr))
206            .collect()
207    }
208
209    fn snapshot(&self, _cx: &mut Cx) -> Result<Expr> {
210        Ok(Expr::List(
211            lock_vector_entries(&self.entries)?
212                .iter()
213                .map(|entry| entry.expr.clone())
214                .collect(),
215        ))
216    }
217
218    fn restore(&self, cx: &mut Cx, snap: Expr) -> Result<()> {
219        if self.path.is_some() {
220            cx.require(&CapabilityName::new(FILE_WRITE_CAPABILITY))?;
221        }
222        let entries = snapshot_entries(snap)?
223            .into_iter()
224            .map(VectorEntry::from_expr)
225            .collect::<Vec<_>>();
226        if let Some(path) = &self.path {
227            let exprs = entries
228                .iter()
229                .map(|entry| entry.expr.clone())
230                .collect::<Vec<_>>();
231            rewrite_memory_log(path, &exprs)?;
232        }
233        *lock_vector_entries(&self.entries)? = entries;
234        Ok(())
235    }
236}
237
238impl MemoryBackend for PersonaMemory {
239    fn append(&self, cx: &mut Cx, msg: Value) -> Result<()> {
240        if self.path.is_some() {
241            cx.require(&CapabilityName::new(FILE_WRITE_CAPABILITY))?;
242        }
243        let note = value_to_snapshot_expr(cx, msg)?;
244        let mut state = lock_persona_state(&self.state)?;
245        state.notes.push(note);
246        if let Some(path) = &self.path {
247            rewrite_memory_log(path, &[state.snapshot()])?;
248        }
249        Ok(())
250    }
251
252    fn recent(&self, cx: &mut Cx, count: u32) -> Result<Vec<Value>> {
253        let state = lock_persona_state(&self.state)?;
254        let count = usize::try_from(count).unwrap_or(usize::MAX);
255        state.notes[state.notes.len().saturating_sub(count)..]
256            .iter()
257            .map(|entry| crate::expr_to_value(cx, entry))
258            .collect()
259    }
260
261    fn search(&self, cx: &mut Cx, query: Expr, k: u32) -> Result<Vec<Value>> {
262        let query_terms = flatten_expr_text(&query)
263            .split_whitespace()
264            .map(str::to_owned)
265            .collect::<Vec<_>>();
266        let state = lock_persona_state(&self.state)?;
267        let filtered = state
268            .notes
269            .iter()
270            .rev()
271            .filter(|expr| {
272                let haystack = flatten_expr_text(expr);
273                query_terms.iter().all(|term| haystack.contains(term))
274            })
275            .take(usize::try_from(k).unwrap_or(usize::MAX))
276            .cloned()
277            .collect::<Vec<_>>();
278        filtered
279            .into_iter()
280            .rev()
281            .map(|expr| crate::expr_to_value(cx, &expr))
282            .collect()
283    }
284
285    fn snapshot(&self, _cx: &mut Cx) -> Result<Expr> {
286        Ok(lock_persona_state(&self.state)?.snapshot())
287    }
288
289    fn restore(&self, cx: &mut Cx, snap: Expr) -> Result<()> {
290        if self.path.is_some() {
291            cx.require(&CapabilityName::new(FILE_WRITE_CAPABILITY))?;
292        }
293        let state = persona_state_from_snapshot(snap)?;
294        if let Some(path) = &self.path {
295            rewrite_memory_log(path, &[state.snapshot()])?;
296        }
297        *lock_persona_state(&self.state)? = state;
298        Ok(())
299    }
300}