mod types;
pub use types::*;
use crate::error::{Result, TinyAgentsError};
use crate::harness::message::Message;
use async_trait::async_trait;
pub fn estimate_tokens(text: &str) -> u64 {
let chars = text.chars().count() as u64;
if chars == 0 { 0 } else { (chars / 4).max(1) }
}
fn message_token_estimate(msg: &Message) -> u64 {
estimate_tokens(&msg.text())
}
fn slice_token_estimate(messages: &[Message]) -> u64 {
messages.iter().map(message_token_estimate).sum()
}
fn partition_system(messages: &[Message]) -> (Vec<Message>, Vec<Message>) {
let system = messages
.iter()
.filter(|m| matches!(m, Message::System(_)))
.cloned()
.collect();
let non_system = messages
.iter()
.filter(|m| !matches!(m, Message::System(_)))
.cloned()
.collect();
(system, non_system)
}
pub fn trim_messages(messages: &[Message], strategy: &TrimStrategy) -> Vec<Message> {
match strategy {
TrimStrategy::KeepLast(n) => {
let (system, non_system) = partition_system(messages);
let keep_start = non_system.len().saturating_sub(*n);
let mut result = system;
result.extend_from_slice(&non_system[keep_start..]);
result
}
TrimStrategy::KeepFirstAndLast { first, last } => {
let (system, non_system) = partition_system(messages);
let len = non_system.len();
let first = *first;
let last = *last;
let mut result = system;
if first + last >= len {
result.extend(non_system);
} else {
result.extend_from_slice(&non_system[..first]);
result.extend_from_slice(&non_system[len - last..]);
}
result
}
TrimStrategy::MaxTokens(limit) => {
let (system, non_system) = partition_system(messages);
let limit = *limit;
let mut candidate: Vec<Message> = non_system;
while !candidate.is_empty() {
let total = slice_token_estimate(&system) + slice_token_estimate(&candidate);
if total <= limit {
break;
}
candidate.remove(0);
}
let mut sys_candidate = system;
while !sys_candidate.is_empty() {
let total = slice_token_estimate(&sys_candidate) + slice_token_estimate(&candidate);
if total <= limit {
break;
}
sys_candidate.remove(0);
}
let mut result = sys_candidate;
result.extend(candidate);
result
}
}
}
#[async_trait]
impl Summarizer for ConcatSummarizer {
async fn summarize(&self, messages: &[Message]) -> Result<SummaryRecord> {
if messages.is_empty() {
return Err(TinyAgentsError::Validation(
"cannot summarize an empty message list".into(),
));
}
let original_token_estimate = slice_token_estimate(messages);
let mut parts: Vec<String> = Vec::with_capacity(messages.len() + 1);
parts.push("=== Conversation Summary ===".to_string());
let source_ids: Vec<String> = messages
.iter()
.enumerate()
.map(|(i, msg)| {
let role = match msg {
Message::System(_) => "system",
Message::User(_) => "user",
Message::Assistant(_) => "assistant",
Message::Tool(_) => "tool",
};
let id = format!("msg-{i}");
parts.push(format!("[{id}] {role}: {}", msg.text()));
id
})
.collect();
let summary_text = parts.join("\n");
let summary_token_estimate = estimate_tokens(&summary_text);
let summary = Message::system(summary_text);
let provenance = CompressionProvenance {
source_ids,
original_token_estimate,
summary_token_estimate,
reason: "ConcatSummarizer: messages concatenated verbatim (no LLM call)".to_string(),
};
Ok(SummaryRecord {
summary,
provenance,
})
}
}
impl SummarizationPolicy {
pub fn from_profile(profile: &crate::harness::model::ModelProfile, threshold: f64) -> Self {
Self {
context_window: profile.max_input_tokens,
threshold_fraction: threshold,
..Self::default()
}
}
pub fn with_context_window(mut self, max_input_tokens: u64) -> Self {
self.context_window = Some(max_input_tokens);
self
}
pub fn with_threshold_fraction(mut self, fraction: f64) -> Self {
self.threshold_fraction = fraction;
self
}
pub fn trigger_budget(&self) -> u64 {
match self.context_window {
Some(window) => (window as f64 * self.threshold_fraction) as u64,
None => self.trigger_tokens,
}
}
pub fn should_summarize(&self, messages: &[Message]) -> bool {
let tokens = slice_token_estimate(messages);
match self.context_window {
Some(_) => tokens >= self.trigger_budget(),
None => tokens > self.trigger_tokens,
}
}
pub fn plan(&self, messages: &[Message]) -> (Vec<Message>, Vec<Message>) {
let (system, non_system) = partition_system(messages);
if non_system.len() <= self.keep_last {
let mut to_keep = system;
to_keep.extend(non_system);
return (Vec::new(), to_keep);
}
let split = non_system.len() - self.keep_last;
let to_summarize = non_system[..split].to_vec();
let to_keep_recent = non_system[split..].to_vec();
let mut to_keep = system;
to_keep.extend(to_keep_recent);
(to_summarize, to_keep)
}
}
#[cfg(test)]
mod test;