use crate::traits::MemoryType;
pub const PRIORITY_BEHAVIORAL: u8 = 0;
pub const PRIORITY_RETRY: u8 = 10;
pub const PRIORITY_SPEC: u8 = 15;
pub const PRIORITY_SIMILAR: u8 = 25;
pub const PRIORITY_LEARNING: u8 = 40;
pub const PRIORITY_HISTORICAL: u8 = 60;
#[derive(Debug, Clone)]
pub struct ContextBudget {
pub max_tokens: usize,
pub tokens_per_char: f32,
}
impl ContextBudget {
pub fn new(max_tokens: usize) -> Self {
Self {
max_tokens,
tokens_per_char: 0.25,
}
}
pub fn with_ratio(max_tokens: usize, tokens_per_char: f32) -> Self {
Self {
max_tokens,
tokens_per_char,
}
}
pub fn estimate_tokens(&self, text: &str) -> usize {
(text.len() as f32 * self.tokens_per_char).ceil() as usize
}
}
impl Default for ContextBudget {
fn default() -> Self {
Self::new(4096)
}
}
#[derive(Debug, Clone)]
pub struct ContextItem {
pub memory_id: i64,
pub content: String,
pub priority: u8,
pub estimated_tokens: usize,
pub relevance_score: f32,
pub memory_type: MemoryType,
pub category: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ContextAssembly {
pub items: Vec<ContextItem>,
pub total_tokens: usize,
pub excluded_count: usize,
pub budget_max: usize,
}
impl ContextAssembly {
pub fn assemble(mut candidates: Vec<ContextItem>, budget: &ContextBudget) -> Self {
candidates.sort_by(|a, b| {
a.priority
.cmp(&b.priority)
.then(b.relevance_score.partial_cmp(&a.relevance_score).unwrap_or(std::cmp::Ordering::Equal))
});
let mut items = Vec::new();
let mut total_tokens = 0;
let mut excluded_count = 0;
for candidate in candidates {
if total_tokens + candidate.estimated_tokens <= budget.max_tokens {
total_tokens += candidate.estimated_tokens;
items.push(candidate);
} else {
excluded_count += 1;
}
}
Self {
items,
total_tokens,
excluded_count,
budget_max: budget.max_tokens,
}
}
pub fn render(&self) -> String {
if self.items.is_empty() {
return String::new();
}
let mut output = String::new();
let mut current_priority = None;
for item in &self.items {
if current_priority != Some(item.priority) {
if !output.is_empty() {
output.push('\n');
}
let section = priority_label(item.priority);
output.push_str(&format!("## {section}\n\n"));
current_priority = Some(item.priority);
}
output.push_str(&item.content);
output.push('\n');
}
output
}
pub fn is_truncated(&self) -> bool {
self.excluded_count > 0
}
pub fn remaining_tokens(&self) -> usize {
self.budget_max.saturating_sub(self.total_tokens)
}
}
fn priority_label(priority: u8) -> &'static str {
match priority {
0 => "Critical Context",
1..=10 => "Retry Context",
11..=15 => "Specifications",
16..=25 => "Similar Tasks",
26..=40 => "Learnings",
_ => "Historical Context",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn item(id: i64, text: &str, priority: u8, tokens: usize, score: f32) -> ContextItem {
ContextItem {
memory_id: id,
content: text.to_string(),
priority,
estimated_tokens: tokens,
relevance_score: score,
memory_type: MemoryType::Semantic,
category: None,
}
}
#[test]
fn budget_token_estimation() {
let budget = ContextBudget::new(1000);
assert_eq!(budget.estimate_tokens("hello"), 2); assert_eq!(budget.estimate_tokens(""), 0);
}
#[test]
fn assemble_within_budget() {
let budget = ContextBudget::new(100);
let candidates = vec![
item(1, "first", 0, 30, 1.0),
item(2, "second", 10, 30, 0.8),
item(3, "third", 25, 30, 0.5),
];
let assembly = ContextAssembly::assemble(candidates, &budget);
assert_eq!(assembly.items.len(), 3);
assert_eq!(assembly.total_tokens, 90);
assert_eq!(assembly.excluded_count, 0);
assert!(!assembly.is_truncated());
}
#[test]
fn assemble_exceeds_budget() {
let budget = ContextBudget::new(50);
let candidates = vec![
item(1, "high priority", 0, 30, 1.0),
item(2, "medium priority", 25, 30, 0.8),
item(3, "low priority", 60, 30, 0.5),
];
let assembly = ContextAssembly::assemble(candidates, &budget);
assert_eq!(assembly.items.len(), 1); assert_eq!(assembly.total_tokens, 30);
assert_eq!(assembly.excluded_count, 2);
assert!(assembly.is_truncated());
}
#[test]
fn priority_ordering() {
let budget = ContextBudget::new(1000);
let candidates = vec![
item(1, "low", 60, 10, 1.0),
item(2, "high", 0, 10, 0.5),
item(3, "mid", 25, 10, 0.8),
];
let assembly = ContextAssembly::assemble(candidates, &budget);
assert_eq!(assembly.items[0].memory_id, 2); assert_eq!(assembly.items[1].memory_id, 3); assert_eq!(assembly.items[2].memory_id, 1); }
#[test]
fn relevance_within_priority() {
let budget = ContextBudget::new(1000);
let candidates = vec![
item(1, "low relevance", 25, 10, 0.3),
item(2, "high relevance", 25, 10, 0.9),
item(3, "mid relevance", 25, 10, 0.6),
];
let assembly = ContextAssembly::assemble(candidates, &budget);
assert_eq!(assembly.items[0].memory_id, 2); assert_eq!(assembly.items[1].memory_id, 3);
assert_eq!(assembly.items[2].memory_id, 1);
}
#[test]
fn render_with_sections() {
let budget = ContextBudget::new(1000);
let candidates = vec![
item(1, "critical info", 0, 10, 1.0),
item(2, "a learning", 40, 10, 0.5),
];
let assembly = ContextAssembly::assemble(candidates, &budget);
let rendered = assembly.render();
assert!(rendered.contains("## Critical Context"));
assert!(rendered.contains("## Learnings"));
assert!(rendered.contains("critical info"));
assert!(rendered.contains("a learning"));
}
#[test]
fn empty_assembly() {
let budget = ContextBudget::new(1000);
let assembly = ContextAssembly::assemble(Vec::new(), &budget);
assert!(assembly.items.is_empty());
assert_eq!(assembly.render(), "");
assert!(!assembly.is_truncated());
assert_eq!(assembly.remaining_tokens(), 1000);
}
#[test]
fn skip_too_large_items() {
let budget = ContextBudget::new(50);
let candidates = vec![
item(1, "small", 0, 20, 1.0),
item(2, "too big", 10, 100, 0.9), item(3, "also small", 25, 20, 0.5), ];
let assembly = ContextAssembly::assemble(candidates, &budget);
assert_eq!(assembly.items.len(), 2);
assert_eq!(assembly.items[0].memory_id, 1);
assert_eq!(assembly.items[1].memory_id, 3);
assert_eq!(assembly.excluded_count, 1);
}
}