use crate::error::{Result, SdkError};
use super::accountant::TokenAccountantRef;
use super::section::{PromptSectionId, RenderedSection};
#[derive(Debug, Clone, PartialEq)]
pub struct BudgetFit {
pub kept: Vec<RenderedSection>,
pub dropped: Vec<PromptSectionId>,
}
pub fn fit_budget(
sections: Vec<RenderedSection>,
accountant: &TokenAccountantRef,
token_budget: u64,
) -> Result<BudgetFit> {
let required_cost: u64 = sections
.iter()
.filter(|section| section.required)
.map(|section| accountant.count_message(§ion.content))
.sum();
if required_cost > token_budget {
return Err(SdkError::Domain(format!(
"required sections alone cost {required_cost} tokens over the \
{token_budget}-token budget; compilation fails closed"
)));
}
let mut drop_order: Vec<usize> = sections
.iter()
.enumerate()
.filter(|(_, section)| !section.required)
.map(|(index, _)| index)
.collect();
drop_order.sort_by(|&a, &b| {
sections[a]
.priority
.cmp(§ions[b].priority)
.then_with(|| sections[a].id.cmp(§ions[b].id))
});
let mut total: u64 = sections
.iter()
.map(|section| accountant.count_message(§ion.content))
.sum();
let mut dropped_indices = Vec::new();
let mut drop_queue = drop_order.into_iter();
while total > token_budget {
let Some(index) = drop_queue.next() else {
return Err(SdkError::Domain(
"budget fitting exhausted optional sections without fitting".into(),
));
};
total -= accountant.count_message(§ions[index].content);
dropped_indices.push(index);
}
let dropped: Vec<PromptSectionId> = dropped_indices
.iter()
.map(|&index| sections[index].id.clone())
.collect();
let kept = sections
.into_iter()
.enumerate()
.filter(|(index, _)| !dropped_indices.contains(index))
.map(|(_, section)| section)
.collect();
Ok(BudgetFit { kept, dropped })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prompt::section::{PromptMessageRole, PromptSectionVersion};
fn section(id: &str, required: bool, priority: u16, bytes: usize) -> RenderedSection {
RenderedSection {
id: PromptSectionId(id.into()),
version: PromptSectionVersion(1),
role: PromptMessageRole::System,
required,
priority,
content_hash: format!("sha256:{id}"),
content: "x".repeat(bytes),
}
}
#[test]
fn the_spec_worked_example_drops_only_repository_hints() {
let accountant = TokenAccountantRef::approx_bytes_v1();
let sections = vec![
section("s/role", true, 0, 336), section("s/tool_protocol", true, 0, 1_896), section("s/correction", true, 0, 2_076), section("s/repository_hints", false, 40, 1_356), section("s/style_guidance", false, 60, 636), section("s/output_contract", true, 0, 306), ];
let fit = fit_budget(sections, &accountant, 1_800).unwrap();
assert_eq!(
fit.dropped,
vec![PromptSectionId("s/repository_hints".into())]
);
let kept: Vec<&str> = fit.kept.iter().map(|s| s.id.0.as_str()).collect();
assert_eq!(
kept,
[
"s/role",
"s/tool_protocol",
"s/correction",
"s/style_guidance",
"s/output_contract"
]
);
}
#[test]
fn required_overflow_fails_closed_and_ties_break_by_id() {
let accountant = TokenAccountantRef::approx_bytes_v1();
assert!(fit_budget(vec![section("s/huge", true, 0, 30_000)], &accountant, 100).is_err());
let sections = vec![
section("s/b", false, 10, 3_000),
section("s/a", false, 10, 3_000),
section("s/base", true, 0, 30),
];
let fit = fit_budget(sections, &accountant, 1_200).unwrap();
assert_eq!(fit.dropped.first().unwrap().0, "s/a", "ties break by id");
}
#[test]
fn fitting_is_deterministic() {
let accountant = TokenAccountantRef::approx_bytes_v1();
let sections = || {
vec![
section("s/base", true, 0, 300),
section("s/opt1", false, 20, 900),
section("s/opt2", false, 30, 900),
]
};
let first = fit_budget(sections(), &accountant, 450).unwrap();
let second = fit_budget(sections(), &accountant, 450).unwrap();
assert_eq!(first, second);
}
}