1use crate::error::{ParseError, ToolResult};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13pub enum CacheControl {
14 Breakpoint,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct TextBlock {
22 pub text: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub cache_control: Option<CacheControl>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct ThinkingBlock {
32 pub thinking: String,
33 pub redacted: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct ImageSource {
40 pub data: String,
42 pub media_type: String,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct ToolCall {
49 pub id: String,
50 pub name: String,
51 pub arguments: serde_json::Value,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum ContentBlock {
59 Text(TextBlock),
60 Thinking(ThinkingBlock),
61 Image { source: ImageSource },
62 ToolCall(ToolCall),
63}
64
65impl ContentBlock {
66 pub fn text(s: impl Into<String>) -> Self {
68 ContentBlock::Text(TextBlock {
69 text: s.into(),
70 cache_control: None,
71 })
72 }
73
74 pub fn text_with_cache(s: String, cache: CacheControl) -> Self {
76 ContentBlock::Text(TextBlock {
77 text: s,
78 cache_control: Some(cache),
79 })
80 }
81
82 pub fn as_text(&self) -> Option<&str> {
83 match self {
84 ContentBlock::Text(block) => Some(&block.text),
85 _ => None,
86 }
87 }
88
89 pub fn flatten_text(blocks: &[ContentBlock]) -> String {
96 blocks
97 .iter()
98 .filter_map(|b| b.as_text())
99 .collect::<Vec<_>>()
100 .join("\n\n")
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(tag = "type", rename_all = "snake_case")]
107pub enum Message {
108 System {
109 content: Vec<ContentBlock>,
110 },
111 User {
112 content: Vec<ContentBlock>,
113 },
114 Assistant {
115 content: Vec<ContentBlock>,
116 },
117 ToolResult {
118 tool_call_id: String,
119 is_error: bool,
121 content: Vec<ContentBlock>,
122 },
123}
124
125impl Message {
126 pub fn system_text(s: &str) -> Self {
142 Message::System {
143 content: text_block(s.to_string()),
144 }
145 }
146
147 pub fn user_text(s: &str) -> Self {
155 Message::User {
156 content: text_block(s.to_string()),
157 }
158 }
159
160 pub fn assistant_text(s: &str) -> Self {
162 Message::Assistant {
163 content: text_block(s.to_string()),
164 }
165 }
166
167 pub fn user_text_image(text: &str, media_type: String, data: String) -> Self {
183 Message::User {
184 content: vec![
185 ContentBlock::text(text),
186 ContentBlock::Image {
187 source: ImageSource { data, media_type },
188 },
189 ],
190 }
191 }
192
193 pub fn user_image(media_type: String, data: String) -> Self {
195 Message::User {
196 content: vec![ContentBlock::Image {
197 source: ImageSource { data, media_type },
198 }],
199 }
200 }
201
202 pub fn system(content: Vec<ContentBlock>) -> Self {
208 Message::System { content }
209 }
210
211 pub fn user(content: Vec<ContentBlock>) -> Self {
213 Message::User { content }
214 }
215
216 pub fn assistant(content: Vec<ContentBlock>) -> Self {
218 Message::Assistant { content }
219 }
220
221 pub fn tool_result_ok(call_id: impl Into<String>, content: String) -> Self {
223 Message::ToolResult {
224 tool_call_id: call_id.into(),
225 is_error: false,
226 content: text_block(content),
227 }
228 }
229
230 pub fn tool_error(call_id: impl Into<String>, error: String) -> Self {
232 Message::ToolResult {
233 tool_call_id: call_id.into(),
234 is_error: true,
235 content: text_block(error),
236 }
237 }
238
239 pub fn content(&self) -> &Vec<ContentBlock> {
245 match self {
246 Message::System { content }
247 | Message::User { content }
248 | Message::Assistant { content }
249 | Message::ToolResult { content, .. } => content,
250 }
251 }
252
253 pub fn tool_call_id(&self) -> String {
255 match self {
256 Message::ToolResult { tool_call_id, .. } => tool_call_id.clone(),
257 _ => String::new(),
258 }
259 }
260
261 pub fn is_tool_error(&self) -> bool {
263 matches!(self, Message::ToolResult { is_error: true, .. })
264 }
265
266 pub fn tool_result(call: &ToolCall, result: &ToolResult) -> Self {
271 let (content_str, is_error) = match result {
272 Ok(v) => (
273 serde_json::to_string(v).unwrap_or_else(|_| v.to_string()),
274 false,
275 ),
276 Err(e) => (format!("tool error: {e}"), true),
277 };
278 Message::ToolResult {
279 tool_call_id: call.id.clone(),
280 is_error,
281 content: text_block(content_str),
282 }
283 }
284
285 pub fn validate(&self) -> Result<(), ParseError> {
293 match self {
294 Message::ToolResult {
295 tool_call_id,
296 is_error: _,
297 content,
298 } => {
299 if tool_call_id.is_empty() {
300 return Err(ParseError {
301 detail: "ToolResult.tool_call_id must not be empty".into(),
302 });
303 }
304 for block in content {
305 match block {
306 ContentBlock::ToolCall(_) => {
307 return Err(ParseError {
308 detail: "ToolResult must not contain ToolCall blocks".into(),
309 });
310 }
311 ContentBlock::Thinking(_) => {
312 return Err(ParseError {
313 detail: "ToolResult must not contain Thinking blocks".into(),
314 });
315 }
316 _ => {}
317 }
318 }
319 }
320 Message::Assistant { content } => {
321 for block in content {
322 if let ContentBlock::ToolCall(tc) = block
323 && tc.id.is_empty()
324 {
325 return Err(ParseError {
326 detail: "Assistant ToolCall.id must not be empty".into(),
327 });
328 }
329 }
330 }
331 Message::User { content } => {
332 for block in content {
333 if let ContentBlock::Thinking(_) = block {
334 return Err(ParseError {
335 detail: "User must not contain Thinking blocks".into(),
336 });
337 }
338 }
339 }
340 Message::System { .. } => {}
341 }
342 Ok(())
343 }
344
345 pub fn extract_tool_calls(&self) -> Vec<ToolCall> {
347 match self {
348 Message::Assistant { content } => content
349 .iter()
350 .filter_map(|b| {
351 if let ContentBlock::ToolCall(tc) = b {
352 Some(tc.clone())
353 } else {
354 None
355 }
356 })
357 .collect(),
358 _ => Vec::new(),
359 }
360 }
361}
362
363pub fn text_block(s: impl Into<String>) -> Vec<ContentBlock> {
365 vec![ContentBlock::text(s)]
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_content_block_text() {
374 let block = ContentBlock::text("hello");
375 assert_eq!(block.as_text(), Some("hello"));
376 }
377
378 #[test]
379 fn test_content_block_tool_call_no_as_text() {
380 let block = ContentBlock::ToolCall(ToolCall {
381 id: "1".into(),
382 name: "test".into(),
383 arguments: serde_json::json!({}),
384 });
385 assert_eq!(block.as_text(), None);
386 }
387
388 #[test]
389 fn test_message_content() {
390 let msg = Message::user_text("hello world");
391 assert_eq!(msg.content().len(), 1);
392 assert_eq!(msg.content()[0].as_text(), Some("hello world"));
393 }
394
395 #[test]
396 fn test_message_extract_tool_calls() {
397 let tc = ToolCall {
398 id: "1".into(),
399 name: "test".into(),
400 arguments: serde_json::json!({}),
401 };
402 let msg = Message::Assistant {
403 content: vec![ContentBlock::ToolCall(tc.clone())],
404 };
405 let calls = msg.extract_tool_calls();
406 assert_eq!(calls.len(), 1);
407 assert_eq!(calls[0].name, "test");
408 }
409
410 #[test]
413 fn test_validate_user_ok() {
414 let msg = Message::User {
415 content: text_block("hello".to_string()),
416 };
417 assert!(msg.validate().is_ok());
418 }
419
420 #[test]
421 fn test_validate_user_reject_thinking() {
422 let msg = Message::User {
423 content: vec![ContentBlock::Thinking(ThinkingBlock {
424 thinking: "hmm".into(),
425 redacted: None,
426 })],
427 };
428 assert!(matches!(msg.validate(), Err(ParseError { .. })));
429 }
430
431 #[test]
432 fn test_validate_assistant_ok() {
433 let msg = Message::Assistant {
434 content: text_block("hi".to_string()),
435 };
436 assert!(msg.validate().is_ok());
437 }
438
439 #[test]
440 fn test_validate_assistant_tool_call_empty_id() {
441 let msg = Message::Assistant {
442 content: vec![ContentBlock::ToolCall(ToolCall {
443 id: String::new(),
444 name: "test".into(),
445 arguments: serde_json::json!({}),
446 })],
447 };
448 assert!(matches!(msg.validate(), Err(ParseError { .. })));
449 }
450
451 #[test]
452 fn test_validate_tool_result_ok() {
453 let msg = Message::ToolResult {
454 tool_call_id: "call_1".to_string(),
455 is_error: false,
456 content: text_block("ok".to_string()),
457 };
458 assert!(msg.validate().is_ok());
459 }
460
461 #[test]
462 fn test_validate_tool_result_empty_id() {
463 let msg = Message::ToolResult {
464 tool_call_id: String::new(),
465 is_error: false,
466 content: text_block("ok".to_string()),
467 };
468 assert!(matches!(msg.validate(), Err(ParseError { .. })));
469 }
470
471 #[test]
472 fn test_validate_tool_result_reject_tool_call() {
473 let msg = Message::ToolResult {
474 tool_call_id: "call_1".to_string(),
475 is_error: false,
476 content: vec![ContentBlock::ToolCall(ToolCall {
477 id: "x".into(),
478 name: "y".into(),
479 arguments: serde_json::json!({}),
480 })],
481 };
482 assert!(matches!(msg.validate(), Err(ParseError { .. })));
483 }
484
485 #[test]
486 fn test_validate_tool_result_reject_thinking() {
487 let msg = Message::ToolResult {
488 tool_call_id: "call_1".to_string(),
489 is_error: false,
490 content: vec![ContentBlock::Thinking(ThinkingBlock {
491 thinking: "hmm".into(),
492 redacted: None,
493 })],
494 };
495 assert!(matches!(msg.validate(), Err(ParseError { .. })));
496 }
497
498 #[test]
499 fn test_validate_system_ok() {
500 let msg = Message::System {
501 content: text_block("you are helpful".to_string()),
502 };
503 assert!(msg.validate().is_ok());
504 }
505
506 #[test]
509 fn test_convenience_system_text() {
510 let msg = Message::system_text("you are helpful");
511 assert!(matches!(msg, Message::System { .. }));
512 assert_eq!(msg.content()[0].as_text(), Some("you are helpful"));
513 }
514
515 #[test]
516 fn test_convenience_user_text() {
517 let msg = Message::user_text("hello");
518 assert!(matches!(msg, Message::User { .. }));
519 assert_eq!(msg.content()[0].as_text(), Some("hello"));
520 }
521
522 #[test]
523 fn test_convenience_assistant_text() {
524 let msg = Message::assistant_text("the answer is 42");
525 assert!(matches!(msg, Message::Assistant { .. }));
526 assert_eq!(msg.content()[0].as_text(), Some("the answer is 42"));
527 }
528
529 #[test]
530 fn test_convenience_system_content() {
531 let msg = Message::system(vec![ContentBlock::text("prompt")]);
532 assert!(matches!(msg, Message::System { .. }));
533 assert_eq!(msg.content()[0].as_text(), Some("prompt"));
534 }
535
536 #[test]
537 fn test_convenience_user_content() {
538 let msg = Message::user(vec![ContentBlock::text("question")]);
539 assert!(matches!(msg, Message::User { .. }));
540 assert_eq!(msg.content()[0].as_text(), Some("question"));
541 }
542
543 #[test]
544 fn test_convenience_tool_result_ok() {
545 let msg = Message::tool_result_ok("call_1", "result data".to_string());
546 assert!(matches!(msg, Message::ToolResult { .. }));
547 assert!(!msg.is_tool_error());
548 assert_eq!(msg.tool_call_id(), "call_1");
549 }
550
551 #[test]
552 fn test_convenience_tool_error() {
553 let msg = Message::tool_error("call_2", "something failed".to_string());
554 assert!(matches!(msg, Message::ToolResult { .. }));
555 assert!(msg.is_tool_error());
556 assert_eq!(msg.tool_call_id(), "call_2");
557 }
558
559 #[test]
560 fn test_content_block_text_with_string() {
561 let s = String::from("dynamic");
562 let block = ContentBlock::text(s);
563 assert_eq!(block.as_text(), Some("dynamic"));
564 }
565
566 #[test]
567 fn test_text_block_with_str() {
568 let blocks = text_block("hello");
569 assert_eq!(blocks.len(), 1);
570 assert_eq!(blocks[0].as_text(), Some("hello"));
571 }
572
573 #[test]
574 fn test_text_block_with_string() {
575 let blocks = text_block(String::from("hello"));
576 assert_eq!(blocks.len(), 1);
577 assert_eq!(blocks[0].as_text(), Some("hello"));
578 }
579
580 #[test]
583 fn test_convenience_user_text_image() {
584 let msg = Message::user_text_image("what's this?", "image/png".into(), "base64data".into());
585 assert!(matches!(msg, Message::User { .. }));
586 assert_eq!(msg.content().len(), 2);
587 assert_eq!(msg.content()[0].as_text(), Some("what's this?"));
588 match &msg.content()[1] {
589 ContentBlock::Image { source } => {
590 assert_eq!(source.media_type, "image/png");
591 assert_eq!(source.data, "base64data");
592 }
593 _ => panic!("expected Image block"),
594 }
595 }
596
597 #[test]
598 fn test_convenience_user_image() {
599 let msg = Message::user_image("image/jpeg".into(), "jpgdata".into());
600 assert!(matches!(msg, Message::User { .. }));
601 assert_eq!(msg.content().len(), 1);
602 match &msg.content()[0] {
603 ContentBlock::Image { source } => {
604 assert_eq!(source.media_type, "image/jpeg");
605 assert_eq!(source.data, "jpgdata");
606 }
607 _ => panic!("expected Image block"),
608 }
609 }
610}