use serde_json::{Map, Value};
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct BudgetReceipt {
pub requested: usize,
pub estimated_tokens: usize,
pub truncated: bool,
pub sections_dropped: Vec<String>,
pub oversized_dropped: usize,
}
pub fn estimate_tokens(s: &str) -> usize {
s.chars().count() / 4
}
fn serialized_tokens(v: &Value) -> usize {
estimate_tokens(&v.to_string())
}
pub fn apply_budget(value: Value, budget: usize) -> Value {
if budget == 0 {
return value;
}
let Some(map) = value.as_object() else {
return value;
};
let mut skeleton = Map::new();
for (k, v) in map {
if v.is_array() {
skeleton.insert(k.clone(), Value::Array(Vec::new()));
} else {
skeleton.insert(k.clone(), v.clone());
}
}
let mut remaining = budget.saturating_sub(serialized_tokens(&Value::Object(skeleton.clone())));
let mut sections_dropped = Vec::new();
let mut oversized_dropped = 0usize;
let mut truncated = false;
for (key, v) in map {
if !v.is_array() {
continue;
}
let elems = v.as_array().expect("checked is_array");
let mut kept = Vec::with_capacity(elems.len());
let mut dropped_here = false;
for elem in elems {
let est = serialized_tokens(elem);
if est > budget {
oversized_dropped += 1;
dropped_here = true;
continue;
}
if est <= remaining {
remaining -= est;
kept.push(elem.clone());
} else {
dropped_here = true;
}
}
if dropped_here {
truncated = true;
sections_dropped.push(key.clone());
}
skeleton.insert(key.clone(), Value::Array(kept));
}
let receipt = BudgetReceipt {
requested: budget,
estimated_tokens: serialized_tokens(&Value::Object(skeleton.clone())),
truncated,
sections_dropped,
oversized_dropped,
};
skeleton.insert(
"budget".to_string(),
serde_json::to_value(&receipt).unwrap_or(Value::Null),
);
Value::Object(skeleton)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn elem(tag: &str, pad_chars: usize) -> Value {
json!({ "name": tag, "pad": "x".repeat(pad_chars) })
}
#[test]
fn estimate_tokens_counts_chars_not_bytes() {
assert_eq!(estimate_tokens("abcdefgh"), 2);
assert_eq!(estimate_tokens(""), 0);
assert_eq!(estimate_tokens("ab"), 0);
assert_eq!(estimate_tokens("上下上下上下上下"), 2);
}
#[test]
fn budget_zero_is_identity() {
let v = json!({"symbol": "a", "incoming": [1, 2, 3]});
let out = apply_budget(v.clone(), 0);
assert_eq!(out, v);
assert!(out.get("budget").is_none());
}
#[test]
fn non_object_passthrough() {
let v = json!([1, 2, 3]);
assert_eq!(apply_budget(v.clone(), 5), v);
}
#[test]
fn scalars_survive_tiniest_budget() {
let v = json!({
"symbol": "parse_source",
"node": {"name": "parse_source", "label": "Function"},
"incoming": [elem("a", 400), elem("b", 400)]
});
let out = apply_budget(v, 1);
assert_eq!(out["symbol"], "parse_source");
assert!(out["node"].is_object());
assert!(out["incoming"].as_array().unwrap().is_empty());
assert_eq!(out["budget"]["truncated"], true);
}
#[test]
fn arrays_are_prefix_truncated_in_key_order() {
let elems: Vec<Value> = (0..10).map(|i| elem(&format!("e{i}"), 40)).collect();
let v = json!({
"symbol": "x",
"incoming": elems,
"outgoing": [elem("o", 40)]
});
let out = apply_budget(v, 100);
let kept = out["incoming"].as_array().unwrap().len();
assert!(
kept > 0 && kept < 10,
"expected partial packing, got {kept}"
);
let out_tokens = out["budget"]["estimated_tokens"].as_u64().unwrap();
assert!(
out_tokens <= 100 + 20,
"estimate wildly over budget: {out_tokens}"
);
assert_eq!(out["budget"]["truncated"], true);
let dropped = out["budget"]["sections_dropped"].as_array().unwrap();
assert!(dropped.contains(&json!("incoming")));
}
#[test]
fn oversized_element_counts_and_drops() {
let big = elem("big", 10_000);
let v = json!({"symbol": "x", "incoming": [big, elem("small", 4)]});
let out = apply_budget(v, 50);
assert_eq!(out["incoming"].as_array().unwrap().len(), 1);
assert_eq!(out["budget"]["oversized_dropped"], 1);
assert_eq!(out["budget"]["truncated"], true);
}
#[test]
fn exact_fit_reports_not_truncated() {
let v = json!({
"symbol": "abc",
"incoming": [elem("a", 8), elem("b", 8)]
});
let natural = serialized_tokens(&v);
let out = apply_budget(v.clone(), natural);
assert_eq!(out["incoming"].as_array().unwrap().len(), 2);
assert_eq!(out["budget"]["truncated"], false);
assert!(out["budget"]["sections_dropped"]
.as_array()
.unwrap()
.is_empty());
}
#[test]
fn empty_array_field_never_marks_truncation() {
let v = json!({"symbol": "x", "incoming": [], "outgoing": [elem("o", 4)]});
let out = apply_budget(
v,
serialized_tokens(&json!({
"symbol": "x", "incoming": [], "outgoing": []
})) + serialized_tokens(&elem("o", 4)),
);
assert_eq!(out["budget"]["truncated"], false);
assert_eq!(out["budget"]["sections_dropped"], json!([]));
}
#[test]
fn nested_object_estimate_matches_serialized_size() {
let nested = json!({"deep": {"deeper": {"leaf": "y".repeat(64)}}});
let v = json!({"symbol": "x", "context": nested, "incoming": [elem("a", 4)]});
let skeleton =
json!({"symbol": "x", "context": {"deep": {"deeper": {"leaf": ""}}}, "incoming": []});
let out = apply_budget(v, 10);
assert_eq!(
out["context"]["deep"]["deeper"]["leaf"]
.as_str()
.unwrap()
.len(),
64
);
assert!(
out["budget"]["estimated_tokens"].as_u64().unwrap()
>= (serialized_tokens(&skeleton) / 4) as u64
);
}
}