use serde_json::{json, Value};
const LOW_BUDGET_PCT: usize = 15;
const DESCRIPTION: &str = "Report your remaining context budget (tokens used / \
ceiling / remaining). Call this before a large read; if remaining is low, \
read in pages (read_file offset+limit) or wrap up. No args; read-only.";
pub fn get_context_remaining_tool_definition() -> Value {
json!({
"type": "function",
"function": {
"name": "get_context_remaining",
"description": DESCRIPTION,
"parameters": { "type": "object", "properties": {}, "required": [] }
}
})
}
pub(crate) fn render_context_budget(
used: usize,
ceiling: Option<usize>,
num_ctx: Option<u32>,
) -> String {
match ceiling {
Some(ceiling) => {
let remaining = ceiling.saturating_sub(used);
let nc = num_ctx
.map(|c| c.to_string())
.unwrap_or_else(|| "unset".to_string());
let mut out = format!(
"Context budget: ~{used} tokens used of an input ceiling of ~{ceiling} \
(80% of num_ctx {nc}). ~{remaining} tokens remaining."
);
if remaining.saturating_mul(100) < ceiling.saturating_mul(LOW_BUDGET_PCT) {
out.push_str(
" Budget is LOW — compact or wrap up soon: read in pages \
(read_file with offset+limit), avoid large reads, and finish with \
what you have.",
);
}
out
}
None => format!(
"Context budget: ~{used} tokens used so far. No input-token ceiling is \
configured this session (num_ctx is not set), so there is no fixed \
remaining budget to report — keep reads modest and read large files in \
pages (read_file with offset+limit) to avoid overflowing."
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_definition_is_no_arg_get_context_remaining() {
let def = get_context_remaining_tool_definition();
assert_eq!(def["function"]["name"], "get_context_remaining");
assert_eq!(
def["function"]["parameters"]["properties"],
serde_json::json!({})
);
assert_eq!(
def["function"]["parameters"]["required"],
serde_json::json!([])
);
}
#[test]
fn ceiling_minus_used_is_remaining() {
let out = render_context_budget(1000, Some(8000), Some(10_000));
assert!(out.contains("1000 tokens used"), "{out}");
assert!(out.contains("ceiling of ~8000"), "{out}");
assert!(out.contains("7000 tokens remaining"), "{out}");
assert!(out.contains("num_ctx 10000"), "{out}");
}
#[test]
fn ample_budget_omits_low_hint() {
let out = render_context_budget(5000, Some(10_000), Some(12_500));
assert!(!out.contains("LOW"), "ample budget must not nudge: {out}");
}
#[test]
fn low_budget_hint_fires_below_threshold() {
let out = render_context_budget(9000, Some(10_000), Some(12_500));
assert!(out.contains("1000 tokens remaining"), "{out}");
assert!(out.contains("LOW"), "low budget must nudge: {out}");
}
#[test]
fn threshold_boundary_at_15_percent_does_not_nudge() {
let out = render_context_budget(8500, Some(10_000), Some(12_500));
assert!(out.contains("1500 tokens remaining"), "{out}");
assert!(!out.contains("LOW"), "15% is not below 15%: {out}");
}
#[test]
fn used_over_ceiling_saturates_remaining_to_zero() {
let out = render_context_budget(12_000, Some(10_000), Some(12_500));
assert!(out.contains("0 tokens remaining"), "{out}");
assert!(out.contains("LOW"), "{out}");
}
#[test]
fn no_ceiling_is_honest_about_no_budget() {
let out = render_context_budget(4321, None, None);
assert!(out.contains("4321 tokens used"), "{out}");
assert!(
out.contains("No input-token ceiling is configured"),
"{out}"
);
assert!(
!out.contains("remaining."),
"must not claim a remaining figure: {out}"
);
}
}