vtcode_core/compaction/
config.rs1use super::*;
4
5const DEFAULT_COMPACTION_TARGET_THRESHOLD: f64 = 0.50;
6const DEFAULT_COMPACTION_KEEP_LAST_MESSAGES: usize = 10;
7const DEFAULT_RETAINED_USER_MESSAGE_TOKENS: usize = 20_000;
8const DEFAULT_RETAINED_USER_MESSAGES: usize = 6;
9pub(crate) const CONTINUITY_TAIL_TARGET_TOKENS: usize = 20_000;
13pub(crate) const COMPACTION_CONTEXT_OVERHEAD_FRACTION_DENOMINATOR: usize = 8;
16pub(crate) const COMPACTION_CONTEXT_FIXED_OVERHEAD_TOKENS: usize = 512;
17pub(crate) const SUMMARY_PREFIX: &str = "Previous conversation summary:\n";
18pub(crate) const ABSTRACT_PREFIX: &str = "Earlier context (abstract):\n";
19pub(crate) const DETAIL_PREFIX: &str = "Recent context (summary):\n";
20
21const DEFAULT_SUMMARY_PROMPT: &str = "Summarize the conversation so far using this exact structure. The goal is continuity: after reading this summary, the next context must feel like a seamless continuation of the same task, not a fresh start.\n\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements, preferences, or constraints from the user]\n\n## What I Was Just Doing\n[The single most recent action in progress: what step the agent was executing, which tool or edit was underway, and where it left off. This is the continuity anchor.]\n\n## Last Action & Result\n[The last completed action and its outcome (success, error, or partial). Include the exact error or status if relevant.]\n\n## Progress\n### Done\n- [Completed work]\n\n### In Progress\n- [Current work]\n\n### Blocked\n- [Blocking issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Reason]\n\n## Next Steps\n1. [Most important next step]\n\n## Critical Context\n- [Facts needed to continue]\n\nKeep it concise and actionable. Always preserve the current task objective and acceptance criteria, file paths that were read or modified, test results and error messages, and decisions with their reasoning.";
26
27#[derive(Debug, Clone)]
29pub struct CompactionConfig {
30 pub trigger_threshold: f64,
32 pub target_threshold: f64,
34 pub summary_prompt: String,
36 pub keep_last_messages: usize,
38 pub retained_user_message_tokens: usize,
40 pub retained_user_messages: usize,
42 pub always_summarize: bool,
44 pub hierarchical: bool,
53 pub auto_compact_suppressed: u8,
57}
58
59impl Default for CompactionConfig {
60 fn default() -> Self {
61 Self {
62 trigger_threshold: DEFAULT_COMPACTION_TRIGGER_RATIO,
63 target_threshold: DEFAULT_COMPACTION_TARGET_THRESHOLD,
64 summary_prompt: DEFAULT_SUMMARY_PROMPT.to_string(),
65 keep_last_messages: DEFAULT_COMPACTION_KEEP_LAST_MESSAGES,
66 retained_user_message_tokens: DEFAULT_RETAINED_USER_MESSAGE_TOKENS,
67 retained_user_messages: DEFAULT_RETAINED_USER_MESSAGES,
68 always_summarize: false,
69 hierarchical: false,
70 auto_compact_suppressed: SUPPRESS_NONE,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct CompactionRoutePolicy {
85 pub threshold_ratio: f64,
88 pub retain_ratio: f64,
90 pub max_overflow_retries: u32,
92}
93
94const MIN_TAIL_TARGET_TOKENS: usize = 1_024;
96
97impl Default for CompactionRoutePolicy {
98 fn default() -> Self {
99 Self {
100 threshold_ratio: 1.0,
101 retain_ratio: 0.16,
102 max_overflow_retries: 1,
103 }
104 }
105}
106
107const ROUTE_POLICIES: &[(&str, &str, CompactionRoutePolicy)] = &[];
111
112impl CompactionRoutePolicy {
113 #[must_use]
116 pub fn resolve(provider_name: &str, model: &str) -> Self {
117 ROUTE_POLICIES
118 .iter()
119 .find(|(provider, model_match, _)| provider_name.contains(provider) && model.contains(model_match))
120 .map(|(_, _, policy)| *policy)
121 .unwrap_or_default()
122 }
123
124 #[allow(
128 clippy::cast_sign_loss,
129 reason = "The ratio is clamped to [0.0, 1.0] and the window is non-negative, so the product cannot be negative."
130 )]
131 #[must_use]
132 pub fn tail_target_tokens(&self, window_tokens: usize) -> usize {
133 if window_tokens == 0 {
134 return CONTINUITY_TAIL_TARGET_TOKENS;
135 }
136 ((window_tokens as f64 * self.retain_ratio.clamp(0.0, 1.0)) as usize)
137 .clamp(MIN_TAIL_TARGET_TOKENS, CONTINUITY_TAIL_TARGET_TOKENS)
138 }
139
140 #[allow(
144 clippy::cast_sign_loss,
145 reason = "The ratio is clamped to [0.0, 1.0] and the window is non-negative, so the product cannot be negative."
146 )]
147 #[must_use]
148 pub fn apply_threshold_cap(&self, threshold_tokens: usize, window_tokens: usize) -> usize {
149 if self.threshold_ratio < 1.0 && window_tokens > 0 {
150 threshold_tokens.min((window_tokens as f64 * self.threshold_ratio.clamp(0.0, 1.0)) as usize)
151 } else {
152 threshold_tokens
153 }
154 }
155}
156
157#[must_use]
160pub(crate) fn route_tail_target_tokens(
161 provider: &dyn LLMProvider,
162 model: &str,
163 context_budget: Option<usize>,
164) -> usize {
165 let policy = CompactionRoutePolicy::resolve(provider.name(), model);
166 let window = context_budget
167 .filter(|value| *value > 0)
168 .unwrap_or_else(|| provider.effective_context_size(model));
169 policy.tail_target_tokens(window)
170}
171
172#[derive(Debug, Clone, Default)]
180pub struct CompactionParentContext {
181 pub system_prompt: Option<Arc<str>>,
183 pub tools: Option<Arc<Vec<ToolDefinition>>>,
186}
187
188impl CompactionParentContext {
189 #[must_use]
190 pub fn is_empty(&self) -> bool {
191 self.system_prompt.is_none() && self.tools.is_none()
192 }
193}
194
195#[must_use]
200pub fn build_cache_safe_compaction_history(history: &[Message], compaction_prompt: &str) -> Vec<Message> {
201 let mut forked = Vec::with_capacity(history.len().saturating_add(1));
202 forked.extend(history.iter().cloned());
203 forked.push(Message::user(compaction_prompt.to_string()));
204 forked
205}