Skip to main content

ai_agent/services/compact/
api_microcompact.rs

1// Source: ~/claudecode/openclaudecode/src/services/compact/apiMicrocompact.ts
2//! API context management - native API context editing strategies.
3//!
4//! Generates ContextManagementConfig with ContextEditStrategy entries for
5//! the API's native context editing feature.
6
7use 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
14/// Default values for context management strategies
15const DEFAULT_MAX_INPUT_TOKENS: usize = 180_000;
16const DEFAULT_TARGET_INPUT_TOKENS: usize = 40_000;
17
18/// Tools whose results can be cleared
19fn 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
31/// Tools whose uses can be cleared (edit-type tools)
32fn 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/// Context edit strategy type
41#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43pub enum ContextEditStrategy {
44    /// Clear old tool uses/results based on input token thresholds
45    ClearToolUses20250919 {
46        /// Trigger threshold
47        #[serde(skip_serializing_if = "Option::is_none")]
48        trigger: Option<TriggerConfig>,
49        /// How many tool results to keep
50        #[serde(skip_serializing_if = "Option::is_none")]
51        keep: Option<KeepConfig>,
52        /// Which tools to clear results for
53        #[serde(skip_serializing_if = "Option::is_none")]
54        clear_tool_inputs: Option<Vec<String>>,
55        /// Which tools to exclude from clearing
56        #[serde(skip_serializing_if = "Option::is_none")]
57        exclude_tools: Option<Vec<String>>,
58        /// Minimum amount to clear
59        #[serde(skip_serializing_if = "Option::is_none")]
60        clear_at_least: Option<TriggerConfig>,
61    },
62    /// Clear thinking blocks, optionally keeping last N turns
63    ClearThinking20251015 {
64        /// How many thinking turns to keep
65        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/// Context management configuration wrapper
95#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
96pub struct ContextManagementConfig {
97    pub edits: Vec<ContextEditStrategy>,
98}
99
100/// Options for API context management
101pub 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
117/// Get API context management configuration.
118/// Returns None if no context management strategies are applicable.
119pub 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    // Preserve thinking blocks in previous assistant turns
126    // Skip when redact-thinking is active (redacted blocks have no model-visible content)
127    // When clear_all_thinking is set (>1h idle = cache miss), keep only the last thinking turn
128    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    // Tool clearing strategies - only for ant builds
141    // For external builds, skip tool clearing
142    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 no tool clearing strategy is enabled, return early
148    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        // Without thinking and without env vars, should return None
230        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        // Should have thinking clear strategy
241        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        // Should have thinking clear with value: 1
261        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}