Skip to main content

aster/agents/error_handling/
overflow_handler.rs

1//! Context Overflow Handler Module
2//!
3//! This module provides automatic handling of context length exceeded errors
4//! by compacting the conversation and retrying the request.
5//!
6//! # Features
7//!
8//! - Automatic detection of context overflow errors
9//! - Conversation compaction with retry
10//! - Configurable retry limits
11//! - Progressive pruning integration
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use aster::agents::error_handling::OverflowHandler;
17//!
18//! let mut handler = OverflowHandler::new(2);
19//!
20//! if OverflowHandler::is_context_overflow(&error) {
21//!     let (compacted, should_retry) = handler.handle_overflow(
22//!         provider.as_ref(),
23//!         &conversation,
24//!         &session,
25//!     ).await?;
26//! }
27//! ```
28
29use crate::context_mgmt::compact_messages;
30use crate::conversation::Conversation;
31use crate::providers::base::{Provider, ProviderUsage};
32use crate::providers::errors::ProviderError;
33use crate::session::Session;
34use anyhow::Result;
35use tracing::{debug, info, warn};
36
37/// Handler for context length exceeded errors.
38///
39/// Provides automatic compaction and retry functionality when
40/// the context length limit is exceeded.
41pub struct OverflowHandler {
42    /// Whether compaction has been attempted in the current request cycle
43    compaction_attempted: bool,
44
45    /// Number of compaction attempts made
46    compaction_attempts: u32,
47
48    /// Maximum number of compaction retries allowed
49    max_retries: u32,
50}
51
52impl Default for OverflowHandler {
53    fn default() -> Self {
54        Self::new(2)
55    }
56}
57
58impl OverflowHandler {
59    /// Create a new OverflowHandler with the specified max retries.
60    ///
61    /// # Arguments
62    ///
63    /// * `max_retries` - Maximum number of compaction retries allowed
64    pub fn new(max_retries: u32) -> Self {
65        Self {
66            compaction_attempted: false,
67            compaction_attempts: 0,
68            max_retries,
69        }
70    }
71
72    /// Check if an error is a context overflow error.
73    ///
74    /// # Arguments
75    ///
76    /// * `error` - The provider error to check
77    ///
78    /// # Returns
79    ///
80    /// `true` if the error is a context length exceeded error.
81    pub fn is_context_overflow(error: &ProviderError) -> bool {
82        matches!(error, ProviderError::ContextLengthExceeded(_))
83    }
84
85    /// Check if compaction has been attempted.
86    pub fn compaction_attempted(&self) -> bool {
87        self.compaction_attempted
88    }
89
90    /// Get the number of compaction attempts made.
91    pub fn compaction_attempts(&self) -> u32 {
92        self.compaction_attempts
93    }
94
95    /// Check if more retries are allowed.
96    pub fn can_retry(&self) -> bool {
97        self.compaction_attempts < self.max_retries
98    }
99
100    /// Reset the handler state for a new request cycle.
101    pub fn reset(&mut self) {
102        self.compaction_attempted = false;
103        self.compaction_attempts = 0;
104    }
105
106    /// Handle a context overflow error by compacting the conversation.
107    ///
108    /// This method attempts to compact the conversation to reduce context size.
109    /// If compaction succeeds, the caller should retry the request with the
110    /// compacted conversation.
111    ///
112    /// # Arguments
113    ///
114    /// * `provider` - The provider to use for summarization during compaction
115    /// * `conversation` - The current conversation to compact
116    /// * `_session` - The current session (for future use)
117    ///
118    /// # Returns
119    ///
120    /// A tuple containing:
121    /// - `Conversation`: The compacted conversation
122    /// - `ProviderUsage`: Usage statistics from the compaction
123    /// - `bool`: Whether the caller should retry the request
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if:
128    /// - Maximum retries have been exceeded
129    /// - Compaction itself fails
130    pub async fn handle_overflow(
131        &mut self,
132        provider: &dyn Provider,
133        conversation: &Conversation,
134        _session: &Session,
135    ) -> Result<(Conversation, ProviderUsage, bool)> {
136        self.compaction_attempts += 1;
137        self.compaction_attempted = true;
138
139        info!(
140            "Handling context overflow (attempt {}/{})",
141            self.compaction_attempts, self.max_retries
142        );
143
144        if self.compaction_attempts > self.max_retries {
145            warn!("Maximum compaction retries ({}) exceeded", self.max_retries);
146            return Err(anyhow::anyhow!(
147                "Context limit exceeded after {} compaction attempts. \
148                 Try using a shorter message, a model with a larger context window, \
149                 or start a new session.",
150                self.max_retries
151            ));
152        }
153
154        debug!("Attempting conversation compaction");
155
156        match compact_messages(provider, conversation, false).await {
157            Ok((compacted_conversation, usage)) => {
158                info!(
159                    "Compaction successful, conversation reduced from {} to {} messages",
160                    conversation.len(),
161                    compacted_conversation.len()
162                );
163                Ok((compacted_conversation, usage, true))
164            }
165            Err(e) => {
166                warn!("Compaction failed: {}", e);
167                Err(anyhow::anyhow!("Failed to compact conversation: {}", e))
168            }
169        }
170    }
171
172    /// Handle overflow with progressive pruning.
173    ///
174    /// This method first attempts progressive pruning before falling back
175    /// to full compaction.
176    ///
177    /// # Arguments
178    ///
179    /// * `provider` - The provider to use for summarization
180    /// * `conversation` - The current conversation
181    /// * `session` - The current session
182    /// * `pruning_config` - Configuration for progressive pruning
183    ///
184    /// # Returns
185    ///
186    /// Same as `handle_overflow`.
187    pub async fn handle_overflow_with_pruning(
188        &mut self,
189        provider: &dyn Provider,
190        conversation: &Conversation,
191        session: &Session,
192        pruning_config: &crate::context::types::PruningConfig,
193    ) -> Result<(Conversation, ProviderUsage, bool)> {
194        use crate::context::pruner::ProgressivePruner;
195        use crate::providers::base::Usage;
196
197        // First try progressive pruning at hard_clear level
198        let pruned_messages = ProgressivePruner::prune_messages(
199            conversation.messages(),
200            pruning_config.hard_clear_ratio + 0.1, // Force hard clear level
201            pruning_config,
202        );
203
204        let pruned_conversation = Conversation::new_unvalidated(pruned_messages);
205
206        // Check if pruning reduced the size significantly
207        let original_len: usize = conversation
208            .messages()
209            .iter()
210            .map(|m| m.as_concat_text().len())
211            .sum();
212        let pruned_len: usize = pruned_conversation
213            .messages()
214            .iter()
215            .map(|m| m.as_concat_text().len())
216            .sum();
217
218        if pruned_len < original_len * 8 / 10 {
219            // Pruning reduced size by at least 20%
220            info!(
221                "Progressive pruning reduced context from {} to {} chars",
222                original_len, pruned_len
223            );
224            return Ok((
225                pruned_conversation,
226                ProviderUsage::new("pruning".to_string(), Usage::default()),
227                true,
228            ));
229        }
230
231        // Fall back to full compaction
232        debug!("Progressive pruning insufficient, falling back to compaction");
233        self.handle_overflow(provider, conversation, session).await
234    }
235}
236
237/// Result of an overflow handling operation.
238#[derive(Debug)]
239pub struct OverflowResult {
240    /// The compacted conversation
241    pub conversation: Conversation,
242    /// Usage statistics from compaction
243    pub usage: ProviderUsage,
244    /// Whether the request should be retried
245    pub should_retry: bool,
246    /// Number of compaction attempts made
247    pub attempts: u32,
248}
249
250// ============================================================================
251// Tests
252// ============================================================================
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_is_context_overflow() {
260        let overflow_error = ProviderError::ContextLengthExceeded("Context too long".to_string());
261        let other_error = ProviderError::ServerError("Server error".to_string());
262
263        assert!(OverflowHandler::is_context_overflow(&overflow_error));
264        assert!(!OverflowHandler::is_context_overflow(&other_error));
265    }
266
267    #[test]
268    fn test_overflow_handler_new() {
269        let handler = OverflowHandler::new(3);
270        assert_eq!(handler.max_retries, 3);
271        assert!(!handler.compaction_attempted);
272        assert_eq!(handler.compaction_attempts, 0);
273    }
274
275    #[test]
276    fn test_overflow_handler_default() {
277        let handler = OverflowHandler::default();
278        assert_eq!(handler.max_retries, 2);
279    }
280
281    #[test]
282    fn test_can_retry() {
283        let mut handler = OverflowHandler::new(2);
284
285        assert!(handler.can_retry());
286
287        handler.compaction_attempts = 1;
288        assert!(handler.can_retry());
289
290        handler.compaction_attempts = 2;
291        assert!(!handler.can_retry());
292    }
293
294    #[test]
295    fn test_reset() {
296        let mut handler = OverflowHandler::new(2);
297        handler.compaction_attempted = true;
298        handler.compaction_attempts = 2;
299
300        handler.reset();
301
302        assert!(!handler.compaction_attempted);
303        assert_eq!(handler.compaction_attempts, 0);
304    }
305
306    #[test]
307    fn test_compaction_attempted() {
308        let mut handler = OverflowHandler::new(2);
309        assert!(!handler.compaction_attempted());
310
311        handler.compaction_attempted = true;
312        assert!(handler.compaction_attempted());
313    }
314}