use crate::agent::{Message, Role};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionStrategy {
pub keep_recent: usize,
pub keep_keywords: Vec<String>,
pub keep_tool_results: bool,
pub keep_system: bool,
}
impl Default for CompactionStrategy {
fn default() -> Self {
Self {
keep_recent: 10,
keep_keywords: vec![
"error".to_string(),
"fix".to_string(),
"bug".to_string(),
"issue".to_string(),
"problem".to_string(),
"solution".to_string(),
"important".to_string(),
"note".to_string(),
"warning".to_string(),
],
keep_tool_results: true,
keep_system: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionResult {
pub messages: Vec<Message>,
pub original_count: usize,
pub compacted_count: usize,
pub tokens_saved: usize,
}
pub fn build_compaction_prompt(messages: &[Message], _strategy: &CompactionStrategy) -> String {
let mut prompt = String::from(
r#"# Structured Conversation Compaction
You are tasked with compacting a conversation history while preserving all essential information.
## Your Goal
Create a concise, structured summary that captures:
1. **User's Original Intent** - What the user wanted to accomplish
2. **Important Decisions** - Key decisions made during the conversation
3. **Code Changes** - What was changed and why
4. **Error Messages** - Any errors encountered and their solutions
5. **Debugging Information** - Important debugging steps and findings
6. **Warnings and Notes** - Any warnings or important notes
## Output Format
Your response MUST follow this exact structure:
```
# Conversation Summary
## User Intent
[Describe what the user wanted to accomplish in 1-2 sentences]
## Key Decisions
- [Decision 1]
- [Decision 2]
- [Decision 3]
## Code Changes
### File: [filename]
- **Change**: [description of change]
- **Rationale**: [why this change was made]
- **Impact**: [what this affects]
### File: [filename]
- **Change**: [description of change]
- **Rationale**: [why this change was made]
- **Impact**: [what this affects]
## Errors and Solutions
### Error: [error description]
- **Location**: [where the error occurred]
- **Solution**: [how it was fixed]
- **Prevention**: [how to prevent this in the future]
## Debugging Steps
1. [Step 1]
2. [Step 2]
3. [Step 3]
## Warnings and Notes
- [Warning or note 1]
- [Warning or note 2]
## Current State
[Describe the current state of the work in 1-2 sentences]
## Next Steps
1. [Next step 1]
2. [Next step 2]
3. [Next step 3]
```
## Guidelines
- Be concise but complete
- Preserve all technical details (function names, file paths, error messages)
- Use bullet points for lists
- Keep each section focused and clear
- If a section has no relevant information, write "None"
- Maintain chronological order where relevant
- Include specific values (numbers, strings, paths) when important
## Original Conversation
"#,
);
for (i, msg) in messages.iter().enumerate() {
let role = match msg.role {
Role::System => "SYSTEM",
Role::User => "USER",
Role::Assistant => "ASSISTANT",
Role::Tool => "TOOL",
};
prompt.push_str(&format!("\n### Message {} [{}]\n\n{}\n", i + 1, role, msg.content));
if !msg.tool_calls.is_empty() {
prompt.push_str("\n**Tool Calls:**\n");
for tc in &msg.tool_calls {
prompt.push_str(&format!("- `{}`: {}\n", tc.name, tc.arguments));
}
}
if let Some(ref result) = msg.tool_result {
prompt.push_str(&format!("\n**Tool Result:**\n{}\n", result.content));
}
}
prompt.push_str(
r#"
--- End of Original Conversation ---
Please provide a structured summary following the exact format specified above.
"#,
);
prompt
}
pub fn compact_messages(messages: Vec<Message>, strategy: &CompactionStrategy) -> CompactionResult {
let original_count = messages.len();
let mut compacted = Vec::new();
if strategy.keep_system {
compacted.extend(
messages
.iter()
.filter(|m| m.role == Role::System)
.cloned(),
);
}
for msg in &messages {
let content_lower = msg.content.to_lowercase();
if strategy
.keep_keywords
.iter()
.any(|kw| content_lower.contains(&kw.to_lowercase()))
{
if !compacted.iter().any(|m| m.content == msg.content) {
compacted.push(msg.clone());
}
}
}
if strategy.keep_tool_results {
for msg in &messages {
if msg.tool_result.is_some() && !msg.tool_calls.is_empty() {
if !compacted.iter().any(|m| m.content == msg.content) {
compacted.push(msg.clone());
}
}
}
}
let recent_start = if messages.len() > strategy.keep_recent {
messages.len() - strategy.keep_recent
} else {
0
};
for msg in &messages[recent_start..] {
if !compacted.iter().any(|m| m.content == msg.content) {
compacted.push(msg.clone());
}
}
compacted.sort_by_key(|m| {
messages
.iter()
.position(|orig| orig.content == m.content)
.unwrap_or(usize::MAX)
});
let compacted_count = compacted.len();
let tokens_saved = estimate_tokens_saved(original_count, compacted_count);
CompactionResult {
messages: compacted,
original_count,
compacted_count,
tokens_saved,
}
}
fn estimate_tokens_saved(original: usize, compacted: usize) -> usize {
let avg_tokens_per_message = 4;
(original - compacted) * avg_tokens_per_message
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsedCompactionSummary {
pub user_intent: String,
pub key_decisions: Vec<String>,
pub code_changes: Vec<CodeChange>,
pub errors_and_solutions: Vec<ErrorSolution>,
pub debugging_steps: Vec<String>,
pub warnings_and_notes: Vec<String>,
pub current_state: String,
pub next_steps: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeChange {
pub file: String,
pub change: String,
pub rationale: String,
pub impact: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorSolution {
pub error: String,
pub location: String,
pub solution: String,
pub prevention: String,
}
pub fn parse_compaction_summary(summary: &str) -> Result<ParsedCompactionSummary, String> {
let mut parsed = ParsedCompactionSummary {
user_intent: String::new(),
key_decisions: Vec::new(),
code_changes: Vec::new(),
errors_and_solutions: Vec::new(),
debugging_steps: Vec::new(),
warnings_and_notes: Vec::new(),
current_state: String::new(),
next_steps: Vec::new(),
};
let lines: Vec<&str> = summary.lines().collect();
let mut current_section: Option<String> = None;
let mut current_code_change: Option<CodeChange> = None;
let mut current_error: Option<ErrorSolution> = None;
for line in lines {
let line = line.trim();
if line.starts_with("## ") {
current_section = Some(line[3..].to_string());
continue;
}
if line.starts_with("### ") {
let subsection = line[4..].to_string();
if let Some(code_change) = current_code_change.take() {
if !code_change.file.is_empty() {
parsed.code_changes.push(code_change);
}
}
if let Some(error) = current_error.take() {
if !error.error.is_empty() {
parsed.errors_and_solutions.push(error);
}
}
if subsection.starts_with("File: ") {
current_code_change = Some(CodeChange {
file: subsection[6..].to_string(),
change: String::new(),
rationale: String::new(),
impact: String::new(),
});
} else if subsection.starts_with("Error: ") {
current_error = Some(ErrorSolution {
error: subsection[7..].to_string(),
location: String::new(),
solution: String::new(),
prevention: String::new(),
});
}
continue;
}
match current_section.as_deref() {
Some("User Intent") => {
parsed.user_intent.push_str(line);
parsed.user_intent.push(' ');
}
Some("Key Decisions") => {
if line.starts_with("- ") {
parsed.key_decisions.push(line[2..].to_string());
}
}
Some("Code Changes") => {
if let Some(ref mut code_change) = current_code_change {
if line.starts_with("- **Change**: ") {
code_change.change = line[12..].to_string();
} else if line.starts_with("- **Rationale**: ") {
code_change.rationale = line[14..].to_string();
} else if line.starts_with("- **Impact**: ") {
code_change.impact = line[11..].to_string();
}
}
}
Some("Errors and Solutions") => {
if let Some(ref mut error) = current_error {
if line.starts_with("- **Location**: ") {
error.location = line[14..].to_string();
} else if line.starts_with("- **Solution**: ") {
error.solution = line[14..].to_string();
} else if line.starts_with("- **Prevention**: ") {
error.prevention = line[15..].to_string();
}
}
}
Some("Debugging Steps") => {
if line.starts_with("1. ") || line.starts_with("2. ") || line.starts_with("3. ") {
parsed.debugging_steps.push(line[3..].to_string());
}
}
Some("Warnings and Notes") => {
if line.starts_with("- ") {
parsed.warnings_and_notes.push(line[2..].to_string());
}
}
Some("Current State") => {
parsed.current_state.push_str(line);
parsed.current_state.push(' ');
}
Some("Next Steps") => {
if line.starts_with("1. ") || line.starts_with("2. ") || line.starts_with("3. ") {
parsed.next_steps.push(line[3..].to_string());
}
}
_ => {}
}
}
if let Some(code_change) = current_code_change {
if !code_change.file.is_empty() {
parsed.code_changes.push(code_change);
}
}
if let Some(error) = current_error {
if !error.error.is_empty() {
parsed.errors_and_solutions.push(error);
}
}
parsed.user_intent = parsed.user_intent.trim().to_string();
parsed.current_state = parsed.current_state.trim().to_string();
Ok(parsed)
}
pub fn summary_to_message(summary: &ParsedCompactionSummary) -> Message {
let mut content = String::from("# Conversation Summary\n\n");
content.push_str("## User Intent\n");
content.push_str(&summary.user_intent);
content.push_str("\n\n");
if !summary.key_decisions.is_empty() {
content.push_str("## Key Decisions\n");
for decision in &summary.key_decisions {
content.push_str("- ");
content.push_str(decision);
content.push_str("\n");
}
content.push_str("\n");
}
if !summary.code_changes.is_empty() {
content.push_str("## Code Changes\n");
for change in &summary.code_changes {
content.push_str(&format!("### File: {}\n", change.file));
content.push_str(&format!("- **Change**: {}\n", change.change));
content.push_str(&format!("- **Rationale**: {}\n", change.rationale));
content.push_str(&format!("- **Impact**: {}\n", change.impact));
content.push_str("\n");
}
}
if !summary.errors_and_solutions.is_empty() {
content.push_str("## Errors and Solutions\n");
for error in &summary.errors_and_solutions {
content.push_str(&format!("### Error: {}\n", error.error));
content.push_str(&format!("- **Location**: {}\n", error.location));
content.push_str(&format!("- **Solution**: {}\n", error.solution));
content.push_str(&format!("- **Prevention**: {}\n", error.prevention));
content.push_str("\n");
}
}
if !summary.debugging_steps.is_empty() {
content.push_str("## Debugging Steps\n");
for (i, step) in summary.debugging_steps.iter().enumerate() {
content.push_str(&format!("{}. {}\n", i + 1, step));
}
content.push_str("\n");
}
if !summary.warnings_and_notes.is_empty() {
content.push_str("## Warnings and Notes\n");
for warning in &summary.warnings_and_notes {
content.push_str("- ");
content.push_str(warning);
content.push_str("\n");
}
content.push_str("\n");
}
content.push_str("## Current State\n");
content.push_str(&summary.current_state);
content.push_str("\n\n");
if !summary.next_steps.is_empty() {
content.push_str("## Next Steps\n");
for (i, step) in summary.next_steps.iter().enumerate() {
content.push_str(&format!("{}. {}\n", i + 1, step));
}
}
Message {
role: Role::System,
content,
tool_calls: vec![],
tool_result: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compaction_strategy_default() {
let strategy = CompactionStrategy::default();
assert_eq!(strategy.keep_recent, 10);
assert!(strategy.keep_keywords.contains(&"error".to_string()));
assert!(strategy.keep_tool_results);
assert!(strategy.keep_system);
}
#[test]
fn test_build_compaction_prompt() {
let messages = vec![
Message {
role: Role::User,
content: "Fix the bug in main.rs".to_string(),
tool_calls: vec![],
tool_result: None,
},
Message {
role: Role::Assistant,
content: "I'll read the file first.".to_string(),
tool_calls: vec![],
tool_result: None,
},
];
let prompt = build_compaction_prompt(&messages, &CompactionStrategy::default());
assert!(prompt.contains("Fix the bug in main.rs"));
assert!(prompt.contains("I'll read the file first."));
assert!(prompt.contains("Original Conversation"));
}
#[test]
fn test_compact_messages() {
let messages = vec![
Message {
role: Role::System,
content: "You are a coding agent.".to_string(),
tool_calls: vec![],
tool_result: None,
},
Message {
role: Role::User,
content: "Fix the error".to_string(),
tool_calls: vec![],
tool_result: None,
},
Message {
role: Role::Assistant,
content: "I'll help.".to_string(),
tool_calls: vec![],
tool_result: None,
},
];
let strategy = CompactionStrategy {
keep_recent: 1,
keep_keywords: vec![],
keep_tool_results: false,
keep_system: false,
};
let result = compact_messages(messages, &strategy);
assert_eq!(result.original_count, 3);
assert!(result.compacted_count > 0);
assert!(result.tokens_saved > 0);
}
#[test]
fn test_compaction_preserves_system_messages() {
let messages = vec![
Message {
role: Role::System,
content: "System prompt".to_string(),
tool_calls: vec![],
tool_result: None,
},
Message {
role: Role::User,
content: "User message".to_string(),
tool_calls: vec![],
tool_result: None,
},
];
let strategy = CompactionStrategy {
keep_system: true,
..Default::default()
};
let result = compact_messages(messages, &strategy);
assert!(result
.messages
.iter()
.any(|m| m.role == Role::System && m.content == "System prompt"));
}
#[test]
fn test_compaction_preserves_keyword_messages() {
let messages = vec![
Message {
role: Role::User,
content: "Fix the error".to_string(),
tool_calls: vec![],
tool_result: None,
},
Message {
role: Role::User,
content: "Regular message".to_string(),
tool_calls: vec![],
tool_result: None,
},
];
let strategy = CompactionStrategy {
keep_keywords: vec!["error".to_string()],
..Default::default()
};
let result = compact_messages(messages, &strategy);
assert!(result
.messages
.iter()
.any(|m| m.content == "Fix the error"));
}
}