pub use bamboo_compression::{
active_messages_for_budget, apply_compression_plan, build_compression_plan_with_summary,
build_forced_compression_plan_with_summary, build_summary_prompt, compression_summary_message,
context_window_usage_percent, estimate_context_compression_exposure,
normalized_trigger_percent, summary_source_messages, CompressionPlan, CompressionPlanError,
ContextCompressionExposure,
};
pub use bamboo_compression::{
create_budget_for_model, prepare_hybrid_context, BudgetError, BudgetStrategy,
HeuristicSummarizer, HeuristicTokenCounter, MessageSegmenter, ModelLimitsRegistry,
PreparedContext, Summarizer, SummaryManager, SummaryTrigger, TiktokenTokenCounter, TokenBudget,
TokenCounter, TokenUsageBreakdown,
};
use crate::memory_store::TemporalGranularity;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GranularityBudgetItem {
pub id: String,
pub granularity: Option<TemporalGranularity>,
pub rendered: String,
}
impl GranularityBudgetItem {
pub fn new(
id: impl Into<String>,
granularity: Option<TemporalGranularity>,
rendered: impl Into<String>,
) -> Self {
Self {
id: id.into(),
granularity,
rendered: rendered.into(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GranularityBudgetSegments {
pub prefix: String,
pub suffix: String,
pub prefix_dropped_ids: Vec<String>,
pub suffix_dropped_ids: Vec<String>,
}
impl GranularityBudgetSegments {
pub fn combined(&self) -> String {
format!("{}{}", self.prefix, self.suffix)
}
}
pub fn segment_by_granularity_budget(
items: &[GranularityBudgetItem],
total_budget_chars: usize,
) -> GranularityBudgetSegments {
let mut prefix_items = Vec::new();
let mut suffix_items = Vec::new();
for item in items {
if TemporalGranularity::is_high_churn(item.granularity) {
suffix_items.push(item);
} else {
prefix_items.push(item);
}
}
let (prefix, prefix_chars, prefix_dropped_ids) =
fill_budget_segment(&prefix_items, total_budget_chars);
let remaining_budget = total_budget_chars.saturating_sub(prefix_chars);
let (suffix, _suffix_chars, suffix_dropped_ids) =
fill_budget_segment(&suffix_items, remaining_budget);
GranularityBudgetSegments {
prefix,
suffix,
prefix_dropped_ids,
suffix_dropped_ids,
}
}
fn fill_budget_segment(
items: &[&GranularityBudgetItem],
budget_chars: usize,
) -> (String, usize, Vec<String>) {
if budget_chars == 0 {
return (
String::new(),
0,
items.iter().map(|item| item.id.clone()).collect(),
);
}
let mut rendered = String::new();
let mut used_chars = 0usize;
let mut dropped_ids = Vec::new();
let mut truncated = false;
for item in items {
if truncated {
dropped_ids.push(item.id.clone());
continue;
}
let cost = item.rendered.chars().count();
if used_chars > 0 && used_chars + cost > budget_chars {
truncated = true;
dropped_ids.push(item.id.clone());
continue;
}
used_chars += cost;
rendered.push_str(&item.rendered);
}
(rendered, used_chars, dropped_ids)
}
#[cfg(test)]
mod granularity_budget_tests {
use super::*;
fn item(
id: &str,
granularity: Option<TemporalGranularity>,
rendered: &str,
) -> GranularityBudgetItem {
GranularityBudgetItem::new(id, granularity, rendered)
}
#[test]
fn coarse_memories_render_before_fine_ones() {
let items = vec![
item("day-1", Some(TemporalGranularity::Day), "DAY."),
item("year-1", Some(TemporalGranularity::Year), "YEAR."),
item("week-1", Some(TemporalGranularity::Week), "WEEK."),
item("quarter-1", Some(TemporalGranularity::Quarter), "QUARTER."),
];
let segments = segment_by_granularity_budget(&items, 1_000);
assert_eq!(segments.prefix, "YEAR.QUARTER.");
assert_eq!(segments.suffix, "DAY.WEEK.");
let combined = segments.combined();
assert!(combined.find("YEAR").unwrap() < combined.find("DAY").unwrap());
assert!(segments.prefix_dropped_ids.is_empty());
assert!(segments.suffix_dropped_ids.is_empty());
}
#[test]
fn none_granularity_is_treated_as_prefix_eligible() {
let items = vec![
item("untagged", None, "UNTAGGED."),
item("day-1", Some(TemporalGranularity::Day), "DAY."),
];
let segments = segment_by_granularity_budget(&items, 1_000);
assert_eq!(segments.prefix, "UNTAGGED.");
assert_eq!(segments.suffix, "DAY.");
}
#[test]
fn changing_or_adding_a_day_memory_does_not_change_the_rendered_prefix_segment() {
let coarse = [
item("year-1", Some(TemporalGranularity::Year), "YEAR-ONE."),
item(
"quarter-1",
Some(TemporalGranularity::Quarter),
"QUARTER-ONE.",
),
];
let before = vec![
coarse[0].clone(),
coarse[1].clone(),
item("day-1", Some(TemporalGranularity::Day), "DAY-ONE."),
];
let segments_before = segment_by_granularity_budget(&before, 500);
let after = vec![
coarse[0].clone(),
coarse[1].clone(),
item(
"day-1",
Some(TemporalGranularity::Day),
"DAY-ONE-REWRITTEN-WITH-MORE-DETAIL.",
),
item(
"day-2",
Some(TemporalGranularity::Day),
"DAY-TWO-BRAND-NEW.",
),
];
let segments_after = segment_by_granularity_budget(&after, 500);
assert_eq!(
segments_before.prefix, segments_after.prefix,
"prefix segment must be byte-identical regardless of suffix churn"
);
assert_ne!(
segments_before.suffix, segments_after.suffix,
"suffix segment is expected to change"
);
}
#[test]
fn budget_exhaustion_drops_fine_before_coarse() {
let items = vec![
item("year-1", Some(TemporalGranularity::Year), &"Y".repeat(40)),
item("day-1", Some(TemporalGranularity::Day), &"D".repeat(40)),
];
let segments = segment_by_granularity_budget(&items, 40);
assert_eq!(segments.prefix.chars().count(), 40);
assert!(segments.prefix_dropped_ids.is_empty());
assert!(
segments.suffix.is_empty(),
"suffix must be fully starved once the prefix consumes the whole budget"
);
assert_eq!(segments.suffix_dropped_ids, vec!["day-1".to_string()]);
}
#[test]
fn budget_exhaustion_drops_suffix_before_prefix_even_if_suffix_item_was_more_relevant() {
let items = vec![
item(
"day-most-relevant",
Some(TemporalGranularity::Day),
&"R".repeat(30),
),
item(
"year-least-relevant",
Some(TemporalGranularity::Year),
&"C".repeat(30),
),
];
let segments = segment_by_granularity_budget(&items, 30);
assert_eq!(
segments.prefix.chars().count(),
30,
"coarse item still renders"
);
assert!(
segments.suffix.is_empty(),
"fine item is dropped by design, despite higher relevance"
);
assert_eq!(
segments.suffix_dropped_ids,
vec!["day-most-relevant".to_string()]
);
}
#[test]
fn leftover_prefix_budget_is_available_to_the_suffix() {
let items = vec![
item("year-1", Some(TemporalGranularity::Year), &"Y".repeat(10)),
item("day-1", Some(TemporalGranularity::Day), &"D".repeat(10)),
];
let segments = segment_by_granularity_budget(&items, 25);
assert_eq!(segments.prefix.chars().count(), 10);
assert_eq!(segments.suffix.chars().count(), 10);
assert!(segments.prefix_dropped_ids.is_empty());
assert!(segments.suffix_dropped_ids.is_empty());
}
#[test]
fn always_includes_at_least_one_oversized_item_per_segment() {
let items = vec![item(
"year-huge",
Some(TemporalGranularity::Year),
&"Y".repeat(500),
)];
let segments = segment_by_granularity_budget(&items, 10);
assert_eq!(segments.prefix.chars().count(), 500);
assert!(segments.prefix_dropped_ids.is_empty());
}
#[test]
fn zero_budget_drops_everything_and_starves_both_segments() {
let items = vec![
item("year-1", Some(TemporalGranularity::Year), "Y"),
item("day-1", Some(TemporalGranularity::Day), "D"),
];
let segments = segment_by_granularity_budget(&items, 0);
assert!(segments.prefix.is_empty());
assert!(segments.suffix.is_empty());
assert_eq!(segments.prefix_dropped_ids, vec!["year-1".to_string()]);
assert_eq!(segments.suffix_dropped_ids, vec!["day-1".to_string()]);
}
#[test]
fn empty_input_yields_empty_segments() {
let segments = segment_by_granularity_budget(&[], 1_000);
assert_eq!(segments, GranularityBudgetSegments::default());
}
}