1use super::compression::CompressionPipeline;
2use super::config::{ContextConfig, PromptBudgetConfig};
3use super::execution::{
4 ContextCandidate, ContextContractError, ContextEntrySource, ContextPlanAction,
5 ContextPreparation, ContextPreparationRequest, ContextSelection, ContextState,
6};
7use super::partitions::ContextPartitions;
8use super::policy::ContextPolicy;
9use super::pressure::{PressureAction, PressureMonitor};
10use super::renderer::InternalRenderedContext;
11use super::renewal::RenewalPolicy;
12use super::skill_catalog::SkillCatalog;
13use super::task_state::{TaskState, TaskUpdate};
14use super::token_engine::ContextTokenEngine;
15use crate::mm::handle::{Handle, HandleId, HandleKind, HandleTable, Residency};
16use crate::types::capability::{Capability, CapabilityKind, CapabilityManifest};
17use crate::types::message::{Content, ContentPart, CoreMessage, ToolSchema};
18use crate::types::skill::SkillMetadata;
19use compact_str::CompactString;
20
21pub const MEMORY_TOOL_NAME: &str = "memory";
22pub const KNOWLEDGE_TOOL_NAME: &str = "knowledge";
23pub const READ_RESULT_TOOL_NAME: &str = "read_result";
25
26const META_TOOL_NAMES: &[&str] = &[
29 "pace",
30 "update_plan",
31 "skill",
32 MEMORY_TOOL_NAME,
33 KNOWLEDGE_TOOL_NAME,
34 READ_RESULT_TOOL_NAME,
35 "submit_workflow_nodes",
36 "start_workflow",
37];
38
39pub(crate) fn is_meta_tool(name: &str) -> bool {
42 META_TOOL_NAMES.contains(&name)
43}
44
45pub(crate) const EXPOSURE_EXEMPT_META_TOOLS: &[&str] = &[
63 "skill",
64 MEMORY_TOOL_NAME,
65 KNOWLEDGE_TOOL_NAME,
66 "update_plan",
67 READ_RESULT_TOOL_NAME,
68];
69
70pub(crate) fn is_exposure_exempt_meta_tool(name: &str) -> bool {
73 EXPOSURE_EXEMPT_META_TOOLS.contains(&name)
74}
75
76#[doc(hidden)]
81pub struct ContextManager {
82 pub partitions: ContextPartitions,
83 pub max_tokens: u32,
84 pub config: ContextConfig,
85 pub engine: ContextTokenEngine,
86 pub prompt_budget: PromptBudgetConfig,
89 pub sprint: u32,
90 pub skills: SkillCatalog,
91 pub active_skills: std::collections::BTreeMap<CompactString, Option<u32>>,
99 pub stable_core_tools: std::collections::HashSet<CompactString>,
103 pub capabilities: CapabilityManifest,
104 pub memory_enabled: bool,
105 pub knowledge_enabled: bool,
106 pub plan_tool_enabled: bool,
107 state_generation: u64,
109 last_observed_prompt_tokens: Option<u32>,
110 compression: CompressionPipeline,
111 pressure: PressureMonitor,
112 renewal: RenewalPolicy,
113
114 pub last_activity_ms: u64,
118
119 pub last_compact_ms: Option<u64>,
122
123 pub handles: HandleTable,
128 next_handle_id: HandleId,
130
131 frozen_history_len: usize,
138
139 pending_knowledge_sweeps: Vec<crate::context::partitions::KnowledgeSweep>,
142
143 knowledge_budget_warned: bool,
146 knowledge_reference_step: u64,
148}
149
150impl ContextManager {
151 pub fn new(max_tokens: u32) -> Self {
152 Self::with_config(
155 max_tokens,
156 ContextConfig::default(),
157 ContextTokenEngine::fallback_estimator(),
158 )
159 }
160
161 pub fn with_config(max_tokens: u32, config: ContextConfig, engine: ContextTokenEngine) -> Self {
162 let compression = CompressionPipeline::new(&config);
163 let pressure = PressureMonitor::new(max_tokens, config.clone());
164 let renewal = RenewalPolicy::from_config(&config);
165 let partitions = ContextPartitions::new(&config);
166 Self {
167 partitions,
168 max_tokens,
169 config,
170 engine,
171 prompt_budget: PromptBudgetConfig::default(),
172 sprint: 0,
173 skills: SkillCatalog::new(),
174 active_skills: std::collections::BTreeMap::new(),
175 stable_core_tools: std::collections::HashSet::new(),
176 capabilities: CapabilityManifest::new(),
177 memory_enabled: false,
178 knowledge_enabled: false,
179 plan_tool_enabled: false,
180 state_generation: 0,
181 last_observed_prompt_tokens: None,
182 compression,
183 pressure,
184 renewal,
185 last_activity_ms: 0,
186 last_compact_ms: None,
187 handles: HandleTable::new(),
188 next_handle_id: 0,
189 frozen_history_len: 0,
190 pending_knowledge_sweeps: Vec::new(),
191 knowledge_budget_warned: false,
192 knowledge_reference_step: 0,
193 }
194 }
195
196 pub fn apply_context_policy(&mut self, policy: &ContextPolicy) {
198 policy.apply_to(&mut self.config);
199 self.compression = CompressionPipeline::new(&self.config);
200 self.pressure = PressureMonitor::new(self.max_tokens, self.config.clone());
201 self.renewal = RenewalPolicy::from_config(&self.config);
202 }
203
204 pub fn record_activity(&mut self, now_ms: u64) {
208 self.last_activity_ms = now_ms;
209 }
210
211 pub fn should_time_decay_compact(&self, now_ms: u64) -> bool {
214 let idle_ms = if let Some(last_compact) = self.last_compact_ms {
215 now_ms.saturating_sub(last_compact)
217 } else {
218 now_ms.saturating_sub(self.last_activity_ms)
220 };
221
222 let idle_minutes = idle_ms / 60_000;
223 idle_minutes >= self.config.micro_compact_idle_minutes as u64
224 }
225
226 pub fn recompute_handle_residency(&mut self) {
241 if self.rho() < self.config.collapse_threshold {
243 return;
244 }
245 let keep = self.config.preserved_tool_results;
246 let total = self
249 .handles
250 .all()
251 .iter()
252 .filter(|h| matches!(h.kind, HandleKind::ToolResult))
253 .count();
254 let cutoff = total.saturating_sub(keep);
255 for (i, handle) in self.handles.tool_result_handles_mut().enumerate() {
256 if i < cutoff && matches!(handle.residency, Residency::Resident) {
259 handle.residency = Residency::Collapsed;
260 }
261 }
262 }
263
264 pub fn reset_collapse_generation(&mut self) {
270 for handle in self.handles.all_mut() {
271 if matches!(handle.residency, Residency::Collapsed) {
272 handle.residency = Residency::Resident;
273 }
274 }
275 }
276
277 pub fn prune_orphaned_handles(&mut self) {
289 let live: std::collections::HashSet<CompactString> = self
290 .partitions
291 .history
292 .messages
293 .iter()
294 .flat_map(|m| match &m.content {
295 Content::Parts(parts) => parts
296 .iter()
297 .filter_map(|p| match p {
298 ContentPart::ToolResult { call_id, .. } => Some(call_id.clone()),
299 _ => None,
300 })
301 .collect::<Vec<_>>(),
302 _ => Vec::new(),
303 })
304 .collect();
305 self.handles.retain(|h| {
306 h.residency.payload_ref().is_some()
307 || h.source.as_ref().is_none_or(|s| live.contains(s))
308 });
309 }
310
311 pub fn set_payload_residency(
326 &mut self,
327 source: &str,
328 kind: HandleKind,
329 tokens: u32,
330 residency: Residency,
331 ) -> Option<Residency> {
332 if let Some(handle) = self
333 .handles
334 .all_mut()
335 .iter_mut()
336 .find(|h| h.source.as_deref() == Some(source))
337 {
338 let previous = std::mem::replace(&mut handle.residency, residency);
339 handle.tokens = tokens;
340 return Some(previous);
341 }
342 let id = self.alloc_handle_id();
343 self.handles.insert(Handle {
344 id,
345 kind,
346 residency,
347 tokens,
348 source: Some(source.into()),
349 });
350 None
351 }
352
353 pub fn payload_residency(&self, source: &str) -> Option<&Residency> {
360 self.handles.residency_for_source(source)
361 }
362
363 pub fn rho(&self) -> f64 {
370 self.pressure.pressure(
371 &self.partitions,
372 &self.engine,
373 self.last_observed_prompt_tokens,
374 )
375 }
376
377 pub fn set_observed_prompt_tokens(&mut self, tokens: u32) {
378 self.last_observed_prompt_tokens = Some(tokens);
379 }
380
381 pub fn should_compress(&self) -> PressureAction {
382 self.pressure.recommend(self.rho())
388 }
389
390 pub fn compress(
391 &mut self,
392 action: PressureAction,
393 ) -> (u32, Option<String>, Vec<CoreMessage>, Option<usize>) {
394 self.compress_with_time(action, None)
395 }
396
397 pub fn compress_with_time(
398 &mut self,
399 action: PressureAction,
400 now_ms: Option<u64>,
401 ) -> (u32, Option<String>, Vec<CoreMessage>, Option<usize>) {
402 let target = self.config.target_tokens(self.max_tokens);
403 self.compress_with_target(action, target, now_ms)
404 }
405
406 pub fn force_compress(&mut self) -> (u32, Option<String>, Vec<CoreMessage>, Option<usize>) {
407 self.compress_with_target(PressureAction::AutoCompact, 0, None)
408 }
409
410 pub fn compress_with_target(
417 &mut self,
418 action: PressureAction,
419 target_tokens: u32,
420 now_ms: Option<u64>,
421 ) -> (u32, Option<String>, Vec<CoreMessage>, Option<usize>) {
422 self.state_generation = self.state_generation.saturating_add(1);
423 let result = self.compression.compress(
424 &mut self.partitions,
425 action,
426 self.max_tokens,
427 target_tokens,
428 &self.engine,
429 );
430 if let Some(ts) = now_ms {
431 self.last_compact_ms = Some(ts);
432 }
433 if !result.2.is_empty() {
435 self.prune_orphaned_handles();
436 self.reset_collapse_generation();
439 self.sweep_knowledge_at_boundary();
442 }
443 if result.3.is_some() {
450 self.frozen_history_len = self.partitions.history.messages.len();
451 }
452 result
453 }
454
455 pub fn plan_compaction_params(&self) -> (u32, usize) {
459 (
460 self.config.target_tokens(self.max_tokens),
461 self.config.preserve_recent_turns,
462 )
463 }
464
465 pub fn should_renew(&self) -> bool {
468 self.renewal
469 .should_renew(&self.pressure, &self.partitions, &self.engine)
470 }
471
472 pub fn renew(&mut self) {
473 self.state_generation = self.state_generation.saturating_add(1);
474 self.partitions = self
475 .renewal
476 .renew(&self.partitions, self.max_tokens, &self.engine);
477 self.sprint += 1;
478 self.prune_orphaned_handles();
481 self.reset_collapse_generation();
482 self.sweep_knowledge_at_boundary();
484 self.frozen_history_len = self.partitions.history.messages.len();
486 }
487
488 pub fn set_prompt_budget(&mut self, prompt_budget: PromptBudgetConfig) {
491 self.prompt_budget = prompt_budget;
492 }
493
494 pub fn available_input_tokens(&self) -> u32 {
495 self.max_tokens
496 .saturating_sub(self.prompt_budget.reserved_tokens())
497 }
498
499 pub fn render(&self) -> InternalRenderedContext {
500 super::renderer::render_projected(
501 &self.partitions,
502 self.available_input_tokens(),
503 &self.engine,
504 self.config.preserve_recent_units,
505 &self.handles,
506 self.frozen_history_len,
507 self.config.collapse_assistant_narration,
508 )
509 }
510
511 pub fn context_state(&self) -> Result<ContextState, ContextContractError> {
513 ContextState::from_partitions_with_handles(
514 &self.partitions,
515 self.state_generation,
516 &self.handles,
517 )
518 }
519
520 pub fn prepare_execution_input(
527 &self,
528 request: &ContextPreparationRequest,
529 ) -> Result<ContextPreparation, ContextContractError> {
530 let (candidate, rendered_projection) = self.prepare_candidate(
531 request.operation_id.clone(),
532 request.step_id.clone(),
533 request.input_sequence,
534 request.policy_digest.clone(),
535 )?;
536 let (plan, execution_input) = candidate.bind(
537 request.prompt_measurement.clone(),
538 request.provider_route.clone(),
539 )?;
540 Ok(ContextPreparation {
541 execution_input,
542 plan,
543 rendered_projection,
544 })
545 }
546
547 pub fn prepare_candidate(
549 &self,
550 operation_id: String,
551 step_id: String,
552 input_sequence: u64,
553 policy_digest: crate::evolution::ContentDigest,
554 ) -> Result<(ContextCandidate, InternalRenderedContext), ContextContractError> {
555 let state = self.context_state()?;
556 let (rendered_projection, trace) = super::renderer::render_projected_with_trace(
557 &self.partitions,
558 self.available_input_tokens(),
559 &self.engine,
560 self.config.preserve_recent_units,
561 &self.handles,
562 self.frozen_history_len,
563 self.config.collapse_assistant_narration,
564 );
565 let rendered_bytes =
566 crate::runtime::kernel::wire::record::canonical_bytes(&rendered_projection)
567 .map_err(|error| ContextContractError::Canonical(error.to_string()))?;
568 let rendered_snapshot =
569 crate::evolution::ContentDigest::from_bytes(rendered_bytes.as_slice());
570 let rendered_history = &rendered_projection.turns;
571 let selections = state
572 .system
573 .iter()
574 .chain(state.knowledge.iter())
575 .chain(state.history.iter())
576 .chain(state.state.iter())
577 .map(|entry| {
578 let (action, reason) = match entry.source {
579 ContextEntrySource::System | ContextEntrySource::Knowledge => {
580 let message = match entry.source {
581 ContextEntrySource::System => {
582 &self.partitions.system.messages[entry.ordinal as usize]
583 }
584 _ => &self.partitions.knowledge.entries[entry.ordinal as usize].message,
585 };
586 if message.content.as_text().is_some() {
587 (ContextPlanAction::Include, "stable_partition".to_string())
588 } else {
589 (
590 ContextPlanAction::Omit,
591 "non_text_system_projection".to_string(),
592 )
593 }
594 }
595 ContextEntrySource::State | ContextEntrySource::Signal => {
596 let included = match entry.source {
597 ContextEntrySource::State => {
598 !self.partitions.task_state.format_compact().is_empty()
599 }
600 _ => !self.partitions.signals[entry.ordinal as usize].is_empty(),
601 };
602 if included && rendered_projection.state_turn.is_some() {
603 (
604 ContextPlanAction::Include,
605 "volatile_state_turn".to_string(),
606 )
607 } else {
608 (
609 ContextPlanAction::Omit,
610 "empty_state_projection".to_string(),
611 )
612 }
613 }
614 ContextEntrySource::History => {
615 let decision = &trace.history[entry.ordinal as usize];
616 (decision.action, decision.reason.to_string())
617 }
618 };
619 ContextSelection {
620 entry_id: entry.entry_id.clone(),
621 action,
622 reason,
623 }
624 })
625 .collect();
626 let cache_prefix = if let Some(entries) = rendered_projection.frozen_prefix_len {
627 let prefix_turns = rendered_history.get(..entries).unwrap_or(rendered_history);
628 let prefix_bytes = crate::runtime::kernel::wire::record::canonical_bytes(&(
629 &rendered_projection.system_stable,
630 &rendered_projection.system_knowledge,
631 prefix_turns,
632 ))
633 .map_err(|error| ContextContractError::Canonical(error.to_string()))?;
634 Some(crate::context::execution::CachePrefixBoundary {
635 digest: crate::evolution::ContentDigest::from_bytes(prefix_bytes.as_slice()),
636 entries: prefix_turns.len() as u32,
637 })
638 } else {
639 None
640 };
641 let knowledge_lifecycle = self
642 .partitions
643 .knowledge
644 .entries
645 .iter()
646 .map(|entry| {
647 let pending = entry
648 .pending
649 .as_ref()
650 .map(|pending| {
651 super::execution::message_digest(&pending.0, &self.handles)
652 .map(|content| (content, pending.1))
653 })
654 .transpose()?;
655 Ok((
656 &entry.key,
657 entry.pinned,
658 entry.evict_at_boundary,
659 pending,
660 entry.use_count,
661 entry.last_used_step,
662 entry.tokens,
663 ))
664 })
665 .collect::<Result<Vec<_>, ContextContractError>>()?;
666 let runtime_bytes = crate::runtime::kernel::wire::record::canonical_bytes(&(
667 &self.handles,
668 &self.partitions.system.measurements,
669 &self.partitions.history.measurements,
670 knowledge_lifecycle,
671 self.knowledge_reference_step,
672 self.knowledge_budget_warned,
673 self.frozen_history_len,
674 ))
675 .map_err(|error| ContextContractError::Canonical(error.to_string()))?;
676 let runtime_inputs = crate::evolution::ContentDigest::from_bytes(runtime_bytes.as_slice());
677 Ok((
678 ContextCandidate {
679 schema: super::execution::CONTEXT_SCHEMA.to_string(),
680 operation_id,
681 step_id,
682 input_sequence,
683 state,
684 runtime_inputs,
685 policy_digest,
686 rendered_snapshot,
687 selections,
688 input_budget_tokens: self.available_input_tokens(),
689 projected_tokens: trace.projected_tokens,
690 pressure_ppm: (self.rho().clamp(0.0, 1.0) * 1_000_000.0).round() as u32,
691 cache_prefix,
692 },
693 rendered_projection,
694 ))
695 }
696
697 pub fn push_history(&mut self, msg: CoreMessage, tokens: u32) {
700 self.state_generation = self.state_generation.saturating_add(1);
701 self.knowledge_reference_step = self.knowledge_reference_step.saturating_add(1);
702 self.partitions
703 .knowledge
704 .observe_references(&msg, self.knowledge_reference_step);
705 if let Content::Parts(parts) = &msg.content {
709 for part in parts {
710 if let ContentPart::ToolResult {
711 call_id, output, ..
712 } = part
713 {
714 let id = self.alloc_handle_id();
715 let tok = self.engine.count(output).max(1);
716 self.handles.insert(Handle::resident_for(
717 id,
718 HandleKind::ToolResult,
719 tok,
720 call_id.clone(),
721 ));
722 }
723 }
724 }
725 self.partitions.history.push(msg, tokens);
726 }
727
728 fn alloc_handle_id(&mut self) -> HandleId {
729 let id = self.next_handle_id;
730 self.next_handle_id = self.next_handle_id.wrapping_add(1);
731 id
732 }
733
734 pub fn next_handle_id(&self) -> HandleId {
739 self.next_handle_id
740 }
741
742 pub fn restore_next_handle_id(&mut self, next: HandleId) {
748 self.next_handle_id = next;
749 }
750
751 pub fn frozen_history_len(&self) -> usize {
752 self.frozen_history_len
753 }
754
755 pub(crate) fn knowledge_checkpoint_state(&self) -> (u64, bool) {
756 (self.knowledge_reference_step, self.knowledge_budget_warned)
757 }
758
759 pub(crate) fn restore_knowledge_checkpoint_state(&mut self, reference_step: u64, warned: bool) {
760 self.knowledge_reference_step = reference_step;
761 self.knowledge_budget_warned = warned;
762 }
763
764 pub fn state_generation(&self) -> u64 {
765 self.state_generation
766 }
767
768 pub fn restore_state_generation(&mut self, generation: u64) {
769 self.state_generation = generation;
770 }
771
772 pub fn restore_frozen_history_len(&mut self, len: usize) -> bool {
773 if len > self.partitions.history.messages.len() {
774 return false;
775 }
776 self.frozen_history_len = len;
777 true
778 }
779
780 pub fn push_knowledge(&mut self, msg: CoreMessage, tokens: u32) {
782 self.state_generation = self.state_generation.saturating_add(1);
783 self.partitions.knowledge.push(msg, tokens);
784 }
785
786 pub fn push_knowledge_entry(
790 &mut self,
791 key: Option<CompactString>,
792 msg: CoreMessage,
793 tokens: u32,
794 pinned: bool,
795 ) {
796 self.state_generation = self.state_generation.saturating_add(1);
797 self.partitions
798 .knowledge
799 .push_entry(key, msg, tokens, pinned);
800 }
801
802 pub fn remove_knowledge(&mut self, key: &str) -> bool {
805 let removed = self.partitions.knowledge.remove(key);
806 if removed {
807 self.state_generation = self.state_generation.saturating_add(1);
808 }
809 removed
810 }
811
812 fn sweep_knowledge_at_boundary(&mut self) {
816 let sweep = self.partitions.knowledge.sweep_at_boundary();
817 if sweep.changed {
818 self.state_generation = self.state_generation.saturating_add(1);
819 if !sweep.removed_keys.is_empty() {
823 self.partitions.signals.push(format!(
824 "[KNOWLEDGE] entries removed at this boundary: {} — re-fetch via the memory tool if still needed.",
825 sweep.removed_keys.join(", ")
826 ));
827 }
828 self.pending_knowledge_sweeps.push(sweep);
829 }
830 self.knowledge_budget_warned = false;
832 }
833
834 pub fn enforce_knowledge_budget(&mut self) -> Option<(u32, u32)> {
843 let ratio = self.config.knowledge_budget_ratio;
844 if ratio <= 0.0 {
845 return None;
846 }
847 let budget = (self.max_tokens as f64 * ratio) as u32;
848 let used = self.partitions.knowledge.token_count;
849 if used <= budget {
850 return None;
851 }
852 let marked: u32 = self
853 .partitions
854 .knowledge
855 .entries
856 .iter()
857 .filter(|e| e.evict_at_boundary)
858 .map(|e| e.tokens)
859 .sum();
860 let mut projected = used.saturating_sub(marked);
861 let mut candidates = self
862 .partitions
863 .knowledge
864 .entries
865 .iter()
866 .enumerate()
867 .filter(|(_, entry)| {
868 !entry.evict_at_boundary
869 && !entry.pinned
870 && !entry
871 .key
872 .as_deref()
873 .is_some_and(|key| key.starts_with("skill:"))
874 })
875 .map(|(index, _)| {
876 let score = self
877 .partitions
878 .knowledge
879 .retention_score(index, self.knowledge_reference_step)
880 .unwrap_or(i64::MIN);
881 (score, index)
882 })
883 .collect::<Vec<_>>();
884 candidates.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
885 for (_, index) in candidates {
886 if projected <= budget {
887 break;
888 }
889 let entry = &mut self.partitions.knowledge.entries[index];
890 entry.evict_at_boundary = true;
891 projected = projected.saturating_sub(entry.tokens);
892 }
893 if self.knowledge_budget_warned {
894 return None;
895 }
896 self.knowledge_budget_warned = true;
897 Some((used, budget))
898 }
899
900 pub fn take_knowledge_sweeps(&mut self) -> Vec<crate::context::partitions::KnowledgeSweep> {
902 std::mem::take(&mut self.pending_knowledge_sweeps)
903 }
904
905 pub fn push_signal(&mut self, text: String) {
909 self.state_generation = self.state_generation.saturating_add(1);
910 self.partitions.signals.push(text);
911 }
912
913 pub fn record_directive(&mut self, text: impl Into<String>) {
917 self.state_generation = self.state_generation.saturating_add(1);
918 self.partitions.task_state.record_directive(text);
919 }
920
921 pub fn init_task(&mut self, goal: String, criteria: Vec<String>) {
924 self.state_generation = self.state_generation.saturating_add(1);
925 self.partitions.task_state = TaskState {
926 goal,
927 criteria,
928 ..Default::default()
929 };
930 }
931
932 pub fn update_task(&mut self, update: TaskUpdate) {
933 self.state_generation = self.state_generation.saturating_add(1);
934 self.partitions.task_state.apply(update);
935 }
936
937 pub fn note_tool_actions(&mut self, calls: &[(String, String)]) {
944 self.state_generation = self.state_generation.saturating_add(1);
945 let summary = calls
946 .iter()
947 .filter(|(name, _)| !is_meta_tool(name))
948 .map(|(name, args)| {
949 if args.is_empty() {
950 name.clone()
951 } else {
952 format!("{name}({args})")
953 }
954 })
955 .collect::<Vec<_>>()
956 .join(", ");
957 self.partitions.task_state.note_actions(summary);
958 }
959
960 pub fn set_available_skills(&mut self, skills: Vec<SkillMetadata>) {
965 self.capabilities.remove_kind(CapabilityKind::Skill);
966 for skill in &skills {
967 self.capabilities.add_skill(skill.clone());
968 }
969 self.skills.set_available(skills);
970 }
971
972 pub fn set_stable_core_tools(&mut self, ids: impl IntoIterator<Item = CompactString>) {
974 self.stable_core_tools = ids.into_iter().collect();
975 }
976
977 pub fn activate_skill(&mut self, name: impl Into<CompactString>) -> bool {
982 self.activate_skill_leased(name, None)
983 }
984
985 pub fn activate_skill_leased(
988 &mut self,
989 name: impl Into<CompactString>,
990 expires_at_turn: Option<u32>,
991 ) -> bool {
992 self.active_skills
993 .insert(name.into(), expires_at_turn)
994 .is_none()
995 }
996
997 pub fn deactivate_skill(&mut self, name: &str) -> bool {
1001 if self.active_skills.remove(name).is_none() {
1002 return false;
1003 }
1004 self.partitions.knowledge.remove(&format!("skill:{name}"));
1005 true
1006 }
1007
1008 pub fn sweep_expired_skill_leases(&mut self, current_turn: u32) {
1011 let expired: Vec<CompactString> = self
1012 .active_skills
1013 .iter()
1014 .filter(|(_, lease)| lease.is_some_and(|t| current_turn >= t))
1015 .map(|(name, _)| name.clone())
1016 .collect();
1017 for name in expired {
1018 self.deactivate_skill(&name);
1019 self.state_generation = self.state_generation.saturating_add(1);
1021 self.partitions.signals.push(format!(
1022 "[SKILL] lease expired: {name} unloaded; the full toolset is restored."
1023 ));
1024 }
1025 }
1026
1027 pub fn active_skill_tool_filter(&self) -> Option<std::collections::HashSet<CompactString>> {
1033 if self.active_skills.is_empty() {
1034 return None;
1035 }
1036 let mut union = std::collections::HashSet::new();
1037 for name in self.active_skills.keys() {
1038 let declared = self.skills.allowed_tools(name);
1039 if declared.is_empty() {
1040 return None; }
1042 union.extend(declared.iter().cloned());
1043 }
1044 Some(union)
1045 }
1046
1047 pub fn active_skill_capabilities(&self) -> Vec<Capability> {
1051 self.active_skills
1052 .keys()
1053 .flat_map(|name| self.skills.capability_grants(name).iter().cloned())
1054 .collect()
1055 }
1056
1057 pub fn skill_capability_grants(&self, name: &str) -> &[Capability] {
1058 self.skills.capability_grants(name)
1059 }
1060
1061 pub fn skill_tool_schema(&self) -> Option<ToolSchema> {
1062 self.skills.build_tool_schema()
1063 }
1064
1065 pub fn skill_available(&self, name: &str) -> bool {
1068 self.skills.is_available(name)
1069 }
1070
1071 pub fn set_memory_enabled(&mut self, enabled: bool) {
1074 self.memory_enabled = enabled;
1075 if enabled {
1076 self.capabilities.add_marker(
1077 CapabilityKind::Memory,
1078 MEMORY_TOOL_NAME,
1079 "Search long-term memory through the memory meta-tool.",
1080 );
1081 } else {
1082 self.capabilities
1083 .remove(CapabilityKind::Memory, MEMORY_TOOL_NAME);
1084 }
1085 }
1086
1087 pub fn set_knowledge_enabled(&mut self, enabled: bool) {
1088 self.knowledge_enabled = enabled;
1089 if enabled {
1090 self.capabilities.add_marker(
1091 CapabilityKind::Knowledge,
1092 KNOWLEDGE_TOOL_NAME,
1093 "Search external knowledge through the knowledge meta-tool.",
1094 );
1095 } else {
1096 self.capabilities
1097 .remove(CapabilityKind::Knowledge, KNOWLEDGE_TOOL_NAME);
1098 }
1099 }
1100
1101 pub fn set_plan_tool_enabled(&mut self, enabled: bool) {
1102 self.plan_tool_enabled = enabled;
1103 if enabled {
1104 self.capabilities.add_marker(
1105 CapabilityKind::Tool,
1106 "update_plan",
1107 "Update task plan and progress through the planning meta-tool.",
1108 );
1109 } else {
1110 self.capabilities
1111 .remove(CapabilityKind::Tool, "update_plan");
1112 }
1113 }
1114
1115 pub fn capability_inventory(&self) -> String {
1116 self.capabilities.format_inventory()
1117 }
1118
1119 pub fn meta_tool_schemas(&self) -> Vec<ToolSchema> {
1120 let mut tools = Vec::new();
1121 if let Some(t) = self.skill_tool_schema() {
1122 tools.push(t);
1123 }
1124 if let Some(t) = self.memory_tool_schema() {
1125 tools.push(t);
1126 }
1127 if let Some(t) = self.knowledge_tool_schema() {
1128 tools.push(t);
1129 }
1130 if let Some(t) = self.plan_tool_schema() {
1131 tools.push(t);
1132 }
1133 if let Some(t) = self.read_result_tool_schema() {
1134 tools.push(t);
1135 }
1136 tools.sort_by(|a, b| a.name.cmp(&b.name));
1137 tools
1138 }
1139
1140 pub fn read_result_tool_schema(&self) -> Option<ToolSchema> {
1146 let any_evicted = self
1147 .handles
1148 .all()
1149 .iter()
1150 .any(|h| !h.residency.occupies_context());
1151 if !any_evicted {
1152 return None;
1153 }
1154 Some(ToolSchema {
1155 name: CompactString::new(READ_RESULT_TOOL_NAME),
1156 description: "Re-read the full content of a tool result that was truncated to save \
1157 context (marked '[Output truncated: … call the read_result tool …]'). \
1158 Pass that marker's call_id; use offset/max_bytes to page through \
1159 large content."
1160 .to_string(),
1161 parameters: serde_json::json!({
1162 "type": "object",
1163 "properties": {
1164 "call_id": { "type": "string" },
1165 "offset": { "type": "integer", "description": "Byte offset to start from (default 0)." },
1166 "max_bytes": { "type": "integer", "description": "Max bytes to return (default 4000)." }
1167 },
1168 "required": ["call_id"]
1169 }),
1170 })
1171 }
1172
1173 pub fn plan_tool_schema(&self) -> Option<ToolSchema> {
1174 if !self.plan_tool_enabled {
1175 return None;
1176 }
1177 Some(ToolSchema {
1178 name: CompactString::new("update_plan"),
1179 description: "Update your task plan and progress. Call this after completing a step or when the plan changes.".to_string(),
1180 parameters: serde_json::json!({
1181 "type": "object",
1182 "properties": {
1183 "plan": { "type": "array", "items": { "type": "string" } },
1184 "current_step": { "type": "integer" },
1185 "progress": { "type": "string" },
1186 "blocked_on": { "type": "array", "items": { "type": "string" } }
1187 }
1188 }),
1189 })
1190 }
1191
1192 pub fn memory_tool_schema(&self) -> Option<ToolSchema> {
1193 if !self.memory_enabled {
1194 return None;
1195 }
1196 Some(ToolSchema {
1197 name: CompactString::new(MEMORY_TOOL_NAME),
1198 description:
1199 "Search your long-term memory for relevant past experiences and knowledge."
1200 .to_string(),
1201 parameters: serde_json::json!({
1202 "type": "object",
1203 "properties": {
1204 "query": { "type": "string" },
1205 "top_k": { "type": "integer" }
1206 },
1207 "required": ["query"]
1208 }),
1209 })
1210 }
1211
1212 pub fn knowledge_tool_schema(&self) -> Option<ToolSchema> {
1213 if !self.knowledge_enabled {
1214 return None;
1215 }
1216 Some(ToolSchema {
1217 name: CompactString::new(KNOWLEDGE_TOOL_NAME),
1218 description:
1219 "Search the external knowledge base for facts, documentation, or reference data."
1220 .to_string(),
1221 parameters: serde_json::json!({
1222 "type": "object",
1223 "properties": {
1224 "query": { "type": "string" },
1225 "top_k": { "type": "integer" }
1226 },
1227 "required": ["query"]
1228 }),
1229 })
1230 }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236 use crate::context::execution::ContextPreparationRequest;
1237 use crate::context::task_state::PlanStep;
1238 use crate::evolution::ContentDigest;
1239 use crate::types::message::CoreMessage;
1240 use crate::types::skill::SkillMetadata;
1241
1242 #[test]
1243 fn preparation_freezes_state_plan_and_render_identity() {
1244 let mut mgr = ContextManager::new(100_000);
1245 mgr.init_task("verify context".to_string(), vec![]);
1246 mgr.push_history(CoreMessage::user("hello"), 2);
1247 let request = ContextPreparationRequest {
1248 operation_id: "op-1".to_string(),
1249 step_id: "step-1".to_string(),
1250 input_sequence: 1,
1251 policy_digest: ContentDigest::from_bytes(b"policy"),
1252 prompt_measurement: ContentDigest::from_bytes(b"measurement"),
1253 provider_route: ContentDigest::from_bytes(b"route"),
1254 };
1255 let prepared = mgr.prepare_execution_input(&request).unwrap();
1256 prepared.plan.verify(&mgr.context_state().unwrap()).unwrap();
1257 prepared.execution_input.verify(&prepared.plan).unwrap();
1258 let binding = crate::evolution::EvaluationContextBinding::from_execution_input(
1259 &prepared.execution_input,
1260 );
1261 binding.verify_digest().unwrap();
1262 assert_eq!(prepared.execution_input.operation_id, request.operation_id);
1263 assert!(!prepared.execution_input.input_digest.as_str().is_empty());
1264 mgr.push_signal("new evidence".to_string());
1265 assert!(prepared.plan.verify(&mgr.context_state().unwrap()).is_err());
1266 }
1267
1268 #[test]
1269 fn note_tool_actions_keys_on_name_and_args_so_legit_loops_dont_false_stop() {
1270 let mut mgr = ContextManager::new(100_000);
1273 mgr.init_task("process items".to_string(), vec![]);
1274 mgr.note_tool_actions(&[("step".to_string(), "{\"n\":1}".to_string())]);
1275 mgr.note_tool_actions(&[("step".to_string(), "{\"n\":2}".to_string())]);
1276 mgr.note_tool_actions(&[("step".to_string(), "{\"n\":3}".to_string())]);
1277 assert_eq!(
1278 mgr.partitions.task_state.recent_actions,
1279 ["step({\"n\":1})", "step({\"n\":2})", "step({\"n\":3})"]
1280 );
1281 let txt = mgr
1282 .render()
1283 .state_turn
1284 .unwrap()
1285 .content
1286 .as_text()
1287 .unwrap()
1288 .to_string();
1289 assert!(
1290 !txt.contains("STOP:"),
1291 "same-tool/diff-args loop must not trip STOP: {txt}"
1292 );
1293
1294 let mut mgr2 = ContextManager::new(100_000);
1296 mgr2.init_task("g".to_string(), vec![]);
1297 for _ in 0..3 {
1298 mgr2.note_tool_actions(&[("document_read".to_string(), "{\"id\":\"x\"}".to_string())]);
1299 }
1300 let txt2 = mgr2
1301 .render()
1302 .state_turn
1303 .unwrap()
1304 .content
1305 .as_text()
1306 .unwrap()
1307 .to_string();
1308 assert!(
1309 txt2.contains("STOP:"),
1310 "identical repeated call must trip STOP: {txt2}"
1311 );
1312
1313 let mut mgr3 = ContextManager::new(100_000);
1315 mgr3.init_task("g".to_string(), vec![]);
1316 mgr3.note_tool_actions(&[(
1317 "update_plan".to_string(),
1318 "{\"current_step\":1}".to_string(),
1319 )]);
1320 assert!(mgr3.partitions.task_state.recent_actions.is_empty());
1321 }
1322
1323 #[test]
1324 fn manager_renew_advances_sprint_and_keeps_goal() {
1325 let mut mgr = ContextManager::new(1_000);
1326 mgr.init_task("test goal".to_string(), vec![]);
1327 mgr.partitions.system.push(CoreMessage::system("rules"), 10);
1328 for i in 0..10 {
1329 mgr.push_history(CoreMessage::user(format!("msg {i}")), 50);
1330 }
1331 mgr.renew();
1332 assert_eq!(mgr.partitions.task_state.goal, "test goal");
1333 assert_eq!(mgr.sprint, 1);
1334 }
1335
1336 #[test]
1337 fn compress_only_touches_history() {
1338 let mut mgr = ContextManager::new(1_000);
1339 mgr.push_knowledge(CoreMessage::system("knowledge content"), 100);
1340 for _ in 0..30 {
1341 mgr.push_history(CoreMessage::user("history msg"), 50);
1342 }
1343 let knowledge_before = mgr.partitions.knowledge.token_count;
1344 let history_before = mgr.partitions.history.token_count;
1345 mgr.compress(PressureAction::AutoCompact);
1346 assert_eq!(mgr.partitions.knowledge.token_count, knowledge_before);
1347 assert!(mgr.partitions.history.token_count < history_before);
1348 }
1349
1350 #[test]
1351 fn init_task_sets_goal_and_criteria() {
1352 let mut mgr = ContextManager::new(1_000);
1353 mgr.init_task("analyse data".to_string(), vec!["criterion A".to_string()]);
1354 assert_eq!(mgr.partitions.task_state.goal, "analyse data");
1355 assert_eq!(mgr.partitions.task_state.criteria, ["criterion A"]);
1356 }
1357
1358 #[test]
1359 fn update_task_applies_plan() {
1360 let mut mgr = ContextManager::new(1_000);
1361 mgr.init_task("g".to_string(), vec![]);
1362 mgr.update_task(TaskUpdate {
1363 plan: Some(vec!["step 1".to_string(), "step 2".to_string()]),
1364 current_step: Some(0),
1365 ..Default::default()
1366 });
1367 assert_eq!(mgr.partitions.task_state.plan.len(), 2);
1368 assert_eq!(mgr.partitions.task_state.current_step, Some(0));
1369 }
1370
1371 #[test]
1372 fn task_state_survives_autocompact() {
1373 let mut mgr = ContextManager::new(1_000);
1374 mgr.init_task("survive compression".to_string(), vec![]);
1375 mgr.update_task(TaskUpdate {
1376 plan: Some(vec!["fetch data".to_string(), "analyse".to_string()]),
1377 ..Default::default()
1378 });
1379 for _ in 0..10 {
1380 mgr.push_history(CoreMessage::user("filler"), 50);
1381 }
1382 mgr.compress(PressureAction::AutoCompact);
1383 assert_eq!(mgr.partitions.task_state.goal, "survive compression");
1384 assert_eq!(mgr.partitions.task_state.plan.len(), 2);
1385 }
1386
1387 #[test]
1388 fn render_includes_task_state_in_state_turn_not_system() {
1389 let mut mgr = ContextManager::new(10_000);
1390 mgr.init_task("find anomalies".to_string(), vec![]);
1391 let rc = mgr.render();
1392 assert!(
1393 !rc.system_text.contains("[TASK STATE]"),
1394 "task_state must not be in system_text"
1395 );
1396 let state = rc.state_turn.as_ref().expect("should have a state turn");
1398 assert!(
1399 state
1400 .content
1401 .as_text()
1402 .unwrap()
1403 .contains("[TASK STATE] goal: find anomalies")
1404 );
1405 }
1406
1407 #[test]
1408 fn renewal_keeps_open_plan_steps_in_task_state() {
1409 let mut mgr = ContextManager::new(1_000);
1410 mgr.init_task("g".to_string(), vec![]);
1411 mgr.partitions.task_state.plan = vec![
1412 PlanStep {
1413 label: "done".to_string(),
1414 done: true,
1415 },
1416 PlanStep {
1417 label: "pending".to_string(),
1418 done: false,
1419 },
1420 ];
1421 mgr.renew();
1422 assert_eq!(mgr.partitions.task_state.open_steps(), vec!["pending"]);
1423 }
1424
1425 #[test]
1428 fn auto_compact_entry_logs_auto_compact_action() {
1429 let mut mgr = ContextManager::new(1_000);
1437 for i in 0..40 {
1438 mgr.push_history(
1439 CoreMessage::user(format!("turn {i}: {}", "ctx ".repeat(40))),
1440 200,
1441 );
1442 }
1443 let (saved, summary, _, _) = mgr.force_compress();
1444 assert!(saved > 0, "force_compress should compact a large history");
1445 assert!(
1446 summary.is_some(),
1447 "auto-compact summarizes the archived turns"
1448 );
1449 let actions: Vec<&str> = mgr
1450 .partitions
1451 .task_state
1452 .compression_log
1453 .iter()
1454 .map(|e| e.action.as_str())
1455 .collect();
1456 assert!(
1457 actions.last() == Some(&"auto_compact"),
1458 "auto-compact entry must log an auto_compact action; got {actions:?}"
1459 );
1460 }
1461
1462 #[test]
1463 fn skill_tool_schema_empty_when_no_skills() {
1464 let mgr = ContextManager::new(10_000);
1465 assert!(mgr.skill_tool_schema().is_none());
1466 }
1467
1468 #[test]
1469 fn skill_tool_schema_present_when_registered() {
1470 let mut mgr = ContextManager::new(10_000);
1471 mgr.set_available_skills(vec![SkillMetadata::new("debug", "Debug helper")]);
1472 assert!(
1473 mgr.skill_tool_schema()
1474 .unwrap()
1475 .description
1476 .contains("debug")
1477 );
1478 }
1479
1480 #[test]
1481 fn available_skills_are_reflected_in_capability_manifest() {
1482 let mut mgr = ContextManager::new(1_000);
1483 mgr.set_available_skills(vec![SkillMetadata::new("debug", "Debug helper")]);
1484 let inventory = mgr.capability_inventory();
1485 assert!(inventory.contains("debug"));
1486 assert!(inventory.contains("Debug helper"));
1487 }
1488
1489 #[test]
1490 fn toggled_meta_tools_are_reflected_in_capability_manifest() {
1491 let mut mgr = ContextManager::new(1_000);
1492 mgr.set_memory_enabled(true);
1493 assert!(mgr.capability_inventory().contains(MEMORY_TOOL_NAME));
1494 mgr.set_memory_enabled(false);
1495 assert!(!mgr.capability_inventory().contains(MEMORY_TOOL_NAME));
1496 }
1497
1498 #[test]
1499 fn meta_tool_schemas_are_sorted() {
1500 let mut mgr = ContextManager::new(1_000);
1501 mgr.set_available_skills(vec![SkillMetadata::new("debug", "Debug helper")]);
1502 mgr.set_memory_enabled(true);
1503 mgr.set_knowledge_enabled(true);
1504 let names = mgr
1505 .meta_tool_schemas()
1506 .into_iter()
1507 .map(|s| s.name.to_string())
1508 .collect::<Vec<_>>();
1509 assert_eq!(names, ["knowledge", "memory", "skill"]);
1510 }
1511
1512 #[test]
1513 fn b1_active_skill_state_and_tool_filter() {
1514 let mut mgr = ContextManager::new(1_000);
1515 let mut debug = SkillMetadata::new("debug", "Debug helper");
1516 debug.allowed_tools = vec![CompactString::new("read"), CompactString::new("grep")];
1517 let mut review = SkillMetadata::new("review", "Reviewer");
1518 review.allowed_tools = vec![CompactString::new("git_diff")];
1519 let plain = SkillMetadata::new("plain", "No tools declared"); mgr.set_available_skills(vec![debug, review, plain]);
1521
1522 assert!(mgr.active_skill_tool_filter().is_none());
1524
1525 assert!(mgr.activate_skill("debug"));
1527 assert!(!mgr.activate_skill("debug")); let f = mgr.active_skill_tool_filter().unwrap();
1531 assert_eq!(f.len(), 2);
1532 assert!(f.contains(&CompactString::new("read")) && f.contains(&CompactString::new("grep")));
1533
1534 mgr.activate_skill("review");
1536 let f = mgr.active_skill_tool_filter().unwrap();
1537 assert_eq!(f.len(), 3);
1538 assert!(f.contains(&CompactString::new("git_diff")));
1539
1540 mgr.activate_skill("plain");
1542 assert!(mgr.active_skill_tool_filter().is_none());
1543 }
1544
1545 #[test]
1546 fn active_skill_capability_grants_follow_activation_deactivation_and_lease_expiry() {
1547 use crate::types::capability::{
1548 ActionSet, Capability, CapabilityId, ConstraintSet, Principal, ResourceSelector,
1549 };
1550
1551 let grant = Capability {
1552 id: CapabilityId("read-src".into()),
1553 kind: CapabilityKind::Tool,
1554 resource: ResourceSelector("/repo/src/**".into()),
1555 actions: ActionSet(["read".into()].into_iter().collect()),
1556 constraints: ConstraintSet::default(),
1557 lease: None,
1558 delegatable: false,
1559 issuer: Principal("root".into()),
1560 };
1561 let mut review = SkillMetadata::new("review", "Review source files");
1562 review.capability_grants = vec![grant.clone()];
1563
1564 let mut mgr = ContextManager::new(1_000);
1565 mgr.set_available_skills(vec![review]);
1566 assert!(mgr.active_skill_capabilities().is_empty());
1567
1568 mgr.activate_skill("review");
1569 assert_eq!(mgr.active_skill_capabilities(), vec![grant.clone()]);
1570
1571 mgr.deactivate_skill("review");
1572 assert!(mgr.active_skill_capabilities().is_empty());
1573
1574 mgr.activate_skill_leased("review", Some(3));
1575 mgr.sweep_expired_skill_leases(2);
1576 assert_eq!(mgr.active_skill_capabilities(), vec![grant]);
1577
1578 mgr.sweep_expired_skill_leases(3);
1579 assert!(mgr.active_skill_capabilities().is_empty());
1580 }
1581
1582 #[test]
1583 fn update_collapse_mode_collapses_old_tool_results_under_pressure() {
1584 let mut mgr = ContextManager::new(1_000);
1585 for i in 0..10 {
1586 let m = CoreMessage::tool(vec![ContentPart::ToolResult {
1587 call_id: format!("c{i}").into(),
1588 output: "x".repeat(40),
1589 is_error: false,
1590 durable_content: None,
1591 }]);
1592 mgr.push_history(m, 40);
1593 }
1594 mgr.set_observed_prompt_tokens(950); assert!(mgr.rho() >= mgr.config.collapse_threshold);
1597
1598 mgr.recompute_handle_residency();
1599 assert_eq!(
1601 mgr.handles.residency_for_source("c0"),
1602 Some(&Residency::Collapsed)
1603 );
1604 assert_eq!(
1605 mgr.handles.residency_for_source("c9"),
1606 Some(&Residency::Resident)
1607 );
1608
1609 mgr.set_observed_prompt_tokens(100); mgr.recompute_handle_residency();
1613 assert_eq!(
1614 mgr.handles.residency_for_source("c0"),
1615 Some(&Residency::Collapsed),
1616 "collapse is sticky until a compaction boundary"
1617 );
1618
1619 mgr.reset_collapse_generation();
1621 assert_eq!(
1622 mgr.handles.residency_for_source("c0"),
1623 Some(&Residency::Resident)
1624 );
1625 }
1626
1627 #[test]
1628 fn frozen_prefix_len_anchors_at_compaction_and_holds_across_appends() {
1629 let mut mgr = ContextManager::new(1_000);
1630 for i in 0..30 {
1632 mgr.push_history(
1633 CoreMessage::user(format!("turn {i}: {}", "ctx ".repeat(30))),
1634 150,
1635 );
1636 }
1637 assert!(
1638 mgr.render().frozen_prefix_len.is_none(),
1639 "no frozen region before any compaction"
1640 );
1641
1642 let (saved, _, archived, _) = mgr.compress(PressureAction::AutoCompact);
1643 assert!(saved > 0 && !archived.is_empty(), "expected archival");
1644
1645 assert!(
1647 mgr.render().frozen_prefix_len.is_none(),
1648 "deep == tail right after compaction"
1649 );
1650
1651 mgr.push_history(CoreMessage::user("new 1"), 5);
1653 let f1 = mgr
1654 .render()
1655 .frozen_prefix_len
1656 .expect("frozen region exists once the tail grows");
1657 mgr.push_history(CoreMessage::assistant("reply 1"), 5);
1658 mgr.push_history(CoreMessage::user("new 2"), 5);
1659 let rc = mgr.render();
1660 let f2 = rc.frozen_prefix_len.expect("frozen region holds");
1661 assert_eq!(
1662 f1, f2,
1663 "the deep boundary is fixed between compactions; only the tail grows"
1664 );
1665 assert!(
1666 f2 < rc.turns.len(),
1667 "deep boundary is distinct from the rolling tail"
1668 );
1669 }
1670
1671 #[test]
1672 fn frozen_boundary_holds_through_a_prefix_safe_compaction() {
1673 let mut mgr = ContextManager::new(10_000);
1676 for i in 0..5 {
1677 mgr.push_history(CoreMessage::user(format!("m{i}")), 5);
1678 }
1679 mgr.frozen_history_len = 3; let (_, _, _, cache_at) = mgr.compress(PressureAction::None);
1684 assert!(cache_at.is_none(), "no-op compaction is prefix-safe");
1685 assert_eq!(
1686 mgr.frozen_history_len, 3,
1687 "prefix-safe compaction preserves the deep-cache anchor"
1688 );
1689 }
1690
1691 #[test]
1692 fn collapse_generation_resets_on_autocompact() {
1693 let mut mgr = ContextManager::new(1_000);
1694 for i in 0..20 {
1697 mgr.push_history(tool_result_msg(&format!("c{i}"), &"x".repeat(120)), 60);
1698 }
1699 mgr.set_observed_prompt_tokens(980); mgr.recompute_handle_residency();
1701 assert_eq!(
1702 mgr.handles.residency_for_source("c0"),
1703 Some(&Residency::Collapsed)
1704 );
1705
1706 let (saved, _, archived, _) = mgr.compress(PressureAction::AutoCompact);
1707 assert!(saved > 0 && !archived.is_empty(), "expected archival");
1708
1709 for h in mgr.handles.all() {
1712 if matches!(h.kind, HandleKind::ToolResult) {
1713 assert_eq!(
1714 h.residency,
1715 Residency::Resident,
1716 "generation reset un-collapses survivors"
1717 );
1718 }
1719 }
1720 }
1721
1722 #[test]
1723 fn push_history_indexes_tool_results_as_resident_handles() {
1724 let mut mgr = ContextManager::new(10_000);
1725 let msg = CoreMessage::tool(vec![ContentPart::ToolResult {
1726 call_id: "call_1".into(),
1727 output: "the tool output".to_string(),
1728 is_error: false,
1729 durable_content: None,
1730 }]);
1731 mgr.push_history(msg, 20);
1732 assert_eq!(mgr.handles.all().len(), 1);
1734 assert_eq!(
1735 mgr.handles.residency_for_source("call_1"),
1736 Some(&Residency::Resident)
1737 );
1738 mgr.push_history(CoreMessage::user("hello"), 5);
1740 assert_eq!(mgr.handles.all().len(), 1);
1741 }
1742
1743 fn tool_result_msg(call_id: &str, output: &str) -> CoreMessage {
1746 CoreMessage::tool(vec![ContentPart::ToolResult {
1747 call_id: call_id.into(),
1748 output: output.to_string(),
1749 is_error: false,
1750 durable_content: None,
1751 }])
1752 }
1753
1754 #[test]
1755 fn prune_orphaned_handles_drops_handles_whose_message_left_history() {
1756 let mut mgr = ContextManager::new(10_000);
1757 mgr.push_history(tool_result_msg("c0", "out 0"), 20);
1758 mgr.push_history(tool_result_msg("c1", "out 1"), 20);
1759 assert_eq!(mgr.handles.all().len(), 2);
1760
1761 mgr.partitions.history.messages.remove(0);
1763 mgr.prune_orphaned_handles();
1764
1765 assert_eq!(mgr.handles.all().len(), 1);
1767 assert!(mgr.handles.residency_for_source("c0").is_none());
1768 assert_eq!(
1769 mgr.handles.residency_for_source("c1"),
1770 Some(&Residency::Resident)
1771 );
1772 }
1773
1774 #[test]
1775 fn autocompact_prunes_handles_for_archived_tool_results() {
1776 let mut mgr = ContextManager::new(1_000);
1777 for i in 0..30 {
1779 mgr.push_history(tool_result_msg(&format!("c{i}"), &"x".repeat(200)), 80);
1780 }
1781 assert_eq!(mgr.handles.all().len(), 30);
1782
1783 let (saved, _, archived, _) = mgr.compress(PressureAction::AutoCompact);
1784 assert!(saved > 0 && !archived.is_empty(), "expected archival");
1785
1786 let live_tool_results = mgr
1789 .partitions
1790 .history
1791 .messages
1792 .iter()
1793 .filter(|m| {
1794 matches!(&m.content, Content::Parts(p)
1795 if p.iter().any(|x| matches!(x, ContentPart::ToolResult { .. })))
1796 })
1797 .count();
1798 assert_eq!(mgr.handles.all().len(), live_tool_results);
1799 assert!(
1800 mgr.handles.all().len() < 30,
1801 "table must shrink with archival"
1802 );
1803 }
1804
1805 #[test]
1806 fn renew_prunes_handles_for_dropped_history() {
1807 let mut mgr = ContextManager::new(1_000);
1808 mgr.init_task("g".to_string(), vec![]);
1809 for i in 0..20 {
1810 mgr.push_history(tool_result_msg(&format!("c{i}"), "data"), 60);
1811 }
1812 mgr.renew();
1813 for h in mgr.handles.all() {
1815 if let Some(src) = h.source.as_ref() {
1816 assert!(
1817 mgr.handles.residency_for_source(src).is_some(),
1818 "no dangling handle survives renewal"
1819 );
1820 }
1821 }
1822 assert!(mgr.handles.all().len() <= 20);
1823 }
1824
1825 #[test]
1828 fn knowledge_budget_uses_stable_order_for_equal_value_and_warns_once() {
1829 let mut mgr = ContextManager::new(100);
1832 mgr.push_knowledge(CoreMessage::system("oldest unkeyed"), 10);
1833 mgr.push_knowledge_entry(Some("a".into()), CoreMessage::system("keyed"), 10, false);
1834 mgr.push_knowledge_entry(Some("p".into()), CoreMessage::system("pinned"), 10, true);
1835 mgr.push_knowledge_entry(
1836 Some("skill:x".into()),
1837 CoreMessage::system("skill"),
1838 10,
1839 false,
1840 );
1841
1842 let warn = mgr.enforce_knowledge_budget();
1843 assert_eq!(warn, Some((40, 25)));
1844 let e = &mgr.partitions.knowledge.entries;
1846 assert!(e[0].evict_at_boundary);
1847 assert!(e[1].evict_at_boundary);
1848 assert!(!e[2].evict_at_boundary, "pinned exempt");
1849 assert!(!e[3].evict_at_boundary, "skill pin exempt");
1850
1851 assert_eq!(mgr.enforce_knowledge_budget(), None);
1853
1854 let sweep = mgr.partitions.knowledge.sweep_at_boundary();
1856 assert_eq!(sweep.tokens_freed, 20);
1857 assert_eq!(mgr.partitions.knowledge.token_count, 20);
1858 assert_eq!(mgr.enforce_knowledge_budget(), None);
1860 }
1861
1862 #[test]
1863 fn knowledge_budget_warning_stands_when_only_exempt_weight_remains() {
1864 let mut mgr = ContextManager::new(100);
1865 mgr.push_knowledge_entry(
1866 Some("p".into()),
1867 CoreMessage::system("pinned heavy"),
1868 30,
1869 true,
1870 );
1871 mgr.push_knowledge_entry(
1872 Some("skill:x".into()),
1873 CoreMessage::system("skill heavy"),
1874 30,
1875 false,
1876 );
1877
1878 assert_eq!(mgr.enforce_knowledge_budget(), Some((60, 25)));
1880 assert!(
1881 mgr.partitions
1882 .knowledge
1883 .entries
1884 .iter()
1885 .all(|e| !e.evict_at_boundary)
1886 );
1887 }
1888
1889 #[test]
1890 fn knowledge_budget_retains_old_referenced_entry_over_new_irrelevant_entry() {
1891 let mut mgr = ContextManager::new(100);
1892 mgr.push_knowledge_entry(
1893 Some("project:orchid".into()),
1894 CoreMessage::system("ORCHID uses the Atlas storage engine"),
1895 10,
1896 false,
1897 );
1898 mgr.push_history(CoreMessage::user("For project:orchid keep using Atlas"), 5);
1900 mgr.push_knowledge_entry(
1901 Some("project:new".into()),
1902 CoreMessage::system("unrelated fresh material"),
1903 10,
1904 false,
1905 );
1906 mgr.push_knowledge_entry(
1907 Some("project:other".into()),
1908 CoreMessage::system("another unused reference"),
1909 10,
1910 false,
1911 );
1912
1913 assert_eq!(mgr.enforce_knowledge_budget(), Some((30, 25)));
1914 let entries = &mgr.partitions.knowledge.entries;
1915 assert!(
1916 !entries[0].evict_at_boundary,
1917 "a real reference must raise retention"
1918 );
1919 assert!(
1920 entries[1].evict_at_boundary,
1921 "lowest-value entry evicts first"
1922 );
1923 assert!(
1924 !entries[2].evict_at_boundary,
1925 "one eviction is enough to fit"
1926 );
1927 }
1928
1929 #[test]
1930 fn knowledge_budget_ratio_zero_disables() {
1931 let mut mgr = ContextManager::new(100);
1932 mgr.config.knowledge_budget_ratio = 0.0;
1933 mgr.push_knowledge(CoreMessage::system("huge"), 90);
1934 assert_eq!(mgr.enforce_knowledge_budget(), None);
1935 assert!(!mgr.partitions.knowledge.entries[0].evict_at_boundary);
1936 }
1937
1938 #[test]
1939 fn provider_and_output_reservations_reduce_the_hard_input_budget() {
1940 use crate::context::config::PromptBudgetConfig;
1941
1942 let mut mgr = ContextManager::new(100);
1943 mgr.set_prompt_budget(PromptBudgetConfig {
1944 prompt_overhead_tokens: 20,
1945 output_reserve_tokens: 20,
1946 safety_margin_tokens: 10,
1947 });
1948 mgr.partitions.system.push(
1956 CoreMessage::system(
1957 "System policy directive number seven requires strict adherence. ".repeat(10),
1958 ),
1959 60,
1960 );
1961
1962 let rendered = mgr.render();
1963 let overflow = rendered
1964 .budget_overflow
1965 .expect("fixed context exceeds input allowance");
1966 assert_eq!(overflow.max_tokens, 50);
1967 assert!(overflow.required_tokens > overflow.max_tokens);
1968 }
1969}