use blazingly_json::{Value, json};
const BYTES_PER_TOKEN: usize = 4;
#[cfg(feature = "search")]
const HONOURED: &[&str] = &[
"context_bundle",
"query_graph",
"read_source",
"search_code",
];
#[cfg(not(feature = "search"))]
const HONOURED: &[&str] = &["context_bundle", "query_graph", "read_source"];
pub(crate) fn annotate_unapplied(
tool: &str,
args: &Value,
report: &mut Value,
) -> Result<(), String> {
let Some(budget) = requested(args)? else {
return Ok(());
};
if HONOURED.contains(&tool) {
return Ok(());
}
let estimated = estimate(report);
if let Some(object) = report.as_object_mut() {
object.insert(
"token_budget".to_owned(),
json!({
"requested": budget,
"estimated_tokens": estimated,
"estimator": "serialized bytes / 4",
"dropped_items": 0,
"fit": estimated <= budget,
"applied": false,
"applied_by": HONOURED
}),
);
}
Ok(())
}
pub(crate) fn requested(args: &Value) -> Result<Option<usize>, String> {
let Some(budget) = super::optional_u64(args, "token_budget")? else {
return Ok(None);
};
if budget == 0 {
return Err("token_budget must be a positive integer".to_owned());
}
Ok(Some(usize::try_from(budget).unwrap_or(usize::MAX)))
}
pub(crate) fn estimate(value: &Value) -> usize {
blazingly_json::to_vec(value).map_or(0, |bytes| bytes.len().div_ceil(BYTES_PER_TOKEN))
}
pub(crate) fn fit(report: &mut Value, budget: Option<usize>, pointers: &[&str]) {
let Some(budget) = budget else {
return;
};
let mut dropped = 0usize;
for pointer in pointers {
dropped += fit_array(report, budget, pointer);
}
let estimated = estimate(report);
if let Some(object) = report.as_object_mut() {
object.insert(
"token_budget".to_owned(),
json!({
"requested": budget,
"estimated_tokens": estimated,
"estimator": "serialized bytes / 4",
"dropped_items": dropped,
"fit": estimated <= budget,
"applied": true
}),
);
}
}
fn fit_array(report: &mut Value, budget: usize, pointer: &str) -> usize {
let mut dropped = 0usize;
while estimate(report) > budget {
let Some(items) = report.pointer_mut(pointer).and_then(Value::as_array_mut) else {
break;
};
if items.is_empty() {
break;
}
let step = if items.len() > 32 { items.len() / 2 } else { 1 };
items.truncate(items.len() - step);
dropped += step;
}
dropped
}
#[cfg(test)]
mod tests {
use super::annotate_unapplied;
use blazingly_json::json;
#[test]
fn an_unapplied_budget_is_recorded_and_the_answer_is_kept() {
let budgeted = json!({"label": "value", "token_budget": 800});
let mut report = json!({"node": "value"});
annotate_unapplied("inspect_symbol", &budgeted, &mut report).unwrap();
assert_eq!(report["token_budget"]["applied"], false);
assert_eq!(report["node"], "value");
let mut applying = json!({"lines": []});
annotate_unapplied("read_source", &budgeted, &mut applying).unwrap();
assert!(
applying.get("token_budget").is_none(),
"an operation that applies the budget reports it itself"
);
let mut unbudgeted = json!({"node": "value"});
annotate_unapplied("inspect_symbol", &json!({}), &mut unbudgeted).unwrap();
assert!(unbudgeted.get("token_budget").is_none());
}
}