Skip to main content

behest_runtime/compaction/
overflow.rs

1//! Compact overflow detection.
2//!
3//! Determines when the conversation context is approaching or exceeding
4//! the model's context window, triggering compaction before or after
5//! a provider turn.
6//!
7//! Ported from OpenCode V1: `packages/opencode/src/session/overflow.ts`.
8
9/// Token headroom reserved between context limit and compaction trigger.
10///
11/// This ensures the compaction LLM call itself (prompt + response) fits
12/// without overflowing. OpenCode uses 20,000 tokens.
13pub const COMPACTION_BUFFER: usize = 20_000;
14
15/// Computes the number of tokens available for conversation history.
16///
17/// The usable space is the model's context window minus:
18/// 1. The maximum output tokens the model can generate
19/// 2. Optional reserved headroom (defaults to `min(COMPACTION_BUFFER, max_output)`)
20///
21/// When `model_context` is 0 (unlimited), returns `usize::MAX`.
22///
23/// # Arguments
24/// * `model_context` — The model's maximum context window in tokens (0 = unlimited).
25/// * `max_output` — The model's maximum output tokens.
26/// * `reserved` — Explicit headroom override; `None` uses `min(COMPACTION_BUFFER, max_output)`.
27#[must_use]
28pub fn usable_tokens(model_context: u32, max_output: u32, reserved: Option<usize>) -> usize {
29    if model_context == 0 {
30        return usize::MAX;
31    }
32
33    let reserved = reserved.unwrap_or_else(|| COMPACTION_BUFFER.min(max_output as usize));
34    let context = model_context as usize;
35
36    context
37        .saturating_sub(max_output as usize)
38        .saturating_sub(reserved)
39}
40
41/// Returns `true` when the conversation history exceeds the usable context window.
42///
43/// Compaction is skipped when:
44/// - `auto_enabled` is `false`
45/// - `model_context` is 0 (no context limit)
46///
47/// # Arguments
48/// * `total_tokens` - Estimated tokens in the full message history.
49/// * `model_context` - The model's maximum context window (0 = unlimited).
50/// * `max_output` - The model's maximum output tokens.
51/// * `auto_enabled` - Whether automatic compaction is enabled.
52#[must_use]
53pub fn is_overflow(
54    total_tokens: usize,
55    model_context: u32,
56    max_output: u32,
57    auto_enabled: bool,
58) -> bool {
59    if !auto_enabled || model_context == 0 {
60        return false;
61    }
62
63    let usable = usable_tokens(model_context, max_output, None);
64    total_tokens >= usable
65}
66
67#[cfg(test)]
68#[allow(clippy::unwrap_used)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn usable_with_typical_gpt4o() {
74        // gpt-4o: 128K context, 16K output
75        let usable = usable_tokens(128_000, 16_384, None);
76        // 128_000 - 16_384 - min(20_000, 16_384) = 128_000 - 16_384 - 16_384 = 95_232
77        assert_eq!(usable, 95_232);
78    }
79
80    #[test]
81    fn usable_with_large_output() {
82        // Model with 200K context, 32K output
83        let usable = usable_tokens(200_000, 32_000, None);
84        // 200_000 - 32_000 - min(20_000, 32_000) = 200_000 - 32_000 - 20_000 = 148_000
85        assert_eq!(usable, 148_000);
86    }
87
88    #[test]
89    fn usable_with_small_output() {
90        // Model with 8K context, 4K output
91        let usable = usable_tokens(8_000, 4_000, None);
92        // 8_000 - 4_000 - min(20_000, 4_000) = 8_000 - 4_000 - 4_000 = 0
93        assert_eq!(usable, 0);
94    }
95
96    #[test]
97    fn usable_unlimited_context() {
98        assert_eq!(usable_tokens(0, 16_384, None), usize::MAX);
99    }
100
101    #[test]
102    fn usable_with_explicit_reserved() {
103        let usable = usable_tokens(128_000, 16_384, Some(10_000));
104        // 128_000 - 16_384 - 10_000 = 101_616
105        assert_eq!(usable, 101_616);
106    }
107
108    #[test]
109    fn overflow_detected() {
110        // gpt-4o: usable = 95_232
111        assert!(!is_overflow(90_000, 128_000, 16_384, true));
112        assert!(is_overflow(96_000, 128_000, 16_384, true));
113    }
114
115    #[test]
116    fn overflow_skipped_when_auto_disabled() {
117        assert!(!is_overflow(200_000, 128_000, 16_384, false));
118    }
119
120    #[test]
121    fn overflow_skipped_when_unlimited_context() {
122        assert!(!is_overflow(1_000_000, 0, 16_384, true));
123    }
124}