1use bevy_ecs::prelude::*;
4use leviath_core::Region;
5use serde::{Deserialize, Serialize};
6
7#[derive(Component, Debug, Clone)]
12pub struct AgentState {
13 pub agent_id: String,
15
16 pub current_stage: String,
18
19 pub iteration: usize,
21
22 pub status: AgentStatus,
24
25 pub spawned_children_ids: Vec<String>,
27
28 pub pending_wait: Option<String>,
30
31 pub accepts_messages: bool,
34}
35
36#[derive(Component, Debug, Clone)]
38pub struct ParentRef {
39 pub parent_entity: Entity,
41
42 pub parent_agent_id: String,
44
45 pub depth: usize,
47}
48
49#[derive(Component, Debug, Clone)]
51pub struct SubAgentChildren {
52 pub children: Vec<Entity>,
54
55 pub max_child_depth: usize,
57}
58
59#[derive(Component, Debug, Clone)]
69pub struct AwaitingInteraction;
70
71#[derive(Component, Debug, Clone, Copy, Default)]
85pub struct GateAutoApprove;
86
87#[derive(Component, Debug, Clone, Copy, Default)]
98pub struct InteractionAutoApprove;
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102pub enum AgentStatus {
103 Idle,
105
106 Active,
108
109 Waiting,
111
112 Complete,
114
115 Error { message: String },
117
118 Cancelled,
120}
121
122#[derive(Debug, Clone)]
124pub struct EvictionResult {
125 pub tokens_freed: usize,
127 pub needs_compaction: Vec<String>,
129}
130
131#[derive(Component, Debug, Clone, Default)]
137pub struct InferenceConfig {
138 pub temperature: Option<f32>,
140 pub max_output_tokens: Option<usize>,
142 pub extra_params: serde_json::Map<String, serde_json::Value>,
147 pub batch_tool_hint: bool,
152 pub request_timeout_secs: Option<u64>,
157}
158
159#[derive(Component, Debug, Clone)]
164pub struct ToolResultRoutingComponent {
165 pub routing: leviath_core::ToolResultRouting,
167}
168
169#[derive(Debug, Clone)]
175pub struct AssembledContext {
176 pub system_blocks: Vec<leviath_providers::SystemBlock>,
178 pub messages: Vec<leviath_providers::Message>,
180}
181
182fn cache_hint_sort_priority(hint: leviath_core::CacheHint) -> u8 {
187 use leviath_core::CacheHint;
188 match hint {
189 CacheHint::Always => 0, CacheHint::SlidingPrefix { .. } => 1, CacheHint::UntilChanged => 2, CacheHint::Never => 3, }
194}
195
196#[derive(Component, Debug, Clone)]
198pub struct ContextWindow {
199 pub regions: Vec<Region>,
201
202 pub current_tokens: usize,
204
205 pub max_tokens: usize,
207
208 pub region_scripts: std::collections::HashMap<
216 String,
217 std::sync::Arc<leviath_scripting::region_hook::RegionScript>,
218 >,
219}
220
221impl ContextWindow {
222 pub fn new(max_tokens: usize) -> Self {
224 Self {
225 regions: Vec::new(),
226 current_tokens: 0,
227 max_tokens,
228 region_scripts: std::collections::HashMap::new(),
229 }
230 }
231
232 fn custom_script_for(
235 &self,
236 region_name: &str,
237 ) -> Option<std::sync::Arc<leviath_scripting::region_hook::RegionScript>> {
238 let region = self.get_region(region_name)?;
239 let leviath_core::RegionKind::Custom { script, .. } = ®ion.kind else {
240 return None;
241 };
242 self.region_scripts.get(script).cloned()
243 }
244
245 fn on_write_outcome(
253 &self,
254 region_name: &str,
255 content: String,
256 tokens: usize,
257 kind: &leviath_core::EntryKind,
258 ) -> Option<(String, usize)> {
259 let Some(script) = self.custom_script_for(region_name) else {
260 return Some((content, tokens));
261 };
262 if !script.has_on_write() {
263 return Some((content, tokens));
264 }
265 let region = self
267 .get_region(region_name)
268 .expect("custom_script_for resolved through this region");
269 match crate::custom_region::apply_on_write(&script, region, content, tokens, kind) {
270 crate::custom_region::OnWriteOutcome::Accept(content, tokens) => {
271 Some((content, tokens))
272 }
273 crate::custom_region::OnWriteOutcome::Drop => None,
274 }
275 }
276
277 fn try_custom_overflow(&mut self, region_name: &str, incoming_tokens: usize) -> bool {
283 let Some(script) = self.custom_script_for(region_name) else {
284 return false;
285 };
286 if !script.has_on_overflow() {
287 return false;
288 }
289 let region = self
290 .get_region_mut(region_name)
291 .expect("custom_script_for resolved through this region");
292 let needed = (region.current_tokens + incoming_tokens).saturating_sub(region.max_tokens);
293 let freed = crate::custom_region::apply_overflow(&script, region, needed);
294 self.current_tokens = self.calculate_tokens();
295 freed >= needed && needed > 0
296 }
297
298 pub fn get_region(&self, name: &str) -> Option<&Region> {
300 self.regions.iter().find(|r| r.name == name)
301 }
302
303 pub fn get_region_mut(&mut self, name: &str) -> Option<&mut Region> {
305 self.regions.iter_mut().find(|r| r.name == name)
306 }
307
308 pub fn add_region(&mut self, region: Region) {
310 self.regions.push(region);
311 self.current_tokens = self.calculate_tokens();
312 }
313
314 pub fn add_to_region(
316 &mut self,
317 region_name: &str,
318 content: String,
319 tokens: usize,
320 ) -> leviath_core::Result<()> {
321 let Some((content, tokens)) =
322 self.on_write_outcome(region_name, content, tokens, &leviath_core::EntryKind::Text)
323 else {
324 return Ok(()); };
326 self.write_to_region(region_name, tokens, &mut |region, tokens| {
327 region.add_entry(content.clone(), tokens)
328 })
329 }
330
331 pub fn replace_region(&mut self, region_name: &str, content: String, tokens: usize) -> bool {
336 let Some((content, tokens)) =
339 self.on_write_outcome(region_name, content, tokens, &leviath_core::EntryKind::Text)
340 else {
341 return self.get_region(region_name).is_some();
343 };
344 if let Some(region) = self.get_region_mut(region_name) {
345 region.clear();
346 let _ = region.add_entry(content, tokens);
347 self.current_tokens = self.calculate_tokens();
348 true
349 } else {
350 false
351 }
352 }
353
354 pub fn add_typed_entry(
360 &mut self,
361 region_name: &str,
362 kind: leviath_core::EntryKind,
363 content: String,
364 tokens: usize,
365 ) -> leviath_core::Result<()> {
366 let Some((content, tokens)) = self.on_write_outcome(region_name, content, tokens, &kind)
367 else {
368 return Ok(());
369 };
370 self.write_to_region(region_name, tokens, &mut |region, tokens| {
371 region.add_typed_entry(content.clone(), tokens, kind.clone())
372 })
373 }
374
375 fn write_to_region(
380 &mut self,
381 region_name: &str,
382 tokens: usize,
383 insert: &mut dyn FnMut(&mut Region, usize) -> leviath_core::Result<()>,
384 ) -> leviath_core::Result<()> {
385 if self.get_region(region_name).is_none() {
386 return Err(leviath_core::Error::RegionNotFound(region_name.to_string()));
387 }
388 let first = {
389 let region = self.get_region_mut(region_name).expect("checked above");
390 insert(region, tokens)
391 };
392 match first {
393 Ok(()) => {
394 self.current_tokens = self.calculate_tokens();
395 Ok(())
396 }
397 Err(leviath_core::Error::TokenBudgetExceeded { .. })
398 if self.try_custom_overflow(region_name, tokens) =>
399 {
400 let region = self.get_region_mut(region_name).expect("checked above");
401 let retried = insert(region, tokens);
402 self.current_tokens = self.calculate_tokens();
403 retried
404 }
405 Err(e) => Err(e),
406 }
407 }
408
409 pub fn calculate_tokens(&self) -> usize {
411 self.regions.iter().map(|r| r.current_tokens).sum()
412 }
413
414 pub fn needs_eviction(&self, threshold: f32) -> bool {
416 let usage_ratio = self.current_tokens as f32 / self.max_tokens as f32;
417 usage_ratio >= threshold
418 }
419
420 pub fn try_evict(&mut self, target_free_tokens: usize) -> leviath_core::Result<EvictionResult> {
426 use leviath_core::RegionKind;
427
428 let initial_tokens = self.current_tokens;
429
430 let has_evictable = self.regions.iter().any(|r| {
432 matches!(
433 r.kind,
434 RegionKind::Clearable
435 | RegionKind::Temporary
436 | RegionKind::Custom {
437 persistent: false,
438 ..
439 }
440 )
441 });
442
443 if !has_evictable {
444 tracing::warn!(
445 "Context window has no Clearable or Temporary regions. \
446 This may be intentional, but usually indicates a configuration error."
447 );
448 }
449
450 for region in &mut self.regions {
452 if matches!(region.kind, RegionKind::Clearable) && !region.content.is_empty() {
453 let freed = region.current_tokens;
454 region.clear();
455 self.current_tokens -= freed;
456 tracing::debug!(
457 region = %region.name,
458 tokens_freed = freed,
459 "Cleared Clearable region (all-or-nothing)"
460 );
461
462 if self.max_tokens.saturating_sub(self.current_tokens) >= target_free_tokens {
463 return Ok(EvictionResult {
464 tokens_freed: initial_tokens - self.current_tokens,
465 needs_compaction: Vec::new(),
466 });
467 }
468 }
469 }
470
471 let mut custom_freed = 0usize;
478 for i in 0..self.regions.len() {
479 let needed = target_free_tokens
480 .saturating_sub(self.max_tokens.saturating_sub(self.current_tokens));
481 if needed == 0 {
482 break;
483 }
484 let region = &self.regions[i];
485 if !matches!(
486 region.kind,
487 RegionKind::Custom {
488 persistent: false,
489 ..
490 }
491 ) || region.content.is_empty()
492 {
493 continue;
494 }
495 let Some(script) = self.custom_script_for(®ion.name.clone()) else {
496 continue;
497 };
498 if !script.has_on_overflow() {
499 continue;
500 }
501 let freed = crate::custom_region::apply_overflow(&script, &mut self.regions[i], needed);
502 self.current_tokens = self.current_tokens.saturating_sub(freed);
503 custom_freed += freed;
504 if freed > 0 {
505 tracing::debug!(
506 region = %self.regions[i].name,
507 tokens_freed = freed,
508 "custom region's on_overflow chose its own evictions"
509 );
510 }
511 }
512 if custom_freed > 0
518 && self.max_tokens.saturating_sub(self.current_tokens) >= target_free_tokens
519 {
520 return Ok(EvictionResult {
521 tokens_freed: initial_tokens - self.current_tokens,
522 needs_compaction: Vec::new(),
523 });
524 }
525
526 loop {
531 let mut evicted_any = false;
532
533 for region in &mut self.regions {
534 if matches!(
535 region.kind,
536 RegionKind::Temporary
537 | RegionKind::Custom {
538 persistent: false,
539 ..
540 }
541 ) && let Some(entry) = region.remove_oldest()
542 {
543 let freed = entry.tokens;
544 self.current_tokens -= freed;
545 evicted_any = true;
546
547 tracing::debug!(
548 region = %region.name,
549 tokens_freed = freed,
550 "Evicted temporary region entry (oldest first)"
551 );
552
553 if self.max_tokens.saturating_sub(self.current_tokens) >= target_free_tokens {
554 return Ok(EvictionResult {
555 tokens_freed: initial_tokens - self.current_tokens,
556 needs_compaction: Vec::new(),
557 });
558 }
559 }
560 }
561
562 if !evicted_any {
563 break;
564 }
565 }
566
567 let mut needs_compaction = Vec::new();
569 if self.max_tokens.saturating_sub(self.current_tokens) < target_free_tokens {
570 for region in &self.regions {
571 if region.needs_compaction() {
572 needs_compaction.push(region.name.clone());
573 }
574 }
575 }
576
577 let pinned_tokens: usize = self
582 .regions
583 .iter()
584 .filter(|r| {
585 matches!(
586 r.kind,
587 RegionKind::Pinned
588 | RegionKind::CompactHistory { .. }
589 | RegionKind::Custom {
590 persistent: true,
591 ..
592 }
593 )
594 })
595 .map(|r| r.current_tokens)
596 .sum();
597
598 if pinned_tokens > self.max_tokens {
599 return Err(leviath_core::Error::PinnedRegionsOverBudget {
600 pinned_tokens,
601 total_budget: self.max_tokens,
602 });
603 }
604
605 Ok(EvictionResult {
606 tokens_freed: initial_tokens - self.current_tokens,
607 needs_compaction,
608 })
609 }
610
611 pub fn assemble(&self) -> AssembledContext {
619 self.assemble_with_meta(&crate::custom_region::AssembleMeta::default())
620 }
621
622 pub fn assemble_with_meta(
627 &self,
628 meta: &crate::custom_region::AssembleMeta,
629 ) -> AssembledContext {
630 use leviath_core::{CacheHint, EntryKind};
631
632 let mut system_blocks = Vec::new();
633 let mut messages: Vec<leviath_providers::Message> = Vec::new();
634
635 for region in &self.regions {
636 let is_custom = matches!(region.kind, leviath_core::RegionKind::Custom { .. });
639 if region.content.is_empty() && !is_custom {
640 continue;
641 }
642
643 match ®ion.kind {
644 leviath_core::RegionKind::Pinned => {
646 let text = region
647 .content
648 .iter()
649 .map(|e| e.content.as_str())
650 .collect::<Vec<_>>()
651 .join("\n\n");
652 system_blocks.push(leviath_providers::SystemBlock {
653 text,
654 cache_hint: CacheHint::Always,
655 });
656 }
657 leviath_core::RegionKind::CompactHistory { .. } => {
658 let text = region
659 .content
660 .iter()
661 .map(|e| e.content.as_str())
662 .collect::<Vec<_>>()
663 .join("\n\n");
664 system_blocks.push(leviath_providers::SystemBlock {
665 text,
666 cache_hint: CacheHint::Always,
667 });
668 }
669
670 leviath_core::RegionKind::SlidingWindow { .. } => {
675 let mut pending_tool_results: Vec<leviath_providers::ContentBlock> = Vec::new();
676
677 for entry in ®ion.content {
678 if !matches!(entry.kind, EntryKind::ToolResult { .. })
680 && !pending_tool_results.is_empty()
681 {
682 messages.push(leviath_providers::Message {
683 role: "user".to_string(),
684 content: leviath_providers::MessageContent::Blocks(std::mem::take(
685 &mut pending_tool_results,
686 )),
687 cache_breakpoint: false,
688 });
689 }
690
691 match &entry.kind {
692 EntryKind::UserMessage => {
693 messages.push(leviath_providers::Message {
694 role: "user".to_string(),
695 content: entry.content.clone().into(),
696 cache_breakpoint: false,
697 });
698 }
699 EntryKind::AssistantTurn { tool_calls } => {
700 if tool_calls.is_empty() {
701 messages.push(leviath_providers::Message {
702 role: "assistant".to_string(),
703 content: entry.content.clone().into(),
704 cache_breakpoint: false,
705 });
706 } else {
707 let mut blocks = Vec::new();
708 if !entry.content.is_empty() {
709 blocks.push(leviath_providers::ContentBlock::Text {
710 text: entry.content.clone(),
711 });
712 }
713 for tc in tool_calls {
714 blocks.push(leviath_providers::ContentBlock::ToolUse {
715 id: tc.id.clone(),
716 name: tc.name.clone(),
717 input: tc.arguments.clone(),
718 thought_signature: tc.thought_signature.clone(),
719 });
720 }
721 messages.push(leviath_providers::Message {
722 role: "assistant".to_string(),
723 content: leviath_providers::MessageContent::Blocks(blocks),
724 cache_breakpoint: false,
725 });
726 }
727 }
728 EntryKind::ToolResult {
729 tool_call_id,
730 is_error,
731 ..
732 } => {
733 pending_tool_results.push(
735 leviath_providers::ContentBlock::ToolResult {
736 tool_use_id: tool_call_id.clone(),
737 content: entry.content.clone(),
738 is_error: *is_error,
739 },
740 );
741 }
742 EntryKind::Text => {
743 let trimmed = entry.content.trim();
744 if let Some(rest) = trimmed.strip_prefix("Assistant: ") {
745 messages.push(leviath_providers::Message {
746 role: "assistant".to_string(),
747 content: rest.to_string().into(),
748 cache_breakpoint: false,
749 });
750 } else if let Some(rest) = trimmed.strip_prefix("User: ") {
751 messages.push(leviath_providers::Message {
752 role: "user".to_string(),
753 content: rest.to_string().into(),
754 cache_breakpoint: false,
755 });
756 } else {
757 messages.push(leviath_providers::Message {
758 role: "user".to_string(),
759 content: entry.content.clone().into(),
760 cache_breakpoint: false,
761 });
762 }
763 }
764 }
765 }
766
767 if !pending_tool_results.is_empty() {
769 messages.push(leviath_providers::Message {
770 role: "user".to_string(),
771 content: leviath_providers::MessageContent::Blocks(std::mem::take(
772 &mut pending_tool_results,
773 )),
774 cache_breakpoint: false,
775 });
776 }
777 }
778
779 leviath_core::RegionKind::Compacting { .. } => {
781 let text = region
782 .content
783 .iter()
784 .map(|e| e.content.as_str())
785 .collect::<Vec<_>>()
786 .join("\n\n");
787 system_blocks.push(leviath_providers::SystemBlock {
788 text: format!("[{}]:\n{}", region.name, text),
789 cache_hint: CacheHint::UntilChanged,
790 });
791 }
792 leviath_core::RegionKind::Temporary => {
793 let text = region
794 .content
795 .iter()
796 .map(|e| e.content.as_str())
797 .collect::<Vec<_>>()
798 .join("\n\n");
799 system_blocks.push(leviath_providers::SystemBlock {
800 text: format!("[{}]:\n{}", region.name, text),
801 cache_hint: CacheHint::Never,
802 });
803 }
804 leviath_core::RegionKind::Clearable => {
805 let text = region
806 .content
807 .iter()
808 .map(|e| e.content.as_str())
809 .collect::<Vec<_>>()
810 .join("\n\n");
811 system_blocks.push(leviath_providers::SystemBlock {
812 text: format!("[{}]:\n{}", region.name, text),
813 cache_hint: CacheHint::Never,
814 });
815 }
816
817 leviath_core::RegionKind::Custom { script, persistent } => {
822 crate::custom_region::render_custom_region(
823 region,
824 self.region_scripts.get(script),
825 *persistent,
826 meta,
827 self.current_tokens,
828 self.max_tokens,
829 &mut system_blocks,
830 &mut messages,
831 );
832 }
833
834 leviath_core::RegionKind::HashMap { .. } => {
836 let text = region
837 .content
838 .iter()
839 .map(|e| {
840 if let Some(key) = &e.key {
841 format!("### [{}]\n{}", key, e.content)
842 } else {
843 e.content.clone()
844 }
845 })
846 .collect::<Vec<_>>()
847 .join("\n\n");
848 system_blocks.push(leviath_providers::SystemBlock {
849 text: format!("[{}]:\n{}", region.name, text),
850 cache_hint: CacheHint::UntilChanged,
851 });
852 }
853 }
854 }
855
856 system_blocks.sort_by_key(|block| cache_hint_sort_priority(block.cache_hint));
863
864 let mut tool_use_ids = std::collections::HashSet::new();
869 let mut tool_result_ids = std::collections::HashSet::new();
870
871 for msg in &messages {
872 if let leviath_providers::MessageContent::Blocks(blocks) = &msg.content {
873 for block in blocks {
874 match block {
875 leviath_providers::ContentBlock::ToolUse { id, .. } => {
876 tool_use_ids.insert(id.clone());
877 }
878 leviath_providers::ContentBlock::ToolResult { tool_use_id, .. } => {
879 tool_result_ids.insert(tool_use_id.clone());
880 }
881 _ => {}
882 }
883 }
884 }
885 }
886
887 let orphaned_tool_uses: std::collections::HashSet<_> =
888 tool_use_ids.difference(&tool_result_ids).cloned().collect();
889 let orphaned_tool_results: std::collections::HashSet<_> =
890 tool_result_ids.difference(&tool_use_ids).cloned().collect();
891
892 if !orphaned_tool_uses.is_empty() || !orphaned_tool_results.is_empty() {
893 tracing::warn!(
894 orphaned_tool_uses = orphaned_tool_uses.len(),
895 orphaned_tool_results = orphaned_tool_results.len(),
896 "Stripping orphaned tool_use/tool_result blocks from assembled context"
897 );
898
899 messages = messages
900 .into_iter()
901 .filter_map(|msg| {
902 if let leviath_providers::MessageContent::Blocks(blocks) = &msg.content {
903 let filtered: Vec<_> = blocks
904 .iter()
905 .filter(|block| match block {
906 leviath_providers::ContentBlock::ToolUse { id, .. } => {
907 !orphaned_tool_uses.contains(id)
908 }
909 leviath_providers::ContentBlock::ToolResult {
910 tool_use_id, ..
911 } => !orphaned_tool_results.contains(tool_use_id),
912 _ => true,
913 })
914 .cloned()
915 .collect();
916
917 if filtered.is_empty() {
918 None
920 } else {
921 Some(leviath_providers::Message {
922 role: msg.role.clone(),
923 content: leviath_providers::MessageContent::Blocks(filtered),
924 cache_breakpoint: msg.cache_breakpoint,
925 })
926 }
927 } else {
928 Some(msg)
929 }
930 })
931 .collect();
932 }
933
934 if messages.len() >= 5 {
947 let bp_idx = messages.len() - 4;
948 messages[bp_idx].cache_breakpoint = true;
949 } else if messages.len() >= 2 {
950 messages[0].cache_breakpoint = true;
952 }
953
954 if !messages.iter().any(|m| m.role == "user") {
956 messages.push(leviath_providers::Message {
957 role: "user".to_string(),
958 content: "Begin.".into(),
959 cache_breakpoint: false,
960 });
961 }
962
963 if messages.last().map(|m| m.role.as_str()) == Some("assistant") {
970 messages.push(leviath_providers::Message {
971 role: "user".to_string(),
972 content: "Continue.".into(),
973 cache_breakpoint: false,
974 });
975 }
976
977 AssembledContext {
978 system_blocks,
979 messages,
980 }
981 }
982
983 pub fn enable_taint_tracking(&mut self) {
985 for region in &mut self.regions {
986 region.enable_taint_tracking();
987 }
988 }
989
990 pub fn add_tainted_to_region(
992 &mut self,
993 region_name: &str,
994 content: String,
995 tokens: usize,
996 taint_level: leviath_core::TaintLevel,
997 ) -> leviath_core::Result<()> {
998 let Some((content, tokens)) =
999 self.on_write_outcome(region_name, content, tokens, &leviath_core::EntryKind::Text)
1000 else {
1001 return Ok(());
1002 };
1003 self.write_to_region(region_name, tokens, &mut |region, tokens| {
1004 region.add_tainted_entry(content.clone(), tokens, taint_level)
1005 })
1006 }
1007
1008 pub fn add_typed_tainted_to_region(
1015 &mut self,
1016 region_name: &str,
1017 kind: leviath_core::EntryKind,
1018 content: String,
1019 tokens: usize,
1020 taint_level: leviath_core::TaintLevel,
1021 ) -> leviath_core::Result<()> {
1022 let Some((content, tokens)) = self.on_write_outcome(region_name, content, tokens, &kind)
1023 else {
1024 return Ok(());
1025 };
1026 self.write_to_region(region_name, tokens, &mut |region, tokens| {
1027 region.add_typed_tainted_entry(content.clone(), tokens, kind.clone(), taint_level)
1028 })
1029 }
1030
1031 pub fn overall_taint(&self) -> Option<leviath_core::TaintLevel> {
1034 let mut max_taint = None;
1035 for region in &self.regions {
1036 if let Some(level) = region.taint_level() {
1037 max_taint = Some(match max_taint {
1038 Some(current) => level.max(current),
1039 None => level,
1040 });
1041 }
1042 }
1043 max_taint
1044 }
1045
1046 pub fn taint_summary(&self) -> Vec<(String, leviath_core::TaintLevel)> {
1048 self.regions
1049 .iter()
1050 .filter_map(|r| r.taint_level().map(|t| (r.name.clone(), t)))
1051 .collect()
1052 }
1053}
1054
1055#[derive(Component, Debug, Clone)]
1060pub struct InferenceResult {
1061 pub response: String,
1063
1064 pub tool_calls: Vec<ToolCall>,
1066
1067 pub tokens_used: usize,
1069
1070 pub timestamp: i64,
1072}
1073
1074#[derive(Debug, Clone, Serialize, Deserialize)]
1076pub struct ToolCall {
1077 pub tool_id: String,
1079
1080 pub name: String,
1082
1083 pub arguments: serde_json::Value,
1085 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub thought_signature: Option<String>,
1089}
1090
1091#[derive(Debug, Clone)]
1093pub struct AgentMessage {
1094 pub agent_id: String,
1096 pub content: String,
1098 pub target_region: Option<String>,
1100 pub priority: i32,
1102}
1103
1104#[derive(Component, Debug, Clone)]
1106pub struct MessageInbox {
1107 pub messages: Vec<AgentMessage>,
1109}
1110
1111impl MessageInbox {
1112 pub fn new() -> Self {
1114 Self {
1115 messages: Vec::new(),
1116 }
1117 }
1118
1119 pub fn push(&mut self, msg: AgentMessage) {
1121 self.messages.push(msg);
1122 self.messages.sort_by_key(|m| std::cmp::Reverse(m.priority));
1124 }
1125
1126 pub fn drain_all(&mut self) -> Vec<AgentMessage> {
1128 std::mem::take(&mut self.messages)
1129 }
1130}
1131
1132impl Default for MessageInbox {
1133 fn default() -> Self {
1134 Self::new()
1135 }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use super::*;
1141 use crate::test_support::with_tracing;
1142 use leviath_core::{EvictionStrategy, Region, RegionKind};
1143
1144 #[test]
1145 fn test_context_window_creation() {
1146 let window = ContextWindow::new(10000);
1147 assert_eq!(window.max_tokens, 10000);
1148 assert_eq!(window.current_tokens, 0);
1149 }
1150
1151 #[test]
1152 fn test_needs_eviction() {
1153 let mut window = ContextWindow::new(10000);
1154 window.current_tokens = 9500;
1155 assert!(window.needs_eviction(0.9));
1156
1157 window.current_tokens = 5000;
1158 assert!(!window.needs_eviction(0.9));
1159 }
1160
1161 #[test]
1162 fn test_add_region() {
1163 let mut window = ContextWindow::new(10000);
1164 let region = Region::new("test".to_string(), RegionKind::Pinned, 1000);
1165 window.add_region(region);
1166 assert_eq!(window.regions.len(), 1);
1167 }
1168
1169 #[test]
1170 fn replace_region_overwrites_existing_and_reports_missing() {
1171 let mut window = ContextWindow::new(10000);
1172 let mut region = Region::new("plan".to_string(), RegionKind::Pinned, 6000);
1173 region.add_entry("old plan".to_string(), 3).unwrap();
1174 window.add_region(region);
1175
1176 assert!(window.replace_region("plan", "new plan".to_string(), 3));
1178 let plan = window.get_region("plan").unwrap();
1179 assert_eq!(plan.content.len(), 1);
1180 assert_eq!(plan.content[0].content, "new plan");
1181
1182 assert!(!window.replace_region("nope", "x".to_string(), 1));
1184 }
1185
1186 #[test]
1187 fn test_clearable_eviction() {
1188 let mut window = ContextWindow::new(10000);
1189 let mut region = Region::new("scratch".to_string(), RegionKind::Clearable, 5000);
1190 region
1191 .add_entry("test content 1".to_string(), 1000)
1192 .unwrap();
1193 region
1194 .add_entry("test content 2".to_string(), 1000)
1195 .unwrap();
1196 window.add_region(region);
1197
1198 assert_eq!(window.current_tokens, 2000);
1199
1200 let result = with_tracing(|| window.try_evict(1000)).unwrap();
1202 assert_eq!(result.tokens_freed, 2000);
1203 assert!(result.needs_compaction.is_empty());
1204 assert_eq!(window.current_tokens, 0);
1205 }
1206
1207 #[test]
1208 fn test_temporary_eviction_oldest_first() {
1209 let mut window = ContextWindow::new(10000);
1210 let mut region = Region::new("temp".to_string(), RegionKind::Temporary, 5000);
1211 region.add_entry("old content".to_string(), 1000).unwrap();
1212 region
1213 .add_entry("middle content".to_string(), 1000)
1214 .unwrap();
1215 region.add_entry("new content".to_string(), 1000).unwrap();
1216 window.add_region(region);
1217
1218 assert_eq!(window.current_tokens, 3000);
1219
1220 let result = with_tracing(|| window.try_evict(500)).unwrap();
1222 assert!(result.tokens_freed >= 1000); assert!(result.needs_compaction.is_empty());
1224
1225 let region = window.get_region("temp").unwrap();
1227 assert_eq!(region.content.len(), 2);
1228 assert_eq!(region.content[0].content, "middle content");
1229 }
1230
1231 fn assert_sliding_window_unreduced(initial_count: usize, after_count: usize) {
1232 assert_eq!(
1233 initial_count, after_count,
1234 "SlidingWindow should never be reduced during eviction"
1235 );
1236 }
1237
1238 #[test]
1239 fn test_sliding_window_never_reduced() {
1240 let mut window = ContextWindow::new(10000);
1241 let mut region = Region::new(
1242 "conversation".to_string(),
1243 RegionKind::SlidingWindow {
1244 max_items: 5,
1245 eviction_strategy: EvictionStrategy::PerItem,
1246 },
1247 5000,
1248 );
1249 region.add_entry("msg 1".to_string(), 1000).unwrap();
1250 region.add_entry("msg 2".to_string(), 1000).unwrap();
1251 region.add_entry("msg 3".to_string(), 1000).unwrap();
1252 window.add_region(region);
1253
1254 let initial_count = window.get_region("conversation").unwrap().content.len();
1255
1256 window.try_evict(1000).ok();
1258
1259 let after_count = window.get_region("conversation").unwrap().content.len();
1260 assert_sliding_window_unreduced(initial_count, after_count);
1261 }
1262
1263 #[test]
1264 #[should_panic(expected = "SlidingWindow should never be reduced during eviction")]
1265 fn test_sliding_window_never_reduced_panics_on_mismatch() {
1266 assert_sliding_window_unreduced(3, 2);
1267 }
1268
1269 fn assert_pinned_unevicted(initial_tokens: usize, after_tokens: usize) {
1270 assert_eq!(
1271 initial_tokens, after_tokens,
1272 "Pinned region should never be evicted"
1273 );
1274 }
1275
1276 #[test]
1277 fn test_pinned_never_touched() {
1278 let mut window = ContextWindow::new(10000);
1279 let mut region = Region::new("architecture".to_string(), RegionKind::Pinned, 3000);
1280 region
1281 .add_entry("architecture diagram".to_string(), 2000)
1282 .unwrap();
1283 window.add_region(region);
1284
1285 let initial_tokens = window.get_region("architecture").unwrap().current_tokens;
1286
1287 window.try_evict(1000).ok();
1289
1290 let after_tokens = window.get_region("architecture").unwrap().current_tokens;
1291 assert_pinned_unevicted(initial_tokens, after_tokens);
1292 }
1293
1294 #[test]
1295 #[should_panic(expected = "Pinned region should never be evicted")]
1296 fn test_pinned_never_touched_panics_on_mismatch() {
1297 assert_pinned_unevicted(2000, 1000);
1298 }
1299
1300 #[test]
1301 fn test_eviction_cascade_order() {
1302 let mut window = ContextWindow::new(10000);
1303
1304 let mut clearable = Region::new("scratch".to_string(), RegionKind::Clearable, 2000);
1306 clearable
1307 .add_entry("scratch data".to_string(), 1000)
1308 .unwrap();
1309 window.add_region(clearable);
1310
1311 let mut temporary = Region::new("temp".to_string(), RegionKind::Temporary, 3000);
1313 temporary
1314 .add_entry("temp data 1".to_string(), 1000)
1315 .unwrap();
1316 temporary
1317 .add_entry("temp data 2".to_string(), 1000)
1318 .unwrap();
1319 window.add_region(temporary);
1320
1321 assert_eq!(window.current_tokens, 3000);
1322
1323 window.try_evict(500).unwrap();
1325
1326 assert_eq!(window.get_region("scratch").unwrap().current_tokens, 0);
1328
1329 assert!(window.get_region("temp").unwrap().current_tokens > 0);
1331 }
1332
1333 #[test]
1334 fn test_message_inbox() {
1335 let mut inbox = MessageInbox::new();
1336 assert!(inbox.messages.is_empty());
1337
1338 inbox.push(AgentMessage {
1339 agent_id: "agent-1".to_string(),
1340 content: "hello".to_string(),
1341 target_region: None,
1342 priority: 0,
1343 });
1344 assert_eq!(inbox.messages.len(), 1);
1345
1346 let drained = inbox.drain_all();
1347 assert_eq!(drained.len(), 1);
1348 assert!(inbox.messages.is_empty());
1349 }
1350
1351 #[test]
1352 fn test_message_inbox_priority_ordering() {
1353 let mut inbox = MessageInbox::new();
1354
1355 inbox.push(AgentMessage {
1356 agent_id: "a".to_string(),
1357 content: "low".to_string(),
1358 target_region: None,
1359 priority: 1,
1360 });
1361 inbox.push(AgentMessage {
1362 agent_id: "a".to_string(),
1363 content: "high".to_string(),
1364 target_region: None,
1365 priority: 10,
1366 });
1367 inbox.push(AgentMessage {
1368 agent_id: "a".to_string(),
1369 content: "medium".to_string(),
1370 target_region: None,
1371 priority: 5,
1372 });
1373
1374 let msgs = inbox.drain_all();
1375 assert_eq!(msgs[0].content, "high");
1376 assert_eq!(msgs[1].content, "medium");
1377 assert_eq!(msgs[2].content, "low");
1378 }
1379
1380 #[test]
1381 fn test_eviction_result_identifies_compaction_regions() {
1382 let mut window = ContextWindow::new(1000);
1384 let mut compacting = Region::new(
1386 "impl".to_string(),
1387 RegionKind::Compacting {
1388 threshold_tokens: 500,
1389 },
1390 900,
1391 );
1392 compacting
1393 .add_entry("lots of content".to_string(), 600)
1394 .unwrap();
1395 window.add_region(compacting);
1396
1397 assert_eq!(window.current_tokens, 600);
1398
1399 let result = window.try_evict(500).unwrap();
1401 assert_eq!(result.tokens_freed, 0);
1402 assert_eq!(result.needs_compaction, vec!["impl".to_string()]);
1403 }
1404
1405 #[test]
1406 fn test_try_evict_returns_needs_compaction_when_full() {
1407 let mut window = ContextWindow::new(1200);
1408
1409 let mut compacting = Region::new(
1411 "analysis".to_string(),
1412 RegionKind::Compacting {
1413 threshold_tokens: 800,
1414 },
1415 1100,
1416 );
1417 compacting.add_entry("data 1".to_string(), 500).unwrap();
1418 compacting.add_entry("data 2".to_string(), 500).unwrap();
1419 window.add_region(compacting);
1420
1421 let result = window.try_evict(500).unwrap();
1423 assert_eq!(result.tokens_freed, 0);
1424 assert!(result.needs_compaction.contains(&"analysis".to_string()));
1425 }
1426
1427 #[test]
1428 fn test_try_evict_errors_when_pinned_regions_exceed_budget() {
1429 let mut window = ContextWindow::new(1000);
1433 let mut pinned = Region::new("architecture".to_string(), RegionKind::Pinned, 2000);
1434 pinned
1435 .add_entry("huge pinned doc".to_string(), 1500)
1436 .unwrap();
1437 window.add_region(pinned);
1438
1439 let result = window.try_evict(100);
1440 assert!(result.is_err());
1441 let err_str = result.unwrap_err().to_string();
1442 assert!(err_str.contains("Pinned regions"));
1443 }
1444
1445 #[test]
1446 fn test_clearable_eviction_continues_past_insufficient_first_region() {
1447 let mut window = ContextWindow::new(2000);
1452
1453 let mut region_a = Region::new("a".to_string(), RegionKind::Clearable, 1000);
1454 region_a.add_entry("small".to_string(), 500).unwrap();
1455 window.add_region(region_a);
1456
1457 let mut region_b = Region::new("b".to_string(), RegionKind::Clearable, 1000);
1458 region_b.add_entry("large".to_string(), 1000).unwrap();
1459 window.add_region(region_b);
1460
1461 assert_eq!(window.current_tokens, 1500);
1462
1463 let result = with_tracing(|| window.try_evict(1400)).unwrap();
1467 assert_eq!(result.tokens_freed, 1500);
1468 assert_eq!(window.current_tokens, 0);
1469 assert_eq!(window.get_region("a").unwrap().current_tokens, 0);
1470 assert_eq!(window.get_region("b").unwrap().current_tokens, 0);
1471 }
1472
1473 #[test]
1474 fn test_agent_status_cancelled() {
1475 assert_eq!(AgentStatus::Cancelled, AgentStatus::Cancelled);
1476 }
1477
1478 #[test]
1479 fn test_parent_ref_component() {
1480 let parent_ref = super::ParentRef {
1481 parent_entity: Entity::from_raw_u32(42)
1482 .expect("a small literal index is always a valid entity id"),
1483 parent_agent_id: "coder-01".to_string(),
1484 depth: 1,
1485 };
1486 assert_eq!(parent_ref.parent_agent_id, "coder-01");
1487 assert_eq!(parent_ref.depth, 1);
1488 }
1489
1490 #[test]
1491 fn test_children_component() {
1492 let children = super::SubAgentChildren {
1493 children: vec![
1494 Entity::from_raw_u32(1).expect("a small literal index is always a valid entity id"),
1495 Entity::from_raw_u32(2).expect("a small literal index is always a valid entity id"),
1496 ],
1497 max_child_depth: 3,
1498 };
1499 assert_eq!(children.children.len(), 2);
1500 assert_eq!(children.max_child_depth, 3);
1501 }
1502
1503 #[test]
1504 fn test_agent_state_with_children_fields() {
1505 let state = AgentState {
1506 agent_id: "test-01".to_string(),
1507 current_stage: "analyze".to_string(),
1508 iteration: 0,
1509 status: AgentStatus::Active,
1510 spawned_children_ids: vec!["child-01".to_string(), "child-02".to_string()],
1511 pending_wait: Some("child-01".to_string()),
1512 accepts_messages: true,
1513 };
1514 assert_eq!(state.spawned_children_ids.len(), 2);
1515 assert_eq!(state.pending_wait, Some("child-01".to_string()));
1516 }
1517
1518 #[test]
1521 fn test_context_window_get_region() {
1522 let mut window = ContextWindow::new(10000);
1523 let region = Region::new("test".to_string(), RegionKind::Pinned, 1000);
1524 window.add_region(region);
1525
1526 assert!(window.get_region("test").is_some());
1527 assert!(window.get_region("nonexistent").is_none());
1528 }
1529
1530 #[test]
1531 fn test_context_window_get_region_mut() {
1532 let mut window = ContextWindow::new(10000);
1533 let region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
1534 window.add_region(region);
1535
1536 let region = window.get_region_mut("test").unwrap();
1537 region.add_entry("new content".to_string(), 50).unwrap();
1538 assert_eq!(region.content.len(), 1);
1539
1540 assert!(window.get_region_mut("nonexistent").is_none());
1541 }
1542
1543 #[test]
1544 fn test_context_window_add_to_region_success() {
1545 let mut window = ContextWindow::new(10000);
1546 let region = Region::new("conv".to_string(), RegionKind::Temporary, 5000);
1547 window.add_region(region);
1548
1549 let result = window.add_to_region("conv", "Hello".to_string(), 10);
1550 assert!(result.is_ok());
1551 assert_eq!(window.current_tokens, 10);
1552 }
1553
1554 #[test]
1555 fn test_context_window_add_to_region_not_found() {
1556 let mut window = ContextWindow::new(10000);
1557 let result = window.add_to_region("nonexistent", "Hello".to_string(), 10);
1558 assert!(result.is_err());
1559 }
1560
1561 #[test]
1562 fn test_context_window_calculate_tokens() {
1563 let mut window = ContextWindow::new(10000);
1564 let mut r1 = Region::new("a".to_string(), RegionKind::Pinned, 5000);
1565 r1.add_entry("x".to_string(), 100).unwrap();
1566 let mut r2 = Region::new("b".to_string(), RegionKind::Temporary, 5000);
1567 r2.add_entry("y".to_string(), 200).unwrap();
1568 window.add_region(r1);
1569 window.add_region(r2);
1570
1571 assert_eq!(window.calculate_tokens(), 300);
1572 }
1573
1574 #[test]
1575 fn test_context_window_needs_eviction_boundary() {
1576 let mut window = ContextWindow::new(100);
1577 window.current_tokens = 90;
1579 assert!(window.needs_eviction(0.9));
1580
1581 window.current_tokens = 89;
1583 assert!(!window.needs_eviction(0.9));
1584 }
1585
1586 #[test]
1587 fn test_eviction_result_default_fields() {
1588 let result = EvictionResult {
1589 tokens_freed: 0,
1590 needs_compaction: Vec::new(),
1591 };
1592 assert_eq!(result.tokens_freed, 0);
1593 assert!(result.needs_compaction.is_empty());
1594 }
1595
1596 #[test]
1597 fn test_message_inbox_default() {
1598 let inbox = MessageInbox::default();
1599 assert!(inbox.messages.is_empty());
1600 }
1601
1602 #[test]
1603 fn test_message_inbox_drain_all_empties() {
1604 let mut inbox = MessageInbox::new();
1605 inbox.push(AgentMessage {
1606 agent_id: "a".to_string(),
1607 content: "msg".to_string(),
1608 target_region: None,
1609 priority: 0,
1610 });
1611 let _ = inbox.drain_all();
1612 assert!(inbox.messages.is_empty());
1613 let result = inbox.drain_all();
1615 assert!(result.is_empty());
1616 }
1617
1618 #[test]
1619 fn test_agent_message_clone() {
1620 let msg = AgentMessage {
1621 agent_id: "agent-1".to_string(),
1622 content: "hello".to_string(),
1623 target_region: Some("conv".to_string()),
1624 priority: 5,
1625 };
1626 let cloned = msg.clone();
1627 assert_eq!(cloned.agent_id, "agent-1");
1628 assert_eq!(cloned.content, "hello");
1629 assert_eq!(cloned.target_region, Some("conv".to_string()));
1630 assert_eq!(cloned.priority, 5);
1631 }
1632
1633 #[test]
1634 fn test_agent_status_serialization() {
1635 let status = AgentStatus::Active;
1636 let json = serde_json::to_string(&status).unwrap();
1637 assert!(json.contains("Active"));
1638
1639 let error_status = AgentStatus::Error {
1640 message: "boom".to_string(),
1641 };
1642 let json = serde_json::to_string(&error_status).unwrap();
1643 assert!(json.contains("boom"));
1644 }
1645
1646 #[test]
1647 fn test_tool_call_serialization() {
1648 let tc = ToolCall {
1649 tool_id: "tool-1".to_string(),
1650 name: "search".to_string(),
1651 arguments: serde_json::json!({"query": "rust"}),
1652 thought_signature: None,
1653 };
1654 let json = serde_json::to_string(&tc).unwrap();
1655 assert!(json.contains("search"));
1656 assert!(json.contains("rust"));
1657 }
1658
1659 #[test]
1660 fn test_eviction_with_only_pinned_region_frees_nothing() {
1661 let mut window = ContextWindow::new(10000);
1663 let mut pinned = Region::new("pinned".to_string(), RegionKind::Pinned, 5000);
1664 pinned
1665 .add_entry("important data".to_string(), 2000)
1666 .unwrap();
1667 window.add_region(pinned);
1668
1669 let result = with_tracing(|| window.try_evict(500)).unwrap();
1670 assert_eq!(result.tokens_freed, 0);
1671 assert!(result.needs_compaction.is_empty());
1672 }
1673
1674 #[test]
1675 fn test_inference_result_fields() {
1676 let ir = InferenceResult {
1677 response: "Hello".to_string(),
1678 tool_calls: vec![ToolCall {
1679 tool_id: "t1".to_string(),
1680 name: "search".to_string(),
1681 arguments: serde_json::json!({}),
1682 thought_signature: None,
1683 }],
1684 tokens_used: 100,
1685 timestamp: 99999,
1686 };
1687 assert_eq!(ir.response, "Hello");
1688 assert_eq!(ir.tool_calls.len(), 1);
1689 assert_eq!(ir.tokens_used, 100);
1690 }
1691
1692 #[test]
1693 fn test_sub_agent_children_clone() {
1694 let children = SubAgentChildren {
1695 children: vec![
1696 Entity::from_raw_u32(1).expect("a small literal index is always a valid entity id"),
1697 ],
1698 max_child_depth: 2,
1699 };
1700 let cloned = children.clone();
1701 assert_eq!(cloned.children.len(), 1);
1702 assert_eq!(cloned.max_child_depth, 2);
1703 }
1704
1705 #[test]
1718 fn try_evict_continues_loop_when_each_entry_removal_is_insufficient() {
1719 let mut window = ContextWindow::new(1000);
1720 let mut temp = Region::new("cache".to_string(), RegionKind::Temporary, 800);
1721 temp.add_entry("entry1".to_string(), 50).unwrap();
1722 temp.add_entry("entry2".to_string(), 50).unwrap();
1723 window.add_region(temp);
1724 window.current_tokens = 950; let result = window.try_evict(200).unwrap();
1728 assert_eq!(result.tokens_freed, 100); }
1730
1731 #[test]
1734 fn test_enable_taint_tracking_on_context_window() {
1735 let mut window = ContextWindow::new(10000);
1736 window.add_region(Region::new(
1737 "conv".to_string(),
1738 RegionKind::SlidingWindow {
1739 max_items: 10,
1740 eviction_strategy: EvictionStrategy::PerItem,
1741 },
1742 5000,
1743 ));
1744 window.add_region(Region::new(
1745 "tools".to_string(),
1746 RegionKind::Temporary,
1747 3000,
1748 ));
1749
1750 assert!(window.overall_taint().is_none());
1751 window.enable_taint_tracking();
1752 assert_eq!(
1753 window.overall_taint(),
1754 Some(leviath_core::TaintLevel::Public)
1755 );
1756 }
1757
1758 #[test]
1759 fn test_add_tainted_to_region() {
1760 let mut window = ContextWindow::new(10000);
1761 let region =
1762 Region::new("tools".to_string(), RegionKind::Temporary, 5000).with_taint_tracking();
1763 window.add_region(region);
1764
1765 window
1766 .add_tainted_to_region(
1767 "tools",
1768 "secret data".to_string(),
1769 10,
1770 leviath_core::TaintLevel::Private,
1771 )
1772 .unwrap();
1773
1774 assert_eq!(
1775 window.get_region("tools").and_then(|r| r.taint_level()),
1776 Some(leviath_core::TaintLevel::Private)
1777 );
1778 assert_eq!(
1779 window.overall_taint(),
1780 Some(leviath_core::TaintLevel::Private)
1781 );
1782 }
1783
1784 #[test]
1785 fn test_add_tainted_to_nonexistent_region() {
1786 let mut window = ContextWindow::new(10000);
1787 let result = window.add_tainted_to_region(
1788 "nope",
1789 "data".to_string(),
1790 10,
1791 leviath_core::TaintLevel::Public,
1792 );
1793 assert!(result.is_err());
1794 }
1795
1796 #[test]
1797 fn test_overall_taint_is_max_across_regions() {
1798 let mut window = ContextWindow::new(10000);
1799 let r1 = Region::new("a".to_string(), RegionKind::Temporary, 5000).with_taint_tracking();
1800 let r2 = Region::new("b".to_string(), RegionKind::Temporary, 5000).with_taint_tracking();
1801 window.add_region(r1);
1802 window.add_region(r2);
1803
1804 window
1805 .add_tainted_to_region("a", "x".to_string(), 5, leviath_core::TaintLevel::Internal)
1806 .unwrap();
1807 window
1808 .add_tainted_to_region("b", "y".to_string(), 5, leviath_core::TaintLevel::Public)
1809 .unwrap();
1810
1811 assert_eq!(
1812 window.overall_taint(),
1813 Some(leviath_core::TaintLevel::Internal)
1814 );
1815 }
1816
1817 #[test]
1818 fn test_taint_summary() {
1819 let mut window = ContextWindow::new(10000);
1820 let r1 = Region::new("conv".to_string(), RegionKind::Temporary, 5000).with_taint_tracking();
1821 let r2 =
1822 Region::new("tools".to_string(), RegionKind::Temporary, 5000).with_taint_tracking();
1823 window.add_region(r1);
1824 window.add_region(r2);
1825
1826 window
1827 .add_tainted_to_region(
1828 "conv",
1829 "x".to_string(),
1830 5,
1831 leviath_core::TaintLevel::Private,
1832 )
1833 .unwrap();
1834
1835 let summary = window.taint_summary();
1836 assert_eq!(summary.len(), 2);
1837 assert!(
1838 summary
1839 .iter()
1840 .any(|(name, level)| name == "conv" && *level == leviath_core::TaintLevel::Private)
1841 );
1842 assert!(
1843 summary
1844 .iter()
1845 .any(|(name, level)| name == "tools" && *level == leviath_core::TaintLevel::Public)
1846 );
1847 }
1848
1849 #[test]
1850 fn test_taint_recovery_through_eviction() {
1851 with_tracing(|| {});
1852 let mut window = ContextWindow::new(100);
1853 let r = Region::new("temp".to_string(), RegionKind::Temporary, 100).with_taint_tracking();
1854 window.add_region(r);
1855
1856 window
1857 .add_tainted_to_region(
1858 "temp",
1859 "private".to_string(),
1860 30,
1861 leviath_core::TaintLevel::Private,
1862 )
1863 .unwrap();
1864 window
1865 .add_tainted_to_region(
1866 "temp",
1867 "public".to_string(),
1868 30,
1869 leviath_core::TaintLevel::Public,
1870 )
1871 .unwrap();
1872
1873 assert_eq!(
1874 window.get_region("temp").and_then(|r| r.taint_level()),
1875 Some(leviath_core::TaintLevel::Private)
1876 );
1877
1878 window.current_tokens = 96; let result = window.try_evict(10).unwrap();
1881 assert!(result.tokens_freed > 0);
1882
1883 assert_eq!(
1885 window.get_region("temp").and_then(|r| r.taint_level()),
1886 Some(leviath_core::TaintLevel::Public)
1887 );
1888 }
1889
1890 #[test]
1893 fn test_assemble_appends_user_nudge_when_conversation_ends_with_assistant() {
1894 let mut window = ContextWindow::new(100_000);
1898 window.add_region(Region::new(
1899 "conversation".to_string(),
1900 RegionKind::SlidingWindow {
1901 max_items: 100,
1902 eviction_strategy: EvictionStrategy::PerItem,
1903 },
1904 50_000,
1905 ));
1906 window
1907 .add_typed_entry(
1908 "conversation",
1909 leviath_core::EntryKind::UserMessage,
1910 "do the task".to_string(),
1911 10,
1912 )
1913 .unwrap();
1914 window
1915 .add_typed_entry(
1916 "conversation",
1917 leviath_core::EntryKind::AssistantTurn { tool_calls: vec![] },
1918 "All done with stage one.".to_string(),
1919 10,
1920 )
1921 .unwrap();
1922
1923 let assembled = window.assemble();
1924 assert_eq!(
1925 assembled.messages.last().map(|m| m.role.as_str()),
1926 Some("user"),
1927 "the assembled conversation must end with a user message"
1928 );
1929 }
1930
1931 #[test]
1932 fn test_assemble_strips_orphaned_tool_use() {
1933 let mut window = ContextWindow::new(100_000);
1934 let region = Region::new(
1935 "conversation".to_string(),
1936 RegionKind::SlidingWindow {
1937 max_items: 100,
1938 eviction_strategy: EvictionStrategy::PerItem,
1939 },
1940 50_000,
1941 );
1942 window.add_region(region);
1943
1944 window
1946 .add_typed_entry(
1947 "conversation",
1948 leviath_core::EntryKind::AssistantTurn {
1949 tool_calls: vec![leviath_core::SerializedToolCall {
1950 id: "tc_orphan".to_string(),
1951 name: "read_file".to_string(),
1952 arguments: serde_json::json!({"path": "foo.rs"}),
1953 thought_signature: None,
1954 }],
1955 },
1956 "Let me read that file.".to_string(),
1957 50,
1958 )
1959 .unwrap();
1960
1961 let assembled = with_tracing(|| window.assemble());
1962
1963 for msg in &assembled.messages {
1965 if let leviath_providers::MessageContent::Blocks(blocks) = &msg.content {
1966 for block in blocks {
1967 assert!(
1968 !matches!(block, leviath_providers::ContentBlock::ToolUse { .. }),
1969 "Orphaned tool_use should have been stripped"
1970 );
1971 }
1972 }
1973 }
1974 assert!(
1976 assembled
1977 .messages
1978 .iter()
1979 .any(|m| m.role == "assistant" && m.content.as_text().contains("read that file"))
1980 );
1981 }
1982
1983 #[test]
1984 fn test_assemble_strips_orphaned_tool_result() {
1985 let mut window = ContextWindow::new(100_000);
1986 let region = Region::new(
1987 "conversation".to_string(),
1988 RegionKind::SlidingWindow {
1989 max_items: 100,
1990 eviction_strategy: EvictionStrategy::PerItem,
1991 },
1992 50_000,
1993 );
1994 window.add_region(region);
1995
1996 window
1998 .add_typed_entry(
1999 "conversation",
2000 leviath_core::EntryKind::UserMessage,
2001 "Hello".to_string(),
2002 10,
2003 )
2004 .unwrap();
2005
2006 window
2008 .add_typed_entry(
2009 "conversation",
2010 leviath_core::EntryKind::ToolResult {
2011 tool_call_id: "tc_missing".to_string(),
2012 tool_name: "read_file".to_string(),
2013 is_error: false,
2014 },
2015 "file contents here".to_string(),
2016 20,
2017 )
2018 .unwrap();
2019
2020 let assembled = with_tracing(|| window.assemble());
2021
2022 assert_eq!(assembled.messages.len(), 1);
2025 assert_eq!(assembled.messages[0].role, "user");
2026 assert_eq!(
2027 assembled.messages[0].content,
2028 leviath_providers::MessageContent::Text("Hello".to_string())
2029 );
2030 }
2031
2032 #[test]
2033 fn test_assemble_paired_tool_use_result_passes_through() {
2034 let mut window = ContextWindow::new(100_000);
2035 let region = Region::new(
2036 "conversation".to_string(),
2037 RegionKind::SlidingWindow {
2038 max_items: 100,
2039 eviction_strategy: EvictionStrategy::PerItem,
2040 },
2041 50_000,
2042 );
2043 window.add_region(region);
2044
2045 window
2047 .add_typed_entry(
2048 "conversation",
2049 leviath_core::EntryKind::UserMessage,
2050 "Fix the bug".to_string(),
2051 10,
2052 )
2053 .unwrap();
2054
2055 window
2057 .add_typed_entry(
2058 "conversation",
2059 leviath_core::EntryKind::AssistantTurn {
2060 tool_calls: vec![leviath_core::SerializedToolCall {
2061 id: "tc_1".to_string(),
2062 name: "read_file".to_string(),
2063 arguments: serde_json::json!({"path": "main.rs"}),
2064 thought_signature: None,
2065 }],
2066 },
2067 "".to_string(),
2068 10,
2069 )
2070 .unwrap();
2071
2072 window
2074 .add_typed_entry(
2075 "conversation",
2076 leviath_core::EntryKind::ToolResult {
2077 tool_call_id: "tc_1".to_string(),
2078 tool_name: "read_file".to_string(),
2079 is_error: false,
2080 },
2081 "fn main() {}".to_string(),
2082 10,
2083 )
2084 .unwrap();
2085
2086 let assembled = window.assemble();
2087
2088 let has_tool_use = assembled.messages.iter().any(|m| {
2090 if let leviath_providers::MessageContent::Blocks(blocks) = &m.content {
2091 blocks
2092 .iter()
2093 .any(|b| matches!(b, leviath_providers::ContentBlock::ToolUse { id, .. } if id == "tc_1"))
2094 } else {
2095 false
2096 }
2097 });
2098 let has_tool_result = assembled.messages.iter().any(|m| {
2099 if let leviath_providers::MessageContent::Blocks(blocks) = &m.content {
2100 blocks
2101 .iter()
2102 .any(|b| matches!(b, leviath_providers::ContentBlock::ToolResult { tool_use_id, .. } if tool_use_id == "tc_1"))
2103 } else {
2104 false
2105 }
2106 });
2107 assert!(has_tool_use, "Paired tool_use should remain");
2108 assert!(has_tool_result, "Paired tool_result should remain");
2109 }
2110
2111 #[test]
2112 fn test_assemble_removes_empty_assistant_after_stripping() {
2113 let mut window = ContextWindow::new(100_000);
2114 let region = Region::new(
2115 "conversation".to_string(),
2116 RegionKind::SlidingWindow {
2117 max_items: 100,
2118 eviction_strategy: EvictionStrategy::PerItem,
2119 },
2120 50_000,
2121 );
2122 window.add_region(region);
2123
2124 window
2126 .add_typed_entry(
2127 "conversation",
2128 leviath_core::EntryKind::UserMessage,
2129 "Do something".to_string(),
2130 10,
2131 )
2132 .unwrap();
2133
2134 window
2136 .add_typed_entry(
2137 "conversation",
2138 leviath_core::EntryKind::AssistantTurn {
2139 tool_calls: vec![leviath_core::SerializedToolCall {
2140 id: "tc_gone".to_string(),
2141 name: "bash".to_string(),
2142 arguments: serde_json::json!({"command": "ls"}),
2143 thought_signature: None,
2144 }],
2145 },
2146 "".to_string(),
2147 10,
2148 )
2149 .unwrap();
2150
2151 let assembled = with_tracing(|| window.assemble());
2152
2153 let assistant_msgs: Vec<_> = assembled
2155 .messages
2156 .iter()
2157 .filter(|m| m.role == "assistant")
2158 .collect();
2159 assert!(
2160 assistant_msgs.is_empty(),
2161 "Assistant message with only orphaned tool_use should be removed entirely"
2162 );
2163 }
2164
2165 #[test]
2166 fn test_assemble_strips_multiple_orphaned_tool_uses_in_one_message() {
2167 let mut window = ContextWindow::new(100_000);
2168 let region = Region::new(
2169 "conversation".to_string(),
2170 RegionKind::SlidingWindow {
2171 max_items: 100,
2172 eviction_strategy: EvictionStrategy::PerItem,
2173 },
2174 50_000,
2175 );
2176 window.add_region(region);
2177
2178 window
2180 .add_typed_entry(
2181 "conversation",
2182 leviath_core::EntryKind::UserMessage,
2183 "Do two things".to_string(),
2184 10,
2185 )
2186 .unwrap();
2187
2188 window
2190 .add_typed_entry(
2191 "conversation",
2192 leviath_core::EntryKind::AssistantTurn {
2193 tool_calls: vec![
2194 leviath_core::SerializedToolCall {
2195 id: "tc_orphan_1".to_string(),
2196 name: "read_file".to_string(),
2197 arguments: serde_json::json!({"path": "a.rs"}),
2198 thought_signature: None,
2199 },
2200 leviath_core::SerializedToolCall {
2201 id: "tc_orphan_2".to_string(),
2202 name: "bash".to_string(),
2203 arguments: serde_json::json!({"cmd": "ls"}),
2204 thought_signature: None,
2205 },
2206 ],
2207 },
2208 "Let me do both.".to_string(),
2209 50,
2210 )
2211 .unwrap();
2212
2213 let assembled = with_tracing(|| window.assemble());
2214
2215 for msg in &assembled.messages {
2217 if let leviath_providers::MessageContent::Blocks(blocks) = &msg.content {
2218 for block in blocks {
2219 assert!(
2220 !matches!(block, leviath_providers::ContentBlock::ToolUse { .. }),
2221 "All orphaned tool_uses should have been stripped"
2222 );
2223 }
2224 }
2225 }
2226 assert!(
2228 assembled
2229 .messages
2230 .iter()
2231 .any(|m| m.role == "assistant" && m.content.as_text().contains("do both"))
2232 );
2233 }
2234
2235 #[test]
2236 fn test_assemble_mixed_valid_and_orphaned_in_same_message() {
2237 let mut window = ContextWindow::new(100_000);
2238 let region = Region::new(
2239 "conversation".to_string(),
2240 RegionKind::SlidingWindow {
2241 max_items: 100,
2242 eviction_strategy: EvictionStrategy::PerItem,
2243 },
2244 50_000,
2245 );
2246 window.add_region(region);
2247
2248 window
2250 .add_typed_entry(
2251 "conversation",
2252 leviath_core::EntryKind::UserMessage,
2253 "Do stuff".to_string(),
2254 10,
2255 )
2256 .unwrap();
2257
2258 window
2260 .add_typed_entry(
2261 "conversation",
2262 leviath_core::EntryKind::AssistantTurn {
2263 tool_calls: vec![
2264 leviath_core::SerializedToolCall {
2265 id: "tc_valid".to_string(),
2266 name: "read_file".to_string(),
2267 arguments: serde_json::json!({"path": "main.rs"}),
2268 thought_signature: None,
2269 },
2270 leviath_core::SerializedToolCall {
2271 id: "tc_orphan".to_string(),
2272 name: "bash".to_string(),
2273 arguments: serde_json::json!({"cmd": "ls"}),
2274 thought_signature: None,
2275 },
2276 ],
2277 },
2278 "".to_string(),
2279 10,
2280 )
2281 .unwrap();
2282
2283 window
2285 .add_typed_entry(
2286 "conversation",
2287 leviath_core::EntryKind::ToolResult {
2288 tool_call_id: "tc_valid".to_string(),
2289 tool_name: "read_file".to_string(),
2290 is_error: false,
2291 },
2292 "fn main() {}".to_string(),
2293 10,
2294 )
2295 .unwrap();
2296
2297 let assembled = with_tracing(|| window.assemble());
2298
2299 let tool_use_ids: Vec<&str> = assembled
2301 .messages
2302 .iter()
2303 .filter_map(|m| match &m.content {
2304 leviath_providers::MessageContent::Blocks(blocks) => Some(blocks),
2305 _ => None,
2306 })
2307 .flatten()
2308 .filter_map(|b| match b {
2309 leviath_providers::ContentBlock::ToolUse { id, .. } => Some(id.as_str()),
2310 _ => None,
2311 })
2312 .collect();
2313 assert!(
2315 tool_use_ids.contains(&"tc_valid"),
2316 "Valid tool_use should remain"
2317 );
2318 assert!(
2319 !tool_use_ids.contains(&"tc_orphan"),
2320 "Orphaned tool_use should be stripped"
2321 );
2322
2323 let has_result = assembled.messages.iter().any(|m| {
2325 if let leviath_providers::MessageContent::Blocks(blocks) = &m.content {
2326 blocks.iter().any(|b| {
2327 matches!(b, leviath_providers::ContentBlock::ToolResult { tool_use_id, .. } if tool_use_id == "tc_valid")
2328 })
2329 } else {
2330 false
2331 }
2332 });
2333 assert!(has_result, "Valid tool_result should remain");
2334 }
2335
2336 #[test]
2339 fn test_assemble_compact_history_region_produces_system_block_always() {
2340 let mut window = ContextWindow::new(100_000);
2341 let mut region = Region::new(
2342 "history".to_string(),
2343 RegionKind::CompactHistory {
2344 source_region: "conv".to_string(),
2345 },
2346 10_000,
2347 );
2348 region
2349 .add_entry("summary of earlier conversation".to_string(), 50)
2350 .unwrap();
2351 window.add_region(region);
2352
2353 let assembled = window.assemble();
2354
2355 assert_eq!(assembled.system_blocks.len(), 1);
2356 assert_eq!(
2357 assembled.system_blocks[0].text,
2358 "summary of earlier conversation"
2359 );
2360 assert_eq!(
2361 assembled.system_blocks[0].cache_hint,
2362 leviath_core::CacheHint::Always
2363 );
2364 }
2365
2366 #[test]
2367 fn test_assemble_compacting_region_produces_system_block_until_changed() {
2368 let mut window = ContextWindow::new(100_000);
2369 let mut region = Region::new(
2370 "impl".to_string(),
2371 RegionKind::Compacting {
2372 threshold_tokens: 500,
2373 },
2374 10_000,
2375 );
2376 region
2377 .add_entry("implementation details".to_string(), 50)
2378 .unwrap();
2379 window.add_region(region);
2380
2381 let assembled = window.assemble();
2382
2383 assert_eq!(assembled.system_blocks.len(), 1);
2384 assert_eq!(
2385 assembled.system_blocks[0].text,
2386 "[impl]:\nimplementation details"
2387 );
2388 assert_eq!(
2389 assembled.system_blocks[0].cache_hint,
2390 leviath_core::CacheHint::UntilChanged
2391 );
2392 }
2393
2394 #[test]
2395 fn test_assemble_temporary_region_produces_system_block_never() {
2396 let mut window = ContextWindow::new(100_000);
2397 let mut region = Region::new("scratch".to_string(), RegionKind::Temporary, 10_000);
2398 region.add_entry("temp data".to_string(), 20).unwrap();
2399 window.add_region(region);
2400
2401 let assembled = window.assemble();
2402
2403 assert_eq!(assembled.system_blocks.len(), 1);
2404 assert_eq!(assembled.system_blocks[0].text, "[scratch]:\ntemp data");
2405 assert_eq!(
2406 assembled.system_blocks[0].cache_hint,
2407 leviath_core::CacheHint::Never
2408 );
2409 }
2410
2411 #[test]
2412 fn test_assemble_clearable_region_produces_system_block_never() {
2413 let mut window = ContextWindow::new(100_000);
2414 let mut region = Region::new("cache".to_string(), RegionKind::Clearable, 10_000);
2415 region.add_entry("clearable data".to_string(), 20).unwrap();
2416 window.add_region(region);
2417
2418 let assembled = window.assemble();
2419
2420 assert_eq!(assembled.system_blocks.len(), 1);
2421 assert_eq!(assembled.system_blocks[0].text, "[cache]:\nclearable data");
2422 assert_eq!(
2423 assembled.system_blocks[0].cache_hint,
2424 leviath_core::CacheHint::Never
2425 );
2426 }
2427
2428 fn custom_kind(script: &str, persistent: bool) -> RegionKind {
2429 RegionKind::Custom {
2430 script: script.to_string(),
2431 persistent,
2432 }
2433 }
2434
2435 #[test]
2436 fn test_assemble_custom_region_falls_back_to_temporary_style_block() {
2437 let mut window = ContextWindow::new(100_000);
2441 let mut region = Region::new("brain".to_string(), custom_kind("b.rhai", false), 10_000);
2442 region.add_entry("thought one".to_string(), 10).unwrap();
2443 region.add_entry("thought two".to_string(), 10).unwrap();
2444 window.add_region(region);
2445
2446 let assembled = window.assemble();
2447
2448 assert_eq!(assembled.system_blocks.len(), 1);
2449 assert_eq!(
2450 assembled.system_blocks[0].text,
2451 "[brain]:\nthought one\n\nthought two"
2452 );
2453 assert_eq!(
2454 assembled.system_blocks[0].cache_hint,
2455 leviath_core::CacheHint::Never
2456 );
2457 }
2458
2459 #[test]
2460 fn try_evict_evicts_non_persistent_custom_regions_oldest_first() {
2461 let mut window = ContextWindow::new(100);
2462 let mut region = Region::new("brain".to_string(), custom_kind("b.rhai", false), 100);
2463 region.add_entry("old".to_string(), 40).unwrap();
2464 region.add_entry("new".to_string(), 40).unwrap();
2465 window.add_region(region);
2466 window.current_tokens = 80;
2467
2468 let result = with_tracing(|| window.try_evict(30).unwrap());
2469 assert!(result.tokens_freed >= 40);
2470 let brain = window.get_region("brain").unwrap();
2471 assert_eq!(brain.content.len(), 1);
2472 assert_eq!(brain.content[0].content, "new");
2473 }
2474
2475 #[test]
2476 fn try_evict_never_touches_persistent_custom_and_counts_it_as_pinned() {
2477 let mut window = ContextWindow::new(50);
2480 let mut vault = Region::new("vault".to_string(), custom_kind("v.rhai", true), 100);
2481 vault.add_entry("precious".to_string(), 60).unwrap();
2482 window.add_region(vault);
2483 window.current_tokens = 60;
2484
2485 let err = with_tracing(|| window.try_evict(10).unwrap_err());
2486 assert_eq!(
2487 err.to_string(),
2488 "Pinned regions (60) exceed total budget (50)"
2489 );
2490 assert_eq!(window.get_region("vault").unwrap().content.len(), 1);
2491 }
2492
2493 fn custom_window(src: &str, persistent: bool) -> ContextWindow {
2496 let mut window = ContextWindow::new(10_000);
2497 window.add_region(Region::new(
2498 "brain".to_string(),
2499 RegionKind::Custom {
2500 script: "s.rhai".to_string(),
2501 persistent,
2502 },
2503 100,
2504 ));
2505 window.region_scripts.insert(
2506 "s.rhai".to_string(),
2507 std::sync::Arc::new(leviath_scripting::region_hook::compile("s.rhai", src).unwrap()),
2508 );
2509 window
2510 }
2511
2512 #[test]
2513 fn custom_region_on_write_fires_across_all_write_methods() {
2514 let src = r#"
2515 fn render(ctx) { "" }
2516 fn on_write(ctx) { `${ctx.entry.kind}:${ctx.entry.content}` }
2517 "#;
2518 let mut window = custom_window(src, false);
2519
2520 window.add_to_region("brain", "a".to_string(), 1).unwrap();
2521 window
2522 .add_typed_entry(
2523 "brain",
2524 leviath_core::EntryKind::UserMessage,
2525 "b".to_string(),
2526 1,
2527 )
2528 .unwrap();
2529 window
2530 .add_tainted_to_region(
2531 "brain",
2532 "c".to_string(),
2533 1,
2534 leviath_core::TaintLevel::Public,
2535 )
2536 .unwrap();
2537 window
2538 .add_typed_tainted_to_region(
2539 "brain",
2540 leviath_core::EntryKind::UserMessage,
2541 "d".to_string(),
2542 1,
2543 leviath_core::TaintLevel::Public,
2544 )
2545 .unwrap();
2546
2547 let contents: Vec<_> = window
2548 .get_region("brain")
2549 .unwrap()
2550 .content
2551 .iter()
2552 .map(|e| e.content.as_str())
2553 .collect();
2554 assert_eq!(
2555 contents,
2556 vec!["text:a", "user_message:b", "text:c", "user_message:d"],
2557 "every write method passes through on_write with the entry kind visible"
2558 );
2559 assert_eq!(window.current_tokens, window.calculate_tokens());
2561
2562 assert!(window.replace_region("brain", "e".to_string(), 1));
2563 let region = window.get_region("brain").unwrap();
2564 assert_eq!(region.content.len(), 1);
2565 assert_eq!(region.content[0].content, "text:e");
2566 }
2567
2568 #[test]
2569 fn custom_region_on_write_drop_reports_success_without_storing() {
2570 let src = r#"
2571 fn render(ctx) { "" }
2572 fn on_write(ctx) { false }
2573 "#;
2574 let mut window = custom_window(src, false);
2575 window
2576 .add_to_region("brain", "spam".to_string(), 1)
2577 .unwrap();
2578 assert!(window.get_region("brain").unwrap().content.is_empty());
2579
2580 assert!(window.replace_region("brain", "more spam".to_string(), 1));
2582 assert!(window.get_region("brain").unwrap().content.is_empty());
2583 }
2584
2585 #[test]
2586 fn custom_region_on_write_drop_covers_typed_and_tainted_methods() {
2587 let src = r#"
2589 fn render(ctx) { "" }
2590 fn on_write(ctx) { false }
2591 "#;
2592 let mut window = custom_window(src, false);
2593 window
2594 .add_typed_entry(
2595 "brain",
2596 leviath_core::EntryKind::UserMessage,
2597 "a".to_string(),
2598 1,
2599 )
2600 .unwrap();
2601 window
2602 .add_tainted_to_region(
2603 "brain",
2604 "b".to_string(),
2605 1,
2606 leviath_core::TaintLevel::Public,
2607 )
2608 .unwrap();
2609 window
2610 .add_typed_tainted_to_region(
2611 "brain",
2612 leviath_core::EntryKind::UserMessage,
2613 "c".to_string(),
2614 1,
2615 leviath_core::TaintLevel::Public,
2616 )
2617 .unwrap();
2618 assert!(window.get_region("brain").unwrap().content.is_empty());
2619 }
2620
2621 #[test]
2622 fn try_evict_skips_custom_region_whose_script_has_no_on_overflow() {
2623 let mut window = ContextWindow::new(100);
2626 window.add_region(Region::new(
2627 "brain".to_string(),
2628 RegionKind::Custom {
2629 script: "s.rhai".to_string(),
2630 persistent: false,
2631 },
2632 100,
2633 ));
2634 window.region_scripts.insert(
2635 "s.rhai".to_string(),
2636 std::sync::Arc::new(
2637 leviath_scripting::region_hook::compile("s.rhai", "fn render(ctx) { \"\" }")
2638 .unwrap(),
2639 ),
2640 );
2641 window
2642 .add_to_region("brain", "old".to_string(), 40)
2643 .unwrap();
2644 window
2645 .add_to_region("brain", "new".to_string(), 40)
2646 .unwrap();
2647
2648 let result = with_tracing(|| window.try_evict(30).unwrap());
2649 assert!(result.tokens_freed >= 40);
2650 let brain = window.get_region("brain").unwrap();
2651 assert_eq!(brain.content.len(), 1);
2652 assert_eq!(brain.content[0].content, "new", "oldest-first fallback ran");
2653 }
2654
2655 #[test]
2656 fn try_evict_falls_to_oldest_first_when_script_frees_nothing() {
2657 let src = r#"
2660 fn render(ctx) { "" }
2661 fn on_overflow(ctx) { [] }
2662 "#;
2663 let mut window = ContextWindow::new(100);
2664 window.add_region(Region::new(
2665 "brain".to_string(),
2666 RegionKind::Custom {
2667 script: "s.rhai".to_string(),
2668 persistent: false,
2669 },
2670 100,
2671 ));
2672 window.region_scripts.insert(
2673 "s.rhai".to_string(),
2674 std::sync::Arc::new(leviath_scripting::region_hook::compile("s.rhai", src).unwrap()),
2675 );
2676 window
2677 .add_to_region("brain", "old".to_string(), 40)
2678 .unwrap();
2679 window
2680 .add_to_region("brain", "new".to_string(), 40)
2681 .unwrap();
2682
2683 let result = with_tracing(|| window.try_evict(30).unwrap());
2684 assert!(result.tokens_freed >= 40);
2685 assert_eq!(window.get_region("brain").unwrap().content.len(), 1);
2686 }
2687
2688 #[test]
2689 fn non_custom_regions_bypass_the_on_write_seam() {
2690 let mut window = custom_window(
2693 "fn render(ctx) { \"\" }\nfn on_write(ctx) { \"MANGLED\" }",
2694 false,
2695 );
2696 window.add_region(Region::new("plain".to_string(), RegionKind::Temporary, 100));
2697 window
2698 .add_to_region("plain", "untouched".to_string(), 2)
2699 .unwrap();
2700 assert_eq!(
2701 window.get_region("plain").unwrap().content[0].content,
2702 "untouched"
2703 );
2704 }
2705
2706 #[test]
2707 fn write_to_missing_region_still_errors() {
2708 let mut window = custom_window("fn render(ctx) { \"\" }", false);
2709 let err = window
2710 .add_to_region("ghost", "x".to_string(), 1)
2711 .unwrap_err();
2712 assert!(err.to_string().contains("ghost"), "{err}");
2713 }
2714
2715 #[test]
2716 fn custom_region_add_time_overflow_retries_after_script_drops() {
2717 let src = r#"
2720 fn render(ctx) { "" }
2721 fn on_overflow(ctx) { [0] }
2722 "#;
2723 let mut window = custom_window(src, false);
2724 window
2725 .add_to_region("brain", "big".to_string(), 90)
2726 .unwrap();
2727 window
2728 .add_to_region("brain", "next".to_string(), 20)
2729 .unwrap();
2730
2731 let region = window.get_region("brain").unwrap();
2732 assert_eq!(region.content.len(), 1);
2733 assert_eq!(region.content[0].content, "next");
2734 assert_eq!(window.current_tokens, 20);
2735 }
2736
2737 #[test]
2738 fn custom_region_add_time_overflow_propagates_when_still_too_big() {
2739 let src = r#"
2742 fn render(ctx) { "" }
2743 fn on_overflow(ctx) { [] }
2744 "#;
2745 let mut window = custom_window(src, false);
2746 window
2747 .add_to_region("brain", "big".to_string(), 90)
2748 .unwrap();
2749 let err =
2750 with_tracing(|| window.add_to_region("brain", "too much".to_string(), 50)).unwrap_err();
2751 assert_eq!(err.to_string(), "Content exceeds token budget: 140 > 100");
2752 assert_eq!(window.get_region("brain").unwrap().content.len(), 1);
2753 }
2754
2755 #[test]
2756 fn custom_region_without_on_overflow_gets_no_retry() {
2757 let mut window = custom_window("fn render(ctx) { \"\" }", false);
2758 window
2759 .add_to_region("brain", "big".to_string(), 90)
2760 .unwrap();
2761 let err = window
2762 .add_to_region("brain", "too much".to_string(), 50)
2763 .unwrap_err();
2764 assert_eq!(err.to_string(), "Content exceeds token budget: 140 > 100");
2765 }
2766
2767 #[test]
2768 fn try_evict_lets_custom_script_choose_what_to_drop() {
2769 let src = r#"
2773 fn render(ctx) { "" }
2774 fn on_overflow(ctx) {
2775 let drops = [];
2776 for (entry, i) in ctx.entries {
2777 if !entry.content.contains("ERROR") { drops.push(i); }
2778 }
2779 drops
2780 }
2781 "#;
2782 let mut window = ContextWindow::new(100);
2783 window.add_region(Region::new(
2784 "brain".to_string(),
2785 RegionKind::Custom {
2786 script: "s.rhai".to_string(),
2787 persistent: false,
2788 },
2789 100,
2790 ));
2791 window.region_scripts.insert(
2792 "s.rhai".to_string(),
2793 std::sync::Arc::new(leviath_scripting::region_hook::compile("s.rhai", src).unwrap()),
2794 );
2795 window
2796 .add_to_region("brain", "ok one".to_string(), 30)
2797 .unwrap();
2798 window
2799 .add_to_region("brain", "ERROR two".to_string(), 30)
2800 .unwrap();
2801 window
2802 .add_to_region("brain", "ok three".to_string(), 30)
2803 .unwrap();
2804
2805 let result = with_tracing(|| window.try_evict(40)).unwrap();
2806 assert!(result.tokens_freed >= 40);
2807 let contents: Vec<_> = window
2808 .get_region("brain")
2809 .unwrap()
2810 .content
2811 .iter()
2812 .map(|e| e.content.as_str())
2813 .collect();
2814 assert_eq!(
2815 contents,
2816 vec!["ERROR two"],
2817 "script retention choice honored"
2818 );
2819 }
2820
2821 #[test]
2824 fn assemble_custom_region_renders_through_script() {
2825 let src = r#"fn render(ctx) { `<brain iter=${ctx.stage_iterations}>` }"#;
2826 let mut window = custom_window(src, false);
2827 window
2828 .add_to_region("brain", "note".to_string(), 2)
2829 .unwrap();
2830
2831 let assembled = window.assemble();
2833 assert_eq!(assembled.system_blocks.len(), 1);
2834 assert_eq!(assembled.system_blocks[0].text, "<brain iter=0>");
2835 assert_eq!(
2836 assembled.system_blocks[0].cache_hint,
2837 leviath_core::CacheHint::UntilChanged
2838 );
2839
2840 let assembled = window.assemble_with_meta(&crate::custom_region::AssembleMeta {
2842 stage_name: "plan".to_string(),
2843 stage_iterations: 7,
2844 model: "m".to_string(),
2845 });
2846 assert_eq!(assembled.system_blocks[0].text, "<brain iter=7>");
2847 }
2848
2849 #[test]
2850 fn assemble_custom_region_renders_even_when_empty() {
2851 let src = r#"fn render(ctx) { `<empty count=${ctx.entries.len()}>` }"#;
2853 let window = custom_window(src, false);
2854 let assembled = window.assemble();
2855 assert_eq!(assembled.system_blocks.len(), 1);
2856 assert_eq!(assembled.system_blocks[0].text, "<empty count=0>");
2857 }
2858
2859 #[test]
2860 fn assemble_custom_conversation_takeover_renders_single_user_message() {
2861 let src = r#"
2865 fn render(ctx) {
2866 let xml = "<context>";
2867 for entry in ctx.entries {
2868 xml += `<event kind="${entry.kind}">${entry.content}</event>`;
2869 }
2870 xml += "</context>";
2871 #{ messages: [ #{ role: "user", content: xml } ] }
2872 }
2873 "#;
2874 let mut window = ContextWindow::new(10_000);
2875 window.add_region(Region::new(
2876 "conversation".to_string(),
2877 RegionKind::Custom {
2878 script: "conv.rhai".to_string(),
2879 persistent: false,
2880 },
2881 5_000,
2882 ));
2883 window.region_scripts.insert(
2884 "conv.rhai".to_string(),
2885 std::sync::Arc::new(leviath_scripting::region_hook::compile("conv.rhai", src).unwrap()),
2886 );
2887 window
2888 .add_typed_entry(
2889 "conversation",
2890 leviath_core::EntryKind::UserMessage,
2891 "do the task".to_string(),
2892 4,
2893 )
2894 .unwrap();
2895 window
2896 .add_typed_entry(
2897 "conversation",
2898 leviath_core::EntryKind::ToolResult {
2899 tool_call_id: "c1".to_string(),
2900 tool_name: "shell".to_string(),
2901 is_error: false,
2902 },
2903 "output".to_string(),
2904 2,
2905 )
2906 .unwrap();
2907
2908 let assembled = window.assemble();
2909 assert!(assembled.system_blocks.is_empty());
2910 assert_eq!(assembled.messages.len(), 1);
2911 assert_eq!(assembled.messages[0].role, "user");
2912 assert_eq!(
2913 assembled.messages[0].content.as_text(),
2914 "<context><event kind=\"user_message\">do the task</event>\
2915 <event kind=\"tool_result\">output</event></context>"
2916 );
2917 }
2918
2919 #[test]
2920 fn assemble_custom_script_emitting_nothing_gets_begin_fallback() {
2921 let window = custom_window("fn render(ctx) { \"\" }", false);
2924 let assembled = window.assemble();
2925 assert!(assembled.system_blocks.is_empty());
2926 assert_eq!(assembled.messages.len(), 1);
2927 assert_eq!(assembled.messages[0].content.as_text(), "Begin.");
2928 }
2929
2930 #[test]
2931 fn assemble_custom_unpaired_tool_blocks_are_sanitized() {
2932 let src = r#"
2936 fn render(ctx) {
2937 #{ messages: [
2938 #{ role: "user", content: "hello" },
2939 #{ role: "user", tool_results: [
2940 #{ tool_call_id: "ghost", content: "orphan" },
2941 ] },
2942 ] }
2943 }
2944 "#;
2945 let window = custom_window(src, false);
2946 let assembled = window.assemble();
2947 assert_eq!(assembled.messages.len(), 1, "orphan tool_result stripped");
2948 assert_eq!(assembled.messages[0].content.as_text(), "hello");
2949 }
2950
2951 #[test]
2954 fn test_assemble_text_entry_with_assistant_prefix() {
2955 let mut window = ContextWindow::new(100_000);
2956 let region = Region::new(
2957 "conv".to_string(),
2958 RegionKind::SlidingWindow {
2959 max_items: 100,
2960 eviction_strategy: EvictionStrategy::PerItem,
2961 },
2962 50_000,
2963 );
2964 window.add_region(region);
2965
2966 window
2967 .add_typed_entry(
2968 "conv",
2969 leviath_core::EntryKind::Text,
2970 "Assistant: I can help with that.".to_string(),
2971 10,
2972 )
2973 .unwrap();
2974
2975 let assembled = window.assemble();
2976
2977 let assistant_msgs: Vec<_> = assembled
2978 .messages
2979 .iter()
2980 .filter(|m| m.role == "assistant")
2981 .collect();
2982 assert_eq!(assistant_msgs.len(), 1);
2983 assert_eq!(assistant_msgs[0].content.as_text(), "I can help with that.");
2984 }
2985
2986 #[test]
2987 fn test_assemble_text_entry_with_user_prefix() {
2988 let mut window = ContextWindow::new(100_000);
2989 let region = Region::new(
2990 "conv".to_string(),
2991 RegionKind::SlidingWindow {
2992 max_items: 100,
2993 eviction_strategy: EvictionStrategy::PerItem,
2994 },
2995 50_000,
2996 );
2997 window.add_region(region);
2998
2999 window
3000 .add_typed_entry(
3001 "conv",
3002 leviath_core::EntryKind::Text,
3003 "User: What is Rust?".to_string(),
3004 10,
3005 )
3006 .unwrap();
3007
3008 let assembled = window.assemble();
3009
3010 let user_msgs: Vec<_> = assembled
3011 .messages
3012 .iter()
3013 .filter(|m| m.role == "user")
3014 .collect();
3015 assert_eq!(user_msgs.len(), 1);
3016 assert_eq!(user_msgs[0].content.as_text(), "What is Rust?");
3017 }
3018
3019 #[test]
3020 fn test_assemble_text_entry_without_prefix_defaults_to_user() {
3021 let mut window = ContextWindow::new(100_000);
3022 let region = Region::new(
3023 "conv".to_string(),
3024 RegionKind::SlidingWindow {
3025 max_items: 100,
3026 eviction_strategy: EvictionStrategy::PerItem,
3027 },
3028 50_000,
3029 );
3030 window.add_region(region);
3031
3032 window
3033 .add_typed_entry(
3034 "conv",
3035 leviath_core::EntryKind::Text,
3036 "some plain text".to_string(),
3037 10,
3038 )
3039 .unwrap();
3040
3041 let assembled = window.assemble();
3042
3043 let user_msgs: Vec<_> = assembled
3044 .messages
3045 .iter()
3046 .filter(|m| m.role == "user")
3047 .collect();
3048 assert_eq!(user_msgs.len(), 1);
3049 assert_eq!(user_msgs[0].content.as_text(), "some plain text");
3050 }
3051
3052 #[test]
3055 fn test_assemble_assistant_turn_with_text_and_tool_calls() {
3056 let mut window = ContextWindow::new(100_000);
3057 let region = Region::new(
3058 "conv".to_string(),
3059 RegionKind::SlidingWindow {
3060 max_items: 100,
3061 eviction_strategy: EvictionStrategy::PerItem,
3062 },
3063 50_000,
3064 );
3065 window.add_region(region);
3066
3067 window
3069 .add_typed_entry(
3070 "conv",
3071 leviath_core::EntryKind::UserMessage,
3072 "Read my file".to_string(),
3073 10,
3074 )
3075 .unwrap();
3076
3077 window
3079 .add_typed_entry(
3080 "conv",
3081 leviath_core::EntryKind::AssistantTurn {
3082 tool_calls: vec![leviath_core::SerializedToolCall {
3083 id: "tc_a".to_string(),
3084 name: "read_file".to_string(),
3085 arguments: serde_json::json!({"path": "foo.rs"}),
3086 thought_signature: None,
3087 }],
3088 },
3089 "Sure, let me read it.".to_string(),
3090 20,
3091 )
3092 .unwrap();
3093
3094 window
3096 .add_typed_entry(
3097 "conv",
3098 leviath_core::EntryKind::ToolResult {
3099 tool_call_id: "tc_a".to_string(),
3100 tool_name: "read_file".to_string(),
3101 is_error: false,
3102 },
3103 "fn main() {}".to_string(),
3104 10,
3105 )
3106 .unwrap();
3107
3108 let assembled = window.assemble();
3109
3110 let assistant_msg = assembled
3112 .messages
3113 .iter()
3114 .find(|m| m.role == "assistant")
3115 .expect("should have assistant message");
3116
3117 assert_eq!(
3120 assistant_msg.content,
3121 leviath_providers::MessageContent::Blocks(vec![
3122 leviath_providers::ContentBlock::Text {
3123 text: "Sure, let me read it.".to_string(),
3124 },
3125 leviath_providers::ContentBlock::ToolUse {
3126 id: "tc_a".to_string(),
3127 name: "read_file".to_string(),
3128 input: serde_json::json!({"path": "foo.rs"}),
3129 thought_signature: None,
3130 },
3131 ])
3132 );
3133 }
3134
3135 #[test]
3136 fn test_assemble_assistant_turn_no_text_only_tool_calls() {
3137 let mut window = ContextWindow::new(100_000);
3138 let region = Region::new(
3139 "conv".to_string(),
3140 RegionKind::SlidingWindow {
3141 max_items: 100,
3142 eviction_strategy: EvictionStrategy::PerItem,
3143 },
3144 50_000,
3145 );
3146 window.add_region(region);
3147
3148 window
3150 .add_typed_entry(
3151 "conv",
3152 leviath_core::EntryKind::UserMessage,
3153 "Do it".to_string(),
3154 10,
3155 )
3156 .unwrap();
3157
3158 window
3160 .add_typed_entry(
3161 "conv",
3162 leviath_core::EntryKind::AssistantTurn {
3163 tool_calls: vec![leviath_core::SerializedToolCall {
3164 id: "tc_b".to_string(),
3165 name: "bash".to_string(),
3166 arguments: serde_json::json!({"cmd": "ls"}),
3167 thought_signature: None,
3168 }],
3169 },
3170 "".to_string(),
3171 10,
3172 )
3173 .unwrap();
3174
3175 window
3177 .add_typed_entry(
3178 "conv",
3179 leviath_core::EntryKind::ToolResult {
3180 tool_call_id: "tc_b".to_string(),
3181 tool_name: "bash".to_string(),
3182 is_error: false,
3183 },
3184 "file1.rs\nfile2.rs".to_string(),
3185 10,
3186 )
3187 .unwrap();
3188
3189 let assembled = window.assemble();
3190
3191 let assistant_msg = assembled
3192 .messages
3193 .iter()
3194 .find(|m| m.role == "assistant")
3195 .expect("should have assistant message");
3196
3197 assert_eq!(
3199 assistant_msg.content,
3200 leviath_providers::MessageContent::Blocks(vec![
3201 leviath_providers::ContentBlock::ToolUse {
3202 id: "tc_b".to_string(),
3203 name: "bash".to_string(),
3204 input: serde_json::json!({"cmd": "ls"}),
3205 thought_signature: None,
3206 },
3207 ])
3208 );
3209 }
3210
3211 #[test]
3214 fn test_assemble_consecutive_tool_results_flushed_on_non_tool_result() {
3215 let mut window = ContextWindow::new(100_000);
3216 let region = Region::new(
3217 "conv".to_string(),
3218 RegionKind::SlidingWindow {
3219 max_items: 100,
3220 eviction_strategy: EvictionStrategy::PerItem,
3221 },
3222 50_000,
3223 );
3224 window.add_region(region);
3225
3226 window
3228 .add_typed_entry(
3229 "conv",
3230 leviath_core::EntryKind::UserMessage,
3231 "Run two tools".to_string(),
3232 10,
3233 )
3234 .unwrap();
3235
3236 window
3238 .add_typed_entry(
3239 "conv",
3240 leviath_core::EntryKind::AssistantTurn {
3241 tool_calls: vec![
3242 leviath_core::SerializedToolCall {
3243 id: "tc_1".to_string(),
3244 name: "read_file".to_string(),
3245 arguments: serde_json::json!({"path": "a.rs"}),
3246 thought_signature: None,
3247 },
3248 leviath_core::SerializedToolCall {
3249 id: "tc_2".to_string(),
3250 name: "read_file".to_string(),
3251 arguments: serde_json::json!({"path": "b.rs"}),
3252 thought_signature: None,
3253 },
3254 ],
3255 },
3256 "".to_string(),
3257 10,
3258 )
3259 .unwrap();
3260
3261 window
3263 .add_typed_entry(
3264 "conv",
3265 leviath_core::EntryKind::ToolResult {
3266 tool_call_id: "tc_1".to_string(),
3267 tool_name: "read_file".to_string(),
3268 is_error: false,
3269 },
3270 "content of a.rs".to_string(),
3271 10,
3272 )
3273 .unwrap();
3274 window
3275 .add_typed_entry(
3276 "conv",
3277 leviath_core::EntryKind::ToolResult {
3278 tool_call_id: "tc_2".to_string(),
3279 tool_name: "read_file".to_string(),
3280 is_error: false,
3281 },
3282 "content of b.rs".to_string(),
3283 10,
3284 )
3285 .unwrap();
3286
3287 window
3289 .add_typed_entry(
3290 "conv",
3291 leviath_core::EntryKind::UserMessage,
3292 "Now fix the bug".to_string(),
3293 10,
3294 )
3295 .unwrap();
3296
3297 let assembled = window.assemble();
3298
3299 assert_eq!(assembled.messages.len(), 4);
3302
3303 let tool_result_msg = &assembled.messages[2];
3305 assert_eq!(tool_result_msg.role, "user");
3306 assert_eq!(
3309 tool_result_msg.content,
3310 leviath_providers::MessageContent::Blocks(vec![
3311 leviath_providers::ContentBlock::ToolResult {
3312 tool_use_id: "tc_1".to_string(),
3313 content: "content of a.rs".to_string(),
3314 is_error: false,
3315 },
3316 leviath_providers::ContentBlock::ToolResult {
3317 tool_use_id: "tc_2".to_string(),
3318 content: "content of b.rs".to_string(),
3319 is_error: false,
3320 },
3321 ])
3322 );
3323
3324 assert_eq!(assembled.messages[3].role, "user");
3326 assert_eq!(assembled.messages[3].content.as_text(), "Now fix the bug");
3327 }
3328
3329 #[test]
3332 fn test_assemble_injects_begin_when_no_user_messages() {
3333 let mut window = ContextWindow::new(100_000);
3334 let mut pinned = Region::new("system".to_string(), RegionKind::Pinned, 10_000);
3336 pinned
3337 .add_entry("You are a helpful assistant.".to_string(), 20)
3338 .unwrap();
3339 window.add_region(pinned);
3340
3341 let assembled = window.assemble();
3342
3343 assert_eq!(assembled.messages.len(), 1);
3345 assert_eq!(assembled.messages[0].role, "user");
3346 assert_eq!(assembled.messages[0].content.as_text(), "Begin.");
3347 }
3348
3349 #[test]
3352 fn test_add_typed_entry_to_nonexistent_region() {
3353 let mut window = ContextWindow::new(10000);
3354 let result = window.add_typed_entry(
3355 "nonexistent",
3356 leviath_core::EntryKind::UserMessage,
3357 "hello".to_string(),
3358 10,
3359 );
3360 assert!(result.is_err());
3361 let err_str = result.unwrap_err().to_string();
3362 assert!(
3363 err_str.contains("nonexistent"),
3364 "Error should mention the missing region name"
3365 );
3366 }
3367
3368 #[test]
3369 fn test_add_typed_tainted_to_nonexistent_region() {
3370 let mut window = ContextWindow::new(10000);
3371 let result = window.add_typed_tainted_to_region(
3372 "ghost",
3373 leviath_core::EntryKind::UserMessage,
3374 "data".to_string(),
3375 10,
3376 leviath_core::TaintLevel::Public,
3377 );
3378 assert!(result.is_err());
3379 let err_str = result.unwrap_err().to_string();
3380 assert!(
3381 err_str.contains("ghost"),
3382 "Error should mention the missing region name"
3383 );
3384 }
3385
3386 #[test]
3387 fn test_assemble_tool_result_before_any_tool_use() {
3388 let mut window = ContextWindow::new(100_000);
3390 let region = Region::new(
3391 "conversation".to_string(),
3392 RegionKind::SlidingWindow {
3393 max_items: 100,
3394 eviction_strategy: EvictionStrategy::PerItem,
3395 },
3396 50_000,
3397 );
3398 window.add_region(region);
3399
3400 window
3402 .add_typed_entry(
3403 "conversation",
3404 leviath_core::EntryKind::ToolResult {
3405 tool_call_id: "tc_nowhere".to_string(),
3406 tool_name: "read_file".to_string(),
3407 is_error: false,
3408 },
3409 "orphan result".to_string(),
3410 10,
3411 )
3412 .unwrap();
3413
3414 let assembled = with_tracing(|| window.assemble());
3415
3416 assert_eq!(assembled.messages.len(), 1);
3419 assert_eq!(assembled.messages[0].role, "user");
3420 assert_eq!(
3421 assembled.messages[0].content,
3422 leviath_providers::MessageContent::Text("Begin.".to_string())
3423 );
3424 }
3425
3426 #[test]
3429 fn test_assemble_sets_cache_breakpoint_on_stable_prefix() {
3430 let mut window = ContextWindow::new(100_000);
3431 let region = Region::new(
3432 "conv".to_string(),
3433 RegionKind::SlidingWindow {
3434 max_items: 100,
3435 eviction_strategy: EvictionStrategy::PerItem,
3436 },
3437 50_000,
3438 );
3439 window.add_region(region);
3440
3441 for i in 0..10 {
3443 let kind = if i % 2 == 0 {
3444 leviath_core::EntryKind::UserMessage
3445 } else {
3446 leviath_core::EntryKind::AssistantTurn { tool_calls: vec![] }
3447 };
3448 window
3449 .add_typed_entry("conv", kind, format!("message {i}"), 10)
3450 .unwrap();
3451 }
3452
3453 let assembled = window.assemble();
3454 assert_eq!(assembled.messages.len(), 11);
3457 assert_eq!(assembled.messages.last().unwrap().role, "user");
3458
3459 let bp_idx = 6;
3462 for (i, msg) in assembled.messages.iter().enumerate() {
3463 if i == bp_idx {
3464 assert!(
3465 msg.cache_breakpoint,
3466 "Message at index {i} should have cache_breakpoint = true"
3467 );
3468 } else {
3469 assert!(
3470 !msg.cache_breakpoint,
3471 "Message at index {i} should have cache_breakpoint = false"
3472 );
3473 }
3474 }
3475 }
3476
3477 #[test]
3478 fn test_assemble_cache_breakpoint_small_conversation() {
3479 let mut window = ContextWindow::new(100_000);
3480 let region = Region::new(
3481 "conv".to_string(),
3482 RegionKind::SlidingWindow {
3483 max_items: 100,
3484 eviction_strategy: EvictionStrategy::PerItem,
3485 },
3486 50_000,
3487 );
3488 window.add_region(region);
3489
3490 window
3492 .add_typed_entry(
3493 "conv",
3494 leviath_core::EntryKind::UserMessage,
3495 "Hello".to_string(),
3496 10,
3497 )
3498 .unwrap();
3499 window
3500 .add_typed_entry(
3501 "conv",
3502 leviath_core::EntryKind::AssistantTurn { tool_calls: vec![] },
3503 "Hi there".to_string(),
3504 10,
3505 )
3506 .unwrap();
3507 window
3508 .add_typed_entry(
3509 "conv",
3510 leviath_core::EntryKind::UserMessage,
3511 "How are you?".to_string(),
3512 10,
3513 )
3514 .unwrap();
3515
3516 let assembled = window.assemble();
3517 assert_eq!(assembled.messages.len(), 3);
3518
3519 assert!(
3521 assembled.messages[0].cache_breakpoint,
3522 "First message should have cache_breakpoint in small conversation"
3523 );
3524 assert!(!assembled.messages[1].cache_breakpoint);
3525 assert!(!assembled.messages[2].cache_breakpoint);
3526 }
3527
3528 #[test]
3529 fn test_assemble_cache_breakpoint_too_few_messages() {
3530 let mut window = ContextWindow::new(100_000);
3531 let region = Region::new(
3532 "conv".to_string(),
3533 RegionKind::SlidingWindow {
3534 max_items: 100,
3535 eviction_strategy: EvictionStrategy::PerItem,
3536 },
3537 50_000,
3538 );
3539 window.add_region(region);
3540
3541 window
3543 .add_typed_entry(
3544 "conv",
3545 leviath_core::EntryKind::UserMessage,
3546 "Solo message".to_string(),
3547 10,
3548 )
3549 .unwrap();
3550
3551 let assembled = window.assemble();
3552 assert_eq!(assembled.messages.len(), 1);
3553
3554 assert!(
3556 !assembled.messages[0].cache_breakpoint,
3557 "Single message should not get a cache breakpoint"
3558 );
3559 }
3560
3561 #[test]
3562 fn test_assemble_system_blocks_sorted_by_cache_stability() {
3563 use leviath_core::CacheHint;
3564
3565 let mut window = ContextWindow::new(100_000);
3566
3567 let mut clearable = Region::new("scratch".to_string(), RegionKind::Clearable, 10_000);
3569 clearable
3570 .add_entry("clearable data".to_string(), 20)
3571 .unwrap();
3572 window.add_region(clearable);
3573
3574 let mut temporary = Region::new("temp".to_string(), RegionKind::Temporary, 10_000);
3575 temporary
3576 .add_entry("temporary data".to_string(), 20)
3577 .unwrap();
3578 window.add_region(temporary);
3579
3580 let mut compacting = Region::new(
3581 "impl".to_string(),
3582 RegionKind::Compacting {
3583 threshold_tokens: 500,
3584 },
3585 10_000,
3586 );
3587 compacting
3588 .add_entry("compacting data".to_string(), 20)
3589 .unwrap();
3590 window.add_region(compacting);
3591
3592 let mut pinned = Region::new("system".to_string(), RegionKind::Pinned, 10_000);
3593 pinned
3594 .add_entry("pinned system prompt".to_string(), 20)
3595 .unwrap();
3596 window.add_region(pinned);
3597
3598 let assembled = window.assemble();
3599
3600 assert_eq!(assembled.system_blocks.len(), 4);
3601
3602 assert_eq!(
3605 assembled.system_blocks[0].cache_hint,
3606 CacheHint::Always,
3607 "First system block should be Always (Pinned)"
3608 );
3609 assert_eq!(
3610 assembled.system_blocks[1].cache_hint,
3611 CacheHint::UntilChanged,
3612 "Second system block should be UntilChanged (Compacting)"
3613 );
3614 assert_eq!(
3615 assembled.system_blocks[2].cache_hint,
3616 CacheHint::Never,
3617 "Third system block should be Never"
3618 );
3619 assert_eq!(
3620 assembled.system_blocks[3].cache_hint,
3621 CacheHint::Never,
3622 "Fourth system block should be Never"
3623 );
3624 }
3625
3626 #[test]
3629 fn test_add_typed_tainted_to_region_success() {
3630 let mut window = ContextWindow::new(10000);
3631 let mut region = Region::new(
3632 "conv".to_string(),
3633 RegionKind::SlidingWindow {
3634 max_items: 50,
3635 eviction_strategy: EvictionStrategy::PerItem,
3636 },
3637 5000,
3638 );
3639 region.enable_taint_tracking();
3640 window.add_region(region);
3641
3642 window
3643 .add_typed_tainted_to_region(
3644 "conv",
3645 leviath_core::EntryKind::ToolResult {
3646 tool_call_id: "tc_1".to_string(),
3647 tool_name: "read_file".to_string(),
3648 is_error: false,
3649 },
3650 "secret data".to_string(),
3651 100,
3652 leviath_core::TaintLevel::Private,
3653 )
3654 .unwrap();
3655
3656 assert_eq!(window.current_tokens, 100);
3657 assert_eq!(
3658 window.get_region("conv").and_then(|r| r.taint_level()),
3659 Some(leviath_core::TaintLevel::Private)
3660 );
3661 }
3662
3663 #[test]
3664 fn test_add_typed_tainted_to_region_not_found() {
3665 let mut window = ContextWindow::new(10000);
3666 let result = window.add_typed_tainted_to_region(
3667 "nonexistent",
3668 leviath_core::EntryKind::Text,
3669 "data".to_string(),
3670 10,
3671 leviath_core::TaintLevel::Public,
3672 );
3673 assert!(result.is_err());
3674 }
3675
3676 #[test]
3677 fn test_assemble_consecutive_tool_results_flushed_at_end() {
3678 let mut window = ContextWindow::new(100_000);
3681 let region = Region::new(
3682 "conv".to_string(),
3683 RegionKind::SlidingWindow {
3684 max_items: 100,
3685 eviction_strategy: EvictionStrategy::PerItem,
3686 },
3687 50_000,
3688 );
3689 window.add_region(region);
3690
3691 window
3693 .add_typed_entry(
3694 "conv",
3695 leviath_core::EntryKind::UserMessage,
3696 "do something".to_string(),
3697 10,
3698 )
3699 .unwrap();
3700 window
3701 .add_typed_entry(
3702 "conv",
3703 leviath_core::EntryKind::AssistantTurn {
3704 tool_calls: vec![leviath_core::SerializedToolCall {
3705 id: "tc_1".to_string(),
3706 name: "read_file".to_string(),
3707 arguments: serde_json::json!({"path": "foo.rs"}),
3708 thought_signature: None,
3709 }],
3710 },
3711 "Let me read that".to_string(),
3712 10,
3713 )
3714 .unwrap();
3715 window
3716 .add_typed_entry(
3717 "conv",
3718 leviath_core::EntryKind::ToolResult {
3719 tool_call_id: "tc_1".to_string(),
3720 tool_name: "read_file".to_string(),
3721 is_error: false,
3722 },
3723 "fn main() {}".to_string(),
3724 10,
3725 )
3726 .unwrap();
3727
3728 let assembled = window.assemble();
3729 assert_eq!(assembled.messages.len(), 3);
3731 assert_eq!(assembled.messages[2].role, "user");
3732 assert_eq!(
3734 assembled.messages[2].content,
3735 leviath_providers::MessageContent::Blocks(vec![
3736 leviath_providers::ContentBlock::ToolResult {
3737 tool_use_id: "tc_1".to_string(),
3738 content: "fn main() {}".to_string(),
3739 is_error: false,
3740 },
3741 ])
3742 );
3743 }
3744
3745 #[test]
3746 fn test_assemble_compact_history_with_sliding_prefix_sorting() {
3747 use leviath_core::CacheHint;
3749
3750 let mut window = ContextWindow::new(100_000);
3751
3752 let mut temp = Region::new("temp".to_string(), RegionKind::Temporary, 10_000);
3753 temp.add_entry("temp data".to_string(), 10).unwrap();
3754 window.add_region(temp);
3755
3756 let mut history = Region::new(
3757 "history".to_string(),
3758 RegionKind::CompactHistory {
3759 source_region: "impl".to_string(),
3760 },
3761 10_000,
3762 );
3763 history.add_entry("summary data".to_string(), 10).unwrap();
3764 window.add_region(history);
3765
3766 let assembled = window.assemble();
3767 assert_eq!(assembled.system_blocks.len(), 2);
3768 assert_eq!(assembled.system_blocks[0].cache_hint, CacheHint::Always);
3770 assert_eq!(assembled.system_blocks[1].cache_hint, CacheHint::Never);
3771 }
3772
3773 #[test]
3774 fn cache_hint_sort_priority_orders_by_stability() {
3775 use leviath_core::CacheHint;
3776 assert_eq!(cache_hint_sort_priority(CacheHint::Always), 0);
3778 assert_eq!(
3779 cache_hint_sort_priority(CacheHint::SlidingPrefix {
3780 stable_fraction: 0.75
3781 }),
3782 1
3783 );
3784 assert_eq!(cache_hint_sort_priority(CacheHint::UntilChanged), 2);
3785 assert_eq!(cache_hint_sort_priority(CacheHint::Never), 3);
3786 assert!(
3788 cache_hint_sort_priority(CacheHint::Always)
3789 < cache_hint_sort_priority(CacheHint::SlidingPrefix {
3790 stable_fraction: 0.5
3791 })
3792 );
3793 }
3794
3795 #[test]
3796 fn test_assemble_empty_regions_skipped() {
3797 let mut window = ContextWindow::new(100_000);
3798 window.add_region(Region::new(
3799 "system".to_string(),
3800 RegionKind::Pinned,
3801 10_000,
3802 ));
3803 let assembled = window.assemble();
3805 assert!(assembled.system_blocks.is_empty());
3806 }
3807
3808 #[test]
3809 fn test_assemble_hashmap_region_with_keys() {
3810 let mut window = ContextWindow::new(100_000);
3811 let mut region = Region::new(
3812 "files".to_string(),
3813 RegionKind::HashMap { max_entries: None },
3814 10_000,
3815 );
3816 region
3817 .upsert_by_key("src/main.rs", "fn main() {}".to_string(), 10)
3818 .unwrap();
3819 region
3820 .upsert_by_key("src/lib.rs", "pub mod foo;".to_string(), 8)
3821 .unwrap();
3822 window.add_region(region);
3823
3824 let assembled = window.assemble();
3825 assert_eq!(assembled.system_blocks.len(), 1);
3826 let block_text = &assembled.system_blocks[0].text;
3827 assert!(block_text.contains("[files]:"));
3828 assert!(block_text.contains("### [src/main.rs]"));
3829 assert!(block_text.contains("fn main() {}"));
3830 assert!(block_text.contains("### [src/lib.rs]"));
3831 assert!(block_text.contains("pub mod foo;"));
3832 }
3833
3834 #[test]
3835 fn test_assemble_hashmap_region_cache_hint() {
3836 let mut window = ContextWindow::new(100_000);
3837 let mut region = Region::new(
3838 "files".to_string(),
3839 RegionKind::HashMap { max_entries: None },
3840 10_000,
3841 );
3842 region
3843 .upsert_by_key("a.rs", "content".to_string(), 5)
3844 .unwrap();
3845 window.add_region(region);
3846
3847 let assembled = window.assemble();
3848 assert_eq!(assembled.system_blocks.len(), 1);
3849 assert_eq!(
3850 assembled.system_blocks[0].cache_hint,
3851 leviath_core::CacheHint::UntilChanged
3852 );
3853 }
3854
3855 #[test]
3858 fn test_assemble_hashmap_single_keyed_entry() {
3859 let mut window = ContextWindow::new(100_000);
3860 let mut region = Region::new(
3861 "context".to_string(),
3862 RegionKind::HashMap { max_entries: None },
3863 10_000,
3864 );
3865 region
3866 .upsert_by_key("config.toml", "key = \"value\"".to_string(), 10)
3867 .unwrap();
3868 window.add_region(region);
3869
3870 let assembled = window.assemble();
3871
3872 assert_eq!(assembled.system_blocks.len(), 1);
3873 let block_text = &assembled.system_blocks[0].text;
3874 assert!(
3875 block_text.starts_with("[context]:"),
3876 "System block should start with [region_name]: prefix"
3877 );
3878 assert!(
3879 block_text.contains("### [config.toml]"),
3880 "Entry should have ### [key] header"
3881 );
3882 assert!(
3883 block_text.contains("key = \"value\""),
3884 "Entry content should be present"
3885 );
3886 }
3887
3888 #[test]
3889 fn test_assemble_hashmap_multiple_keyed_entries() {
3890 let mut window = ContextWindow::new(100_000);
3891 let mut region = Region::new(
3892 "tracked_files".to_string(),
3893 RegionKind::HashMap { max_entries: None },
3894 10_000,
3895 );
3896 region
3897 .upsert_by_key("alpha.rs", "fn alpha() {}".to_string(), 10)
3898 .unwrap();
3899 region
3900 .upsert_by_key("beta.rs", "fn beta() {}".to_string(), 10)
3901 .unwrap();
3902 region
3903 .upsert_by_key("gamma.rs", "fn gamma() {}".to_string(), 10)
3904 .unwrap();
3905 window.add_region(region);
3906
3907 let assembled = window.assemble();
3908
3909 assert_eq!(assembled.system_blocks.len(), 1);
3910 let block_text = &assembled.system_blocks[0].text;
3911 assert!(block_text.starts_with("[tracked_files]:"));
3912 assert!(block_text.contains("### [alpha.rs]"));
3913 assert!(block_text.contains("fn alpha() {}"));
3914 assert!(block_text.contains("### [beta.rs]"));
3915 assert!(block_text.contains("fn beta() {}"));
3916 assert!(block_text.contains("### [gamma.rs]"));
3917 assert!(block_text.contains("fn gamma() {}"));
3918 }
3919
3920 #[test]
3921 fn test_assemble_hashmap_empty_region_skipped() {
3922 let mut window = ContextWindow::new(100_000);
3923 let region = Region::new(
3924 "empty_map".to_string(),
3925 RegionKind::HashMap { max_entries: None },
3926 10_000,
3927 );
3928 window.add_region(region);
3930
3931 let assembled = window.assemble();
3932
3933 assert!(
3934 assembled.system_blocks.is_empty(),
3935 "Empty HashMap region should not produce a system block"
3936 );
3937 }
3938
3939 #[test]
3940 fn test_assemble_mixed_pinned_hashmap_sliding_window() {
3941 use leviath_core::CacheHint;
3942
3943 let mut window = ContextWindow::new(100_000);
3944
3945 let mut pinned = Region::new("system".to_string(), RegionKind::Pinned, 10_000);
3947 pinned
3948 .add_entry("You are a helpful assistant.".to_string(), 20)
3949 .unwrap();
3950 window.add_region(pinned);
3951
3952 let mut hashmap = Region::new(
3954 "files".to_string(),
3955 RegionKind::HashMap { max_entries: None },
3956 10_000,
3957 );
3958 hashmap
3959 .upsert_by_key("main.rs", "fn main() {}".to_string(), 10)
3960 .unwrap();
3961 window.add_region(hashmap);
3962
3963 let mut sliding = Region::new(
3965 "conv".to_string(),
3966 RegionKind::SlidingWindow {
3967 max_items: 100,
3968 eviction_strategy: EvictionStrategy::PerItem,
3969 },
3970 50_000,
3971 );
3972 sliding
3973 .add_typed_entry(
3974 "Hello there".to_string(),
3975 10,
3976 leviath_core::EntryKind::UserMessage,
3977 )
3978 .unwrap();
3979 window.add_region(sliding);
3980
3981 let assembled = window.assemble();
3982
3983 assert_eq!(assembled.system_blocks.len(), 2);
3985
3986 assert_eq!(
3988 assembled.system_blocks[0].cache_hint,
3989 CacheHint::Always,
3990 "Pinned region should sort first (Always cache hint)"
3991 );
3992 assert!(
3993 assembled.system_blocks[0]
3994 .text
3995 .contains("You are a helpful assistant."),
3996 "First system block should be the pinned content"
3997 );
3998
3999 assert_eq!(
4000 assembled.system_blocks[1].cache_hint,
4001 CacheHint::UntilChanged,
4002 "HashMap region should sort second (UntilChanged cache hint)"
4003 );
4004 assert!(
4005 assembled.system_blocks[1].text.starts_with("[files]:"),
4006 "HashMap system block should have [region_name]: prefix"
4007 );
4008 assert!(
4009 assembled.system_blocks[1].text.contains("### [main.rs]"),
4010 "HashMap system block should contain ### [key] header"
4011 );
4012
4013 assert!(
4015 assembled
4016 .messages
4017 .iter()
4018 .any(|m| m.role == "user" && m.content.as_text().contains("Hello there")),
4019 "SlidingWindow entries should appear as messages"
4020 );
4021 }
4022
4023 #[test]
4024 fn test_add_tainted_to_region_propagates_budget_error() {
4025 let mut window = ContextWindow::new(10_000);
4028 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 10);
4029 region.enable_taint_tracking();
4030 window.add_region(region);
4031
4032 let result = window.add_tainted_to_region(
4033 "conv",
4034 "far too many tokens".to_string(),
4035 100,
4036 leviath_core::TaintLevel::Private,
4037 );
4038 assert!(result.is_err());
4039 }
4040
4041 #[test]
4042 fn test_add_typed_tainted_to_region_propagates_budget_error() {
4043 let mut window = ContextWindow::new(10_000);
4046 let region = Region::new("conv".to_string(), RegionKind::Temporary, 10);
4047 window.add_region(region);
4048
4049 let result = window.add_typed_tainted_to_region(
4050 "conv",
4051 leviath_core::EntryKind::Text,
4052 "far too many tokens".to_string(),
4053 100,
4054 leviath_core::TaintLevel::Public,
4055 );
4056 assert!(result.is_err());
4057 }
4058
4059 #[test]
4060 fn test_assemble_hashmap_region_entry_without_key() {
4061 let mut window = ContextWindow::new(10_000);
4064 let region = Region::new(
4065 "kv".to_string(),
4066 RegionKind::HashMap { max_entries: None },
4067 5000,
4068 );
4069 window.add_region(region);
4070 window
4072 .add_to_region("kv", "keyless content".to_string(), 10)
4073 .unwrap();
4074
4075 let assembled = window.assemble();
4076 assert!(
4077 assembled
4078 .system_blocks
4079 .iter()
4080 .any(|b| b.text.contains("keyless content")),
4081 "keyless HashMap entry should appear verbatim in a system block"
4082 );
4083 }
4084}