Skip to main content

runtime/
reminder.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! How full the window is, from the provider's own count.
7//!
8//! Usage is the `input_tokens` the provider reported for the last call
9//! — a fact — against the model's `context_tokens` from the endpoint
10//! book. Never a byte count of the window, which is an estimate, and
11//! never a tokenizer of this repository's own, which would be a second
12//! one.
13//!
14//! Two thresholds, and each sounds exactly once per run. At the first
15//! rung — 30% of the window — only the usage is reported; at the second
16//! rung — 65% of the window
17//! unless a layer moved it — the run is told that what
18//! is left is still enough to write a handoff and `succeed`, and that
19//! past this point it will not be. A gauge that jumps past both in one
20//! reading sounds the higher one only: two sentences stacked at once
21//! are noise, and the lower one has nothing left to say.
22
23use 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/// Which thresholds have sounded. Monotone: a gauge never goes back to
31/// a lower state, so a threshold cannot sound twice.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum Sounded {
34    Nothing,
35    First,
36    Handover,
37}
38
39/// The line the run is given, with the figures it is computed from.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ContextReminder {
42    /// The first rung is reached: the figures, nothing more.
43    Usage { used: Tokens, window: Tokens },
44    /// The second rung is reached: still enough to hand over, not for long.
45    HandoverWindow { used: Tokens, window: Tokens },
46}
47
48impl ContextReminder {
49    /// The sentence, defined here once. Changing it changes window bytes
50    /// and passes through the SPEC.
51    #[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
71/// Whole percent, floored; zero for a window nobody stated, full for a
72/// product that will not fit.
73fn 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/// The provider's count for the last call a run completed, shared with
81/// whoever reports it.
82///
83/// One cell with one writer: the run records each count before its gauge
84/// reads it, and `status` reads the cell when a model asks. A snapshot
85/// frozen at dispatch cannot carry this figure, because nothing has been
86/// called at dispatch and the figure moves every turn. Relaxed ordering
87/// is enough: the value is one word and nothing is ordered against it.
88#[derive(Debug, Clone, Default)]
89pub struct ContextReading(Arc<AtomicU64>);
90
91impl ContextReading {
92    /// The last count recorded; zero before the first call completes.
93    #[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/// One run's gauge. Fed the provider's `input_tokens` after every call;
104/// answers a reminder the first time each threshold is crossed.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ContextGauge {
107    window: Tokens,
108    /// Where the second rung sits, as a whole percent: the city's rung
109    /// when a layer stated one, else `CTX_REMINDER_SECOND_DEFAULT`.
110    second_at: u64,
111    sounded: Sounded,
112}
113
114impl ContextGauge {
115    /// `second` is the configuration ladder's answer for this run, and
116    /// `None` means no layer spoke: the default applies here, at the one
117    /// place a rung is read.
118    #[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    /// Reads one usage figure. `None` when no threshold was newly
128    /// crossed, and always `None` for a window of zero: without a
129    /// denominator there is no percentage to report.
130    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    /// A layer that moved the second rung moves the line with it: the
210    /// handover claim is about the budget left at that rung, so the rung
211    /// is the city's to set (`crates/kernel/Spec.lean` §8-22).
212    #[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}