synwire_core/messages/
utils.rs1use crate::messages::{Message, MessageContent};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum TrimStrategy {
9 First,
11 Last,
13}
14
15fn estimate_tokens(msg: &Message) -> usize {
19 let text_len = msg.content().as_text().len();
20 text_len.div_ceil(4)
21}
22
23pub fn trim_messages(
45 messages: &[Message],
46 max_tokens: usize,
47 strategy: &TrimStrategy,
48) -> Vec<Message> {
49 let total: usize = messages.iter().map(estimate_tokens).sum();
50
51 if total <= max_tokens {
52 return messages.to_vec();
53 }
54
55 match strategy {
56 TrimStrategy::First => {
57 let mut result = Vec::new();
59 let mut budget = max_tokens;
60
61 for msg in messages.iter().rev() {
62 let tokens = estimate_tokens(msg);
63 if tokens <= budget {
64 result.push(msg.clone());
65 budget -= tokens;
66 } else {
67 break;
68 }
69 }
70
71 result.reverse();
72 result
73 }
74 TrimStrategy::Last => {
75 let mut result = Vec::new();
77 let mut budget = max_tokens;
78
79 for msg in messages {
80 let tokens = estimate_tokens(msg);
81 if tokens <= budget {
82 result.push(msg.clone());
83 budget -= tokens;
84 } else {
85 break;
86 }
87 }
88
89 result
90 }
91 }
92}
93
94pub fn merge_message_runs(messages: &[Message]) -> Vec<Message> {
116 if messages.is_empty() {
117 return Vec::new();
118 }
119
120 let mut result: Vec<Message> = Vec::new();
121
122 for msg in messages {
123 let should_merge = result
124 .last()
125 .is_some_and(|last| last.message_type() == msg.message_type());
126
127 if should_merge {
128 if let Some(last) = result.last_mut() {
130 let combined = format!("{}\n{}", last.content().as_text(), msg.content().as_text());
131 let new_content = MessageContent::Text(combined);
132 replace_content(last, new_content);
133 }
134 } else {
135 result.push(msg.clone());
136 }
137 }
138
139 result
140}
141
142fn replace_content(msg: &mut Message, new_content: MessageContent) {
144 match msg {
145 Message::Human { content, .. }
146 | Message::AI { content, .. }
147 | Message::System { content, .. }
148 | Message::Tool { content, .. }
149 | Message::Chat { content, .. } => {
150 *content = new_content;
151 }
152 }
153}
154
155#[cfg(test)]
156#[allow(clippy::unwrap_used)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_trim_messages_first_strategy() {
162 let messages = vec![
164 Message::system("You are a helpful assistant"),
165 Message::human("Hello there"),
166 Message::ai("Hi"),
167 ];
168
169 let trimmed = trim_messages(&messages, 2, &TrimStrategy::First);
171 assert!(!trimmed.is_empty());
173 assert!(trimmed.len() < messages.len());
174 assert_eq!(trimmed.last().map(Message::message_type), Some("ai"));
176 }
177
178 #[test]
179 fn test_trim_messages_last_strategy() {
180 let messages = vec![
181 Message::human("Hi"),
182 Message::ai("Hello! How can I help you today with your questions?"),
183 Message::human("Tell me about Rust"),
184 ];
185
186 let trimmed = trim_messages(&messages, 3, &TrimStrategy::Last);
188 assert!(!trimmed.is_empty());
189 assert_eq!(trimmed[0].message_type(), "human");
190 }
191
192 #[test]
193 fn test_trim_messages_within_budget() {
194 let messages = vec![Message::human("Hi"), Message::ai("Hello")];
195
196 let trimmed = trim_messages(&messages, 1000, &TrimStrategy::First);
198 assert_eq!(trimmed.len(), 2);
199 }
200
201 #[test]
202 fn test_trim_messages_empty() {
203 let trimmed = trim_messages(&[], 100, &TrimStrategy::First);
204 assert!(trimmed.is_empty());
205 }
206
207 #[test]
208 fn test_merge_message_runs() {
209 let messages = vec![
210 Message::human("Hello"),
211 Message::human("How are you?"),
212 Message::ai("I'm fine"),
213 Message::ai("Thanks for asking"),
214 Message::human("Great"),
215 ];
216
217 let merged = merge_message_runs(&messages);
218 assert_eq!(merged.len(), 3);
219 assert_eq!(merged[0].content().as_text(), "Hello\nHow are you?");
220 assert_eq!(merged[1].content().as_text(), "I'm fine\nThanks for asking");
221 assert_eq!(merged[2].content().as_text(), "Great");
222 }
223
224 #[test]
225 fn test_merge_message_runs_no_consecutive() {
226 let messages = vec![
227 Message::human("Hello"),
228 Message::ai("Hi"),
229 Message::human("How are you?"),
230 ];
231
232 let merged = merge_message_runs(&messages);
233 assert_eq!(merged.len(), 3);
234 }
235
236 #[test]
237 fn test_merge_message_runs_empty() {
238 let merged = merge_message_runs(&[]);
239 assert!(merged.is_empty());
240 }
241
242 #[test]
243 fn test_merge_message_runs_single() {
244 let messages = vec![Message::human("Hello")];
245 let merged = merge_message_runs(&messages);
246 assert_eq!(merged.len(), 1);
247 assert_eq!(merged[0].content().as_text(), "Hello");
248 }
249}