pub mod selector;
pub use selector::ContextSelector;
use crate::llm::types::Message;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourcePriority {
Critical = 10,
High = 8,
Medium = 5,
Low = 3,
BestEffort = 1,
}
#[derive(Debug, Clone)]
pub struct ContextBudget {
pub total_tokens: usize,
pub user_reserve: usize,
pub history_max: usize,
pub tool_results_max: usize,
pub memory_max: usize,
}
impl Default for ContextBudget {
fn default() -> Self {
Self {
total_tokens: 128_000,
user_reserve: 500,
history_max: 50_000,
tool_results_max: 20_000,
memory_max: 5_000,
}
}
}
impl ContextBudget {
pub fn new(total_tokens: usize) -> Self {
Self {
total_tokens,
..Default::default()
}
}
}
#[derive(Clone, Default)]
pub struct ContextSources {
pub system_prompt: Option<String>,
pub developer_instructions: Vec<String>,
pub project_rules: Vec<String>,
pub task_state: Option<String>,
pub hook_injected: Vec<String>,
pub memory_recall: Option<String>,
pub conversation_history: Vec<Message>,
pub tool_results: Vec<Message>,
pub subagent_reports: Vec<Message>,
pub user_message: Option<Message>,
}
pub struct ContextAssembler {
pub budget: Option<ContextBudget>,
}
impl Default for ContextAssembler {
fn default() -> Self {
Self::new()
}
}
impl ContextAssembler {
pub fn new() -> Self {
Self { budget: None }
}
pub fn with_budget(mut self, budget: ContextBudget) -> Self {
self.budget = Some(budget);
self
}
pub fn assemble(&self, sources: ContextSources) -> Vec<Message> {
let mut messages = Vec::new();
if let Some(sys) = &sources.system_prompt {
messages.push(Message::system(sys.clone()));
}
for inst in &sources.developer_instructions {
messages.push(Message::system(format!("[Developer Instructions]\n{inst}")));
}
for rule in &sources.project_rules {
messages.push(Message::system(format!("[Project Rule]\n{rule}")));
}
if let Some(state) = &sources.task_state {
messages.push(Message::system(format!("[Task State]\n{state}")));
}
for hook_msg in &sources.hook_injected {
messages.push(Message::system(hook_msg.clone()));
}
if let Some(mem) = &sources.memory_recall {
let mem_text = if let Some(ref budget) = self.budget {
if mem.len() > budget.memory_max {
let cut = (0..=budget.memory_max)
.rev()
.find(|i| mem.is_char_boundary(*i))
.unwrap_or(budget.memory_max);
format!("{}...", &mem[..cut])
} else {
mem.clone()
}
} else {
mem.clone()
};
messages.push(Message::user(mem_text));
}
let history = if let Some(ref budget) = self.budget {
let mut token_est = 0usize;
let keep_count = sources
.conversation_history
.iter()
.rev()
.take_while(|m| {
let t = m.content.as_text().map(|c| c.len() / 4).unwrap_or(0);
token_est += t;
token_est <= budget.history_max
})
.count();
let start = sources
.conversation_history
.len()
.saturating_sub(keep_count);
sources.conversation_history[start..].to_vec()
} else {
sources.conversation_history.clone()
};
messages.extend(history);
messages.extend(sources.subagent_reports);
let tool_results = if let Some(ref budget) = self.budget {
let mut token_est = 0usize;
let keep_count = sources
.tool_results
.iter()
.rev()
.take_while(|m| {
let t = m.content.as_text().map(|c| c.len() / 4).unwrap_or(0);
token_est += t;
token_est <= budget.tool_results_max
})
.count();
let start = sources.tool_results.len().saturating_sub(keep_count);
sources.tool_results[start..].to_vec()
} else {
sources.tool_results.clone()
};
messages.extend(tool_results);
if let Some(user_msg) = sources.user_message {
messages.push(user_msg);
}
messages
}
}