use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::OnceLock;
const DEFAULT_READ_LIMIT: usize = 2_000;
pub(super) const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;
const DEFAULT_OUTPUT_HEAD_TOKENS: usize = 1_500;
pub(super) const DEFAULT_OUTPUT_CAP_CHARS_PER_TOKEN: usize = 3;
static MAX_OUTPUT_TOKENS: AtomicUsize = AtomicUsize::new(DEFAULT_MAX_OUTPUT_TOKENS);
static OUTPUT_HEAD_TOKENS: AtomicUsize = AtomicUsize::new(DEFAULT_OUTPUT_HEAD_TOKENS);
static OUTPUT_CAP_CHARS_PER_TOKEN: AtomicUsize =
AtomicUsize::new(DEFAULT_OUTPUT_CAP_CHARS_PER_TOKEN);
pub fn set_max_output_tokens(max_tokens: usize) {
MAX_OUTPUT_TOKENS.store(max_tokens, Ordering::Relaxed);
}
pub fn set_output_head_tokens(head_tokens: usize) {
OUTPUT_HEAD_TOKENS.store(head_tokens, Ordering::Relaxed);
}
pub(super) fn max_output_tokens() -> usize {
MAX_OUTPUT_TOKENS.load(Ordering::Relaxed)
}
pub(super) fn output_head_tokens() -> usize {
OUTPUT_HEAD_TOKENS.load(Ordering::Relaxed)
}
pub fn set_output_cap_chars_per_token(chars_per_token: usize) {
OUTPUT_CAP_CHARS_PER_TOKEN.store(chars_per_token, Ordering::Relaxed);
}
pub(super) fn output_cap_chars_per_token() -> usize {
OUTPUT_CAP_CHARS_PER_TOKEN.load(Ordering::Relaxed)
}
pub(super) fn cap_estimator() -> crate::tokens::TokenEstimation {
crate::tokens::TokenEstimation::new(output_cap_chars_per_token())
}
pub(super) fn should_spill_full_output(
out_bytes: usize,
out_chars: usize,
max_tokens: usize,
tool_offload: bool,
) -> bool {
if max_tokens == 0 || !tool_offload {
return false;
}
let over_model_budget = cap_estimator().tokens_for_chars(out_bytes) > max_tokens;
let over_spill_budget = out_chars > crate::agentic::content_spill::TOOL_RESULT_SPILL_CAP;
over_model_budget || over_spill_budget
}
pub(super) fn cap_model_output(text: &str, max_tokens: usize) -> String {
cap_model_output_with_handle(text, max_tokens, output_head_tokens(), None)
}
pub(super) fn cap_model_output_with_handle(
text: &str,
max_tokens: usize,
head_tokens: usize,
spill_id: Option<&str>,
) -> String {
let est = cap_estimator();
if max_tokens == 0 || est.tokens_for_chars(text.len()) <= max_tokens {
return match spill_id {
Some(id) => format!(
"{text}\n\n{}",
crate::agentic::content_spill::tool_output_retrieval_hint(id)
),
None => text.to_string(),
};
}
let max_chars = est.chars_for_tokens(max_tokens);
let head_tokens = head_tokens.min(max_tokens);
let head_chars = est.chars_for_tokens(head_tokens).min(max_chars);
let tail_chars = max_chars.saturating_sub(head_chars);
let total_chars = text.chars().count();
let shown_chars = head_chars.saturating_add(tail_chars).min(total_chars);
let elided = total_chars.saturating_sub(shown_chars);
let head = take_chars(text, head_chars);
let tail = take_tail_chars(text, tail_chars);
let marker = match spill_id {
Some(id) => format!(
"[… {elided} chars elided (head+tail shown). {} …]",
crate::agentic::content_spill::tool_output_retrieval_hint(id)
),
None => format!(
"[… {elided} chars elided (head+tail shown; ~{max_tokens} token budget). \
Narrow the command or use a more specific grep/filter if needed …]"
),
};
format!("{head}\n\n{marker}\n\n{tail}")
}
fn take_chars(text: &str, max_chars: usize) -> String {
text.chars().take(max_chars).collect()
}
fn take_tail_chars(text: &str, max_chars: usize) -> String {
let mut chars: Vec<char> = text.chars().rev().take(max_chars).collect();
chars.reverse();
chars.into_iter().collect()
}
fn page_mac_key() -> &'static [u8; 32] {
static KEY: OnceLock<[u8; 32]> = OnceLock::new();
KEY.get_or_init(|| {
let a = uuid::Uuid::new_v4();
let b = uuid::Uuid::new_v4();
let mut key = [0u8; 32];
key[..16].copy_from_slice(a.as_bytes());
key[16..].copy_from_slice(b.as_bytes());
key
})
}
const PAGE_MAC_VERSION: u8 = 0x01;
fn page_mac_tag(
path: &str,
first_line: usize,
char_offset_in: usize,
body: &[u8],
next_offset: usize,
next_char_offset: usize,
) -> String {
let mut h = blake3::Hasher::new_keyed(page_mac_key());
h.update(&[PAGE_MAC_VERSION]);
h.update(&(path.len() as u64).to_le_bytes());
h.update(path.as_bytes());
h.update(&(first_line as u64).to_le_bytes());
h.update(&(char_offset_in as u64).to_le_bytes());
h.update(&(body.len() as u64).to_le_bytes());
h.update(body);
h.update(&(next_offset as u64).to_le_bytes());
h.update(&(next_char_offset as u64).to_le_bytes());
let digest = h.finalize();
digest.as_bytes()[..16]
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
pub(crate) fn verify_page_tag(
path: &str,
first_line: usize,
char_offset_in: usize,
body: &str,
next_offset: usize,
next_char_offset: usize,
tag: &str,
) -> bool {
page_mac_tag(
path,
first_line,
char_offset_in,
body.as_bytes(),
next_offset,
next_char_offset,
) == tag
}
#[cfg(test)]
pub(super) fn paginate_read(
contents: &str,
offset: Option<usize>,
limit: Option<usize>,
max_output_tokens: usize,
) -> String {
paginate_read_from("", contents, offset, limit, max_output_tokens, None)
}
pub(crate) fn paginate_read_from(
path: &str,
contents: &str,
offset: Option<usize>,
limit: Option<usize>,
max_output_tokens: usize,
char_offset: Option<usize>,
) -> String {
let max_chars = if max_output_tokens == 0 {
usize::MAX
} else {
cap_estimator().chars_for_tokens(max_output_tokens)
};
let total = contents.lines().count();
let start = offset.filter(|&o| o > 0).unwrap_or(1); let limit = limit.filter(|&l| l > 0).unwrap_or(DEFAULT_READ_LIMIT);
let char_offset = char_offset.unwrap_or(0);
if char_offset == 0 && start == 1 && limit >= total && contents.len() <= max_chars {
if crate::prune::final_line_is_a_verified_pagination_footer(contents, path, 1, 0) {
return format!("{contents}{REPLAY_DISAMBIGUATOR}");
}
return contents.to_string();
}
let start0 = start - 1;
if start0 >= total {
return format!("(offset {start} is past end of file — {total} lines total)");
}
let mut lines = contents.lines().skip(start0);
let first_line_full = lines.next().expect("start0 < total, checked above");
let first_line_chars = first_line_full.chars().count();
if char_offset > 0 && char_offset >= first_line_chars && start0 + 1 == total {
return format!(
"(char_offset {char_offset} is past the end of line {start}, which has \
{first_line_chars} chars; call read_file with offset={} to continue)",
start + 1
);
}
let first_line_body: String = if char_offset > 0 {
first_line_full.chars().skip(char_offset).collect()
} else {
first_line_full.to_string()
};
let rest: Vec<&str> = lines.take(limit.saturating_sub(1)).collect();
let end = start0 + 1 + rest.len(); let mut body = first_line_body;
for line in &rest {
body.push('\n');
body.push_str(line);
}
let char_capped = body.len() > max_chars;
let mut whole_through = None;
let mut mid_line = None;
if char_capped {
let mut cut = max_chars;
while cut > 0 && !body.is_char_boundary(cut) {
cut -= 1;
}
match body[..cut].rfind('\n') {
Some(newline) => {
body.truncate(newline);
whole_through = Some(start0 + body.lines().count());
}
None => {
let emitted_chars = body[..cut].chars().count();
let next_char_offset = char_offset + emitted_chars;
body.truncate(cut);
if next_char_offset >= first_line_chars {
whole_through = Some(start0 + 1);
} else {
mid_line = Some((start, next_char_offset));
}
}
}
}
let footer = if let Some((line, next_char_offset)) = mid_line {
let tag = page_mac_tag(
path,
start,
char_offset,
body.as_bytes(),
line,
next_char_offset,
);
Some(format!(
"payload truncated to {max_chars} chars (~{max_output_tokens} tokens); line {line} \
continues: call read_file with offset={line} char_offset={next_char_offset} to \
continue page={tag}"
))
} else if let Some(last) = whole_through {
let next = last + 1;
let tag = page_mac_tag(path, start, char_offset, body.as_bytes(), next, 0);
Some(format!(
"payload truncated to {max_chars} chars (~{max_output_tokens} tokens) at line \
{last} of {total}; call read_file with offset={next} to continue page={tag}"
))
} else if end < total {
let next = end + 1;
let tag = page_mac_tag(path, start, char_offset, body.as_bytes(), next, 0);
Some(format!(
"showing lines {start}-{end} of {total}; \
call read_file with offset={next} to continue page={tag}"
))
} else {
None
};
match footer {
Some(f) => format!("{body}\n\n[{f}]"),
None => {
if crate::prune::final_line_is_a_verified_pagination_footer(
&body,
path,
start,
char_offset,
) {
format!("{body}{REPLAY_DISAMBIGUATOR}")
} else {
body
}
}
}
}
pub(crate) const REPLAY_DISAMBIGUATOR: &str =
"\n[end of file: the bracketed line above is part of the file]";
pub(super) fn read_file_page(
path: &str,
contents: &str,
offset: Option<usize>,
limit: Option<usize>,
char_offset: Option<usize>,
tool_offload: bool,
) -> String {
paginate_read_from(
path,
contents,
offset,
limit,
read_page_tokens(tool_offload),
char_offset,
)
}
pub(super) fn paginate_unspillable(
path: &str,
contents: &str,
offset: Option<usize>,
limit: Option<usize>,
char_offset: Option<usize>,
) -> String {
paginate_read_from(
path,
contents,
offset,
limit,
read_page_tokens(true),
char_offset,
)
}
pub(super) fn read_page_tokens(tool_offload: bool) -> usize {
if !tool_offload {
return max_output_tokens();
}
const FOOTER_HEADROOM: usize = 512;
let unspillable = cap_estimator()
.tokens_for_chars(crate::agentic::content_spill::TOOL_RESULT_SPILL_CAP - FOOTER_HEADROOM);
match max_output_tokens() {
0 => unspillable,
budget => budget.min(unspillable),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spill_gate_uses_the_conservative_cap_ratio_not_the_estimate() {
let out_bytes = 3_500;
let out_chars = 3_500; assert!(
out_chars < crate::agentic::content_spill::TOOL_RESULT_SPILL_CAP,
"isolate over_model_budget: stay under the raw spill cap"
);
assert!(should_spill_full_output(out_bytes, out_chars, 1_000, true));
assert!(crate::tokens::TokenEstimation::default().tokens_for_chars(out_bytes) <= 1_000);
assert!(cap_estimator().tokens_for_chars(out_bytes) > 1_000);
}
#[test]
fn spill_gate_off_when_no_offload_or_no_budget() {
assert!(!should_spill_full_output(
1_000_000, 1_000_000, 10_000, false
));
assert!(!should_spill_full_output(1_000_000, 1_000_000, 0, true));
}
#[test]
fn spill_gate_fires_on_raw_size_even_when_under_token_budget() {
let big = crate::agentic::content_spill::TOOL_RESULT_SPILL_CAP + 1;
assert!(should_spill_full_output(big, big, usize::MAX, true));
}
#[test]
fn a_line_longer_than_the_page_budget_is_never_lost_across_pagination() {
let long_line = "x".repeat(40_000);
let original = format!("short one\n{long_line}\nshort two\nshort three\n");
let budget_tokens = 5_000; let max_chars = cap_estimator().chars_for_tokens(budget_tokens);
let mut reconstructed = String::new();
let mut offset = None;
let mut char_offset = None;
for _ in 0..20 {
let page = paginate_read_from("", &original, offset, None, budget_tokens, char_offset);
let (body, footer) = match page.rfind("\n\n[") {
Some(marker_start) => (&page[..marker_start], Some(&page[marker_start..])),
None => (page.as_str(), None),
};
assert!(
page.len() <= max_chars + 300,
"every page must stay under the char budget (~{max_chars} chars): {} bytes",
page.len()
);
let mut next_offset = None;
let mut next_char_offset = None;
if let Some(footer) = footer {
for tok in footer.split_whitespace() {
if let Some(v) = tok.strip_prefix("offset=") {
next_offset = v
.trim_end_matches(|c: char| !c.is_ascii_digit())
.parse()
.ok();
} else if let Some(v) = tok.strip_prefix("char_offset=") {
next_char_offset = v
.trim_end_matches(|c: char| !c.is_ascii_digit())
.parse()
.ok();
}
}
}
if char_offset.is_none() && !reconstructed.is_empty() {
reconstructed.push('\n');
}
reconstructed.push_str(body);
match next_offset {
Some(n) => {
offset = Some(n);
char_offset = next_char_offset;
}
None => break,
}
}
assert_eq!(
reconstructed,
original.trim_end_matches('\n'),
"every byte of the original file must appear exactly once, in order"
);
}
}