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().chars().count() / 2
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: Ground answers in (a) the provided memories, (b) the live conversation history above this message, and (c) general knowledge. If a *fact about the user* is genuinely absent from memory AND not present in the conversation, state: "I don't have that in my memory yet." Do NOT say this when the user is asking about things discussed earlier in the current conversation — answer from the message thread itself.
1622. SEAMLESS RECALL: Reference memories and prior turns naturally ("You mentioned earlier...", "Based on what we discussed...").
1633. COGNITIVE CLARITY: Be concise, direct, and insightful. Avoid corporate fluff. Match response length to the question — simple greetings get one or two sentences, not tables.
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.
1666. FORMATTING: The user's terminal renders markdown. Use it lightly when it helps (lists for multi-item answers, **bold** for emphasis, `code` for identifiers). Skip headings and tables for short replies.
167
168You are the user's partner in thought. Your goal is to make their digital life feel like a continuous, coherent stream of intelligence."#
169 .to_string()
170 }
171
172 pub fn assemble(
177 &self,
178 user_message: &str,
179 memories: &[Memory],
180 conversation_history: &[Message],
181 ) -> Vec<Message> {
182 self.assemble_with_addendum(user_message, memories, conversation_history, None)
183 }
184
185 pub fn assemble_with_addendum(
189 &self,
190 user_message: &str,
191 memories: &[Memory],
192 conversation_history: &[Message],
193 addendum: Option<&str>,
194 ) -> Vec<Message> {
195 let mut messages = Vec::new();
196 let memory_budget = self.budget.memory_budget();
197
198 let base_prompt = match addendum {
200 Some(extra) if !extra.is_empty() => {
201 format!("{}{}", self.system_prompt, extra)
202 }
203 _ => self.system_prompt.clone(),
204 };
205 let system_content = if self.user_profile.estimate_tokens() > 0 {
206 format!(
207 "{}\n\nUser Profile: {}",
208 base_prompt,
209 self.user_profile.to_context_string()
210 )
211 } else {
212 base_prompt
213 };
214 messages.push(Message {
215 role: Role::System,
216 content: system_content,
217 });
218
219 let mut current_tokens = messages[0].content.chars().count() / 2;
221 let mut memory_context = String::new();
222
223 for memory in memories {
224 let memory_text = if let Some(ref agent) = memory.agent {
225 format!(
226 "- [{:?}, agent: {}] {}\n",
227 memory.source, agent, memory.content
228 )
229 } else {
230 format!("- [{:?}] {}\n", memory.source, memory.content)
231 };
232 let memory_tokens = memory_text.chars().count() / 2;
233
234 if current_tokens + memory_tokens > memory_budget {
235 break;
236 }
237
238 memory_context.push_str(&memory_text);
239 current_tokens += memory_tokens;
240 }
241
242 if !memory_context.is_empty() {
243 messages.push(Message {
244 role: Role::System,
245 content: format!("Relevant memories:\n{}", memory_context),
246 });
247 }
248
249 let mut history_tokens: usize = 0;
251 let mut included_history: Vec<Message> = Vec::new();
252
253 for msg in conversation_history.iter().rev() {
255 let msg_tokens = msg.content.chars().count() / 2;
256 if history_tokens + msg_tokens > self.budget.conversation_history {
257 break;
258 }
259 included_history.push(msg.clone());
260 history_tokens += msg_tokens;
261 }
262
263 included_history.reverse();
265 messages.extend(included_history);
266
267 messages.push(Message {
269 role: Role::User,
270 content: user_message.to_string(),
271 });
272
273 messages
274 }
275
276 pub fn estimate_tokens(messages: &[Message]) -> usize {
278 messages.iter().map(|m| m.content.chars().count() / 2).sum()
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_token_budget_memory_allocation() {
288 let budget = TokenBudget::default();
289 let memory_budget = budget.memory_budget();
290
291 assert_eq!(memory_budget, 4992);
293 }
294
295 #[test]
296 fn test_token_budget_for_context_size() {
297 let budget = TokenBudget::for_context_size(128000);
298 assert_eq!(budget.total_context, 128000);
299 assert_eq!(budget.memory_budget(), 128000 - 500 - 300 - 2000 - 400);
300 }
301
302 #[test]
303 fn test_user_profile_to_context() {
304 let profile = UserProfile {
305 name: Some("Alice".to_string()),
306 preferences: vec!["coffee".to_string(), "quiet mornings".to_string()],
307 goals: vec!["learn Rust".to_string()],
308 facts: vec!["works remotely".to_string()],
309 };
310
311 let context = profile.to_context_string();
312 assert!(context.contains("Alice"));
313 assert!(context.contains("coffee"));
314 assert!(context.contains("learn Rust"));
315 }
316
317 #[test]
318 fn test_assemble_with_addendum_injects_into_system_prompt() {
319 let assembler = ContextAssembler::with_defaults();
320 let messages = assembler.assemble_with_addendum("hi", &[], &[], Some(ONBOARDING_ADDENDUM));
321
322 let system = messages
323 .iter()
324 .find(|m| matches!(m.role, Role::System))
325 .expect("system message");
326 assert!(
327 system.content.contains("[ONBOARDING MODE"),
328 "onboarding addendum should be present in system prompt"
329 );
330 }
331
332 #[test]
333 fn test_assemble_without_addendum_matches_plain_assemble() {
334 let assembler = ContextAssembler::with_defaults();
335 let a = assembler.assemble("hi", &[], &[]);
336 let b = assembler.assemble_with_addendum("hi", &[], &[], None);
337 assert_eq!(a.len(), b.len());
338 assert_eq!(a[0].content, b[0].content);
339 }
340
341 #[test]
342 fn test_context_assembler_basic() {
343 use hippocampus::search::MemorySource;
344
345 let assembler = ContextAssembler::with_defaults();
346
347 let memories = vec![Memory {
348 id: "1".to_string(),
349 content: "User likes Rust programming".to_string(),
350 source: MemorySource::Semantic,
351 score: 0.9,
352 importance: 0.8,
353 timestamp: "2026-01-01".to_string(),
354 agent: None,
355 }];
356
357 let history = vec![];
358 let messages = assembler.assemble("What language should I learn?", &memories, &history);
359
360 assert!(messages.len() >= 2);
362 assert_eq!(
363 messages.last().unwrap().content,
364 "What language should I learn?"
365 );
366 assert_eq!(messages.last().unwrap().role, Role::User);
367 }
368
369 #[test]
370 fn test_context_assembler_agent_attribution() {
371 use hippocampus::search::MemorySource;
372
373 let assembler = ContextAssembler::with_defaults();
374
375 let memories = vec![
376 Memory {
377 id: "1".to_string(),
378 content: "User likes coffee".to_string(),
379 source: MemorySource::Episodic,
380 score: 0.9,
381 importance: 0.8,
382 timestamp: "2026-01-01".to_string(),
383 agent: Some("chat-bot".to_string()),
384 },
385 Memory {
386 id: "2".to_string(),
387 content: "User works remotely".to_string(),
388 source: MemorySource::Semantic,
389 score: 0.85,
390 importance: 0.7,
391 timestamp: "2026-01-02".to_string(),
392 agent: None,
393 },
394 ];
395
396 let messages = assembler.assemble("Tell me about the user", &memories, &[]);
397
398 let memory_msg = messages
399 .iter()
400 .find(|m| m.content.contains("Relevant memories"))
401 .expect("should have memory context message");
402
403 assert!(
404 memory_msg.content.contains("agent: chat-bot"),
405 "memory with agent should include attribution"
406 );
407 assert!(
408 !memory_msg.content.contains("agent: ")
409 || memory_msg.content.matches("agent: ").count() == 1,
410 "memory without agent should NOT include agent label"
411 );
412 }
413
414 #[test]
415 fn test_context_assembler_with_history() {
416 let assembler = ContextAssembler::with_defaults();
417
418 let history = vec![
419 Message {
420 role: Role::User,
421 content: "Hello".to_string(),
422 },
423 Message {
424 role: Role::Assistant,
425 content: "Hi there!".to_string(),
426 },
427 ];
428
429 let messages = assembler.assemble("How are you?", &[], &history);
430
431 assert!(messages.len() >= 3);
433 assert_eq!(messages.last().unwrap().content, "How are you?");
434 }
435
436 #[test]
437 fn test_default_prompt_core_instructions() {
438 let assembler = ContextAssembler::with_defaults();
439 let messages = assembler.assemble("How do I connect OpenClaw?", &[], &[]);
440 let system = &messages[0].content;
441
442 assert!(system.contains("Brain"));
443 assert!(system.contains("SOUL"));
444 assert!(system.contains("biologically-inspired"));
445 assert!(system.contains("Episodic Memory"));
446 assert!(system.contains("Semantic Memory"));
447 assert!(system.contains("Proactivity"));
448 assert!(system.contains("TRUTH OVER HALLUCINATION"));
449 assert!(
450 system.contains("CURIOSITY"),
451 "SOUL prompt must include CURIOSITY operating principle"
452 );
453 }
454
455 #[test]
456 fn test_onboarding_greeting_exists() {
457 assert!(
458 ONBOARDING_GREETING.contains("Brain"),
459 "greeting must mention Brain"
460 );
461 assert!(
462 ONBOARDING_GREETING.contains("name"),
463 "greeting must ask for the user's name"
464 );
465 }
466
467 #[test]
468 fn test_onboarding_addendum_exists() {
469 assert!(
470 ONBOARDING_ADDENDUM.contains("ONBOARDING MODE"),
471 "addendum must contain ONBOARDING MODE marker"
472 );
473 assert!(
474 ONBOARDING_ADDENDUM.contains("follow-up question"),
475 "addendum must instruct follow-up questions"
476 );
477 }
478
479 #[test]
480 fn test_estimate_tokens() {
481 let messages = vec![Message {
482 role: Role::User,
483 content: "Hello world".to_string(),
484 }];
485
486 let tokens = ContextAssembler::estimate_tokens(&messages);
487 assert!(tokens > 0);
488 assert_eq!(tokens, 11 / 2); }
490}