#[derive(Debug, Clone)]
pub struct SimpleMessage {
pub role: String,
pub content: String,
}
pub const DEFAULT_MAX_MESSAGE_LENGTH: usize = 10_000;
pub const DEFAULT_MAX_SUMMARY_LENGTH: usize = 500;
pub const ESTIMATED_TOKENS_PER_CHAR: f64 = 0.25;
pub fn format_message(role: &str, content: &str) -> String {
let role_display = match role {
"user" => "You",
"assistant" => "Assistant",
"system" => "System",
"tool" => "Tool",
"tool_result" => "Result",
"bashExecution" => "Bash",
"custom" => "Custom",
"branchSummary" | "compactionSummary" => "Summary",
_ => role,
};
format!("[{}]\n{}", role_display, content)
}
pub fn format_message_with_prefix(role: &str, content: &str, prefix: &str) -> String {
let role_display = match role {
"user" => "You",
"assistant" => "Assistant",
"system" => "System",
"tool" => "Tool",
"tool_result" => "Result",
_ => role,
};
if prefix.is_empty() {
format!("[{}]\n{}", role_display, content)
} else {
format!("[{}] {}\n{}", role_display, prefix, content)
}
}
pub fn truncate_message(content: &str, max_length: usize, suffix: &str) -> String {
if content.len() <= max_length {
return content.to_string();
}
let truncated = &content[..max_length];
if let Some(last_newline) = truncated.rfind('\n') {
if last_newline > max_length / 2 {
return format!("{}{}", &content[..last_newline], suffix);
}
}
if let Some(last_period) = truncated.rfind(". ") {
if last_period > max_length / 2 {
return format!("{}{}", &content[..last_period + 1], suffix);
}
}
format!("{}{}", truncated, suffix)
}
pub fn truncate_message_default(content: &str) -> String {
truncate_message(
content,
DEFAULT_MAX_MESSAGE_LENGTH,
"\n\n... [message truncated]",
)
}
pub fn estimate_tokens(content: &str) -> usize {
(content.len() as f64 * ESTIMATED_TOKENS_PER_CHAR) as usize
}
pub fn estimate_messages_tokens(messages: &[SimpleMessage]) -> usize {
messages.iter().map(|m| estimate_tokens(&m.content)).sum()
}
pub fn exceeds_token_limit(content: &str, limit: usize) -> bool {
estimate_tokens(content) > limit
}
pub fn summarize_conversation(messages: &[SimpleMessage], max_length: usize) -> String {
if messages.is_empty() {
return String::new();
}
let mut summary_parts = Vec::new();
if let Some(first) = messages.first() {
if first.role == "user" {
let content = truncate_message(&first.content, 200, "...");
summary_parts.push(format!("Started with: {}", content));
}
}
let mut user_count = 0;
let mut assistant_count = 0;
let mut tool_count = 0;
for msg in messages {
match msg.role.as_str() {
"user" => user_count += 1,
"assistant" => assistant_count += 1,
"tool" | "tool_result" => tool_count += 1,
_ => {}
}
}
summary_parts.push(format!(
"{} user message(s), {} assistant response(s), {} tool use(s)",
user_count, assistant_count, tool_count
));
if let Some(last) = messages.last() {
if last.role == "assistant" {
let content = truncate_message(&last.content, 300, "...");
summary_parts.push(format!("Last response: {}", content));
}
}
let summary = summary_parts.join("\n");
truncate_message(&summary, max_length, "...")
}
pub fn compact_messages(
messages: &[SimpleMessage],
max_messages: usize,
summary_prefix: &str,
summary_suffix: &str,
) -> Vec<SimpleMessage> {
if messages.len() <= max_messages {
return messages.to_vec();
}
let to_keep = max_messages / 2;
let _to_summarize = messages.len() - to_keep;
let kept: Vec<SimpleMessage> = messages.iter().take(to_keep).cloned().collect();
let to_summarize_msgs = &messages[to_keep..messages.len()];
let summary = summarize_conversation(to_summarize_msgs, 300);
let mut result = kept;
result.push(SimpleMessage {
role: "system".to_string(),
content: format!("{}{}{}", summary_prefix, summary, summary_suffix),
});
result.extend_from_slice(&messages[messages.len().saturating_sub(to_keep)..]);
result
}
pub fn format_bash_execution(command: &str, output: &str, exit_code: Option<i32>) -> String {
let mut result = format!("$ {}\n", command);
if !output.is_empty() {
result.push_str(output);
if !output.ends_with('\n') {
result.push('\n');
}
}
if let Some(code) = exit_code {
if code == 0 {
result.push_str(&format!("[exited with code {}]", code));
} else {
result.push_str(&format!("[error: exited with code {}]", code));
}
}
result
}
pub fn format_tool_result(tool_name: &str, result: &str) -> String {
format!("[Tool: {}]\n{}\n", tool_name, result)
}
pub fn get_preview(content: &str, max_length: usize) -> String {
let trimmed = content.trim();
if trimmed.len() <= max_length {
return trimmed.to_string();
}
let preview = &trimmed[..max_length];
if let Some(last_newline) = preview.rfind('\n') {
if last_newline > max_length / 2 {
return format!("{}...", &trimmed[..last_newline]);
}
}
format!("{}...", preview.trim_end())
}
pub fn count_messages_by_role(
messages: &[SimpleMessage],
) -> std::collections::HashMap<String, usize> {
let mut counts = std::collections::HashMap::new();
for msg in messages {
*counts.entry(msg.role.clone()).or_insert(0) += 1;
}
counts
}
pub fn calculate_context_usage(messages: &[SimpleMessage], context_window: usize) -> (usize, f64) {
let total_tokens = estimate_messages_tokens(messages);
let usage = (total_tokens as f64 / context_window as f64) * 100.0;
(total_tokens, usage)
}
pub fn format_tokens(tokens: usize) -> String {
if tokens < 1000 {
format!("{} tokens", tokens)
} else if tokens < 1_000_000 {
format!("{:.1}K tokens", tokens as f64 / 1000.0)
} else {
format!("{:.1}M tokens", tokens as f64 / 1_000_000.0)
}
}
pub fn is_empty_message(msg: &SimpleMessage) -> bool {
msg.content.trim().is_empty()
}
pub fn filter_empty_messages(messages: &[SimpleMessage]) -> Vec<SimpleMessage> {
messages
.iter()
.filter(|m| !is_empty_message(m))
.cloned()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_message() {
let formatted = format_message("user", "Hello, world!");
assert!(formatted.contains("You"));
assert!(formatted.contains("Hello, world!"));
}
#[test]
fn test_truncate_message_short() {
let content = "Short message";
let result = truncate_message(content, 100, "...");
assert_eq!(result, content);
}
#[test]
fn test_truncate_message_long() {
let content = "a".repeat(200);
let result = truncate_message(&content, 100, "...[truncated]");
assert!(result.ends_with("...[truncated]"));
assert!(result.len() <= 100 + "...[truncated]".len());
}
#[test]
fn test_truncate_message_at_newline() {
let content = format!("line1\nline2\nline3\n{}", "a".repeat(200));
let result = truncate_message(&content, 20, "...");
assert!(result.contains("line1\nline2\nline3"));
}
#[test]
fn test_estimate_tokens() {
let content = "Hello, world!";
let tokens = estimate_tokens(content);
assert!(tokens >= 2 && tokens <= 6);
}
#[test]
fn test_exceeds_token_limit() {
let content = "a".repeat(1000);
assert!(exceeds_token_limit(&content, 100));
assert!(!exceeds_token_limit(&content, 500));
}
#[test]
fn test_summarize_conversation_empty() {
let messages: Vec<SimpleMessage> = vec![];
let summary = summarize_conversation(&messages, 100);
assert!(summary.is_empty());
}
#[test]
fn test_summarize_conversation() {
let messages = vec![
SimpleMessage {
role: "user".to_string(),
content: "Hello, I need help with Rust".to_string(),
},
SimpleMessage {
role: "assistant".to_string(),
content: "I'd be happy to help with Rust! What specifically do you need?"
.to_string(),
},
];
let summary = summarize_conversation(&messages, 200);
assert!(summary.contains("Started with"));
assert!(summary.contains("1 user message"));
assert!(summary.contains("1 assistant response"));
}
#[test]
fn test_compact_messages() {
let messages: Vec<SimpleMessage> = (0..10)
.map(|i| SimpleMessage {
role: "user".to_string(),
content: format!("SimpleMessage {}", i),
})
.collect();
let compacted = compact_messages(&messages, 4, "<summary>", "</summary>");
assert!(compacted.len() < messages.len());
assert!(compacted.iter().any(|m| m.content.contains("<summary>")));
}
#[test]
fn test_format_bash_execution() {
let result = format_bash_execution("echo hello", "hello\n", Some(0));
assert!(result.contains("echo hello"));
assert!(result.contains("hello"));
}
#[test]
fn test_get_preview() {
let content = "This is a very long message that should be truncated";
let preview = get_preview(content, 20);
assert!(preview.len() <= 23); assert!(preview.starts_with("This is a very "));
}
#[test]
fn test_count_messages_by_role() {
let messages = vec![
SimpleMessage {
role: "user".to_string(),
content: "msg1".to_string(),
},
SimpleMessage {
role: "assistant".to_string(),
content: "msg2".to_string(),
},
SimpleMessage {
role: "user".to_string(),
content: "msg3".to_string(),
},
];
let counts = count_messages_by_role(&messages);
assert_eq!(counts.get("user"), Some(&2));
assert_eq!(counts.get("assistant"), Some(&1));
}
#[test]
fn test_calculate_context_usage() {
let messages = vec![SimpleMessage {
role: "user".to_string(),
content: "a".repeat(1000),
}];
let (tokens, usage) = calculate_context_usage(&messages, 10000);
assert!(tokens > 0);
assert!(usage < 100.0);
}
#[test]
fn test_format_tokens() {
assert_eq!(format_tokens(500), "500 tokens");
assert_eq!(format_tokens(1500), "1.5K tokens");
assert_eq!(format_tokens(1_500_000), "1.5M tokens");
}
#[test]
fn test_is_empty_message() {
let empty = SimpleMessage {
role: "user".to_string(),
content: " ".to_string(),
};
assert!(is_empty_message(&empty));
let non_empty = SimpleMessage {
role: "user".to_string(),
content: "Hello".to_string(),
};
assert!(!is_empty_message(&non_empty));
}
#[test]
fn test_filter_empty_messages() {
let messages = vec![
SimpleMessage {
role: "user".to_string(),
content: "Hello".to_string(),
},
SimpleMessage {
role: "user".to_string(),
content: " ".to_string(),
},
SimpleMessage {
role: "assistant".to_string(),
content: "Hi there".to_string(),
},
];
let filtered = filter_empty_messages(&messages);
assert_eq!(filtered.len(), 2);
}
}