Skip to main content

codetether_agent/session/
event_compaction.rs

1//! Context-compaction payloads for [`SessionEvent`].
2//!
3//! These types describe the lifecycle of a single compaction attempt and
4//! its fallback cascade:
5//!
6//! ```text
7//!   CompactionStarted
8//!        │
9//!        ▼
10//!   ┌──────────┐        success        ┌────────────────────┐
11//!   │   RLM    │ ────────────────────▶ │ CompactionCompleted│
12//!   └──────────┘                       └────────────────────┘
13//!        │ exhausted / failed                   ▲
14//!        ▼                                      │ success
15//!   ┌──────────────┐   still over budget   ┌────────────┐
16//!   │chunk-compress│ ────────────────────▶ │  truncate  │
17//!   └──────────────┘                       └────────────┘
18//!        │ success                              │ still fails
19//!        ▼                                      ▼
20//!   CompactionCompleted               CompactionFailed
21//! ```
22//!
23//! [`SessionEvent`]: crate::session::SessionEvent
24
25use serde::{Deserialize, Serialize};
26use uuid::Uuid;
27
28/// Which fallback path the compaction pipeline selected.
29///
30/// Ordered from highest to lowest fidelity. Emitting this as structured
31/// telemetry (rather than a free-form `reason` string) is what lets the
32/// trace-driven self-tuning job group runs by strategy.
33///
34/// # Examples
35///
36/// ```rust
37/// use codetether_agent::session::FallbackStrategy;
38///
39/// assert_eq!(FallbackStrategy::Rlm.as_str(), "rlm");
40/// assert!(FallbackStrategy::Truncate.is_terminal());
41/// assert!(!FallbackStrategy::Rlm.is_terminal());
42/// ```
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum FallbackStrategy {
46    /// Recursive Language Model summarisation (preferred path).
47    Rlm,
48    /// Deterministic chunk-based compression (when RLM is unavailable or
49    /// exhausted without converging).
50    ChunkCompress,
51    /// Hard truncation to a fixed fraction of the budget. Terminal
52    /// successful fallback — guarantees a valid request at the cost of
53    /// silently dropping older context.
54    Truncate,
55}
56
57impl FallbackStrategy {
58    /// Stable wire-format identifier.
59    pub const fn as_str(self) -> &'static str {
60        match self {
61            Self::Rlm => "rlm",
62            Self::ChunkCompress => "chunk_compress",
63            Self::Truncate => "truncate",
64        }
65    }
66
67    /// Returns `true` when this strategy is the last resort before
68    /// [`CompactionFailure`].
69    ///
70    /// # Examples
71    ///
72    /// ```rust
73    /// use codetether_agent::session::FallbackStrategy;
74    ///
75    /// assert!(FallbackStrategy::Truncate.is_terminal());
76    /// assert!(!FallbackStrategy::ChunkCompress.is_terminal());
77    /// ```
78    pub const fn is_terminal(self) -> bool {
79        matches!(self, Self::Truncate)
80    }
81}
82
83/// Emitted when a compaction pass begins.
84///
85/// # Examples
86///
87/// ```rust
88/// use codetether_agent::session::CompactionStart;
89/// use uuid::Uuid;
90///
91/// let s = CompactionStart {
92///     trace_id: Uuid::nil(),
93///     reason: "context_budget".into(),
94///     before_tokens: 140_000,
95///     budget: 128_000,
96/// };
97/// assert!(s.before_tokens > s.budget);
98/// ```
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct CompactionStart {
101    /// Correlates subsequent completion / failure / truncation events.
102    pub trace_id: Uuid,
103    /// Short machine-readable reason (e.g. `"context_budget"`, `"user_requested"`).
104    pub reason: String,
105    /// Estimated request tokens *before* compaction runs.
106    pub before_tokens: usize,
107    /// Usable budget the compaction is trying to fit under.
108    pub budget: usize,
109}
110
111/// Emitted when a compaction pass finishes successfully.
112///
113/// # Examples
114///
115/// ```rust
116/// use codetether_agent::session::{CompactionOutcome, FallbackStrategy};
117/// use uuid::Uuid;
118///
119/// let o = CompactionOutcome {
120///     trace_id: Uuid::nil(),
121///     strategy: FallbackStrategy::Rlm,
122///     before_tokens: 140_000,
123///     after_tokens: 22_400,
124///     kept_messages: 12,
125/// };
126/// assert!(o.reduction() > 0.8);
127/// ```
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct CompactionOutcome {
130    /// Correlates with the originating [`CompactionStart`].
131    pub trace_id: Uuid,
132    /// Which strategy ultimately produced the final context.
133    pub strategy: FallbackStrategy,
134    /// Estimated request tokens before compaction.
135    pub before_tokens: usize,
136    /// Estimated request tokens after compaction.
137    pub after_tokens: usize,
138    /// Verbatim messages retained in the compacted transcript.
139    pub kept_messages: usize,
140}
141
142impl CompactionOutcome {
143    /// `1.0 - (after / before)`, clamped to `[0.0, 1.0]`.
144    ///
145    /// A reduction of `0.9` means 90 % of tokens were removed. Returns
146    /// `0.0` for pathological inputs (`before_tokens == 0`).
147    ///
148    /// # Examples
149    ///
150    /// ```rust
151    /// use codetether_agent::session::{CompactionOutcome, FallbackStrategy};
152    /// use uuid::Uuid;
153    ///
154    /// let o = CompactionOutcome {
155    ///     trace_id: Uuid::nil(),
156    ///     strategy: FallbackStrategy::Rlm,
157    ///     before_tokens: 1000, after_tokens: 100, kept_messages: 0,
158    /// };
159    /// assert!((o.reduction() - 0.9).abs() < 1e-9);
160    /// ```
161    pub fn reduction(&self) -> f64 {
162        if self.before_tokens == 0 {
163            0.0
164        } else {
165            (1.0 - self.after_tokens as f64 / self.before_tokens as f64).clamp(0.0, 1.0)
166        }
167    }
168}
169
170/// Emitted when the terminal truncation fallback fires.
171///
172/// Distinct from [`CompactionOutcome`] with [`FallbackStrategy::Truncate`]
173/// because truncation silently drops context — consumers that care about
174/// data loss (the TUI, the flywheel) need an explicit signal to attach
175/// user-visible warnings and archive the dropped prefix.
176///
177/// # Examples
178///
179/// ```rust
180/// use codetether_agent::session::ContextTruncation;
181/// use uuid::Uuid;
182///
183/// let t = ContextTruncation {
184///     trace_id: Uuid::nil(),
185///     dropped_tokens: 98_000,
186///     kept_messages: 6,
187///     archive_ref: Some("minio://codetether/ctx/abc123".into()),
188/// };
189/// assert!(t.dropped_tokens > 0);
190/// ```
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ContextTruncation {
193    /// Correlates with the originating [`CompactionStart`].
194    pub trace_id: Uuid,
195    /// How many tokens the truncation removed from the transcript.
196    pub dropped_tokens: usize,
197    /// Verbatim messages retained after truncation.
198    pub kept_messages: usize,
199    /// Optional pointer (e.g. MinIO URI) to the archived pre-truncation
200    /// prefix so it can be restored with a `/recall` action.
201    pub archive_ref: Option<String>,
202}
203
204/// Emitted when *every* fallback strategy failed to fit under budget.
205///
206/// At this point the caller must surface an error to the user — sending
207/// the request to the provider would 400. `fell_back_to` is `None`
208/// explicitly (rather than omitted) so JSONL consumers can distinguish
209/// "never attempted truncation" from "attempted and it also failed".
210///
211/// # Examples
212///
213/// ```rust
214/// use codetether_agent::session::{CompactionFailure, FallbackStrategy};
215/// use uuid::Uuid;
216///
217/// let f = CompactionFailure {
218///     trace_id: Uuid::nil(),
219///     fell_back_to: Some(FallbackStrategy::Truncate),
220///     reason: "truncation below minimum viable request".into(),
221///     after_tokens: 9_200,
222///     budget: 8_000,
223/// };
224/// assert!(f.after_tokens > f.budget);
225/// ```
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct CompactionFailure {
228    /// Correlates with the originating [`CompactionStart`].
229    pub trace_id: Uuid,
230    /// Last strategy attempted before giving up, or `None` if compaction
231    /// never managed to run at all.
232    pub fell_back_to: Option<FallbackStrategy>,
233    /// Human-readable diagnostic.
234    pub reason: String,
235    /// Final estimated request tokens after the last attempt.
236    pub after_tokens: usize,
237    /// Budget the request still fails to fit under.
238    pub budget: usize,
239}