Repository navigation
Expand file tree
/
Copy pathcompactor.rs
More file actions
168 lines (143 loc) · 4.96 KB
/
Copy pathcompactor.rs
File metadata and controls
168 lines (143 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//! Compaction execution.
use crate::summarizer::ConversationItem;
use crate::{CompactionConfig, Result, Summarizer, Summary};
use serde::{Deserialize, Serialize};
use tracing::info;
/// Result of compaction.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionResult {
/// Whether compaction was performed.
pub compacted: bool,
/// The summary if compaction was performed.
pub summary: Option<Summary>,
/// Tokens before compaction.
pub tokens_before: usize,
/// Tokens after compaction.
pub tokens_after: usize,
/// Items removed.
pub items_removed: usize,
}
impl CompactionResult {
pub fn no_compaction(current_tokens: usize) -> Self {
Self {
compacted: false,
summary: None,
tokens_before: current_tokens,
tokens_after: current_tokens,
items_removed: 0,
}
}
pub fn success(
summary: Summary,
tokens_before: usize,
tokens_after: usize,
items_removed: usize,
) -> Self {
Self {
compacted: true,
summary: Some(summary),
tokens_before,
tokens_after,
items_removed,
}
}
/// Tokens saved by compaction.
pub fn tokens_saved(&self) -> usize {
self.tokens_before.saturating_sub(self.tokens_after)
}
}
/// Compactor for conversation history.
pub struct Compactor {
config: CompactionConfig,
summarizer: Summarizer,
}
impl Compactor {
pub fn new(config: CompactionConfig) -> Self {
let summarizer = Summarizer::new(config.clone());
Self { config, summarizer }
}
/// Check if compaction is needed.
pub fn needs_compaction(&self, current_tokens: usize, max_tokens: usize) -> bool {
self.config.should_compact(current_tokens, max_tokens)
}
/// Perform compaction on conversation history.
/// Returns the items to keep and the compaction result.
pub fn compact(
&self,
items: Vec<ConversationItem>,
summary_text: String,
current_tokens: usize,
) -> Result<(Vec<ConversationItem>, CompactionResult)> {
let to_compact = self.summarizer.select_items_to_compact(&items);
if to_compact.is_empty() {
return Ok((items, CompactionResult::no_compaction(current_tokens)));
}
let items_removed = to_compact.len();
let tokens_in_compacted: usize = to_compact
.iter()
.map(|i| self.summarizer.estimate_tokens(&item_text(i)))
.sum();
let summary_tokens = self.summarizer.estimate_tokens(&summary_text);
let summary = Summary::new(
summary_text,
tokens_in_compacted,
summary_tokens,
items_removed / 2, // Approximate turns
);
// Build new items list: summary + preserved items
let preserved_start =
items.len() - (self.config.preserve_recent_turns * 2).min(items.len());
let mut new_items = vec![ConversationItem::Assistant {
text: summary.formatted(),
}];
new_items.extend(items.into_iter().skip(preserved_start));
let tokens_after = current_tokens - tokens_in_compacted + summary_tokens;
let result =
CompactionResult::success(summary, current_tokens, tokens_after, items_removed);
info!(
"Compacted conversation: {} tokens -> {} tokens ({} items removed)",
result.tokens_before, result.tokens_after, result.items_removed
);
Ok((new_items, result))
}
/// Build the prompt to send to the model for summarization.
pub fn build_summarization_prompt(&self, items: &[ConversationItem]) -> String {
let to_compact = self.summarizer.select_items_to_compact(items);
self.summarizer.build_prompt(to_compact)
}
/// Get config reference.
pub fn config(&self) -> &CompactionConfig {
&self.config
}
}
fn item_text(item: &ConversationItem) -> String {
match item {
ConversationItem::User { text } => text.clone(),
ConversationItem::Assistant { text } => text.clone(),
ConversationItem::ToolUse { name, result } => format!("{}: {}", name, result),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compactor() {
let config = CompactionConfig::default();
let compactor = Compactor::new(config);
// Create some conversation items
let items: Vec<ConversationItem> = (0..10)
.flat_map(|i| {
vec![
ConversationItem::User {
text: format!("User message {}", i),
},
ConversationItem::Assistant {
text: format!("Assistant response {}", i),
},
]
})
.collect();
let prompt = compactor.build_summarization_prompt(&items);
assert!(prompt.contains("User message"));
}
}