1use crate::llm::{Message, Role};
11use hippocampus::search::Memory;
12
13pub const TOKEN_BUDGETS: TokenBudget = TokenBudget {
15 system_prompt: 500,
16 user_model: 300,
17 conversation_history: 2000,
18 response_buffer: 400,
19 total_context: 8192, };
21
22pub const ONBOARDING_GREETING: &str = "Hey! I'm Brain \u{2014} your personal memory engine. \
25I run locally on your machine and I'm here to remember what matters to you. \
26I don't know anything about you yet, so let's fix that. What's your name?";
27
28pub const ONBOARDING_ADDENDUM: &str = r#"
31
32[ONBOARDING MODE — the user is new and you know very little about them]
33- After every user message, end your response with ONE short, focused follow-up question to learn about the user (name, role, projects, interests).
34- Keep responses to 1-3 sentences plus the question.
35- Sound warm, curious, and conversational — not like an intake form.
36- NEVER say "I don't have that in my memory yet" — instead, be proactive about learning.
37- Once you learn something, acknowledge it naturally and ask about the next thing."#;
38
39#[derive(Debug, Clone, Copy)]
41pub struct TokenBudget {
42 pub system_prompt: usize,
43 pub user_model: usize,
44 pub conversation_history: usize,
45 pub response_buffer: usize,
46 pub total_context: usize,
47}
48
49impl TokenBudget {
50 pub fn memory_budget(&self) -> usize {
52 self.total_context
53 .saturating_sub(self.system_prompt)
54 .saturating_sub(self.user_model)
55 .saturating_sub(self.conversation_history)
56 .saturating_sub(self.response_buffer)
57 }
58
59 pub fn for_context_size(total_tokens: usize) -> Self {
61 let mut budget = TOKEN_BUDGETS;
62 budget.total_context = total_tokens;
63 budget
64 }
65}
66
67impl Default for TokenBudget {
68 fn default() -> Self {
69 TOKEN_BUDGETS
70 }
71}
72
73#[derive(Debug, Clone, Default)]
75pub struct UserProfile {
76 pub name: Option<String>,
77 pub preferences: Vec<String>,
78 pub goals: Vec<String>,
79 pub facts: Vec<String>,
80}
81
82impl UserProfile {
83 pub fn to_context_string(&self) -> String {
85 let mut parts = Vec::new();
86
87 if let Some(name) = &self.name {
88 parts.push(format!("The user's name is {}.", name));
89 }
90
91 if !self.preferences.is_empty() {
92 parts.push(format!("User preferences: {}", self.preferences.join(", ")));
93 }
94
95 if !self.goals.is_empty() {
96 parts.push(format!("User goals: {}", self.goals.join(", ")));
97 }
98
99 if !self.facts.is_empty() {
100 parts.push(format!("Key facts: {}", self.facts.join("; ")));
101 }
102
103 parts.join(" ")
104 }
105
106 pub fn estimate_tokens(&self) -> usize {
108 self.to_context_string().len() / 4
109 }
110}
111
112pub struct ContextAssembler {
114 budget: TokenBudget,
115 system_prompt: String,
116 user_profile: UserProfile,
117}
118
119impl ContextAssembler {
120 pub fn new(budget: TokenBudget) -> Self {
122 Self {
123 budget,
124 system_prompt: Self::default_system_prompt(),
125 user_profile: UserProfile::default(),
126 }
127 }
128
129 pub fn with_defaults() -> Self {
131 Self::new(TOKEN_BUDGETS)
132 }
133
134 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
136 self.system_prompt = prompt.into();
137 self
138 }
139
140 pub fn with_user_profile(mut self, profile: UserProfile) -> Self {
142 self.user_profile = profile;
143 self
144 }
145
146 fn default_system_prompt() -> String {
148 r#"You are the SOUL of Brain OS — a biologically-inspired, proactive cognitive engine. You are not just an assistant; you are the user's digital hippocampus and prefrontal cortex, operating with deep context and long-term memory.
149
150Your Identity:
151- You are "Brain", the central intelligence of a local-first memory system.
152- You are private, secure, and run entirely on the user's machine.
153- Your purpose is to eliminate "context amnesia" by bridging the gap between siloed tools and the user's life.
154
155Your Capabilities:
156- Episodic Memory: You recall past experiences and conversations provided as context.
157- Semantic Memory: You maintain a web of facts about the user's world, projects, and habits.
158- Proactivity: You don't just react; you anticipate needs based on established patterns (provided in context).
159
160Operating Principles:
1611. TRUTH OVER HALLUCINATION: Answer based ONLY on the provided memories and general knowledge. If information is missing from memory, state: "I don't have that in my memory yet."
1622. SEAMLESS RECALL: Reference memories naturally ("You mentioned earlier...", "Based on what we discussed...").
1633. COGNITIVE CLARITY: Be concise, direct, and insightful. Avoid corporate fluff.
1644. CONTEXTUAL AWARENESS: Use the provided User Profile to tailor your tone and relevance.
1655. CURIOSITY: When you lack context about the user, ask one focused follow-up question. Learning about the user is part of your job — don't wait to be told.
166
167You are the user's partner in thought. Your goal is to make their digital life feel like a continuous, coherent stream of intelligence."#
168 .to_string()
169 }
170
171 pub fn assemble(
176 &self,
177 user_message: &str,
178 memories: &[Memory],
179 conversation_history: &[Message],
180 ) -> Vec<Message> {
181 let mut messages = Vec::new();
182 let memory_budget = self.budget.memory_budget();
183
184 let system_content = if self.user_profile.estimate_tokens() > 0 {
186 format!(
187 "{}\n\nUser Profile: {}",
188 self.system_prompt,
189 self.user_profile.to_context_string()
190 )
191 } else {
192 self.system_prompt.clone()
193 };
194 messages.push(Message {
195 role: Role::System,
196 content: system_content,
197 });
198
199 let mut current_tokens = messages[0].content.len() / 4;
201 let mut memory_context = String::new();
202
203 for memory in memories {
204 let memory_text = if let Some(ref agent) = memory.agent {
205 format!(
206 "- [{:?}, agent: {}] {}\n",
207 memory.source, agent, memory.content
208 )
209 } else {
210 format!("- [{:?}] {}\n", memory.source, memory.content)
211 };
212 let memory_tokens = memory_text.len() / 4;
213
214 if current_tokens + memory_tokens > memory_budget {
215 break;
216 }
217
218 memory_context.push_str(&memory_text);
219 current_tokens += memory_tokens;
220 }
221
222 if !memory_context.is_empty() {
223 messages.push(Message {
224 role: Role::System,
225 content: format!("Relevant memories:\n{}", memory_context),
226 });
227 }
228
229 let mut history_tokens: usize = 0;
231 let mut included_history: Vec<Message> = Vec::new();
232
233 for msg in conversation_history.iter().rev() {
235 let msg_tokens = msg.content.len() / 4;
236 if history_tokens + msg_tokens > self.budget.conversation_history {
237 break;
238 }
239 included_history.push(msg.clone());
240 history_tokens += msg_tokens;
241 }
242
243 included_history.reverse();
245 messages.extend(included_history);
246
247 messages.push(Message {
249 role: Role::User,
250 content: user_message.to_string(),
251 });
252
253 messages
254 }
255
256 pub fn estimate_tokens(messages: &[Message]) -> usize {
258 messages.iter().map(|m| m.content.len() / 4).sum()
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_token_budget_memory_allocation() {
268 let budget = TokenBudget::default();
269 let memory_budget = budget.memory_budget();
270
271 assert_eq!(memory_budget, 4992);
273 }
274
275 #[test]
276 fn test_token_budget_for_context_size() {
277 let budget = TokenBudget::for_context_size(128000);
278 assert_eq!(budget.total_context, 128000);
279 assert_eq!(budget.memory_budget(), 128000 - 500 - 300 - 2000 - 400);
280 }
281
282 #[test]
283 fn test_user_profile_to_context() {
284 let profile = UserProfile {
285 name: Some("Alice".to_string()),
286 preferences: vec!["coffee".to_string(), "quiet mornings".to_string()],
287 goals: vec!["learn Rust".to_string()],
288 facts: vec!["works remotely".to_string()],
289 };
290
291 let context = profile.to_context_string();
292 assert!(context.contains("Alice"));
293 assert!(context.contains("coffee"));
294 assert!(context.contains("learn Rust"));
295 }
296
297 #[test]
298 fn test_context_assembler_basic() {
299 use hippocampus::search::MemorySource;
300
301 let assembler = ContextAssembler::with_defaults();
302
303 let memories = vec![Memory {
304 id: "1".to_string(),
305 content: "User likes Rust programming".to_string(),
306 source: MemorySource::Semantic,
307 score: 0.9,
308 importance: 0.8,
309 timestamp: "2026-01-01".to_string(),
310 agent: None,
311 }];
312
313 let history = vec![];
314 let messages = assembler.assemble("What language should I learn?", &memories, &history);
315
316 assert!(messages.len() >= 2);
318 assert_eq!(
319 messages.last().unwrap().content,
320 "What language should I learn?"
321 );
322 assert_eq!(messages.last().unwrap().role, Role::User);
323 }
324
325 #[test]
326 fn test_context_assembler_agent_attribution() {
327 use hippocampus::search::MemorySource;
328
329 let assembler = ContextAssembler::with_defaults();
330
331 let memories = vec![
332 Memory {
333 id: "1".to_string(),
334 content: "User likes coffee".to_string(),
335 source: MemorySource::Episodic,
336 score: 0.9,
337 importance: 0.8,
338 timestamp: "2026-01-01".to_string(),
339 agent: Some("slack-bot".to_string()),
340 },
341 Memory {
342 id: "2".to_string(),
343 content: "User works remotely".to_string(),
344 source: MemorySource::Semantic,
345 score: 0.85,
346 importance: 0.7,
347 timestamp: "2026-01-02".to_string(),
348 agent: None,
349 },
350 ];
351
352 let messages = assembler.assemble("Tell me about the user", &memories, &[]);
353
354 let memory_msg = messages
355 .iter()
356 .find(|m| m.content.contains("Relevant memories"))
357 .expect("should have memory context message");
358
359 assert!(
360 memory_msg.content.contains("agent: slack-bot"),
361 "memory with agent should include attribution"
362 );
363 assert!(
364 !memory_msg.content.contains("agent: ")
365 || memory_msg.content.matches("agent: ").count() == 1,
366 "memory without agent should NOT include agent label"
367 );
368 }
369
370 #[test]
371 fn test_context_assembler_with_history() {
372 let assembler = ContextAssembler::with_defaults();
373
374 let history = vec![
375 Message {
376 role: Role::User,
377 content: "Hello".to_string(),
378 },
379 Message {
380 role: Role::Assistant,
381 content: "Hi there!".to_string(),
382 },
383 ];
384
385 let messages = assembler.assemble("How are you?", &[], &history);
386
387 assert!(messages.len() >= 3);
389 assert_eq!(messages.last().unwrap().content, "How are you?");
390 }
391
392 #[test]
393 fn test_default_prompt_core_instructions() {
394 let assembler = ContextAssembler::with_defaults();
395 let messages = assembler.assemble("How do I connect OpenClaw?", &[], &[]);
396 let system = &messages[0].content;
397
398 assert!(system.contains("Brain"));
399 assert!(system.contains("SOUL"));
400 assert!(system.contains("biologically-inspired"));
401 assert!(system.contains("Episodic Memory"));
402 assert!(system.contains("Semantic Memory"));
403 assert!(system.contains("Proactivity"));
404 assert!(system.contains("TRUTH OVER HALLUCINATION"));
405 assert!(
406 system.contains("CURIOSITY"),
407 "SOUL prompt must include CURIOSITY operating principle"
408 );
409 }
410
411 #[test]
412 fn test_onboarding_greeting_exists() {
413 assert!(
414 ONBOARDING_GREETING.contains("Brain"),
415 "greeting must mention Brain"
416 );
417 assert!(
418 ONBOARDING_GREETING.contains("name"),
419 "greeting must ask for the user's name"
420 );
421 }
422
423 #[test]
424 fn test_onboarding_addendum_exists() {
425 assert!(
426 ONBOARDING_ADDENDUM.contains("ONBOARDING MODE"),
427 "addendum must contain ONBOARDING MODE marker"
428 );
429 assert!(
430 ONBOARDING_ADDENDUM.contains("follow-up question"),
431 "addendum must instruct follow-up questions"
432 );
433 }
434
435 #[test]
436 fn test_estimate_tokens() {
437 let messages = vec![Message {
438 role: Role::User,
439 content: "Hello world".to_string(),
440 }];
441
442 let tokens = ContextAssembler::estimate_tokens(&messages);
443 assert!(tokens > 0);
444 assert_eq!(tokens, 11 / 4); }
446}