use crate::high_level::complete;
use crate::high_level::tokens::estimate as estimate_tokens;
use crate::{
Api, AssistantMessage, ContentBlock, Context, Message, Model, Provider, StreamOptions,
TextContent, UserMessage,
};
fn safe_truncate(s: &str, max_chars: usize) -> String {
if s.len() <= max_chars { return s.to_string(); }
let boundary = s.char_indices()
.take_while(|(i, _)| *i <= max_chars)
.last()
.map(|(i, c)| i + c.len_utf8())
.unwrap_or(0);
format!("{}...", &s[..boundary])
}
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct CompactionConfig {
pub keep_recent: usize,
pub max_batch: usize,
pub target_ratio: f32,
pub summary_max_tokens: usize,
pub temperature: f32,
pub timeout: Duration,
pub custom_instruction: Option<String>,
}
impl CompactionConfig {
pub fn new() -> Self {
Self {
keep_recent: 4,
max_batch: 20,
target_ratio: 0.5,
summary_max_tokens: 1024,
temperature: 0.3,
timeout: Duration::from_secs(60),
custom_instruction: None,
}
}
pub fn with_keep_recent(mut self, count: usize) -> Self {
self.keep_recent = count;
self
}
pub fn with_max_batch(mut self, count: usize) -> Self {
self.max_batch = count;
self
}
pub fn with_target_ratio(mut self, ratio: f32) -> Self {
self.target_ratio = ratio.clamp(0.1, 0.9);
self
}
pub fn with_summary_max_tokens(mut self, tokens: usize) -> Self {
self.summary_max_tokens = tokens;
self
}
pub fn with_temperature(mut self, temp: f32) -> Self {
self.temperature = temp.clamp(0.0, 1.0);
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_custom_instruction(mut self, instruction: impl Into<String>) -> Self {
self.custom_instruction = Some(instruction.into());
self
}
}
impl Default for CompactionConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionMetadata {
pub original_tokens: usize,
pub compacted_tokens: usize,
pub messages_compacted: usize,
pub messages_kept: usize,
pub timestamp: DateTime<Utc>,
pub target_ratio: f32,
pub actual_ratio: f32,
pub success: bool,
pub error: Option<String>,
}
impl CompactionMetadata {
pub fn new(
original_tokens: usize,
compacted_tokens: usize,
messages_compacted: usize,
messages_kept: usize,
target_ratio: f32,
) -> Self {
let actual_ratio = if original_tokens > 0 {
compacted_tokens as f32 / original_tokens as f32
} else {
1.0
};
Self {
original_tokens,
compacted_tokens,
messages_compacted,
messages_kept,
timestamp: Utc::now(),
target_ratio,
actual_ratio,
success: true,
error: None,
}
}
pub fn failed(
original_tokens: usize,
messages_compacted: usize,
target_ratio: f32,
error: impl Into<String>,
) -> Self {
Self {
original_tokens,
compacted_tokens: original_tokens,
messages_compacted,
messages_kept: 0,
timestamp: Utc::now(),
target_ratio,
actual_ratio: 1.0,
success: false,
error: Some(error.into()),
}
}
pub fn compression_factor(&self) -> f32 {
if self.actual_ratio > 0.0 {
1.0 - self.actual_ratio
} else {
0.0
}
}
pub fn tokens_saved(&self) -> usize {
self.original_tokens.saturating_sub(self.compacted_tokens)
}
}
#[derive(Debug, Clone)]
pub struct CompactedContext {
pub summary: String,
pub kept_messages: Vec<Message>,
pub compacted_count: usize,
pub metadata: CompactionMetadata,
}
impl CompactedContext {
pub fn new(
summary: String,
kept_messages: Vec<Message>,
compacted_count: usize,
metadata: CompactionMetadata,
) -> Self {
Self {
summary,
kept_messages,
compacted_count,
metadata,
}
}
pub fn summary(&self) -> &str {
&self.summary
}
pub fn kept_count(&self) -> usize {
self.kept_messages.len()
}
pub fn compacted_count(&self) -> usize {
self.compacted_count
}
pub fn metadata(&self) -> &CompactionMetadata {
&self.metadata
}
pub fn is_success(&self) -> bool {
self.metadata.success
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CompactionStrategy {
Disabled,
Threshold(f32),
EveryNTurns(usize),
AbsoluteTokens(usize),
}
impl CompactionStrategy {
pub fn should_compact(
&self,
context_tokens: usize,
context_window: usize,
iteration: usize,
) -> bool {
match self {
CompactionStrategy::Disabled => false,
CompactionStrategy::Threshold(threshold) => {
if context_window == 0 {
return false;
}
let usage = context_tokens as f32 / context_window as f32;
usage >= *threshold
}
CompactionStrategy::EveryNTurns(n) => iteration > 0 && iteration % n == 0,
CompactionStrategy::AbsoluteTokens(max_tokens) => context_tokens >= *max_tokens,
}
}
}
impl Default for CompactionStrategy {
fn default() -> Self {
CompactionStrategy::Threshold(0.8)
}
}
#[derive(Debug, Clone)]
pub enum CompactionError {
LlmError(String),
NoMessagesToCompact,
TooFewMessages { total: usize, keep_recent: usize },
CompactionDisabled,
NoContextWindow,
}
impl std::fmt::Display for CompactionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompactionError::LlmError(msg) => write!(f, "LLM compaction failed: {}", msg),
CompactionError::NoMessagesToCompact => write!(f, "No messages to compact"),
CompactionError::TooFewMessages { total, keep_recent } => {
write!(
f,
"Not enough messages ({}) to compact (need at least {} for keep_recent)",
total,
keep_recent + 1
)
}
CompactionError::CompactionDisabled => write!(f, "Compaction is disabled"),
CompactionError::NoContextWindow => write!(f, "Context window not configured"),
}
}
}
impl std::error::Error for CompactionError {}
#[async_trait]
pub trait Compactor: Send + Sync {
async fn compact(
&self,
messages: &[Message],
instruction: Option<&str>,
) -> std::result::Result<CompactedContext, CompactionError>;
fn estimate_tokens(&self, messages: &[Message]) -> usize {
messages
.iter()
.map(|msg| estimate_tokens(&msg.text_content().unwrap_or_default()))
.sum()
}
}
pub struct LlmCompactor {
model: Model,
_provider: Arc<dyn Provider>,
config: CompactionConfig,
}
impl LlmCompactor {
pub fn new(model: Model, provider: Arc<dyn Provider>) -> Self {
Self {
model,
_provider: provider,
config: CompactionConfig::new(),
}
}
pub fn with_config(
model: Model,
provider: Arc<dyn Provider>,
config: CompactionConfig,
) -> Self {
Self {
model,
_provider: provider,
config,
}
}
pub fn with_keep_recent(mut self, count: usize) -> Self {
self.config.keep_recent = count;
self
}
pub fn with_max_batch(mut self, count: usize) -> Self {
self.config.max_batch = count;
self
}
pub fn with_target_ratio(mut self, ratio: f32) -> Self {
self.config.target_ratio = ratio.clamp(0.1, 0.9);
self
}
fn build_summarize_prompt(&self, messages: &[Message], instruction: Option<&str>) -> String {
let mut prompt = String::new();
prompt.push_str("Summarize the following conversation concisely. ");
prompt.push_str("Capture the key points, decisions, and any ongoing tasks or context.\n\n");
if let Some(instr) = instruction {
prompt.push_str(&format!("Focus areas: {}\n\n", instr));
} else if let Some(ref custom_instr) = self.config.custom_instruction {
prompt.push_str(&format!("Focus areas: {}\n\n", custom_instr));
}
prompt.push_str("## Conversation to summarize:\n");
for (i, msg) in messages.iter().enumerate() {
let role = match msg {
Message::User(_) => "User",
Message::Assistant(_) => "Assistant",
Message::ToolResult(_) => "Tool",
};
let content = msg.text_content().unwrap_or_default();
let content_preview = safe_truncate(&content, 500);
prompt.push_str(&format!("[{} {}]: {}\n", role, i + 1, content_preview));
}
prompt.push_str("\n## Summary:\n");
prompt
.push_str("Provide a concise summary that captures the essence of this conversation.");
prompt
}
async fn compact_with_fallback(
&self,
old_messages: &[Message],
recent_messages: &[Message],
instruction: Option<&str>,
) -> std::result::Result<CompactedContext, CompactionError> {
match self.summarize_with_llm(old_messages, instruction).await {
Ok(summary) => {
let mut summary_msg =
AssistantMessage::new(Api::AnthropicMessages, "compactor", &self.model.id);
summary_msg.content = vec![ContentBlock::Text(TextContent::new(format!(
"[Previous conversation summarized: {}]",
summary
)))];
let mut kept = vec![Message::Assistant(summary_msg)];
kept.extend(recent_messages.iter().cloned());
let original_tokens = self.estimate_tokens(old_messages);
let compacted_tokens = self.estimate_tokens(&kept);
let kept_len = kept.len();
Ok(CompactedContext::new(
summary,
kept,
old_messages.len(),
CompactionMetadata::new(
original_tokens,
compacted_tokens,
old_messages.len(),
kept_len,
self.config.target_ratio,
),
))
}
Err(llm_err) => {
self.compact_fallback(old_messages, recent_messages)
.await
.map_err(|_| CompactionError::LlmError(llm_err.to_string()))
}
}
}
async fn summarize_with_llm(
&self,
messages: &[Message],
instruction: Option<&str>,
) -> std::result::Result<String, CompactionError> {
let prompt = self.build_summarize_prompt(messages, instruction);
let mut context = Context::new();
context.set_system_prompt(
"You are a helpful assistant that summarizes conversations concisely.",
);
context.add_message(Message::User(UserMessage::new(prompt)));
let options = StreamOptions {
temperature: Some(self.config.temperature as f64),
max_tokens: Some(self.config.summary_max_tokens),
..Default::default()
};
let summary_message = complete(&self.model, &context, Some(options))
.await
.map_err(|e| CompactionError::LlmError(e.to_string()))?;
Ok(summary_message.text_content())
}
async fn compact_fallback(
&self,
old_messages: &[Message],
recent_messages: &[Message],
) -> std::result::Result<CompactedContext, CompactionError> {
let mut summary_parts = Vec::new();
if old_messages.len() > 2 {
if let Some(first) = old_messages.first() {
let content = first.text_content().unwrap_or_default();
let preview = safe_truncate(&content, 200);
summary_parts.push(format!("Started discussing: {}", preview));
}
if let Some(last) = old_messages.last() {
let content = last.text_content().unwrap_or_default();
let preview = safe_truncate(&content, 200);
summary_parts.push(format!("Ended with: {}", preview));
}
summary_parts.push(format!(
"({} messages omitted)",
old_messages.len().saturating_sub(2)
));
} else if !old_messages.is_empty() {
if let Some(msg) = old_messages.first() {
let content = msg.text_content().unwrap_or_default();
summary_parts.push(format!("Conversation started: {}", content));
}
}
let summary = summary_parts.join(" ");
let mut summary_msg =
AssistantMessage::new(Api::AnthropicMessages, "compactor", &self.model.id);
summary_msg.content = vec![ContentBlock::Text(TextContent::new(format!(
"[Previous conversation summary: {}]",
summary
)))];
let mut kept = vec![Message::Assistant(summary_msg)];
kept.extend(recent_messages.iter().cloned());
let original_tokens = self.estimate_tokens(old_messages);
let compacted_tokens = self.estimate_tokens(&kept);
let kept_len = kept.len();
Ok(CompactedContext::new(
summary,
kept,
old_messages.len(),
CompactionMetadata::new(
original_tokens,
compacted_tokens,
old_messages.len(),
kept_len,
self.config.target_ratio,
),
))
}
}
#[async_trait]
impl Compactor for LlmCompactor {
async fn compact(
&self,
messages: &[Message],
instruction: Option<&str>,
) -> std::result::Result<CompactedContext, CompactionError> {
if messages.is_empty() {
return Err(CompactionError::NoMessagesToCompact);
}
if messages.len() <= self.config.keep_recent {
let original_tokens = self.estimate_tokens(messages);
return Ok(CompactedContext::new(
String::new(),
messages.to_vec(),
0,
CompactionMetadata::new(
original_tokens,
original_tokens,
0,
messages.len(),
self.config.target_ratio,
),
));
}
let keep_count = self.config.keep_recent.min(messages.len());
let old_messages: Vec<Message> = messages[..messages.len() - keep_count].to_vec();
let recent_messages: Vec<Message> = messages[messages.len() - keep_count..].to_vec();
if old_messages.is_empty() {
return Err(CompactionError::NoMessagesToCompact);
}
self.compact_with_fallback(&old_messages, &recent_messages, instruction)
.await
}
}
impl LlmCompactor {
pub async fn summarize_branch(
&self,
messages: &[Message],
branch_name: &str,
) -> std::result::Result<String, CompactionError> {
if messages.is_empty() {
return Ok(format!("Branch '{}' is empty", branch_name));
}
let mut prompt = String::new();
prompt.push_str(&format!(
"Summarize the conversation branch '{}' concisely. ",
branch_name
));
prompt.push_str("Focus on: what was discussed, decisions made, and current state.\n\n");
prompt.push_str("## Branch messages:\n");
for (i, msg) in messages.iter().enumerate() {
let role = match msg {
Message::User(_) => "User",
Message::Assistant(_) => "Assistant",
Message::ToolResult(_) => "Tool",
};
let content = msg.text_content().unwrap_or_default();
let content_preview = safe_truncate(&content, 300);
prompt.push_str(&format!("[{} {}]: {}\n", role, i + 1, content_preview));
}
prompt.push_str("\n## Summary (be concise):\n");
let mut context = Context::new();
context.set_system_prompt(
"You are a helpful assistant that summarizes conversation branches. ",
);
context.add_message(Message::User(UserMessage::new(prompt)));
let options = StreamOptions {
temperature: Some(0.3),
max_tokens: Some(512),
..Default::default()
};
let summary_message = complete(&self.model, &context, Some(options))
.await
.map_err(|e| CompactionError::LlmError(e.to_string()))?;
Ok(summary_message.text_content())
}
}
pub struct CompactionManager {
strategy: CompactionStrategy,
compactor: Option<Arc<dyn Compactor>>,
context_window: usize,
config: CompactionConfig,
}
impl CompactionManager {
pub fn new(strategy: CompactionStrategy, context_window: usize) -> Self {
Self {
strategy,
compactor: None,
context_window,
config: CompactionConfig::new(),
}
}
pub fn with_config(
strategy: CompactionStrategy,
context_window: usize,
config: CompactionConfig,
) -> Self {
Self {
strategy,
compactor: None,
context_window,
config,
}
}
pub fn with_compactor<C: Compactor + 'static>(mut self, compactor: Arc<C>) -> Self {
self.compactor = Some(compactor);
self
}
pub fn set_compactor(&mut self, compactor: Arc<dyn Compactor>) {
self.compactor = Some(compactor);
}
pub fn should_compact(&self, context_tokens: usize, iteration: usize) -> bool {
self.strategy
.should_compact(context_tokens, self.context_window, iteration)
}
pub fn strategy(&self) -> &CompactionStrategy {
&self.strategy
}
pub fn config(&self) -> &CompactionConfig {
&self.config
}
pub fn set_config(&mut self, config: CompactionConfig) {
self.config = config;
}
pub async fn compact_if_needed(
&self,
messages: &[Message],
instruction: Option<&str>,
context_tokens: usize,
iteration: usize,
) -> std::result::Result<Option<CompactedContext>, CompactionError> {
if !self.should_compact(context_tokens, iteration) {
return Ok(None);
}
let compactor = match &self.compactor {
Some(c) => c,
None => return Err(CompactionError::CompactionDisabled),
};
let result = compactor.compact(messages, instruction).await?;
Ok(Some(result))
}
pub async fn compact_now(
&self,
messages: &[Message],
instruction: Option<&str>,
) -> std::result::Result<CompactedContext, CompactionError> {
let compactor = match &self.compactor {
Some(c) => c,
None => return Err(CompactionError::CompactionDisabled),
};
compactor.compact(messages, instruction).await
}
pub fn estimate_tokens(&self, messages: &[Message]) -> usize {
messages
.iter()
.map(|msg| estimate_tokens(&msg.text_content().unwrap_or_default()))
.sum()
}
}
impl Default for CompactionManager {
fn default() -> Self {
Self::new(CompactionStrategy::default(), 128_000)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_user_message(content: &str) -> Message {
Message::user(content)
}
fn make_assistant_message(content: &str) -> Message {
Message::Assistant({
let mut msg = AssistantMessage::new(Api::AnthropicMessages, "test", "test-model");
msg.content = vec![ContentBlock::Text(TextContent::new(content))];
msg
})
}
fn make_test_model() -> Model {
Model::new(
"test-model",
"Test Model",
Api::AnthropicMessages,
"test",
"https://test.example.com",
)
}
#[test]
fn test_compaction_config_defaults() {
let config = CompactionConfig::new();
assert_eq!(config.keep_recent, 4);
assert_eq!(config.max_batch, 20);
assert!((config.target_ratio - 0.5).abs() < 0.001);
assert_eq!(config.summary_max_tokens, 1024);
assert!((config.temperature - 0.3).abs() < 0.001);
}
#[test]
fn test_compaction_config_builder_pattern() {
let config = CompactionConfig::new()
.with_keep_recent(10)
.with_max_batch(30)
.with_target_ratio(0.3)
.with_temperature(0.5);
assert_eq!(config.keep_recent, 10);
assert_eq!(config.max_batch, 30);
assert!((config.target_ratio - 0.3).abs() < 0.001);
assert!((config.temperature - 0.5).abs() < 0.001);
}
#[test]
fn test_compaction_config_ratio_clamping() {
let config = CompactionConfig::new().with_target_ratio(1.5);
assert!((config.target_ratio - 0.9).abs() < 0.001);
let config = CompactionConfig::new().with_target_ratio(-0.5);
assert!((config.target_ratio - 0.1).abs() < 0.001);
}
#[test]
fn test_compaction_metadata_success() {
let metadata = CompactionMetadata::new(
1000, 500, 10, 5, 0.5, );
assert!(metadata.success);
assert_eq!(metadata.original_tokens, 1000);
assert_eq!(metadata.compacted_tokens, 500);
assert_eq!(metadata.messages_compacted, 10);
assert_eq!(metadata.messages_kept, 5);
assert!((metadata.actual_ratio - 0.5).abs() < 0.001);
assert!((metadata.compression_factor() - 0.5).abs() < 0.001);
assert_eq!(metadata.tokens_saved(), 500);
assert!(metadata.error.is_none());
}
#[test]
fn test_compaction_metadata_failure() {
let metadata = CompactionError::LlmError("test error".to_string());
assert!(metadata.to_string().contains("test error"));
}
#[test]
fn test_compaction_metadata_compression_factor() {
let metadata = CompactionMetadata::new(0, 0, 0, 0, 0.5);
assert!((metadata.actual_ratio - 1.0).abs() < 0.001);
assert!((metadata.compression_factor() - 0.0).abs() < 0.001);
let metadata = CompactionMetadata::new(1000, 100, 10, 5, 0.5);
assert!((metadata.compression_factor() - 0.9).abs() < 0.001);
}
#[test]
fn test_compaction_metadata_tokens_saved() {
let metadata = CompactionMetadata::new(1000, 400, 10, 5, 0.5);
assert_eq!(metadata.tokens_saved(), 600);
let metadata = CompactionMetadata::new(1000, 1000, 0, 0, 0.5);
assert_eq!(metadata.tokens_saved(), 0);
let metadata = CompactionMetadata::new(500, 600, 5, 3, 0.5);
assert_eq!(metadata.tokens_saved(), 0); }
#[test]
fn test_compaction_strategy_disabled() {
let strategy = CompactionStrategy::Disabled;
assert!(!strategy.should_compact(100_000, 128_000, 5));
assert!(!strategy.should_compact(120_000, 128_000, 10));
assert!(!strategy.should_compact(0, 128_000, 1));
}
#[test]
fn test_compaction_strategy_threshold() {
let strategy = CompactionStrategy::Threshold(0.8);
assert!(!strategy.should_compact(100_000, 128_000, 1));
assert!(strategy.should_compact(102_400, 128_000, 1));
assert!(strategy.should_compact(120_000, 128_000, 1));
assert!(!strategy.should_compact(100_000, 0, 1));
}
#[test]
fn test_compaction_strategy_every_n_turns() {
let strategy = CompactionStrategy::EveryNTurns(5);
assert!(!strategy.should_compact(0, 128_000, 0));
assert!(!strategy.should_compact(0, 128_000, 3));
assert!(!strategy.should_compact(0, 128_000, 4));
assert!(strategy.should_compact(0, 128_000, 5));
assert!(strategy.should_compact(0, 128_000, 10));
assert!(strategy.should_compact(0, 128_000, 15));
assert!(!strategy.should_compact(0, 128_000, 6));
assert!(!strategy.should_compact(0, 128_000, 9));
}
#[test]
fn test_compaction_strategy_absolute_tokens() {
let strategy = CompactionStrategy::AbsoluteTokens(100_000);
assert!(!strategy.should_compact(50_000, 128_000, 0));
assert!(!strategy.should_compact(99_999, 128_000, 0));
assert!(strategy.should_compact(100_000, 128_000, 0));
assert!(strategy.should_compact(150_000, 128_000, 0));
}
#[test]
fn test_compacted_context_basic() {
let metadata = CompactionMetadata::new(1000, 500, 10, 5, 0.5);
let ctx = CompactedContext::new(
"Test summary".to_string(),
vec![make_user_message("test")],
10,
metadata,
);
assert_eq!(ctx.summary(), "Test summary");
assert_eq!(ctx.kept_count(), 1);
assert_eq!(ctx.compacted_count(), 10);
assert!(ctx.is_success());
assert_eq!(ctx.metadata().tokens_saved(), 500);
}
#[test]
fn test_compacted_context_with_empty_summary() {
let metadata = CompactionMetadata::new(100, 100, 0, 2, 0.5);
let ctx = CompactedContext::new(
String::new(), vec![make_user_message("test1"), make_user_message("test2")],
0,
metadata,
);
assert_eq!(ctx.summary(), "");
assert_eq!(ctx.kept_count(), 2);
assert_eq!(ctx.compacted_count(), 0);
}
#[test]
fn test_llm_compactor_config_builder() {
use crate::providers::OpenAiProvider;
let provider = OpenAiProvider::new();
let model = make_test_model();
let compactor = LlmCompactor::new(model, Arc::new(provider))
.with_keep_recent(6)
.with_max_batch(25)
.with_target_ratio(0.6);
assert!(compactor.config.keep_recent >= 4);
assert!(compactor.config.max_batch >= 20);
}
#[test]
fn test_compaction_error_display() {
let err = CompactionError::NoMessagesToCompact;
assert_eq!(err.to_string(), "No messages to compact");
let err = CompactionError::TooFewMessages {
total: 3,
keep_recent: 5,
};
assert!(err.to_string().contains("3"));
assert!(err.to_string().contains("6"));
let err = CompactionError::CompactionDisabled;
assert_eq!(err.to_string(), "Compaction is disabled");
let err = CompactionError::NoContextWindow;
assert_eq!(err.to_string(), "Context window not configured");
let err = CompactionError::LlmError("API timeout".to_string());
assert!(err.to_string().contains("API timeout"));
}
#[test]
fn test_compaction_manager_default() {
let manager = CompactionManager::default();
assert!(matches!(
manager.strategy(),
CompactionStrategy::Threshold(_)
));
assert_eq!(manager.config().keep_recent, 4);
}
#[test]
fn test_compaction_manager_with_custom_strategy() {
let strategy = CompactionStrategy::AbsoluteTokens(50_000);
let manager = CompactionManager::new(strategy, 200_000);
assert!(!manager.should_compact(30_000, 0));
assert!(manager.should_compact(60_000, 0));
}
#[test]
fn test_compaction_manager_with_config() {
let config = CompactionConfig::new()
.with_keep_recent(8)
.with_target_ratio(0.4);
let manager =
CompactionManager::with_config(CompactionStrategy::default(), 128_000, config);
assert_eq!(manager.config().keep_recent, 8);
assert!((manager.config().target_ratio - 0.4).abs() < 0.001);
}
#[test]
fn test_compaction_manager_should_compact_integration() {
let manager = CompactionManager::new(CompactionStrategy::Threshold(0.75), 100_000);
assert!(!manager.should_compact(70_000, 0));
assert!(manager.should_compact(75_000, 0));
assert!(manager.should_compact(80_000, 0));
assert!(manager.should_compact(100_000, 0));
}
#[test]
fn test_compaction_manager_no_compactor_set() {
let manager = CompactionManager::new(CompactionStrategy::EveryNTurns(5), 128_000);
assert!(manager.should_compact(0, 5)); }
#[test]
fn test_token_estimation_helper() {
use crate::providers::OpenAiProvider;
let provider = OpenAiProvider::new();
let model = make_test_model();
let compactor = LlmCompactor::new(model, Arc::new(provider));
let messages = vec![
make_user_message("Hello world, this is a test message."),
make_assistant_message("This is a response with some content."),
];
let tokens = compactor.estimate_tokens(&messages);
assert!(tokens > 0, "Should estimate tokens for messages");
}
#[test]
fn test_compaction_config_custom_instruction() {
let config = CompactionConfig::new()
.with_custom_instruction("Focus on code changes and technical decisions");
assert!(config.custom_instruction.is_some());
assert!(config.custom_instruction.unwrap().contains("code changes"));
}
#[test]
fn test_compaction_metadata_timestamp_is_set() {
let metadata = CompactionMetadata::new(1000, 500, 10, 5, 0.5);
assert!(metadata.timestamp <= Utc::now());
}
#[test]
fn test_compaction_ratio_achievement() {
let metadata = CompactionMetadata::new(1000, 500, 10, 5, 0.5);
assert!((metadata.actual_ratio - 0.5).abs() < 0.001);
let metadata = CompactionMetadata::new(1000, 300, 10, 5, 0.5);
assert!((metadata.actual_ratio - 0.3).abs() < 0.001);
assert!(metadata.compression_factor() > 0.5);
let metadata = CompactionMetadata::new(1000, 700, 10, 5, 0.5);
assert!((metadata.actual_ratio - 0.7).abs() < 0.001);
assert!(metadata.compression_factor() < 0.5);
}
#[test]
fn test_compaction_manager_config_updates() {
let mut manager = CompactionManager::default();
let new_config = CompactionConfig::new()
.with_keep_recent(12)
.with_target_ratio(0.3);
manager.set_config(new_config);
assert_eq!(manager.config().keep_recent, 12);
assert!((manager.config().target_ratio - 0.3).abs() < 0.001);
}
#[test]
fn test_llm_compactor_has_summarize_branch() {
use crate::providers::OpenAiProvider;
let provider = OpenAiProvider::new();
let model = make_test_model();
let compactor = LlmCompactor::new(model, Arc::new(provider));
let messages = vec![
make_user_message("Test message 1"),
make_assistant_message("Test response 1"),
make_user_message("Test message 2"),
];
let branch_name = "test-branch";
let _future = compactor.summarize_branch(&messages, branch_name);
}
#[test]
fn test_summarize_branch_returns_error_on_llm_failure() {
use crate::providers::OpenAiProvider;
let provider = OpenAiProvider::new();
let model = make_test_model();
let compactor = LlmCompactor::new(model, Arc::new(provider));
let messages: Vec<Message> = vec![];
let _future = compactor.summarize_branch(&messages, "empty-branch");
}
}