Skip to main content

oxios_kernel/persona/
mod.rs

1//! Persona system: multiple AI characters with distinct voices.
2//!
3//! Personas allow different AI "characters" to participate in conversations,
4//! each with their own system prompt, role, and personality traits.
5//! This foundation supports future multi-agent chat scenarios.
6
7pub mod manager;
8pub mod store;
9
10pub use manager::PersonaManager;
11pub use store::PersonaStore;
12
13use serde::{Deserialize, Serialize};
14
15/// A persona is an AI character with its own voice and specialization.
16/// Multiple personas can be active simultaneously (future multi-agent chat support).
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Persona {
19    /// Unique identifier.
20    pub id: String,
21    /// Display name.
22    pub name: String,
23    /// Role or archetype (developer, qa, architect, researcher...).
24    pub role: String,
25    /// Brief description of this persona.
26    pub description: String,
27    /// The persona's character definition (system prompt).
28    pub system_prompt: String,
29    /// Whether this persona is enabled for use.
30    pub enabled: bool,
31    /// Optional model override for this persona.
32    pub model: Option<String>,
33    /// Personality traits (curious, skeptical, creative...).
34    pub personality_traits: Vec<String>,
35}
36
37impl Default for Persona {
38    fn default() -> Self {
39        Self {
40            id: uuid::Uuid::new_v4().to_string(),
41            name: "Default".to_string(),
42            role: "assistant".to_string(),
43            description: "Default AI assistant persona".to_string(),
44            system_prompt: "You are a helpful AI assistant.".to_string(),
45            enabled: true,
46            model: None,
47            personality_traits: vec![],
48        }
49    }
50}
51
52impl Persona {
53    /// Creates a new persona with the given parameters.
54    pub fn new(name: &str, role: &str, description: &str, system_prompt: &str) -> Self {
55        Self {
56            id: uuid::Uuid::new_v4().to_string(),
57            name: name.to_string(),
58            role: role.to_string(),
59            description: description.to_string(),
60            system_prompt: system_prompt.to_string(),
61            enabled: true,
62            model: None,
63            personality_traits: vec![],
64        }
65    }
66
67    /// Creates a persona with the given ID (used when loading from storage).
68    pub fn with_id(
69        id: &str,
70        name: &str,
71        role: &str,
72        description: &str,
73        system_prompt: &str,
74    ) -> Self {
75        Self {
76            id: id.to_string(),
77            name: name.to_string(),
78            role: role.to_string(),
79            description: description.to_string(),
80            system_prompt: system_prompt.to_string(),
81            enabled: true,
82            model: None,
83            personality_traits: vec![],
84        }
85    }
86}
87
88/// Creates the three default personas for Oxios.
89pub fn default_personas() -> Vec<Persona> {
90    vec![
91        Persona {
92            id: "dev".to_string(),
93            name: "Dev".to_string(),
94            role: "developer".to_string(),
95            description: "Pragmatic developer focused on implementation".to_string(),
96            system_prompt: "You are Dev, a pragmatic software developer. You ship.\n\
97                \n## Philosophy\n\
98                \"Perfect is the enemy of shipped.\" You value working code over elegant theory.\n\
99                When faced with ambiguity, you choose the path that produces running output fastest.\n\
100                You can always iterate — but you can't iterate on nothing.\n\
101                \n## Approach\n\
102                1. Identify the minimum viable change\n\
103                2. Implement it with proven tools and patterns\n\
104                3. Verify it works before refining\n\
105                4. Ship, then measure — don't speculate\n\
106                \n## What You Do NOT Do\n\
107                - Architect systems when a function would do\n\
108                - Debate frameworks when the user asked for a feature\n\
109                - Write tests for code that doesn't exist yet\n\
110                - Refactor code that works without being asked\n\
111                \n## Voice\n\
112                Direct, practical, code-first. You show code, you don't describe it.\n\
113                When you're uncertain, you say so — you don't hedge."
114                .to_string(),
115            enabled: true,
116            model: None,
117            personality_traits: vec![
118                "pragmatic".to_string(),
119                "action-oriented".to_string(),
120                "practical".to_string(),
121            ],
122        },
123        Persona {
124            id: "review".to_string(),
125            name: "Review".to_string(),
126            role: "qa".to_string(),
127            description: "Quality-focused reviewer with skepticism for assumptions".to_string(),
128            system_prompt: "You are Review, a quality assurance specialist. You find what others miss.\n\
129                \n## Philosophy\n\
130                \"Assumptions are bugs waiting to happen.\" You are not cynical — you are thorough.\n\
131                Every edge case is someone's 3 AM incident. Your job is to make sure it's not yours.\n\
132                \n## Approach\n\
133                1. Read the code like an adversary — what inputs break it?\n\
134                2. Trace every error path — are errors handled or swallowed?\n\
135                3. Check boundaries — off-by-one, null, empty, overflow, race\n\
136                4. Verify intent — does it do what the author THINKS it does?\n\
137                \n## What You Do NOT Do\n\
138                - Rubber-stamp code without reading it\n\
139                - Suggest rewrites when a targeted fix would do\n\
140                - Comment on style when security issues exist\n\
141                - Say \"looks good to me\" without evidence\n\
142                \n## Voice\n\
143                Precise, evidence-based. Every finding has a file:line reference.\n\
144                Severity is honest — critical means critical, not \"I want attention.\""
145                .to_string(),
146            enabled: true,
147            model: None,
148            personality_traits: vec![
149                "skeptical".to_string(),
150                "thorough".to_string(),
151                "quality-focused".to_string(),
152            ],
153        },
154        Persona {
155            id: "research".to_string(),
156            name: "Research".to_string(),
157            role: "researcher".to_string(),
158            description: "Curious researcher focused on understanding and evidence".to_string(),
159            system_prompt: "You are Research, an investigative analyst. You go deeper.\n\
160                \n## Philosophy\n\
161                \"The first answer is rarely the best answer.\" You don't accept surface-level\n\
162                explanations. You dig for root causes, benchmarks, and evidence before concluding.\n\
163                \n## Approach\n\
164                1. Clarify the question — what are we actually trying to learn?\n\
165                2. Search broadly — the answer might be in an unexpected place\n\
166                3. Compare approaches with evidence, not opinion\n\
167                4. Present findings with confidence levels — \"proven\" vs \"likely\" vs \"speculative\"\n\
168                \n## What You Do NOT Do\n\
169                - Recommend without evidence\n\
170                - Confuse popular with correct\n\
171                - Skip \"why does this work?\" and jump to \"use this\"\n\
172                - Ignore contradictory evidence\n\
173                \n## Voice\n\
174                Analytical, measured, evidence-first. You cite your sources.\n\
175                You distinguish \"I know\" from \"I believe\" from \"I suspect.\""
176                .to_string(),
177            enabled: true,
178            model: None,
179            personality_traits: vec![
180                "curious".to_string(),
181                "analytical".to_string(),
182                "evidence-focused".to_string(),
183            ],
184        },
185    ]
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_persona_default() {
194        let p = Persona::default();
195        assert!(!p.id.is_empty());
196        assert_eq!(p.name, "Default");
197        assert_eq!(p.role, "assistant");
198        assert!(p.enabled);
199        assert!(p.model.is_none());
200        assert!(p.personality_traits.is_empty());
201    }
202
203    #[test]
204    fn test_persona_new() {
205        let p = Persona::new("Dev", "developer", "A dev", "You are a dev");
206        assert!(!p.id.is_empty());
207        assert_eq!(p.name, "Dev");
208        assert_eq!(p.role, "developer");
209        assert!(p.enabled);
210    }
211
212    #[test]
213    fn test_persona_with_id() {
214        let p = Persona::with_id("dev", "Dev", "developer", "A dev", "You are a dev");
215        assert_eq!(p.id, "dev");
216        assert_eq!(p.name, "Dev");
217    }
218
219    #[test]
220    fn test_persona_serialization_roundtrip() {
221        let mut p = Persona::new("Test", "tester", "Test persona", "Test prompt");
222        p.model = Some("anthropic/claude-sonnet-4".to_string());
223        p.personality_traits = vec!["curious".to_string(), "thorough".to_string()];
224
225        let json = serde_json::to_string(&p).unwrap();
226        let restored: Persona = serde_json::from_str(&json).unwrap();
227        assert_eq!(restored.id, p.id);
228        assert_eq!(restored.name, "Test");
229        assert_eq!(restored.model.as_deref(), Some("anthropic/claude-sonnet-4"));
230        assert_eq!(restored.personality_traits.len(), 2);
231    }
232
233    #[test]
234    fn test_default_personas_contains_three() {
235        let personas = default_personas();
236        assert_eq!(personas.len(), 3);
237
238        let ids: Vec<&str> = personas.iter().map(|p| p.id.as_str()).collect();
239        assert!(ids.contains(&"dev"));
240        assert!(ids.contains(&"review"));
241        assert!(ids.contains(&"research"));
242
243        // All should be enabled
244        for p in &personas {
245            assert!(p.enabled);
246            assert!(!p.system_prompt.is_empty());
247            assert!(!p.personality_traits.is_empty());
248        }
249    }
250
251    #[test]
252    fn test_default_personas_have_unique_roles() {
253        let personas = default_personas();
254        let roles: std::collections::HashSet<&str> =
255            personas.iter().map(|p| p.role.as_str()).collect();
256        assert_eq!(roles.len(), 3);
257    }
258
259    #[test]
260    fn test_persona_with_disabled() {
261        let mut p = Persona::new("Off", "unused", "Disabled persona", "N/A");
262        p.enabled = false;
263        assert!(!p.enabled);
264
265        let json = serde_json::to_string(&p).unwrap();
266        let restored: Persona = serde_json::from_str(&json).unwrap();
267        assert!(!restored.enabled);
268    }
269}