ai_agent/services/compact/
api_microcompact.rs1use crate::tools::config_tools::{
8 BASH_TOOL_NAME, FILE_EDIT_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_WRITE_TOOL_NAME, GLOB_TOOL_NAME,
9 GREP_TOOL_NAME, NOTEBOOK_EDIT_TOOL_NAME, POWERSHELL_TOOL_NAME, WEB_FETCH_TOOL_NAME,
10 WEB_SEARCH_TOOL_NAME,
11};
12use crate::utils::env_utils;
13
14const DEFAULT_MAX_INPUT_TOKENS: usize = 180_000;
16const DEFAULT_TARGET_INPUT_TOKENS: usize = 40_000;
17
18fn tools_clearable_results() -> Vec<&'static str> {
20 vec![
21 BASH_TOOL_NAME,
22 POWERSHELL_TOOL_NAME,
23 GLOB_TOOL_NAME,
24 GREP_TOOL_NAME,
25 FILE_READ_TOOL_NAME,
26 WEB_FETCH_TOOL_NAME,
27 WEB_SEARCH_TOOL_NAME,
28 ]
29}
30
31fn tools_clearable_uses() -> Vec<&'static str> {
33 vec![
34 FILE_EDIT_TOOL_NAME,
35 FILE_WRITE_TOOL_NAME,
36 NOTEBOOK_EDIT_TOOL_NAME,
37 ]
38}
39
40#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43pub enum ContextEditStrategy {
44 ClearToolUses20250919 {
46 #[serde(skip_serializing_if = "Option::is_none")]
48 trigger: Option<TriggerConfig>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 keep: Option<KeepConfig>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 clear_tool_inputs: Option<Vec<String>>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 exclude_tools: Option<Vec<String>>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 clear_at_least: Option<TriggerConfig>,
61 },
62 ClearThinking20251015 {
64 keep: ThinkingKeepConfig,
66 },
67}
68
69#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
70pub struct TriggerConfig {
71 #[serde(rename = "type")]
72 pub trigger_type: String,
73 pub value: usize,
74}
75
76#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
77pub struct KeepConfig {
78 #[serde(rename = "type")]
79 pub keep_type: String,
80 pub value: usize,
81}
82
83#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
84#[serde(untagged)]
85pub enum ThinkingKeepConfig {
86 KeepAll,
87 KeepTurns {
88 #[serde(rename = "type")]
89 keep_type: String,
90 value: usize,
91 },
92}
93
94#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
96pub struct ContextManagementConfig {
97 pub edits: Vec<ContextEditStrategy>,
98}
99
100pub struct ContextManagementOptions {
102 pub has_thinking: bool,
103 pub is_redact_thinking_active: bool,
104 pub clear_all_thinking: bool,
105}
106
107impl Default for ContextManagementOptions {
108 fn default() -> Self {
109 Self {
110 has_thinking: false,
111 is_redact_thinking_active: false,
112 clear_all_thinking: false,
113 }
114 }
115}
116
117pub fn get_api_context_management(
120 options: Option<ContextManagementOptions>,
121) -> Option<ContextManagementConfig> {
122 let opts = options.unwrap_or_default();
123 let mut strategies = Vec::new();
124
125 if opts.has_thinking && !opts.is_redact_thinking_active {
129 let keep = if opts.clear_all_thinking {
130 ThinkingKeepConfig::KeepTurns {
131 keep_type: "thinking_turns".to_string(),
132 value: 1,
133 }
134 } else {
135 ThinkingKeepConfig::KeepAll
136 };
137 strategies.push(ContextEditStrategy::ClearThinking20251015 { keep });
138 }
139
140 let use_clear_tool_results =
143 env_utils::is_env_truthy(std::env::var("USE_API_CLEAR_TOOL_RESULTS").ok().as_deref());
144 let use_clear_tool_uses =
145 env_utils::is_env_truthy(std::env::var("USE_API_CLEAR_TOOL_USES").ok().as_deref());
146
147 if !use_clear_tool_results && !use_clear_tool_uses {
149 if strategies.is_empty() {
150 return None;
151 }
152 return Some(ContextManagementConfig { edits: strategies });
153 }
154
155 if use_clear_tool_results {
156 let trigger_threshold = std::env::var("API_MAX_INPUT_TOKENS")
157 .ok()
158 .and_then(|v| v.parse::<usize>().ok())
159 .unwrap_or(DEFAULT_MAX_INPUT_TOKENS);
160 let keep_target = std::env::var("API_TARGET_INPUT_TOKENS")
161 .ok()
162 .and_then(|v| v.parse::<usize>().ok())
163 .unwrap_or(DEFAULT_TARGET_INPUT_TOKENS);
164
165 strategies.push(ContextEditStrategy::ClearToolUses20250919 {
166 trigger: Some(TriggerConfig {
167 trigger_type: "input_tokens".to_string(),
168 value: trigger_threshold,
169 }),
170 keep: None,
171 clear_tool_inputs: Some(
172 tools_clearable_results()
173 .into_iter()
174 .map(|s| s.to_string())
175 .collect(),
176 ),
177 exclude_tools: None,
178 clear_at_least: Some(TriggerConfig {
179 trigger_type: "input_tokens".to_string(),
180 value: trigger_threshold.saturating_sub(keep_target),
181 }),
182 });
183 }
184
185 if use_clear_tool_uses {
186 let trigger_threshold = std::env::var("API_MAX_INPUT_TOKENS")
187 .ok()
188 .and_then(|v| v.parse::<usize>().ok())
189 .unwrap_or(DEFAULT_MAX_INPUT_TOKENS);
190 let keep_target = std::env::var("API_TARGET_INPUT_TOKENS")
191 .ok()
192 .and_then(|v| v.parse::<usize>().ok())
193 .unwrap_or(DEFAULT_TARGET_INPUT_TOKENS);
194
195 strategies.push(ContextEditStrategy::ClearToolUses20250919 {
196 trigger: Some(TriggerConfig {
197 trigger_type: "input_tokens".to_string(),
198 value: trigger_threshold,
199 }),
200 keep: None,
201 clear_tool_inputs: None,
202 exclude_tools: Some(
203 tools_clearable_uses()
204 .into_iter()
205 .map(|s| s.to_string())
206 .collect(),
207 ),
208 clear_at_least: Some(TriggerConfig {
209 trigger_type: "input_tokens".to_string(),
210 value: trigger_threshold.saturating_sub(keep_target),
211 }),
212 });
213 }
214
215 if strategies.is_empty() {
216 None
217 } else {
218 Some(ContextManagementConfig { edits: strategies })
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_get_api_context_management_no_thinking() {
228 let result = get_api_context_management(None);
229 assert!(result.is_none());
231 }
232
233 #[test]
234 fn test_get_api_context_management_with_thinking() {
235 let opts = ContextManagementOptions {
236 has_thinking: true,
237 ..Default::default()
238 };
239 let result = get_api_context_management(Some(opts));
240 assert!(result.is_some());
242 let config = result.unwrap();
243 assert!(!config.edits.is_empty());
244 assert!(matches!(
245 &config.edits[0],
246 ContextEditStrategy::ClearThinking20251015 { .. }
247 ));
248 }
249
250 #[test]
251 fn test_get_api_context_management_clear_all_thinking() {
252 let opts = ContextManagementOptions {
253 has_thinking: true,
254 clear_all_thinking: true,
255 ..Default::default()
256 };
257 let result = get_api_context_management(Some(opts));
258 assert!(result.is_some());
259 let config = result.unwrap();
260 match &config.edits[0] {
262 ContextEditStrategy::ClearThinking20251015 { keep } => match keep {
263 ThinkingKeepConfig::KeepTurns { value, .. } => {
264 assert_eq!(*value, 1);
265 }
266 _ => panic!("Expected KeepTurns"),
267 },
268 _ => panic!("Expected ClearThinking20251015"),
269 }
270 }
271}