1use crate::agent_loop::ActionResult;
6
7pub fn norm(v: &str) -> String {
19 if let Some(stripped) = v.strip_prefix("K") {
20 stripped.to_ascii_lowercase()
21 } else {
22 v.to_string()
23 }
24}
25
26pub fn norm_owned(v: String) -> String {
28 if let Some(stripped) = v.strip_prefix('K') {
29 stripped.to_ascii_lowercase()
30 } else {
31 v
32 }
33}
34
35pub fn action_result_json(value: &serde_json::Value) -> ActionResult {
39 ActionResult {
40 output: serde_json::to_string(value).unwrap_or_default(),
41 done: false,
42 }
43}
44
45pub fn action_result_from<E: std::fmt::Display>(
49 result: Result<serde_json::Value, E>,
50) -> ActionResult {
51 match result {
52 Ok(v) => action_result_json(&v),
53 Err(e) => action_result_json(&serde_json::json!({"error": e.to_string()})),
54 }
55}
56
57pub fn action_result_done(summary: &str) -> ActionResult {
59 ActionResult {
60 output: summary.to_string(),
61 done: true,
62 }
63}
64
65pub fn truncate_json_array(value: &mut serde_json::Value, key: &str, max: usize) {
76 if let Some(arr) = value.get_mut(key).and_then(|v| v.as_array_mut()) {
77 let total = arr.len();
78 if total > max {
79 arr.truncate(max);
80 arr.push(serde_json::json!(format!(
81 "... showing {} of {} total",
82 max, total
83 )));
84 }
85 }
86}
87
88pub fn load_manifesto() -> String {
93 load_manifesto_from(std::path::Path::new("."))
94}
95
96pub fn load_manifesto_from(dir: &std::path::Path) -> String {
98 for name in &["agent.md", ".director/agent.md"] {
99 let path = dir.join(name);
100 if let Ok(content) = std::fs::read_to_string(&path) {
101 return format!("Project Agent Manifesto:\n---\n{}\n---", content);
102 }
103 }
104 String::new()
105}
106
107#[derive(Debug, Default)]
138pub struct AgentContext {
139 pub parts: Vec<(String, String)>, }
142
143impl AgentContext {
144 pub fn load(home_dir: &str) -> Self {
146 let dir = std::path::Path::new(home_dir);
147 let mut ctx = Self::default();
148
149 const KNOWN_FILES: &[(&str, &str)] = &[
150 ("SOUL.md", "Soul"),
151 ("IDENTITY.md", "Identity"),
152 ("MANIFESTO.md", "Manifesto"),
153 ("RULES.md", "Rules"),
154 ("MEMORY.md", "Memory (user notes)"),
155 ];
156
157 for (filename, label) in KNOWN_FILES {
158 let path = dir.join(filename);
159 if let Ok(content) = std::fs::read_to_string(&path) {
160 if !content.trim().is_empty() {
161 ctx.parts.push((label.to_string(), content));
162 }
163 }
164 }
165
166 let jsonl_path = dir.join("MEMORY.jsonl");
168 if let Some(formatted) = format_memory_jsonl(&jsonl_path) {
169 ctx.parts.push(("Memory (learned)".to_string(), formatted));
170 }
171
172 load_rules_dir(&dir.join("context"), &mut ctx);
174
175 ctx
176 }
177
178 pub fn load_project(project_dir: &std::path::Path) -> Self {
182 let mut ctx = Self::default();
183
184 let project_files: &[(&str, &str)] = &[
186 ("AGENTS.md", "Project Instructions"),
187 ("CLAUDE.md", "Project Instructions"),
188 (".claude/CLAUDE.md", "Project Instructions"),
189 ];
190 for (filename, label) in project_files {
191 let path = project_dir.join(filename);
192 if let Ok(content) = std::fs::read_to_string(&path) {
193 if !content.trim().is_empty() {
194 let expanded = expand_imports(&content, project_dir, 0);
195 ctx.parts.push((label.to_string(), expanded));
196 break; }
198 }
199 }
200
201 let local_files: &[(&str, &str)] = &[
203 ("AGENTS.local.md", "Local Instructions"),
204 ("CLAUDE.local.md", "Local Instructions"),
205 ];
206 for (filename, label) in local_files {
207 let path = project_dir.join(filename);
208 if let Ok(content) = std::fs::read_to_string(&path) {
209 if !content.trim().is_empty() {
210 let expanded = expand_imports(&content, project_dir, 0);
211 ctx.parts.push((label.to_string(), expanded));
212 break;
213 }
214 }
215 }
216
217 let rules_dirs = [
219 project_dir.join(".agents/rules"),
220 project_dir.join(".claude/rules"),
221 ];
222 for rules_dir in &rules_dirs {
223 if rules_dir.is_dir() {
224 load_rules_dir(rules_dir, &mut ctx);
225 break; }
227 }
228
229 ctx
230 }
231
232 pub fn merge(&mut self, other: Self) {
234 self.parts.extend(other.parts);
235 }
236
237 pub fn is_empty(&self) -> bool {
239 self.parts.is_empty()
240 }
241
242 pub fn to_system_message(&self) -> Option<String> {
244 if self.parts.is_empty() {
245 return None;
246 }
247 let sections: Vec<String> = self
248 .parts
249 .iter()
250 .map(|(label, content)| format!("## {}\n{}", label, content.trim()))
251 .collect();
252 Some(sections.join("\n\n"))
253 }
254
255 pub fn to_system_message_with_budget(&self, max_tokens: usize) -> Option<String> {
264 if self.parts.is_empty() {
265 return None;
266 }
267
268 fn priority(label: &str) -> u8 {
270 match label {
271 "Soul" => 10,
272 "Memory (user notes)" => 9,
273 "Identity" => 8,
274 "Rules" => 8,
275 "Project Instructions" | "Local Instructions" => 7,
276 "Memory (learned)" => 6,
277 "Manifesto" => 5,
278 _ => 3, }
280 }
281
282 let mut indexed: Vec<(u8, &str, &str)> = self
283 .parts
284 .iter()
285 .map(|(label, content)| (priority(label), label.as_str(), content.as_str()))
286 .collect();
287 indexed.sort_by(|a, b| b.0.cmp(&a.0));
289
290 let max_chars = max_tokens * 4;
291 let mut total_chars: usize = indexed.iter().map(|(_, l, c)| l.len() + c.len() + 10).sum();
292
293 while total_chars > max_chars && !indexed.is_empty() {
295 let last = indexed.last().unwrap();
296 if last.0 >= 9 {
297 break;
298 } total_chars -= last.1.len() + last.2.len() + 10;
300 indexed.pop();
301 }
302
303 if indexed.is_empty() {
304 return None;
305 }
306
307 indexed.sort_by(|a, b| b.0.cmp(&a.0));
309 let sections: Vec<String> = indexed
310 .iter()
311 .map(|(_, label, content)| format!("## {}\n{}", label, content.trim()))
312 .collect();
313 Some(sections.join("\n\n"))
314 }
315}
316
317fn format_memory_jsonl(path: &std::path::Path) -> Option<String> {
323 let content = std::fs::read_to_string(path).ok()?;
324 let mut entries: Vec<serde_json::Value> = content
325 .lines()
326 .filter_map(|line| serde_json::from_str(line).ok())
327 .collect();
328 if entries.is_empty() {
329 return None;
330 }
331
332 let now_secs = std::time::SystemTime::now()
334 .duration_since(std::time::UNIX_EPOCH)
335 .unwrap_or_default()
336 .as_secs();
337 let seven_days = 7 * 24 * 3600;
338 let before_gc = entries.len();
339 entries.retain(|e| {
340 let confidence = e["confidence"].as_str().unwrap_or("tentative");
341 if confidence == "confirmed" {
342 return true;
343 }
344 let created = e["created"].as_u64().unwrap_or(now_secs);
345 now_secs.saturating_sub(created) < seven_days
346 });
347
348 if entries.len() < before_gc {
350 let lines: Vec<String> = entries
351 .iter()
352 .filter_map(|e| serde_json::to_string(e).ok())
353 .collect();
354 let _ = std::fs::write(path, lines.join("\n") + "\n");
355 }
356
357 if entries.is_empty() {
358 return None;
359 }
360
361 let entries = if entries.len() > 50 {
363 &entries[entries.len() - 50..]
364 } else {
365 &entries[..]
366 };
367 let mut sections: std::collections::BTreeMap<String, Vec<String>> =
368 std::collections::BTreeMap::new();
369 for entry in entries {
370 let section = entry["section"].as_str().unwrap_or("General").to_string();
371 let category = entry["category"].as_str().unwrap_or("note");
372 let confidence = entry["confidence"].as_str().unwrap_or("tentative");
373 let content = entry["content"].as_str().unwrap_or("");
374 let marker = if confidence == "confirmed" {
375 "✓"
376 } else {
377 "?"
378 };
379 sections
380 .entry(section)
381 .or_default()
382 .push(format!("- [{}|{}] {}", marker, category, content));
383 }
384
385 let mut out = String::new();
386 for (section, items) in §ions {
387 out.push_str(&format!("### {}\n", section));
388 for item in items {
389 out.push_str(item);
390 out.push('\n');
391 }
392 out.push('\n');
393 }
394 Some(out)
395}
396
397fn expand_imports(content: &str, base_dir: &std::path::Path, depth: u8) -> String {
402 if depth > 5 {
403 return content.to_string();
404 }
405
406 let mut result = String::with_capacity(content.len());
407 for line in content.lines() {
408 let trimmed = line.trim();
409 let expanded = expand_line_imports(trimmed, base_dir, depth);
411 result.push_str(&expanded);
412 result.push('\n');
413 }
414 result
415}
416
417fn expand_line_imports(line: &str, base_dir: &std::path::Path, depth: u8) -> String {
419 let mut result = String::new();
420 let mut rest = line;
421
422 while let Some(at_pos) = rest.find('@') {
423 result.push_str(&rest[..at_pos]);
424 let after_at = &rest[at_pos + 1..];
425
426 let path_end = after_at
428 .find(|c: char| c.is_whitespace() || c == ',' || c == ')' || c == ']')
429 .unwrap_or(after_at.len());
430 let ref_path = &after_at[..path_end];
431
432 if ref_path.is_empty() || ref_path.starts_with('{') {
433 result.push('@');
435 rest = after_at;
436 continue;
437 }
438
439 let resolved = if ref_path.starts_with('~') {
441 let home = std::env::var("HOME").unwrap_or_default();
442 std::path::PathBuf::from(ref_path.replacen('~', &home, 1))
443 } else {
444 base_dir.join(ref_path)
445 };
446
447 if resolved.is_file() {
448 if let Ok(file_content) = std::fs::read_to_string(&resolved) {
449 let parent = resolved.parent().unwrap_or(base_dir);
450 let expanded = expand_imports(&file_content, parent, depth + 1);
451 result.push_str(expanded.trim());
452 } else {
453 result.push('@');
454 result.push_str(ref_path);
455 }
456 } else {
457 result.push('@');
459 result.push_str(ref_path);
460 }
461
462 rest = &after_at[path_end..];
463 }
464 result.push_str(rest);
465 result
466}
467
468fn load_rules_dir(dir: &std::path::Path, ctx: &mut AgentContext) {
470 if !dir.is_dir() {
471 return;
472 }
473 if let Ok(entries) = std::fs::read_dir(dir) {
474 let mut files: Vec<_> = entries
475 .filter_map(|e| e.ok())
476 .filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
477 .collect();
478 files.sort_by_key(|e| e.file_name());
479
480 for entry in files {
481 if let Ok(content) = std::fs::read_to_string(entry.path()) {
482 if !content.trim().is_empty() {
483 let label = entry
484 .path()
485 .file_stem()
486 .and_then(|s| s.to_str())
487 .unwrap_or("rule")
488 .to_string();
489 ctx.parts.push((label, content));
490 }
491 }
492 }
493 }
494}
495
496pub fn load_context_dir(dir: &str) -> Option<String> {
500 let ctx = AgentContext::load(dir);
501 ctx.to_system_message()
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn norm_strips_k_prefix() {
510 assert_eq!(norm("Ksystem"), "system");
511 assert_eq!(norm("Kuser"), "user");
512 assert_eq!(norm("Kassistant"), "assistant");
513 assert_eq!(norm("Kdefault"), "default");
514 assert_eq!(norm("Karchive_master"), "archive_master");
515 }
516
517 #[test]
518 fn norm_preserves_clean_values() {
519 assert_eq!(norm("system"), "system");
520 assert_eq!(norm("already_clean"), "already_clean");
521 assert_eq!(norm(""), "");
522 }
523
524 #[test]
525 fn action_result_json_works() {
526 let val = serde_json::json!({"ok": true, "count": 5});
527 let ar = action_result_json(&val);
528 assert!(!ar.done);
529 assert!(ar.output.contains("\"ok\":true") || ar.output.contains("\"ok\": true"));
530 }
531
532 #[test]
533 fn action_result_from_error() {
534 let err: Result<serde_json::Value, String> = Err("something broke".into());
535 let ar = action_result_from(err);
536 assert!(!ar.done);
537 assert!(ar.output.contains("something broke"));
538 }
539
540 #[test]
541 fn action_result_done_sets_flag() {
542 let ar = action_result_done("all complete");
543 assert!(ar.done);
544 assert_eq!(ar.output, "all complete");
545 }
546
547 #[test]
548 fn truncate_json_array_works() {
549 let mut v = serde_json::json!({"items": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]});
550 truncate_json_array(&mut v, "items", 3);
551 let arr = v["items"].as_array().unwrap();
552 assert_eq!(arr.len(), 4); assert!(arr[3].as_str().unwrap().contains("12 total"));
554 }
555
556 #[test]
557 fn truncate_json_array_noop_if_small() {
558 let mut v = serde_json::json!({"items": [1, 2, 3]});
559 truncate_json_array(&mut v, "items", 10);
560 assert_eq!(v["items"].as_array().unwrap().len(), 3);
561 }
562
563 #[test]
564 fn truncate_json_array_missing_key_noop() {
565 let mut v = serde_json::json!({"other": "value"});
566 truncate_json_array(&mut v, "items", 3);
567 assert!(v.get("items").is_none());
568 }
569
570 #[test]
571 fn load_manifesto_returns_empty_when_not_found() {
572 let m = load_manifesto_from(std::path::Path::new("/nonexistent"));
573 assert!(m.is_empty());
574 }
575
576 #[test]
577 fn agent_context_loads_known_files() {
578 let dir = std::env::temp_dir().join("baml_test_agent_ctx");
579 let _ = std::fs::remove_dir_all(&dir);
580 std::fs::create_dir_all(&dir).unwrap();
581 std::fs::write(dir.join("SOUL.md"), "Be direct and honest.").unwrap();
582 std::fs::write(
583 dir.join("IDENTITY.md"),
584 "Name: rust-code\nRole: coding agent",
585 )
586 .unwrap();
587 std::fs::write(dir.join("MANIFESTO.md"), "TDD first. Ship > perfect.").unwrap();
588
589 let ctx = AgentContext::load(dir.to_str().unwrap());
590 assert_eq!(ctx.parts.len(), 3);
591 assert_eq!(ctx.parts[0].0, "Soul");
592 assert_eq!(ctx.parts[1].0, "Identity");
593 assert_eq!(ctx.parts[2].0, "Manifesto");
594
595 let msg = ctx.to_system_message().unwrap();
596 assert!(msg.contains("Be direct"));
597 assert!(msg.contains("rust-code"));
598 assert!(msg.contains("TDD first"));
599
600 let _ = std::fs::remove_dir_all(&dir);
601 }
602
603 #[test]
604 fn agent_context_loads_extras_from_context_dir() {
605 let dir = std::env::temp_dir().join("baml_test_agent_ctx_extras");
606 let _ = std::fs::remove_dir_all(&dir);
607 let ctx_dir = dir.join("context");
608 std::fs::create_dir_all(&ctx_dir).unwrap();
609 std::fs::write(dir.join("RULES.md"), "Validate at boundaries.").unwrap();
610 std::fs::write(ctx_dir.join("stacks.md"), "Rust + Tokio").unwrap();
611 std::fs::write(ctx_dir.join("ignore.txt"), "not loaded").unwrap();
612
613 let ctx = AgentContext::load(dir.to_str().unwrap());
614 assert_eq!(ctx.parts.len(), 2); assert_eq!(ctx.parts[1].0, "stacks");
616
617 let msg = ctx.to_system_message().unwrap();
618 assert!(msg.contains("Validate at boundaries"));
619 assert!(msg.contains("Rust + Tokio"));
620 assert!(!msg.contains("not loaded"));
621
622 let _ = std::fs::remove_dir_all(&dir);
623 }
624
625 #[test]
626 fn agent_context_empty_when_no_dir() {
627 let ctx = AgentContext::load("/nonexistent/path");
628 assert!(ctx.is_empty());
629 assert!(ctx.to_system_message().is_none());
630 }
631
632 #[test]
633 fn load_project_prefers_agents_md() {
634 let dir = std::env::temp_dir().join("baml_test_project_agents");
635 let _ = std::fs::remove_dir_all(&dir);
636 std::fs::create_dir_all(&dir).unwrap();
637 std::fs::write(dir.join("AGENTS.md"), "Use pnpm.").unwrap();
638 std::fs::write(dir.join("CLAUDE.md"), "Use npm.").unwrap();
639
640 let ctx = AgentContext::load_project(&dir);
641 assert_eq!(ctx.parts.len(), 1);
642 assert_eq!(ctx.parts[0].0, "Project Instructions");
643 assert!(ctx.parts[0].1.contains("pnpm")); let _ = std::fs::remove_dir_all(&dir);
646 }
647
648 #[test]
649 fn load_project_falls_back_to_claude_md() {
650 let dir = std::env::temp_dir().join("baml_test_project_claude");
651 let _ = std::fs::remove_dir_all(&dir);
652 std::fs::create_dir_all(&dir).unwrap();
653 std::fs::write(dir.join("CLAUDE.md"), "Build with cargo.").unwrap();
654
655 let ctx = AgentContext::load_project(&dir);
656 assert_eq!(ctx.parts.len(), 1);
657 assert!(ctx.parts[0].1.contains("cargo"));
658
659 let _ = std::fs::remove_dir_all(&dir);
660 }
661
662 #[test]
663 fn load_project_loads_local_and_rules() {
664 let dir = std::env::temp_dir().join("baml_test_project_full");
665 let _ = std::fs::remove_dir_all(&dir);
666 let rules_dir = dir.join(".claude/rules");
667 std::fs::create_dir_all(&rules_dir).unwrap();
668 std::fs::write(dir.join("CLAUDE.md"), "Project X").unwrap();
669 std::fs::write(dir.join("CLAUDE.local.md"), "My sandbox URL").unwrap();
670 std::fs::write(rules_dir.join("testing.md"), "Run pytest").unwrap();
671 std::fs::write(rules_dir.join("style.md"), "Use black").unwrap();
672
673 let ctx = AgentContext::load_project(&dir);
674 assert_eq!(ctx.parts.len(), 4); assert_eq!(ctx.parts[0].0, "Project Instructions");
676 assert_eq!(ctx.parts[1].0, "Local Instructions");
677 assert_eq!(ctx.parts[2].0, "style");
679 assert_eq!(ctx.parts[3].0, "testing");
680
681 let _ = std::fs::remove_dir_all(&dir);
682 }
683
684 #[test]
685 fn load_project_agents_rules_over_claude_rules() {
686 let dir = std::env::temp_dir().join("baml_test_project_agents_rules");
687 let _ = std::fs::remove_dir_all(&dir);
688 std::fs::create_dir_all(dir.join(".agents/rules")).unwrap();
689 std::fs::create_dir_all(dir.join(".claude/rules")).unwrap();
690 std::fs::write(dir.join(".agents/rules/main.md"), "Agents rule").unwrap();
691 std::fs::write(dir.join(".claude/rules/main.md"), "Claude rule").unwrap();
692
693 let ctx = AgentContext::load_project(&dir);
694 assert_eq!(ctx.parts.len(), 1);
695 assert!(ctx.parts[0].1.contains("Agents rule")); let _ = std::fs::remove_dir_all(&dir);
698 }
699
700 #[test]
701 fn memory_jsonl_loaded_into_context() {
702 let dir = std::env::temp_dir().join("baml_test_memory_jsonl");
703 let _ = std::fs::remove_dir_all(&dir);
704 std::fs::create_dir_all(&dir).unwrap();
705 std::fs::write(dir.join("SOUL.md"), "Be direct.").unwrap();
706 std::fs::write(dir.join("MEMORY.jsonl"), concat!(
707 r#"{"category":"decision","section":"Build System","content":"Use cargo, not make","context":"tested both","confidence":"confirmed","created":1772700000}"#, "\n",
708 r#"{"category":"pattern","section":"Build System","content":"Always run check before test","context":null,"confidence":"tentative","created":1772700100}"#, "\n",
709 r#"{"category":"preference","section":"Style","content":"User prefers short commits","context":"observed","confidence":"confirmed","created":1772700200}"#, "\n",
710 )).unwrap();
711
712 let ctx = AgentContext::load(dir.to_str().unwrap());
713 assert!(ctx.parts.iter().any(|(l, _)| l == "Memory (learned)"));
715 let mem = ctx
716 .parts
717 .iter()
718 .find(|(l, _)| l == "Memory (learned)")
719 .unwrap();
720 assert!(mem.1.contains("Use cargo, not make"));
721 assert!(mem.1.contains("[✓|decision]")); assert!(mem.1.contains("[?|pattern]")); assert!(mem.1.contains("### Style"));
724
725 let _ = std::fs::remove_dir_all(&dir);
726 }
727
728 #[test]
729 fn memory_jsonl_missing_is_ok() {
730 let dir = std::env::temp_dir().join("baml_test_no_jsonl");
731 let _ = std::fs::remove_dir_all(&dir);
732 std::fs::create_dir_all(&dir).unwrap();
733 std::fs::write(dir.join("SOUL.md"), "Be direct.").unwrap();
734
735 let ctx = AgentContext::load(dir.to_str().unwrap());
736 assert!(!ctx.parts.iter().any(|(l, _)| l.contains("learned")));
737
738 let _ = std::fs::remove_dir_all(&dir);
739 }
740
741 #[test]
742 fn merge_combines_contexts() {
743 let mut a = AgentContext::default();
744 a.parts.push(("Soul".into(), "Be direct.".into()));
745
746 let mut b = AgentContext::default();
747 b.parts.push(("Project".into(), "Use Rust.".into()));
748
749 a.merge(b);
750 assert_eq!(a.parts.len(), 2);
751 assert_eq!(a.parts[0].0, "Soul");
752 assert_eq!(a.parts[1].0, "Project");
753 }
754
755 #[test]
756 fn gc_removes_old_tentative_entries() {
757 let dir = std::env::temp_dir().join("baml_test_memory_gc");
758 let _ = std::fs::remove_dir_all(&dir);
759 std::fs::create_dir_all(&dir).unwrap();
760
761 let now = std::time::SystemTime::now()
762 .duration_since(std::time::UNIX_EPOCH)
763 .unwrap()
764 .as_secs();
765 let old = now - 8 * 24 * 3600; let entries = format!(
768 "{}\n{}\n{}\n",
769 serde_json::json!({"category":"decision","section":"A","content":"confirmed old","confidence":"confirmed","created":old}),
770 serde_json::json!({"category":"pattern","section":"B","content":"tentative old","confidence":"tentative","created":old}),
771 serde_json::json!({"category":"insight","section":"C","content":"tentative recent","confidence":"tentative","created":now}),
772 );
773 let path = dir.join("MEMORY.jsonl");
774 std::fs::write(&path, &entries).unwrap();
775
776 let formatted = format_memory_jsonl(&path).unwrap();
777 assert!(!formatted.contains("tentative old"));
779 assert!(formatted.contains("confirmed old"));
781 assert!(formatted.contains("tentative recent"));
783
784 let remaining = std::fs::read_to_string(&path).unwrap();
786 assert!(!remaining.contains("tentative old"));
787 assert_eq!(remaining.lines().count(), 2);
788
789 let _ = std::fs::remove_dir_all(&dir);
790 }
791
792 #[test]
793 fn import_expands_file_refs() {
794 let dir = std::env::temp_dir().join("baml_test_import");
795 let _ = std::fs::remove_dir_all(&dir);
796 std::fs::create_dir_all(&dir).unwrap();
797 std::fs::write(dir.join("README.md"), "# My Project\nThis is the readme.").unwrap();
798 std::fs::write(
799 dir.join("CLAUDE.md"),
800 "See @README.md for overview.\nDo stuff.",
801 )
802 .unwrap();
803
804 let ctx = AgentContext::load_project(&dir);
805 let msg = ctx.to_system_message().unwrap();
806 assert!(msg.contains("This is the readme")); assert!(msg.contains("Do stuff")); let _ = std::fs::remove_dir_all(&dir);
810 }
811
812 #[test]
813 fn import_nonexistent_file_kept_as_is() {
814 let dir = std::env::temp_dir().join("baml_test_import_missing");
815 let _ = std::fs::remove_dir_all(&dir);
816 std::fs::create_dir_all(&dir).unwrap();
817 std::fs::write(dir.join("CLAUDE.md"), "See @nonexistent.md for info.").unwrap();
818
819 let ctx = AgentContext::load_project(&dir);
820 let msg = ctx.to_system_message().unwrap();
821 assert!(msg.contains("@nonexistent.md")); let _ = std::fs::remove_dir_all(&dir);
824 }
825
826 #[test]
827 fn token_budget_drops_low_priority() {
828 let mut ctx = AgentContext::default();
829 ctx.parts.push(("Soul".into(), "Be direct.".into())); ctx.parts.push(("Manifesto".into(), "x".repeat(10000))); ctx.parts.push(("Identity".into(), "Name: test".into())); let msg = ctx.to_system_message_with_budget(100).unwrap(); assert!(msg.contains("Be direct")); assert!(msg.contains("Name: test")); assert!(!msg.contains("xxxxxxxxx")); }
839
840 #[test]
841 fn token_budget_never_drops_soul() {
842 let mut ctx = AgentContext::default();
843 ctx.parts.push(("Soul".into(), "x".repeat(5000)));
844
845 let msg = ctx.to_system_message_with_budget(10).unwrap();
847 assert!(msg.contains("xxxxx"));
848 }
849}