1use std::sync::Arc;
24use std::sync::atomic::{AtomicU64, Ordering};
25
26use kernel::Tokens;
27use kernel::config::SecondThreshold;
28use kernel::consts_policy::{CTX_REMINDER_FIRST_PERCENT, CTX_REMINDER_SECOND_DEFAULT};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum Sounded {
34 Nothing,
35 First,
36 Handover,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ContextReminder {
42 Usage { used: Tokens, window: Tokens },
44 HandoverWindow { used: Tokens, window: Tokens },
46}
47
48impl ContextReminder {
49 #[must_use]
52 pub fn render(&self) -> String {
53 match self {
54 ContextReminder::Usage { used, window } => format!(
55 "[context] {}% of the window used ({} of {} input tokens).",
56 percent(*used, *window),
57 used.get(),
58 window.get()
59 ),
60 ContextReminder::HandoverWindow { used, window } => format!(
61 "[context] {}% of the window used ({} of {} input tokens). What is left is \
62 still enough to write a handoff and `succeed`; past this point it will not be.",
63 percent(*used, *window),
64 used.get(),
65 window.get()
66 ),
67 }
68 }
69}
70
71fn percent(used: Tokens, window: Tokens) -> u64 {
74 used.get()
75 .checked_mul(100)
76 .and_then(|scaled| scaled.checked_div(window.get()))
77 .unwrap_or(if window.get() == 0 { 0 } else { u64::MAX })
78}
79
80#[derive(Debug, Clone, Default)]
89pub struct ContextReading(Arc<AtomicU64>);
90
91impl ContextReading {
92 #[must_use]
94 pub fn tokens(&self) -> Tokens {
95 Tokens::new(self.0.load(Ordering::Relaxed))
96 }
97
98 pub(crate) fn record(&self, used: Tokens) {
99 self.0.store(used.get(), Ordering::Relaxed);
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ContextGauge {
107 window: Tokens,
108 second_at: u64,
111 sounded: Sounded,
112}
113
114impl ContextGauge {
115 #[must_use]
119 pub fn new(window: Tokens, second: Option<SecondThreshold>) -> ContextGauge {
120 ContextGauge {
121 window,
122 second_at: second.map_or(CTX_REMINDER_SECOND_DEFAULT, SecondThreshold::percent),
123 sounded: Sounded::Nothing,
124 }
125 }
126
127 pub fn observe(&mut self, used: Tokens) -> Option<ContextReminder> {
131 if self.window.get() == 0 {
132 return None;
133 }
134 let at = percent(used, self.window);
135 let window = self.window;
136 match self.sounded {
137 Sounded::Nothing | Sounded::First if at >= self.second_at => {
138 self.sounded = Sounded::Handover;
139 Some(ContextReminder::HandoverWindow { used, window })
140 }
141 Sounded::Nothing if at >= CTX_REMINDER_FIRST_PERCENT => {
142 self.sounded = Sounded::First;
143 Some(ContextReminder::Usage { used, window })
144 }
145 Sounded::Nothing | Sounded::First | Sounded::Handover => None,
146 }
147 }
148}
149
150#[cfg(test)]
151#[allow(
152 clippy::unwrap_used,
153 clippy::expect_used,
154 clippy::panic,
155 clippy::indexing_slicing,
156 reason = "test code"
157)]
158mod tests {
159 use super::*;
160
161 fn gauge() -> ContextGauge {
162 ContextGauge::new(Tokens::new(1_000), None)
163 }
164
165 #[test]
166 fn each_threshold_sounds_once_and_only_on_the_way_up() {
167 let mut gauge = gauge();
168 assert_eq!(gauge.observe(Tokens::new(290)), None);
169 assert_eq!(
170 gauge.observe(Tokens::new(300)),
171 Some(ContextReminder::Usage {
172 used: Tokens::new(300),
173 window: Tokens::new(1_000)
174 })
175 );
176 assert_eq!(
177 gauge.observe(Tokens::new(400)),
178 None,
179 "the first rung sounded already"
180 );
181 assert_eq!(
182 gauge.observe(Tokens::new(650)),
183 Some(ContextReminder::HandoverWindow {
184 used: Tokens::new(650),
185 window: Tokens::new(1_000)
186 })
187 );
188 assert_eq!(
189 gauge.observe(Tokens::new(900)),
190 None,
191 "and so did two thirds"
192 );
193 }
194
195 #[test]
196 fn a_jump_past_both_sounds_the_higher_one_and_spends_the_lower() {
197 let mut gauge = gauge();
198 assert!(matches!(
199 gauge.observe(Tokens::new(700)),
200 Some(ContextReminder::HandoverWindow { .. })
201 ));
202 assert_eq!(
203 gauge.observe(Tokens::new(300)),
204 None,
205 "the first rung is spent too"
206 );
207 }
208
209 #[test]
213 fn the_second_rung_moves_when_a_layer_moved_it() {
214 let moved = kernel::config::SecondThreshold::parse(31).unwrap();
215 let mut gauge = ContextGauge::new(Tokens::new(1_000), Some(moved));
216 assert_eq!(
217 gauge.observe(Tokens::new(300)),
218 Some(ContextReminder::Usage {
219 used: Tokens::new(300),
220 window: Tokens::new(1_000)
221 })
222 );
223 assert_eq!(
224 gauge.observe(Tokens::new(310)),
225 Some(ContextReminder::HandoverWindow {
226 used: Tokens::new(310),
227 window: Tokens::new(1_000)
228 }),
229 "the moved rung sounds where it was moved to"
230 );
231 }
232
233 #[test]
234 fn no_window_means_no_percentage() {
235 let mut gauge = ContextGauge::new(Tokens::new(0), None);
236 assert_eq!(gauge.observe(Tokens::new(u64::MAX)), None);
237 }
238
239 #[test]
240 fn the_sentences_carry_the_figures_the_provider_reported() {
241 let usage = ContextReminder::Usage {
242 used: Tokens::new(300),
243 window: Tokens::new(1_000),
244 };
245 assert_eq!(
246 usage.render(),
247 "[context] 30% of the window used (300 of 1000 input tokens)."
248 );
249 let handover = ContextReminder::HandoverWindow {
250 used: Tokens::new(700),
251 window: Tokens::new(1_000),
252 };
253 assert!(
254 handover
255 .render()
256 .starts_with("[context] 70% of the window used")
257 );
258 assert!(
259 handover
260 .render()
261 .contains("still enough to write a handoff and `succeed`")
262 );
263 }
264
265 #[test]
266 fn an_overflowing_product_reads_as_full_rather_than_wrapping() {
267 assert_eq!(percent(Tokens::new(u64::MAX), Tokens::new(1_000)), u64::MAX);
268 }
269}