Skip to main content

oxios_kernel/
persona_manager.rs

1//! Persona manager: coordinates persona-aware execution.
2//!
3//! The PersonaManager manages persona lifecycle and provides
4//! the active persona for orchestrator and agent runtime.
5
6use anyhow::Result;
7use parking_lot::RwLock;
8
9use super::persona::{default_personas, Persona};
10use super::persona_store::PersonaStore;
11
12/// Manages persona lifecycle and coordinates persona-aware execution.
13#[derive(Debug)]
14pub struct PersonaManager {
15    store: PersonaStore,
16    active_persona_id: RwLock<Option<String>>,
17}
18
19impl PersonaManager {
20    /// Creates a new persona manager with default personas.
21    pub fn new() -> Self {
22        let store = PersonaStore::new();
23        let manager = Self {
24            store,
25            active_persona_id: RwLock::new(None),
26        };
27        manager.create_default_personas();
28        manager
29    }
30
31    /// Creates a new persona manager, optionally loading from existing data.
32    pub fn with_defaults(personas: Vec<Persona>) -> Self {
33        let store = PersonaStore::new();
34        store.load_from_slice(&personas);
35        let this = Self {
36            store,
37            active_persona_id: RwLock::new(None),
38        };
39        // Set the first enabled persona as active by default.
40        if let Some(first) = this.store.list_enabled().into_iter().next() {
41            *this.active_persona_id.write() = Some(first.id);
42        }
43        this
44    }
45
46    /// Returns the current active persona, if any.
47    pub fn get_active_persona(&self) -> Option<Persona> {
48        let active_id = self.active_persona_id.read().clone();
49        active_id.and_then(|id| self.store.get(&id))
50    }
51
52    /// Sets the active persona by ID.
53    pub fn set_active_persona(&self, id: &str) -> Result<()> {
54        // Verify the persona exists and is enabled.
55        let persona = self
56            .store
57            .get(id)
58            .ok_or_else(|| anyhow::anyhow!("Persona '{}' not found", id))?;
59        if !persona.enabled {
60            anyhow::bail!("Persona '{}' is disabled", id);
61        }
62        *self.active_persona_id.write() = Some(id.to_string());
63        tracing::info!(persona_id = %id, name = %persona.name, "Active persona set");
64        Ok(())
65    }
66
67    /// Returns the system prompt for the active persona.
68    /// Falls back to a default prompt if no active persona.
69    pub fn active_system_prompt(&self) -> String {
70        self.get_active_persona()
71            .map(|p| p.system_prompt.clone())
72            .unwrap_or_else(|| {
73                "You are a helpful AI assistant that follows the Ouroboros methodology: \
74                 specify before you build, evaluate before you ship."
75                    .to_string()
76            })
77    }
78
79    /// Creates the three default personas (Dev, Review, Research).
80    pub fn create_default_personas(&self) {
81        let defaults = default_personas();
82        for persona in defaults {
83            // Only register if not already present.
84            if self.store.get(&persona.id).is_none() {
85                self.store.register(persona);
86            }
87        }
88        // Set first persona as active if none is set.
89        {
90            let mut active = self.active_persona_id.write();
91            if active.is_none() {
92                *active = Some("dev".to_string());
93            }
94        }
95        tracing::info!("Default personas initialized");
96    }
97
98    /// Returns the first enabled persona, for wiring into OuroborosEngine.
99    pub fn first_enabled(&self) -> Option<Persona> {
100        self.store.list_enabled().into_iter().next()
101    }
102
103    /// Returns the persona store for direct access.
104    pub fn store(&self) -> &PersonaStore {
105        &self.store
106    }
107
108    /// Returns the ID of the active persona.
109    pub fn active_persona_id(&self) -> Option<String> {
110        self.active_persona_id.read().clone()
111    }
112}
113
114impl Default for PersonaManager {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120impl Clone for PersonaManager {
121    fn clone(&self) -> Self {
122        let personas: Vec<Persona> = self.store.list_all();
123        let store = PersonaStore::new();
124        store.load_from_slice(&personas);
125        Self {
126            store,
127            active_persona_id: RwLock::new(self.active_persona_id.read().clone()),
128        }
129    }
130}