Skip to main content

oxios_kernel/persona/
store.rs

1//! In-memory store for persona registry.
2//!
3//! PersonaStore manages CRUD operations for personas in memory only.
4//! Persistence is delegated to `PersonaManager::load_from_state_store` /
5//! `PersonaManager::persist`, which serialize the full registry under
6//! `~/.oxios/state/personas/index.json` via `StateStore::save_json`
7//! (RFC-039).
8
9use anyhow::{Result, anyhow};
10use parking_lot::RwLock;
11use std::collections::HashMap;
12use std::sync::Arc;
13
14use super::Persona;
15
16/// Thread-safe in-memory persona registry.
17#[derive(Debug, Default)]
18pub struct PersonaStore {
19    personas: RwLock<HashMap<String, Persona>>,
20}
21
22impl PersonaStore {
23    /// Creates a new empty persona store.
24    pub fn new() -> Self {
25        Self {
26            personas: RwLock::new(HashMap::new()),
27        }
28    }
29
30    /// Registers a new persona, replacing any existing one with the same ID.
31    pub fn register(&self, persona: Persona) {
32        let mut personas = self.personas.write();
33        personas.insert(persona.id.clone(), persona);
34    }
35
36    /// Gets a persona by ID.
37    pub fn get(&self, id: &str) -> Option<Persona> {
38        let personas = self.personas.read();
39        personas.get(id).cloned()
40    }
41
42    /// Returns all enabled personas, sorted by ID for deterministic ordering.
43    pub fn list_enabled(&self) -> Vec<Persona> {
44        let personas = self.personas.read();
45        let mut result: Vec<Persona> = personas.values().filter(|p| p.enabled).cloned().collect();
46        result.sort_by(|a, b| a.id.cmp(&b.id));
47        result
48    }
49
50    /// Returns all personas (enabled and disabled).
51    pub fn list_all(&self) -> Vec<Persona> {
52        let personas = self.personas.read();
53        personas.values().cloned().collect()
54    }
55
56    /// Sets the enabled state of a persona.
57    pub fn set_enabled(&self, id: &str, enabled: bool) -> Result<()> {
58        let mut personas = self.personas.write();
59        match personas.get_mut(id) {
60            Some(p) => {
61                p.enabled = enabled;
62                Ok(())
63            }
64            None => Err(anyhow!("Persona '{id}' not found")),
65        }
66    }
67
68    /// Deletes a persona by ID.
69    pub fn delete(&self, id: &str) -> Result<()> {
70        let mut personas = self.personas.write();
71        if personas.remove(id).is_some() {
72            Ok(())
73        } else {
74            Err(anyhow!("Persona '{id}' not found"))
75        }
76    }
77
78    /// Updates an existing persona.
79    pub fn update(&self, id: &str, updated: Persona) -> Result<()> {
80        let mut personas = self.personas.write();
81        if personas.contains_key(id) {
82            personas.insert(id.to_string(), updated);
83            Ok(())
84        } else {
85            Err(anyhow!("Persona '{id}' not found"))
86        }
87    }
88
89    /// Returns the count of registered personas.
90    pub fn len(&self) -> usize {
91        let personas = self.personas.read();
92        personas.len()
93    }
94
95    /// Returns true if there are no registered personas.
96    pub fn is_empty(&self) -> bool {
97        let personas = self.personas.read();
98        personas.is_empty()
99    }
100
101    /// Loads personas from a serializable slice.
102    pub fn load_from_slice(&self, personas: &[Persona]) {
103        let mut store = self.personas.write();
104        for p in personas {
105            store.insert(p.id.clone(), p.clone());
106        }
107    }
108}
109
110/// A handle to the persona store for sharing across components.
111#[derive(Clone)]
112pub struct PersonaStoreHandle {
113    store: Arc<PersonaStore>,
114}
115
116impl PersonaStoreHandle {
117    /// Creates a new handle wrapping the given store.
118    pub fn from_store(store: Arc<PersonaStore>) -> Self {
119        Self { store }
120    }
121
122    /// Returns a clone of the inner store.
123    pub fn inner(&self) -> Arc<PersonaStore> {
124        Arc::clone(&self.store)
125    }
126}
127
128impl Default for PersonaStoreHandle {
129    fn default() -> Self {
130        Self {
131            store: Arc::new(PersonaStore::new()),
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_register_and_get() {
142        let store = PersonaStore::new();
143        let persona = Persona::new("Test", "assistant", "A test persona", "You are a test.");
144        store.register(persona.clone());
145        assert_eq!(store.get(&persona.id).unwrap().name, "Test");
146    }
147
148    #[test]
149    fn test_list_enabled() {
150        let store = PersonaStore::new();
151        let mut p1 = Persona::new("A", "dev", "Desc A", "Prompt A");
152        p1.enabled = true;
153        let mut p2 = Persona::new("B", "dev", "Desc B", "Prompt B");
154        p2.enabled = false;
155        store.register(p1);
156        store.register(p2);
157
158        let enabled = store.list_enabled();
159        assert_eq!(enabled.len(), 1);
160        assert_eq!(enabled[0].name, "A");
161    }
162
163    #[test]
164    fn test_set_enabled() {
165        let store = PersonaStore::new();
166        let persona = Persona::new("Test", "dev", "Desc", "Prompt");
167        store.register(persona.clone());
168        store.set_enabled(&persona.id, false).unwrap();
169        assert!(store.list_enabled().is_empty());
170
171        store.set_enabled(&persona.id, true).unwrap();
172        assert_eq!(store.list_enabled().len(), 1);
173    }
174
175    #[test]
176    fn test_delete() {
177        let store = PersonaStore::new();
178        let persona = Persona::new("Test", "dev", "Desc", "Prompt");
179        let id = persona.id.clone();
180        store.register(persona);
181        store.delete(&id).unwrap();
182        assert!(store.get(&id).is_none());
183    }
184}