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/// Canonical ordering for default personas. Ensures `list_enabled()`
17/// returns "dev" first (the primary persona) rather than alphabetical order.
18fn persona_priority(id: &str) -> usize {
19    const PRIORITY: &[&str] = &[
20        "dev",
21        "review",
22        "research",
23        "architect",
24        "mentor",
25        "ops",
26        "security",
27        "writer",
28        "planner",
29    ];
30    PRIORITY.iter().position(|&p| p == id).unwrap_or(usize::MAX)
31}
32
33/// Thread-safe in-memory persona registry.
34#[derive(Debug, Default)]
35pub struct PersonaStore {
36    personas: RwLock<HashMap<String, Persona>>,
37}
38
39impl PersonaStore {
40    /// Creates a new empty persona store.
41    pub fn new() -> Self {
42        Self {
43            personas: RwLock::new(HashMap::new()),
44        }
45    }
46
47    /// Registers a new persona, replacing any existing one with the same ID.
48    pub fn register(&self, persona: Persona) {
49        let mut personas = self.personas.write();
50        personas.insert(persona.id.clone(), persona);
51    }
52
53    /// Gets a persona by ID.
54    pub fn get(&self, id: &str) -> Option<Persona> {
55        let personas = self.personas.read();
56        personas.get(id).cloned()
57    }
58
59    /// Returns all enabled personas in canonical priority order.
60    pub fn list_enabled(&self) -> Vec<Persona> {
61        let personas = self.personas.read();
62        let mut result: Vec<Persona> = personas.values().filter(|p| p.enabled).cloned().collect();
63        result.sort_by_key(|p| persona_priority(&p.id));
64        result
65    }
66
67    /// Returns all personas (enabled and disabled).
68    pub fn list_all(&self) -> Vec<Persona> {
69        let personas = self.personas.read();
70        personas.values().cloned().collect()
71    }
72
73    /// Sets the enabled state of a persona.
74    pub fn set_enabled(&self, id: &str, enabled: bool) -> Result<()> {
75        let mut personas = self.personas.write();
76        match personas.get_mut(id) {
77            Some(p) => {
78                p.enabled = enabled;
79                Ok(())
80            }
81            None => Err(anyhow!("Persona '{id}' not found")),
82        }
83    }
84
85    /// Deletes a persona by ID.
86    pub fn delete(&self, id: &str) -> Result<()> {
87        let mut personas = self.personas.write();
88        if personas.remove(id).is_some() {
89            Ok(())
90        } else {
91            Err(anyhow!("Persona '{id}' not found"))
92        }
93    }
94
95    /// Updates an existing persona.
96    pub fn update(&self, id: &str, updated: Persona) -> Result<()> {
97        let mut personas = self.personas.write();
98        if personas.contains_key(id) {
99            personas.insert(id.to_string(), updated);
100            Ok(())
101        } else {
102            Err(anyhow!("Persona '{id}' not found"))
103        }
104    }
105
106    /// Returns the count of registered personas.
107    pub fn len(&self) -> usize {
108        let personas = self.personas.read();
109        personas.len()
110    }
111
112    /// Returns true if there are no registered personas.
113    pub fn is_empty(&self) -> bool {
114        let personas = self.personas.read();
115        personas.is_empty()
116    }
117
118    /// Loads personas from a serializable slice.
119    pub fn load_from_slice(&self, personas: &[Persona]) {
120        let mut store = self.personas.write();
121        for p in personas {
122            store.insert(p.id.clone(), p.clone());
123        }
124    }
125}
126
127/// A handle to the persona store for sharing across components.
128#[derive(Clone)]
129pub struct PersonaStoreHandle {
130    store: Arc<PersonaStore>,
131}
132
133impl PersonaStoreHandle {
134    /// Creates a new handle wrapping the given store.
135    pub fn from_store(store: Arc<PersonaStore>) -> Self {
136        Self { store }
137    }
138
139    /// Returns a clone of the inner store.
140    pub fn inner(&self) -> Arc<PersonaStore> {
141        Arc::clone(&self.store)
142    }
143}
144
145impl Default for PersonaStoreHandle {
146    fn default() -> Self {
147        Self {
148            store: Arc::new(PersonaStore::new()),
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_register_and_get() {
159        let store = PersonaStore::new();
160        let persona = Persona::new("Test", "assistant", "A test persona", "You are a test.");
161        store.register(persona.clone());
162        assert_eq!(store.get(&persona.id).unwrap().name, "Test");
163    }
164
165    #[test]
166    fn test_list_enabled() {
167        let store = PersonaStore::new();
168        let mut p1 = Persona::new("A", "dev", "Desc A", "Prompt A");
169        p1.enabled = true;
170        let mut p2 = Persona::new("B", "dev", "Desc B", "Prompt B");
171        p2.enabled = false;
172        store.register(p1);
173        store.register(p2);
174
175        let enabled = store.list_enabled();
176        assert_eq!(enabled.len(), 1);
177        assert_eq!(enabled[0].name, "A");
178    }
179
180    #[test]
181    fn test_set_enabled() {
182        let store = PersonaStore::new();
183        let persona = Persona::new("Test", "dev", "Desc", "Prompt");
184        store.register(persona.clone());
185        store.set_enabled(&persona.id, false).unwrap();
186        assert!(store.list_enabled().is_empty());
187
188        store.set_enabled(&persona.id, true).unwrap();
189        assert_eq!(store.list_enabled().len(), 1);
190    }
191
192    #[test]
193    fn test_delete() {
194        let store = PersonaStore::new();
195        let persona = Persona::new("Test", "dev", "Desc", "Prompt");
196        let id = persona.id.clone();
197        store.register(persona);
198        store.delete(&id).unwrap();
199        assert!(store.get(&id).is_none());
200    }
201}