use crate::Event;
use crate::Part;
#[derive(Debug, Clone)]
pub struct IntraCompactionConfig {
pub token_threshold: u64,
pub overlap_event_count: usize,
pub chars_per_token: u32,
}
impl Default for IntraCompactionConfig {
fn default() -> Self {
Self { token_threshold: 100_000, overlap_event_count: 10, chars_per_token: 4 }
}
}
pub fn estimate_tokens(events: &[Event], chars_per_token: u32) -> u64 {
if chars_per_token == 0 {
return 0;
}
let total_chars: u64 = events.iter().map(|e| estimate_event_chars(e) as u64).sum();
total_chars / chars_per_token as u64
}
fn estimate_event_chars(event: &Event) -> usize {
let mut chars = 0;
if let Some(content) = &event.llm_response.content {
for part in &content.parts {
chars += estimate_part_chars(part);
}
}
if !event.actions.state_delta.is_empty() {
if let Ok(json) = serde_json::to_string(&event.actions.state_delta) {
chars += json.len();
}
}
chars
}
fn estimate_part_chars(part: &Part) -> usize {
match part {
Part::Text { text } => text.len(),
Part::Thinking { thinking, .. } => thinking.len(),
Part::FunctionCall { name, args, .. } => {
name.len() + serde_json::to_string(args).map_or(0, |s| s.len())
}
Part::FunctionResponse { function_response, .. } => {
function_response.name.len()
+ serde_json::to_string(&function_response.response).map_or(0, |s| s.len())
}
Part::InlineData { .. } | Part::FileData { .. } => 0,
Part::ServerToolCall { server_tool_call } => {
serde_json::to_string(server_tool_call).map_or(0, |s| s.len())
}
Part::ServerToolResponse { server_tool_response } => {
serde_json::to_string(server_tool_response).map_or(0, |s| s.len())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Content, FunctionResponseData};
#[test]
fn test_default_config() {
let config = IntraCompactionConfig::default();
assert_eq!(config.token_threshold, 100_000);
assert_eq!(config.overlap_event_count, 10);
assert_eq!(config.chars_per_token, 4);
}
#[test]
fn test_estimate_tokens_empty() {
assert_eq!(estimate_tokens(&[], 4), 0);
}
#[test]
fn test_estimate_tokens_zero_ratio() {
let mut event = Event::new("inv-1");
event.set_content(Content::new("user").with_text("Hello"));
assert_eq!(estimate_tokens(&[event], 0), 0);
}
#[test]
fn test_estimate_tokens_text_only() {
let mut event = Event::new("inv-1");
event.set_content(Content::new("user").with_text("Hello"));
assert_eq!(estimate_tokens(&[event], 4), 1);
}
#[test]
fn test_estimate_tokens_multiple_events() {
let mut e1 = Event::new("inv-1");
e1.set_content(Content::new("user").with_text("Hello")); let mut e2 = Event::new("inv-1");
e2.set_content(Content::new("model").with_text("World!")); assert_eq!(estimate_tokens(&[e1, e2], 4), 2);
}
#[test]
fn test_estimate_tokens_with_function_call() {
let mut event = Event::new("inv-1");
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![Part::FunctionCall {
name: "get_weather".to_string(),
args: serde_json::json!({"city": "NYC"}),
id: None,
thought_signature: None,
}],
});
let tokens = estimate_tokens(&[event], 4);
assert!(tokens > 0);
}
#[test]
fn test_estimate_tokens_with_function_response() {
let mut event = Event::new("inv-1");
event.llm_response.content = Some(Content {
role: "function".to_string(),
parts: vec![Part::FunctionResponse {
function_response: FunctionResponseData::new(
"get_weather",
serde_json::json!({"temp": 72}),
),
id: None,
}],
});
let tokens = estimate_tokens(&[event], 4);
assert!(tokens > 0);
}
}