1use super::message::*;
9
10pub fn ensure_tool_result_pairing(messages: &mut Vec<Message>) {
13 let mut pending_tool_ids: Vec<String> = Vec::new();
14
15 let mut i = 0;
16 while i < messages.len() {
17 match &messages[i] {
18 Message::Assistant(a) => {
19 for block in &a.content {
21 if let ContentBlock::ToolUse { id, .. } = block {
22 pending_tool_ids.push(id.clone());
23 }
24 }
25 }
26 Message::User(u) => {
27 for block in &u.content {
29 if let ContentBlock::ToolResult { tool_use_id, .. } = block {
30 pending_tool_ids.retain(|id| id != tool_use_id);
31 }
32 }
33 }
34 _ => {}
35 }
36 i += 1;
37 }
38
39 if !pending_tool_ids.is_empty() {
41 for id in pending_tool_ids {
42 messages.push(tool_result_message(
43 &id,
44 "(tool execution was interrupted)",
45 true,
46 ));
47 }
48 }
49}
50
51pub fn strip_empty_blocks(messages: &mut [Message]) {
53 for msg in messages.iter_mut() {
54 match msg {
55 Message::User(u) => {
56 u.content.retain(|b| match b {
57 ContentBlock::Text { text } => !text.is_empty(),
58 _ => true,
59 });
60 }
61 Message::Assistant(a) => {
62 a.content.retain(|b| match b {
63 ContentBlock::Text { text } => !text.is_empty(),
64 _ => true,
65 });
66 }
67 _ => {}
68 }
69 }
70}
71
72pub fn validate_alternation(messages: &[Message]) -> Result<(), String> {
75 let mut expect_user = true;
76
77 for (i, msg) in messages.iter().enumerate() {
78 match msg {
79 Message::System(_) => continue, Message::User(_) => {
81 if !expect_user {
82 return Err(format!("Message {i}: expected assistant, got user"));
83 }
84 expect_user = false;
85 }
86 Message::Assistant(_) => {
87 if expect_user {
88 return Err(format!("Message {i}: expected user, got assistant"));
89 }
90 expect_user = true;
91 }
92 }
93 }
94
95 Ok(())
96}
97
98pub fn remove_empty_messages(messages: &mut Vec<Message>) {
100 messages.retain(|msg| match msg {
101 Message::User(u) => !u.content.is_empty(),
102 Message::Assistant(a) => !a.content.is_empty(),
103 Message::System(_) => true,
104 });
105}
106
107pub fn cap_document_blocks(messages: &mut [Message], max_bytes: usize) {
109 for msg in messages.iter_mut() {
110 let content = match msg {
111 Message::User(u) => &mut u.content,
112 Message::Assistant(a) => &mut a.content,
113 _ => continue,
114 };
115 for block in content.iter_mut() {
116 if let ContentBlock::Document { data, title, .. } = block
117 && data.len() > max_bytes
118 {
119 let name = title.as_deref().unwrap_or("document");
120 *block = ContentBlock::Text {
121 text: format!(
122 "(Document '{name}' too large for context: {} bytes, max {max_bytes})",
123 data.len()
124 ),
125 };
126 }
127 }
128 }
129}
130
131pub fn merge_consecutive_user_messages(messages: &mut Vec<Message>) {
134 let mut i = 0;
135 while i + 1 < messages.len() {
136 let both_user = matches!(&messages[i], Message::User(_))
137 && matches!(&messages[i + 1], Message::User(_));
138
139 if both_user {
140 if let Message::User(next) = messages.remove(i + 1)
142 && let Message::User(ref mut current) = messages[i]
143 {
144 current.content.extend(next.content);
145 }
146 } else {
147 i += 1;
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use uuid::Uuid;
156
157 #[test]
158 fn test_tool_result_pairing() {
159 let mut messages = vec![
160 Message::Assistant(AssistantMessage {
161 uuid: Uuid::new_v4(),
162 timestamp: String::new(),
163 content: vec![ContentBlock::ToolUse {
164 id: "call_1".into(),
165 name: "Bash".into(),
166 input: serde_json::json!({}),
167 }],
168 model: None,
169 usage: None,
170 stop_reason: None,
171 request_id: None,
172 }),
173 ];
175
176 ensure_tool_result_pairing(&mut messages);
177
178 assert_eq!(messages.len(), 2);
180 if let Message::User(u) = &messages[1] {
181 assert!(matches!(
182 &u.content[0],
183 ContentBlock::ToolResult { is_error: true, .. }
184 ));
185 } else {
186 panic!("Expected user message with tool result");
187 }
188 }
189
190 #[test]
191 fn test_merge_consecutive_users() {
192 let mut messages = vec![
193 user_message("hello"),
194 user_message("world"),
195 Message::Assistant(AssistantMessage {
196 uuid: Uuid::new_v4(),
197 timestamp: String::new(),
198 content: vec![ContentBlock::Text { text: "hi".into() }],
199 model: None,
200 usage: None,
201 stop_reason: None,
202 request_id: None,
203 }),
204 ];
205
206 merge_consecutive_user_messages(&mut messages);
207 assert_eq!(messages.len(), 2); }
209
210 #[test]
211 fn test_strip_empty_blocks() {
212 let mut messages = vec![Message::User(UserMessage {
213 uuid: Uuid::new_v4(),
214 timestamp: String::new(),
215 content: vec![
216 ContentBlock::Text {
217 text: "".into(), },
219 ContentBlock::Text {
220 text: "keep me".into(),
221 },
222 ],
223 is_meta: false,
224 is_compact_summary: false,
225 })];
226 strip_empty_blocks(&mut messages);
227 if let Message::User(u) = &messages[0] {
228 assert_eq!(u.content.len(), 1);
229 assert_eq!(u.content[0].as_text(), Some("keep me"));
230 }
231 }
232
233 #[test]
234 fn test_validate_alternation_valid() {
235 let messages = vec![
236 user_message("hello"),
237 Message::Assistant(AssistantMessage {
238 uuid: Uuid::new_v4(),
239 timestamp: String::new(),
240 content: vec![ContentBlock::Text { text: "hi".into() }],
241 model: None,
242 usage: None,
243 stop_reason: None,
244 request_id: None,
245 }),
246 ];
247 assert!(validate_alternation(&messages).is_ok());
248 }
249
250 #[test]
251 fn test_validate_alternation_invalid() {
252 let messages = vec![
253 user_message("hello"),
254 user_message("world"), ];
256 assert!(validate_alternation(&messages).is_err());
257 }
258
259 #[test]
260 fn test_remove_empty_messages() {
261 let mut messages = vec![
262 user_message("keep"),
263 Message::User(UserMessage {
264 uuid: Uuid::new_v4(),
265 timestamp: String::new(),
266 content: vec![], is_meta: false,
268 is_compact_summary: false,
269 }),
270 user_message("also keep"),
271 ];
272 remove_empty_messages(&mut messages);
273 assert_eq!(messages.len(), 2);
274 }
275
276 #[test]
277 fn test_cap_document_blocks() {
278 let mut messages = vec![Message::User(UserMessage {
279 uuid: Uuid::new_v4(),
280 timestamp: String::new(),
281 content: vec![ContentBlock::Document {
282 media_type: "application/pdf".into(),
283 data: "x".repeat(1000),
284 title: Some("big.pdf".into()),
285 }],
286 is_meta: false,
287 is_compact_summary: false,
288 })];
289 cap_document_blocks(&mut messages, 500);
291 if let Message::User(u) = &messages[0] {
292 assert!(matches!(&u.content[0], ContentBlock::Text { .. }));
293 if let ContentBlock::Text { text } = &u.content[0] {
294 assert!(text.contains("big.pdf"));
295 assert!(text.contains("too large"));
296 }
297 }
298 }
299
300 #[test]
301 fn test_cap_document_blocks_within_limit() {
302 let mut messages = vec![Message::User(UserMessage {
303 uuid: Uuid::new_v4(),
304 timestamp: String::new(),
305 content: vec![ContentBlock::Document {
306 media_type: "application/pdf".into(),
307 data: "small".into(),
308 title: Some("small.pdf".into()),
309 }],
310 is_meta: false,
311 is_compact_summary: false,
312 })];
313 cap_document_blocks(&mut messages, 500);
315 if let Message::User(u) = &messages[0] {
316 assert!(matches!(&u.content[0], ContentBlock::Document { .. }));
317 }
318 }
319}