1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
//! The per-turn footer that surfaces token usage inline in the transcript.
//!
//! This is the always-visible usage surface: cumulative session totals ride
//! along with the per-turn figures the footer already carried, and the
//! context-window utilisation is appended when it can be known. `/usage`
//! remains the detailed breakdown; this line is the glanceable one.
//!
//! Every unknown stays absent: without both a known window and a provider
//! report of the current input there is no honest percentage, and a segment
//! that rendered `0%` for "we do not know" would be a lie, not a default.
use super::usage_totals::UsageTotals;
use saya_agent::{CONTEXT_COMPACT_PERCENT, TokenUsage, context_utilisation_percent};
/// Builds the footer pushed to the transcript after a turn that reported
/// usage: the per-turn counts, the cumulative answering totals (this turn
/// included), and — only when both are known — the last reported input as a
/// share of the model's context window.
///
/// The session segment covers the answering calls only: the extraction call
/// is billed apart in `/usage`, and folding it in here would silently merge
/// the two labelled totals. The context numerator is the last *answering*
/// call's `input_tokens` — the aggregate at the end of a run sums every
/// round's re-sent conversation, so it would inflate the percentage on
/// exactly the tool-heavy turns where the figure matters most.
pub(crate) fn transcript_footer(
turn: &TokenUsage,
session: &UsageTotals,
last_answering_input: Option<u64>,
window: Option<u64>,
) -> String {
let mut footer = format!(
"{} tokens in · {} tokens out",
turn.input_tokens, turn.output_tokens
);
footer.push_str(&format!(
" · session {} in / {} out",
session.input_tokens, session.output_tokens
));
if let (Some(input), Some(window)) = (last_answering_input, window)
&& let Some(percent) = context_utilisation_percent(Some(input), Some(window))
{
footer.push_str(&format!(
" · ctx {}% of {}",
percent,
compact_tokens(window)
));
}
footer
}
/// Builds the one-shot context-window warning pushed after the footer when
/// utilisation crosses the warn threshold upward. States the percentage, the
/// window, and what fires at the compact threshold — the user learns the
/// behaviour before it happens, not when it fires.
///
/// Returns `None` below the threshold; the armed/fired crossing lives in the
/// caller (`SessionState::context_warned`), so this stays a pure renderer.
pub(crate) fn context_warn_notice(percent: u64, window: u64) -> Option<String> {
if percent < saya_agent::CONTEXT_WARN_PERCENT {
return None;
}
Some(format!(
"Context is at {percent}% of {} tokens. At {CONTEXT_COMPACT_PERCENT}% saya will summarise older turns to keep going; /clear resets the working memory now if you prefer.",
compact_tokens(window)
))
}
/// Renders a token count compactly for the footer: exact below a thousand,
/// three-significant-figure shorthand above it. The shorthand rounds for
/// display only — the session totals beside it stay exact.
fn compact_tokens(tokens: u64) -> String {
let (divisor, suffix) = if tokens >= 1_000_000 {
(1_000_000.0, "M")
} else if tokens >= 1_000 {
(1_000.0, "k")
} else {
return tokens.to_string();
};
let rendered = format!("{:.1}{suffix}", tokens as f64 / divisor);
rendered.replace(".0", "")
}
#[cfg(test)]
mod tests {
use super::*;
fn totals(input: u64, output: u64) -> UsageTotals {
UsageTotals {
input_tokens: input,
output_tokens: output,
..UsageTotals::default()
}
}
/// The footer keeps the original per-turn wording first, then carries the
/// cumulative session totals inline — the glanceable answer to "what has
/// this session cost" that previously required typing `/usage`.
#[test]
fn the_footer_keeps_the_turn_wording_and_adds_session_totals() {
let footer = transcript_footer(&TokenUsage::new(300, 100), &totals(1300, 600), None, None);
assert_eq!(
footer, "300 tokens in · 100 tokens out · session 1300 in / 600 out",
"the footer must carry the cumulative session totals inline"
);
}
/// With a known window and a provider report of the current input, the
/// footer shows the utilisation. The window renders compactly but the
/// percentage stays exact to the display precision.
#[test]
fn context_utilisation_renders_against_a_compact_window() {
let footer = transcript_footer(
&TokenUsage::new(64_000, 20),
&totals(64_000, 20),
Some(64_000),
Some(128_000),
);
assert!(
footer.ends_with("· ctx 50% of 128k"),
"ctx must show the last reported input against the known window: {footer}"
);
}
/// No window, or no provider report of the input, means no ctx figure —
/// "we do not know" must render as absence, never as `0%`.
#[test]
fn context_utilisation_is_absent_without_window_or_input_report() {
let no_window = transcript_footer(
&TokenUsage::new(64_000, 20),
&totals(64_000, 20),
Some(64_000),
None,
);
assert!(
!no_window.contains("ctx"),
"no window means no ctx figure: {no_window}"
);
let no_report = transcript_footer(
&TokenUsage::new(64_000, 20),
&totals(64_000, 20),
None,
Some(128_000),
);
assert!(
!no_report.contains("ctx"),
"no provider report means no ctx figure: {no_report}"
);
}
/// The window shorthand stays three significant figures and never rounds
/// a distinct window up into another model's window.
#[test]
fn window_shorthand_keeps_three_significant_figures() {
assert_eq!(compact_tokens(131_072), "131.1k");
assert_eq!(compact_tokens(128_000), "128k");
assert_eq!(compact_tokens(1_048_576), "1M");
assert_eq!(compact_tokens(999), "999");
}
/// A *reported* zero input is a fact the provider gave us, so `0%` is the
/// honest rendering — the same discipline as a reported zero cache rate,
/// which is a cold cache, not an unknown.
#[test]
fn a_reported_zero_input_renders_zero_percent_not_absent() {
let footer = transcript_footer(
&TokenUsage::new(0, 100),
&totals(0, 100),
Some(0),
Some(128_000),
);
assert!(
footer.contains("ctx 0% of 128k"),
"a reported zero input is a fact, so 0% renders: {footer}"
);
}
/// The notice states the percentage, the window, and what happens at the
/// compact threshold — the user learns the behaviour before it fires.
/// The trigger and the text both derive from the shared thresholds, so a
/// later slice cannot drift one without the other.
#[test]
fn the_notice_derives_from_the_shared_thresholds() {
let notice = context_warn_notice(72, 200_000).expect("a notice renders");
assert!(
notice.contains("72%"),
"the notice states the percentage: {notice}"
);
assert!(
notice.contains("200k"),
"the notice states the window: {notice}"
);
assert!(
notice.contains(&saya_agent::CONTEXT_COMPACT_PERCENT.to_string()),
"the notice names the compact threshold: {notice}"
);
}
}