1pub mod manager;
8pub mod store;
9
10pub use manager::PersonaManager;
11pub use store::PersonaStore;
12
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Persona {
19 pub id: String,
21 pub name: String,
23 pub role: String,
25 pub description: String,
27 pub system_prompt: String,
29 pub enabled: bool,
31 pub model: Option<String>,
33 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 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 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
88pub 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 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}