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}