1#![cfg(feature = "llm")]
7
8use std::{sync::Arc, vec};
9
10use im::Vector;
11use serde::{Deserialize, Serialize};
12
13use crate::error::AgentError;
14use crate::value::AgentValue;
15
16#[cfg(feature = "image")]
17use photon_rs::PhotonImage;
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum ContentBlock {
26 Text {
28 text: String,
30 },
31
32 Thinking {
34 thinking: String,
37
38 #[serde(skip_serializing_if = "Option::is_none", default)]
42 signature: Option<String>,
43
44 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
47 redacted: bool,
48 },
49
50 #[cfg(feature = "image")]
52 Image {
53 data: String,
55
56 mime_type: String,
58 },
59}
60
61#[derive(Debug, Clone, PartialEq)]
68pub enum MessageContent {
69 Text(String),
71
72 Blocks(Vec<ContentBlock>),
74}
75
76impl Default for MessageContent {
77 fn default() -> Self {
78 MessageContent::Text(String::new())
79 }
80}
81
82impl From<String> for MessageContent {
83 fn from(s: String) -> Self {
84 MessageContent::Text(s)
85 }
86}
87
88impl From<&str> for MessageContent {
89 fn from(s: &str) -> Self {
90 MessageContent::Text(s.to_string())
91 }
92}
93
94impl MessageContent {
95 pub fn text(&self) -> String {
97 match self {
98 MessageContent::Text(s) => s.clone(),
99 MessageContent::Blocks(blocks) => blocks
100 .iter()
101 .filter_map(|b| match b {
102 ContentBlock::Text { text } => Some(text.as_str()),
103 _ => None,
104 })
105 .collect(),
106 }
107 }
108}
109
110fn absorb_legacy_thinking(content: MessageContent, thinking: String) -> MessageContent {
114 let mut blocks = match content {
115 MessageContent::Text(s) if s.is_empty() => vec![],
116 MessageContent::Text(s) => vec![ContentBlock::Text { text: s }],
117 MessageContent::Blocks(blocks) => blocks,
118 };
119 blocks.insert(
120 0,
121 ContentBlock::Thinking {
122 thinking,
123 signature: None,
124 redacted: false,
125 },
126 );
127 MessageContent::Blocks(blocks)
128}
129
130#[derive(Debug, Default, Clone)]
160pub struct Message {
161 pub id: Option<String>,
163
164 pub role: String,
166
167 pub content: MessageContent,
171
172 pub tokens: Option<usize>,
174
175 pub streaming: bool,
177
178 pub tool_calls: Option<Vector<ToolCall>>,
180
181 pub tool_name: Option<String>,
183
184 pub is_error: Option<bool>,
186
187 pub stop_reason: Option<String>,
191
192 pub usage: Option<Usage>,
196
197 #[cfg(feature = "image")]
199 pub image: Option<Arc<PhotonImage>>,
200}
201
202impl Message {
203 pub fn new(role: String, content: String) -> Self {
210 Self {
211 id: None,
212 role,
213 content: MessageContent::Text(content),
214 tokens: None,
215 streaming: false,
216 tool_calls: None,
217 tool_name: None,
218 is_error: None,
219 stop_reason: None,
220 usage: None,
221
222 #[cfg(feature = "image")]
223 image: None,
224 }
225 }
226
227 pub fn assistant(content: String) -> Self {
229 Message::new("assistant".to_string(), content)
230 }
231
232 pub fn system(content: String) -> Self {
236 Message::new("system".to_string(), content)
237 }
238
239 pub fn user(content: String) -> Self {
241 Message::new("user".to_string(), content)
242 }
243
244 pub fn tool(tool_name: String, content: String) -> Self {
254 let mut message = Message::new("tool".to_string(), content);
255 message.tool_name = Some(tool_name);
256 message
257 }
258
259 pub fn tool_with_content(tool_name: String, content: MessageContent) -> Self {
270 let mut message = Message::new("tool".to_string(), String::new());
271 message.content = content;
272 message.tool_name = Some(tool_name);
273 message
274 }
275
276 #[cfg(feature = "image")]
280 pub fn with_image(mut self, image: Arc<PhotonImage>) -> Self {
281 self.image = Some(image);
282 self
283 }
284
285 pub fn text(&self) -> String {
287 self.content.text()
288 }
289
290 pub fn thinking(&self) -> Option<String> {
297 let MessageContent::Blocks(blocks) = &self.content else {
298 return None;
299 };
300 let parts: Vec<&str> = blocks
301 .iter()
302 .filter_map(|b| match b {
303 ContentBlock::Thinking { redacted: true, .. } => Some("[redacted]"),
304 ContentBlock::Thinking { thinking, .. } => Some(thinking.as_str()),
305 _ => None,
306 })
307 .collect();
308 if parts.is_empty() {
309 None
310 } else {
311 Some(parts.join("\n"))
312 }
313 }
314}
315
316impl PartialEq for Message {
317 fn eq(&self, other: &Self) -> bool {
318 self.id == other.id && self.role == other.role && self.content == other.content
319 }
320}
321
322impl Serialize for Message {
323 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
324 where
325 S: serde::Serializer,
326 {
327 let mut map = serde_json::Map::new();
328 if let Some(id) = &self.id {
329 map.insert("id".to_string(), serde_json::Value::String(id.clone()));
330 }
331 map.insert(
332 "role".to_string(),
333 serde_json::Value::String(self.role.clone()),
334 );
335 let content_value = match &self.content {
340 MessageContent::Text(s) => serde_json::Value::String(s.clone()),
341 MessageContent::Blocks(blocks)
342 if blocks
343 .iter()
344 .all(|b| matches!(b, ContentBlock::Text { .. })) =>
345 {
346 serde_json::Value::String(self.content.text())
347 }
348 MessageContent::Blocks(blocks) => {
349 serde_json::to_value(blocks).map_err(serde::ser::Error::custom)?
350 }
351 };
352 map.insert("content".to_string(), content_value);
353 if let Some(tokens) = &self.tokens {
354 map.insert(
355 "tokens".to_string(),
356 serde_json::Value::Number((*tokens).into()),
357 );
358 }
359 if self.streaming {
360 map.insert("streaming".to_string(), serde_json::Value::Bool(true));
361 }
362 if let Some(tool_calls) = &self.tool_calls {
363 let mut tool_calls_vec = vec![];
364 for call in tool_calls {
365 tool_calls_vec.push(serde_json::to_value(call).map_err(serde::ser::Error::custom)?);
366 }
367 map.insert(
368 "tool_calls".to_string(),
369 serde_json::Value::Array(tool_calls_vec),
370 );
371 }
372 if let Some(tool_name) = &self.tool_name {
373 map.insert(
374 "tool_name".to_string(),
375 serde_json::Value::String(tool_name.clone()),
376 );
377 }
378 if let Some(is_error) = &self.is_error {
381 map.insert("is_error".to_string(), serde_json::Value::Bool(*is_error));
382 }
383 if let Some(stop_reason) = &self.stop_reason {
384 map.insert(
385 "stop_reason".to_string(),
386 serde_json::Value::String(stop_reason.clone()),
387 );
388 }
389 if let Some(usage) = &self.usage {
390 map.insert(
391 "usage".to_string(),
392 serde_json::to_value(usage).map_err(serde::ser::Error::custom)?,
393 );
394 }
395 #[cfg(feature = "image")]
396 {
397 if let Some(image) = &self.image {
398 map.insert(
399 "image".to_string(),
400 serde_json::Value::String(image.get_base64()),
401 );
402 }
403 }
404 map.serialize(serializer)
405 }
406}
407
408impl<'de> Deserialize<'de> for Message {
409 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
410 where
411 D: serde::Deserializer<'de>,
412 {
413 let mut message = Message::user(String::default());
414 let map = serde_json::Map::deserialize(deserializer)?;
415
416 if let Some(id) = map.get("id") {
417 message.id = id.as_str().map(|s| s.to_string());
418 }
419 if let Some(role) = map.get("role") {
420 message.role = role
421 .as_str()
422 .ok_or_else(|| serde::de::Error::custom("role must be a string"))?
423 .to_string();
424 }
425 if let Some(content) = map.get("content") {
426 message.content = match content {
427 serde_json::Value::String(s) => MessageContent::Text(s.clone()),
428 serde_json::Value::Array(_) => {
429 let blocks: Vec<ContentBlock> = serde_json::from_value(content.clone())
430 .map_err(|e| {
431 serde::de::Error::custom(format!("invalid content blocks: {e}"))
432 })?;
433 MessageContent::Blocks(blocks)
434 }
435 _ => {
436 return Err(serde::de::Error::custom(
437 "content must be a string or an array of content blocks",
438 ));
439 }
440 };
441 }
442 if let Some(tokens) = map.get("tokens") {
443 message.tokens = tokens.as_u64().map(|u| u as usize);
444 }
445 if let Some(thinking) = map.get("thinking").and_then(|v| v.as_str()) {
448 message.content =
449 absorb_legacy_thinking(std::mem::take(&mut message.content), thinking.to_string());
450 }
451 if let Some(streaming) = map.get("streaming") {
452 message.streaming = streaming.as_bool().unwrap_or(false);
453 }
454 if let Some(tool_calls) = map.get("tool_calls") {
455 let tool_calls = serde_json::from_value::<Vec<ToolCall>>(tool_calls.clone())
456 .map_err(|e| serde::de::Error::custom(e.to_string()))?;
457 message.tool_calls = Some(tool_calls.into());
458 }
459 if let Some(tool_name) = map.get("tool_name") {
460 message.tool_name = tool_name.as_str().map(|s| s.to_string());
461 }
462 message.is_error = map.get("is_error").and_then(|v| v.as_bool());
463 message.stop_reason = map
464 .get("stop_reason")
465 .and_then(|v| v.as_str())
466 .map(|s| s.to_string());
467 message.usage = map
470 .get("usage")
471 .and_then(|v| serde_json::from_value(v.clone()).ok());
472 #[cfg(feature = "image")]
473 if let Some(image) = map.get("image") {
474 let image_str = image
475 .as_str()
476 .ok_or_else(|| serde::de::Error::custom("image must be a string"))?;
477 let image = Arc::new(PhotonImage::new_from_base64(image_str));
478 message.image = Some(image);
479 }
480 Ok(message)
481 }
482}
483
484#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
489pub struct Usage {
490 #[serde(default)]
492 pub input_tokens: u64,
493
494 #[serde(default)]
496 pub output_tokens: u64,
497
498 #[serde(default)]
500 pub cache_read_tokens: u64,
501
502 #[serde(default)]
504 pub cache_write_tokens: u64,
505}
506
507#[cfg(feature = "image")]
512const IMAGE_TOKENS: u64 = 1200;
513
514pub fn estimate_message_tokens(m: &Message) -> u64 {
531 let mut chars: usize = 0;
532 #[cfg_attr(not(feature = "image"), allow(unused_mut))]
533 let mut image_tokens: u64 = 0;
534
535 match &m.content {
536 MessageContent::Text(s) => chars += s.len(),
537 MessageContent::Blocks(blocks) => {
538 for block in blocks {
539 match block {
540 ContentBlock::Text { text } => chars += text.len(),
541 ContentBlock::Thinking { thinking, .. } => chars += thinking.len(),
542
543 #[cfg(feature = "image")]
544 ContentBlock::Image { .. } => image_tokens += IMAGE_TOKENS,
545 }
546 }
547 }
548 }
549
550 if let Some(tool_calls) = &m.tool_calls {
551 for call in tool_calls {
552 chars += call.function.name.len();
553 chars += serde_json::to_string(&call.function.parameters).map_or(0, |s| s.len());
554 }
555 }
556
557 #[cfg(feature = "image")]
558 if m.image.is_some() {
559 image_tokens += IMAGE_TOKENS;
560 }
561
562 (chars as u64).div_ceil(4) + image_tokens
563}
564
565pub fn estimate_context_tokens(messages: &[Message]) -> u64 {
575 for (i, m) in messages.iter().enumerate().rev() {
576 if m.role == "assistant"
577 && let Some(usage) = &m.usage
578 {
579 let anchor = usage.input_tokens
580 + usage.output_tokens
581 + usage.cache_read_tokens
582 + usage.cache_write_tokens;
583 return anchor
584 + messages[i + 1..]
585 .iter()
586 .map(estimate_message_tokens)
587 .sum::<u64>();
588 }
589 }
590 messages.iter().map(estimate_message_tokens).sum()
591}
592
593#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
598pub struct ToolCall {
599 pub function: ToolCallFunction,
601}
602
603#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
608pub struct ToolCallFunction {
609 pub name: String,
611
612 pub parameters: serde_json::Value,
614
615 #[serde(skip_serializing_if = "Option::is_none")]
617 pub id: Option<String>,
618
619 #[serde(skip_serializing_if = "Option::is_none", default)]
623 pub parse_error: Option<String>,
624}
625
626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
650#[serde(tag = "type", rename_all = "snake_case")]
651pub enum MessageEvent {
652 Start {
654 partial: Message,
656 },
657
658 TextDelta {
660 delta: String,
662 partial: Message,
664 },
665
666 ThinkingDelta {
668 delta: String,
670 partial: Message,
672 },
673
674 ToolCallStart {
676 index: usize,
678 partial: Message,
680 },
681
682 ToolCallDelta {
684 index: usize,
686 delta: String,
688 partial: Message,
690 },
691
692 ToolCallEnd {
694 index: usize,
696 tool_call: ToolCall,
698 partial: Message,
700 },
701
702 Done {
704 message: Message,
706 },
707
708 Error {
710 message: Message,
712 error: String,
714 },
715}
716
717impl TryFrom<MessageEvent> for AgentValue {
718 type Error = AgentError;
719
720 fn try_from(event: MessageEvent) -> Result<Self, AgentError> {
721 let json = serde_json::to_value(&event).map_err(|e| {
724 AgentError::InvalidValue(format!("Failed to serialize MessageEvent: {e}"))
725 })?;
726 AgentValue::from_json(json)
727 }
728}
729
730impl TryFrom<AgentValue> for Message {
731 type Error = AgentError;
732
733 fn try_from(value: AgentValue) -> Result<Self, Self::Error> {
734 match value {
735 AgentValue::Message(msg) => Ok((*msg).clone()),
736 AgentValue::String(s) => Ok(Message::user(s.to_string())),
737
738 #[cfg(feature = "image")]
739 AgentValue::Image(img) => {
740 let mut message = Message::user("".to_string());
741 message.image = Some(img.clone());
742 Ok(message)
743 }
744 AgentValue::Object(obj) => {
745 let role = obj
746 .get("role")
747 .and_then(|r| r.as_str())
748 .unwrap_or("user")
749 .to_string();
750 let content_value = obj.get("content").ok_or_else(|| {
751 AgentError::InvalidValue("Message object missing 'content' field".to_string())
752 })?;
753 let content = match content_value {
754 AgentValue::String(s) => MessageContent::Text(s.to_string()),
755 AgentValue::Array(_) => {
756 let blocks: Vec<ContentBlock> =
757 serde_json::from_value(content_value.to_json()).map_err(|e| {
758 AgentError::InvalidValue(format!("Invalid content blocks: {e}"))
759 })?;
760 MessageContent::Blocks(blocks)
761 }
762 _ => {
763 return Err(AgentError::InvalidValue(
764 "'content' field must be a string or an array of content blocks"
765 .to_string(),
766 ));
767 }
768 };
769 let mut message = Message::new(role, String::new());
770 message.content = content;
771
772 let id = obj
773 .get("id")
774 .and_then(|i| i.as_str())
775 .map(|s| s.to_string());
776 message.id = id;
777
778 if let Some(thinking) = obj.get("thinking").and_then(|t| t.as_str()) {
781 message.content = absorb_legacy_thinking(
782 std::mem::take(&mut message.content),
783 thinking.to_string(),
784 );
785 }
786
787 message.streaming = obj
788 .get("streaming")
789 .and_then(|st| st.as_bool())
790 .unwrap_or_default();
791
792 message.is_error = obj.get("is_error").and_then(|v| v.as_bool());
793
794 message.stop_reason = obj
795 .get("stop_reason")
796 .and_then(|v| v.as_str())
797 .map(|s| s.to_string());
798
799 message.usage = obj
802 .get("usage")
803 .and_then(|v| serde_json::from_value(v.to_json()).ok());
804
805 if let Some(tool_name) = obj.get("tool_name") {
806 message.tool_name = Some(
807 tool_name
808 .as_str()
809 .ok_or_else(|| {
810 AgentError::InvalidValue(
811 "'tool_name' field must be a string".to_string(),
812 )
813 })?
814 .to_string(),
815 );
816 }
817
818 if let Some(tool_calls) = obj.get("tool_calls") {
819 let mut calls = vec![];
820 for call_value in tool_calls.as_array().ok_or_else(|| {
821 AgentError::InvalidValue("'tool_calls' field must be an array".to_string())
822 })? {
823 let id = call_value
824 .get("id")
825 .and_then(|i| i.as_str())
826 .map(|s| s.to_string());
827 let function = call_value.get("function").ok_or_else(|| {
828 AgentError::InvalidValue(
829 "Tool call missing 'function' field".to_string(),
830 )
831 })?;
832 let tool_name = function.get_str("name").ok_or_else(|| {
833 AgentError::InvalidValue(
834 "Tool call function missing 'name' field".to_string(),
835 )
836 })?;
837 let parameters = function.get("parameters").ok_or_else(|| {
838 AgentError::InvalidValue(
839 "Tool call function missing 'parameters' field".to_string(),
840 )
841 })?;
842 let call = ToolCall {
843 function: ToolCallFunction {
844 id,
845 name: tool_name.to_string(),
846 parameters: parameters.to_json(),
847 parse_error: None,
848 },
849 };
850 calls.push(call);
851 }
852 message.tool_calls = Some(calls.into());
853 }
854
855 #[cfg(feature = "image")]
856 {
857 if let Some(image_value) = obj.get("image") {
858 match image_value {
859 AgentValue::String(s) => {
860 message.image = Some(Arc::new(PhotonImage::new_from_base64(
861 s.trim_start_matches("data:image/png;base64,"),
862 )));
863 }
864 AgentValue::Image(img) => {
865 message.image = Some(img.clone());
866 }
867 _ => {}
868 }
869 }
870 }
871
872 Ok(message)
873 }
874 _ => Err(AgentError::InvalidValue(
875 "Cannot convert AgentValue to Message".to_string(),
876 )),
877 }
878 }
879}
880
881impl From<Message> for AgentValue {
882 fn from(msg: Message) -> Self {
883 AgentValue::Message(Arc::new(msg))
884 }
885}
886
887impl From<Vec<Message>> for AgentValue {
888 fn from(msgs: Vec<Message>) -> Self {
889 let agent_msgs: Vector<AgentValue> = msgs.into_iter().map(|m| m.into()).collect();
890 AgentValue::Array(agent_msgs)
891 }
892}
893
894#[cfg(test)]
895mod tests {
896 use im::{hashmap, vector};
897
898 use super::*;
899
900 #[test]
903 fn test_tool_call_function_parse_error_serde() {
904 let func = ToolCallFunction {
907 name: "t".to_string(),
908 parameters: serde_json::json!({}),
909 id: Some("call1".to_string()),
910 parse_error: None,
911 };
912 let json = serde_json::to_value(&func).unwrap();
913 assert!(json.get("parse_error").is_none());
914 let restored: ToolCallFunction = serde_json::from_value(json).unwrap();
915 assert_eq!(restored.parse_error, None);
916
917 let func = ToolCallFunction {
919 name: "t".to_string(),
920 parameters: serde_json::json!({}),
921 id: Some("call1".to_string()),
922 parse_error: Some("bad json".to_string()),
923 };
924 let json = serde_json::to_value(&func).unwrap();
925 assert_eq!(
926 json.get("parse_error").and_then(|v| v.as_str()),
927 Some("bad json")
928 );
929 let restored: ToolCallFunction = serde_json::from_value(json).unwrap();
930 assert_eq!(restored.parse_error.as_deref(), Some("bad json"));
931 }
932
933 #[test]
934 fn test_message_to_from_agent_value() {
935 let msg = Message::user("What is the weather today?".to_string());
936
937 let value: AgentValue = msg.into();
938 assert!(value.is_message());
939 let msg_ref = value.as_message().unwrap();
940 assert_eq!(msg_ref.role, "user");
941 assert_eq!(msg_ref.text(), "What is the weather today?");
942
943 let msg_converted: Message = value.try_into().unwrap();
944 assert_eq!(msg_converted.role, "user");
945 assert_eq!(msg_converted.text(), "What is the weather today?");
946 }
947
948 #[test]
949 fn test_message_with_tool_calls_to_from_agent_value() {
950 let mut msg = Message::assistant("".to_string());
951 msg.tool_calls = Some(vector![ToolCall {
952 function: ToolCallFunction {
953 id: Some("call1".to_string()),
954 name: "get_weather".to_string(),
955 parameters: serde_json::json!({"location": "San Francisco"}),
956 parse_error: None,
957 },
958 }]);
959
960 let value: AgentValue = msg.into();
961 assert!(value.is_message());
962 let msg_ref = value.as_message().unwrap();
963 assert_eq!(msg_ref.role, "assistant");
964 assert_eq!(msg_ref.text(), "");
965 let tool_calls = msg_ref.tool_calls.as_ref().unwrap();
966 assert_eq!(tool_calls.len(), 1);
967 let first_call = &tool_calls[0];
968 assert_eq!(first_call.function.name, "get_weather");
969 assert_eq!(first_call.function.parameters["location"], "San Francisco");
970
971 let msg_converted: Message = value.try_into().unwrap();
972 dbg!(&msg_converted);
973 assert_eq!(msg_converted.role, "assistant");
974 assert_eq!(msg_converted.text(), "");
975 let tool_calls = msg_converted.tool_calls.unwrap();
976 assert_eq!(tool_calls.len(), 1);
977 assert_eq!(tool_calls[0].function.name, "get_weather");
978 assert_eq!(
979 tool_calls[0].function.parameters,
980 serde_json::json!({"location": "San Francisco"})
981 );
982 }
983
984 #[test]
985 fn test_tool_message_to_from_agent_value() {
986 let msg = Message::tool("get_time".to_string(), "2025-01-02 03:04:05".to_string());
987
988 let value: AgentValue = msg.clone().into();
989 let msg_ref = value.as_message().unwrap();
990 assert_eq!(msg_ref.role, "tool");
991 assert_eq!(msg_ref.tool_name.as_deref().unwrap(), "get_time");
992 assert_eq!(msg_ref.text(), "2025-01-02 03:04:05");
993
994 let msg_converted: Message = value.try_into().unwrap();
995 assert_eq!(msg_converted.role, "tool");
996 assert_eq!(msg_converted.tool_name.as_deref(), Some("get_time"));
997 assert_eq!(msg_converted.text(), "2025-01-02 03:04:05");
998 }
999
1000 #[test]
1001 fn test_message_from_string_value() {
1002 let value = AgentValue::string("Just a simple message");
1003 let msg: Message = value.try_into().unwrap();
1004 assert_eq!(msg.role, "user");
1005 assert_eq!(msg.text(), "Just a simple message");
1006 }
1007
1008 #[test]
1009 fn test_message_from_object_value() {
1010 let value = AgentValue::object(hashmap! {
1011 "role".into() => AgentValue::string("assistant"),
1012 "content".into() =>
1013 AgentValue::string("Here is some information."),
1014 });
1015 let msg: Message = value.try_into().unwrap();
1016 assert_eq!(msg.role, "assistant");
1017 assert_eq!(msg.text(), "Here is some information.");
1018 }
1019
1020 #[test]
1021 fn test_message_from_object_value_reads_is_error() {
1022 let value = AgentValue::object(hashmap! {
1023 "role".into() => AgentValue::string("tool"),
1024 "content".into() => AgentValue::string("boom"),
1025 "tool_name".into() => AgentValue::string("failing_tool"),
1026 "is_error".into() => AgentValue::boolean(true),
1027 });
1028 let msg: Message = value.try_into().unwrap();
1029 assert_eq!(msg.is_error, Some(true));
1030 }
1031
1032 #[test]
1033 fn test_message_from_invalid_value() {
1034 let value = AgentValue::integer(42);
1035 let result: Result<Message, AgentError> = value.try_into();
1036 assert!(result.is_err());
1037 }
1038
1039 #[test]
1040 fn test_message_invalid_object() {
1041 let value =
1042 AgentValue::object(hashmap! {"some_key".into() => AgentValue::string("some_value")});
1043 let result: Result<Message, AgentError> = value.try_into();
1044 assert!(result.is_err());
1045 }
1046
1047 #[test]
1048 fn test_message_to_agent_value_with_tool_calls() {
1049 let message = Message {
1050 role: "assistant".to_string(),
1051 content: MessageContent::default(),
1052 tokens: None,
1053 streaming: false,
1054 tool_calls: Some(vector![ToolCall {
1055 function: ToolCallFunction {
1056 id: Some("call1".to_string()),
1057 name: "active_applications".to_string(),
1058 parameters: serde_json::json!({}),
1059 parse_error: None,
1060 },
1061 }]),
1062 id: None,
1063 tool_name: None,
1064 is_error: None,
1065 stop_reason: None,
1066 usage: None,
1067 #[cfg(feature = "image")]
1068 image: None,
1069 };
1070
1071 let value: AgentValue = message.into();
1072 let msg_ref = value.as_message().unwrap();
1073
1074 assert_eq!(msg_ref.role, "assistant");
1075 assert_eq!(msg_ref.text(), "");
1076
1077 let tool_calls = msg_ref.tool_calls.as_ref().unwrap();
1078 assert_eq!(tool_calls.len(), 1);
1079
1080 assert_eq!(tool_calls[0].function.name, "active_applications");
1081 assert!(
1082 tool_calls[0]
1083 .function
1084 .parameters
1085 .as_object()
1086 .unwrap()
1087 .is_empty()
1088 );
1089 }
1090
1091 #[test]
1092 fn test_message_is_error_serde_round_trip() {
1093 let mut msg = Message::tool("failing_tool".to_string(), "boom".to_string());
1094 msg.id = Some("call1".to_string());
1095 msg.is_error = Some(true);
1096
1097 let json = serde_json::to_value(&msg).unwrap();
1098 assert_eq!(json["is_error"], serde_json::json!(true));
1099
1100 let restored: Message = serde_json::from_value(json).unwrap();
1101 assert_eq!(restored.is_error, Some(true));
1102 assert_eq!(restored.id.as_deref(), Some("call1"));
1103 assert_eq!(restored.tool_name.as_deref(), Some("failing_tool"));
1104 }
1105
1106 #[test]
1107 fn test_message_without_is_error_deserializes_to_none() {
1108 let json = serde_json::json!({
1109 "role": "tool",
1110 "content": "ok",
1111 "tool_name": "some_tool",
1112 });
1113 let msg: Message = serde_json::from_value(json).unwrap();
1114 assert_eq!(msg.is_error, None);
1115 }
1116
1117 #[test]
1118 fn test_message_is_error_none_serializes_without_key() {
1119 let msg = Message::tool("some_tool".to_string(), "ok".to_string());
1120 assert_eq!(msg.is_error, None);
1121
1122 let json = serde_json::to_value(&msg).unwrap();
1123 assert!(json.as_object().unwrap().get("is_error").is_none());
1124 }
1125
1126 #[test]
1127 fn test_message_stop_reason_serde_round_trip() {
1128 let mut msg = Message::assistant("partial answer".to_string());
1129 msg.stop_reason = Some("length".to_string());
1130
1131 let json = serde_json::to_value(&msg).unwrap();
1132 assert_eq!(json["stop_reason"], serde_json::json!("length"));
1133
1134 let restored: Message = serde_json::from_value(json).unwrap();
1135 assert_eq!(restored.stop_reason.as_deref(), Some("length"));
1136 }
1137
1138 #[test]
1139 fn test_message_without_stop_reason_deserializes_to_none() {
1140 let json = serde_json::json!({
1142 "role": "assistant",
1143 "content": "ok",
1144 });
1145 let msg: Message = serde_json::from_value(json).unwrap();
1146 assert_eq!(msg.stop_reason, None);
1147 }
1148
1149 #[test]
1150 fn test_message_stop_reason_none_serializes_without_key() {
1151 let msg = Message::assistant("ok".to_string());
1152 assert_eq!(msg.stop_reason, None);
1153
1154 let json = serde_json::to_value(&msg).unwrap();
1155 assert!(json.as_object().unwrap().get("stop_reason").is_none());
1156 }
1157
1158 #[test]
1159 fn test_message_from_object_value_reads_stop_reason() {
1160 let value = AgentValue::object(hashmap! {
1161 "role".into() => AgentValue::string("assistant"),
1162 "content".into() => AgentValue::string("truncated"),
1163 "stop_reason".into() => AgentValue::string("length"),
1164 });
1165 let msg: Message = value.try_into().unwrap();
1166 assert_eq!(msg.stop_reason.as_deref(), Some("length"));
1167 }
1168
1169 #[test]
1170 fn test_message_usage_serde_round_trip() {
1171 let mut msg = Message::assistant("ok".to_string());
1172 msg.usage = Some(Usage {
1173 input_tokens: 100,
1174 output_tokens: 20,
1175 cache_read_tokens: 50,
1176 cache_write_tokens: 10,
1177 });
1178
1179 let json = serde_json::to_value(&msg).unwrap();
1180 assert_eq!(
1181 json["usage"],
1182 serde_json::json!({
1183 "input_tokens": 100,
1184 "output_tokens": 20,
1185 "cache_read_tokens": 50,
1186 "cache_write_tokens": 10,
1187 })
1188 );
1189
1190 let restored: Message = serde_json::from_value(json).unwrap();
1191 assert_eq!(restored.usage, msg.usage);
1192 }
1193
1194 #[test]
1195 fn test_message_without_usage_deserializes_to_none() {
1196 let json = serde_json::json!({
1198 "role": "assistant",
1199 "content": "ok",
1200 });
1201 let msg: Message = serde_json::from_value(json).unwrap();
1202 assert_eq!(msg.usage, None);
1203 }
1204
1205 #[test]
1206 fn test_message_usage_none_serializes_without_key() {
1207 let msg = Message::assistant("ok".to_string());
1208 assert_eq!(msg.usage, None);
1209
1210 let json = serde_json::to_value(&msg).unwrap();
1211 assert!(json.as_object().unwrap().get("usage").is_none());
1212 }
1213
1214 #[test]
1215 fn test_message_from_object_value_reads_usage() {
1216 let value = AgentValue::object(hashmap! {
1217 "role".into() => AgentValue::string("assistant"),
1218 "content".into() => AgentValue::string("ok"),
1219 "usage".into() => AgentValue::object(hashmap! {
1220 "input_tokens".into() => AgentValue::integer(7),
1221 "output_tokens".into() => AgentValue::integer(3),
1222 }),
1223 });
1224 let msg: Message = value.try_into().unwrap();
1225 assert_eq!(
1226 msg.usage,
1227 Some(Usage {
1228 input_tokens: 7,
1229 output_tokens: 3,
1230 cache_read_tokens: 0,
1231 cache_write_tokens: 0,
1232 })
1233 );
1234 }
1235
1236 #[test]
1237 fn test_message_partial_usage_object_deserializes_with_defaults() {
1238 let json = serde_json::json!({
1239 "role": "assistant",
1240 "content": "ok",
1241 "usage": { "input_tokens": 42 },
1242 });
1243 let msg: Message = serde_json::from_value(json).unwrap();
1244 assert_eq!(
1245 msg.usage,
1246 Some(Usage {
1247 input_tokens: 42,
1248 output_tokens: 0,
1249 cache_read_tokens: 0,
1250 cache_write_tokens: 0,
1251 })
1252 );
1253 }
1254
1255 #[test]
1256 fn test_message_unparseable_usage_deserializes_to_none() {
1257 let json = serde_json::json!({
1258 "role": "assistant",
1259 "content": "ok",
1260 "usage": "not an object",
1261 });
1262 let msg: Message = serde_json::from_value(json).unwrap();
1263 assert_eq!(msg.usage, None);
1264 }
1265
1266 #[test]
1269 fn test_message_event_text_delta_serde_round_trip() {
1270 let mut partial = Message::assistant("Hel".to_string());
1271 partial.streaming = true;
1272 let event = MessageEvent::TextDelta {
1273 delta: "l".to_string(),
1274 partial,
1275 };
1276
1277 let json = serde_json::to_value(&event).unwrap();
1278 assert_eq!(json["type"], serde_json::json!("text_delta"));
1279 assert_eq!(json["delta"], serde_json::json!("l"));
1280 assert_eq!(json["partial"]["content"], serde_json::json!("Hel"));
1281
1282 let restored: MessageEvent = serde_json::from_value(json).unwrap();
1283 assert_eq!(restored, event);
1284 let MessageEvent::TextDelta { delta, partial } = restored else {
1287 panic!("wrong variant");
1288 };
1289 assert_eq!(delta, "l");
1290 assert!(partial.streaming);
1291 }
1292
1293 #[test]
1294 fn test_message_event_done_serde_round_trip() {
1295 let mut msg = Message::assistant("Hello".to_string());
1296 msg.id = Some("msg1".to_string());
1297 msg.stop_reason = Some("stop".to_string());
1298 msg.usage = Some(Usage {
1299 input_tokens: 10,
1300 output_tokens: 5,
1301 cache_read_tokens: 2,
1302 cache_write_tokens: 1,
1303 });
1304 let event = MessageEvent::Done {
1305 message: msg.clone(),
1306 };
1307
1308 let json = serde_json::to_value(&event).unwrap();
1309 assert_eq!(json["type"], serde_json::json!("done"));
1310 assert_eq!(json["message"]["role"], serde_json::json!("assistant"));
1311 assert_eq!(json["message"]["content"], serde_json::json!("Hello"));
1312
1313 let restored: MessageEvent = serde_json::from_value(json).unwrap();
1314 assert_eq!(restored, event);
1315 let MessageEvent::Done { message } = restored else {
1318 panic!("wrong variant");
1319 };
1320 assert!(!message.streaming);
1321 assert_eq!(message.stop_reason, msg.stop_reason);
1322 assert_eq!(message.usage, msg.usage);
1323 }
1324
1325 #[test]
1326 fn test_message_event_tool_call_end_serde_round_trip() {
1327 let tool_call = ToolCall {
1328 function: ToolCallFunction {
1329 id: Some("call1".to_string()),
1330 name: "get_weather".to_string(),
1331 parameters: serde_json::json!({"location": "Tokyo"}),
1332 parse_error: None,
1333 },
1334 };
1335 let mut partial = Message::assistant("".to_string());
1336 partial.streaming = true;
1337 partial.tool_calls = Some(vector![tool_call.clone()]);
1338 let event = MessageEvent::ToolCallEnd {
1339 index: 0,
1340 tool_call,
1341 partial,
1342 };
1343
1344 let json = serde_json::to_value(&event).unwrap();
1345 assert_eq!(json["type"], serde_json::json!("tool_call_end"));
1346 assert_eq!(json["index"], serde_json::json!(0));
1347 assert_eq!(
1348 json["tool_call"]["function"]["name"],
1349 serde_json::json!("get_weather")
1350 );
1351
1352 let restored: MessageEvent = serde_json::from_value(json).unwrap();
1353 assert_eq!(restored, event);
1354 let MessageEvent::ToolCallEnd {
1357 index,
1358 tool_call,
1359 partial,
1360 } = restored
1361 else {
1362 panic!("wrong variant");
1363 };
1364 assert_eq!(index, 0);
1365 assert_eq!(tool_call.function.name, "get_weather");
1366 assert_eq!(
1367 tool_call.function.parameters,
1368 serde_json::json!({"location": "Tokyo"})
1369 );
1370 assert!(partial.streaming);
1371 let restored_calls = partial.tool_calls.unwrap();
1372 assert_eq!(restored_calls.len(), 1);
1373 assert_eq!(restored_calls[0].function.id, Some("call1".to_string()));
1374 }
1375
1376 #[test]
1377 fn test_message_event_to_agent_value() {
1378 let event = MessageEvent::Done {
1379 message: Message::assistant("Hello".to_string()),
1380 };
1381
1382 let value = AgentValue::try_from(event).unwrap();
1383 assert!(value.is_object());
1384 assert_eq!(value.get_str("type"), Some("done"));
1385 let message = value.get("message").unwrap();
1386 assert_eq!(message.get_str("role"), Some("assistant"));
1387 assert_eq!(message.get_str("content"), Some("Hello"));
1388 }
1389
1390 #[test]
1391 fn test_message_event_error_to_agent_value() {
1392 let event = MessageEvent::Error {
1393 message: Message::assistant("partial".to_string()),
1394 error: "connection reset".to_string(),
1395 };
1396
1397 let value = AgentValue::try_from(event).unwrap();
1398 assert_eq!(value.get_str("type"), Some("error"));
1399 assert_eq!(value.get_str("error"), Some("connection reset"));
1400 }
1401
1402 #[test]
1403 fn test_message_partial_eq() {
1404 let msg1 = Message::user("hello".to_string());
1405 let msg2 = Message::user("hello".to_string());
1406 let msg3 = Message::user("world".to_string());
1407
1408 assert_eq!(msg1, msg2);
1409 assert_ne!(msg1, msg3);
1410
1411 let mut msg4 = Message::user("hello".to_string());
1412 msg4.id = Some("123".to_string());
1413 assert_ne!(msg1, msg4);
1414 }
1415
1416 #[test]
1419 fn test_message_legacy_thinking_field_absorbed_on_deserialize() {
1420 let json = serde_json::json!({
1421 "role": "assistant",
1422 "content": "hi",
1423 "thinking": "t",
1424 });
1425 let msg: Message = serde_json::from_value(json).unwrap();
1426 assert_eq!(
1427 msg.content,
1428 MessageContent::Blocks(vec![
1429 ContentBlock::Thinking {
1430 thinking: "t".to_string(),
1431 signature: None,
1432 redacted: false,
1433 },
1434 ContentBlock::Text {
1435 text: "hi".to_string()
1436 },
1437 ])
1438 );
1439 assert_eq!(msg.text(), "hi");
1440 assert_eq!(msg.thinking().as_deref(), Some("t"));
1441 }
1442
1443 #[test]
1444 fn test_message_pure_text_serializes_as_plain_string() {
1445 let msg = Message::assistant("hello".to_string());
1446 let json = serde_json::to_value(&msg).unwrap();
1447 assert_eq!(json["content"], serde_json::json!("hello"));
1448
1449 let mut msg = Message::assistant(String::new());
1451 msg.content = MessageContent::Blocks(vec![
1452 ContentBlock::Text {
1453 text: "hel".to_string(),
1454 },
1455 ContentBlock::Text {
1456 text: "lo".to_string(),
1457 },
1458 ]);
1459 let json = serde_json::to_value(&msg).unwrap();
1460 assert_eq!(json["content"], serde_json::json!("hello"));
1461 }
1462
1463 #[test]
1464 fn test_message_thinking_blocks_serde_round_trip() {
1465 let mut msg = Message::assistant(String::new());
1466 msg.content = MessageContent::Blocks(vec![
1467 ContentBlock::Thinking {
1468 thinking: "reasoning".to_string(),
1469 signature: Some("sig123".to_string()),
1470 redacted: false,
1471 },
1472 ContentBlock::Thinking {
1473 thinking: "opaque-payload".to_string(),
1474 signature: None,
1475 redacted: true,
1476 },
1477 ContentBlock::Text {
1478 text: "answer".to_string(),
1479 },
1480 ]);
1481
1482 let json = serde_json::to_value(&msg).unwrap();
1483 assert!(json["content"].is_array());
1484 assert!(json.get("thinking").is_none());
1486
1487 let restored: Message = serde_json::from_value(json).unwrap();
1488 assert_eq!(restored.content, msg.content);
1489 }
1490
1491 #[test]
1492 fn test_message_thinking_redacts_and_joins_with_newline() {
1493 let mut msg = Message::assistant(String::new());
1497 msg.content = MessageContent::Blocks(vec![
1498 ContentBlock::Thinking {
1499 thinking: "Let me think...".to_string(),
1500 signature: Some("sig".to_string()),
1501 redacted: false,
1502 },
1503 ContentBlock::Thinking {
1504 thinking: "EqQBCgIYAg-ciphertext".to_string(),
1505 signature: None,
1506 redacted: true,
1507 },
1508 ContentBlock::Text {
1509 text: "answer".to_string(),
1510 },
1511 ]);
1512 assert_eq!(
1513 msg.thinking().as_deref(),
1514 Some("Let me think...\n[redacted]")
1515 );
1516 }
1517
1518 #[test]
1519 fn test_message_mixed_block_order_preserved() {
1520 let blocks = vec![
1521 ContentBlock::Text {
1522 text: "before".to_string(),
1523 },
1524 ContentBlock::Thinking {
1525 thinking: "mid".to_string(),
1526 signature: Some("s".to_string()),
1527 redacted: false,
1528 },
1529 ContentBlock::Text {
1530 text: "after".to_string(),
1531 },
1532 ];
1533 let mut msg = Message::assistant(String::new());
1534 msg.content = MessageContent::Blocks(blocks.clone());
1535
1536 let json = serde_json::to_value(&msg).unwrap();
1537 let restored: Message = serde_json::from_value(json).unwrap();
1538 assert_eq!(restored.content, MessageContent::Blocks(blocks));
1539 assert_eq!(restored.text(), "beforeafter");
1540 assert_eq!(restored.thinking().as_deref(), Some("mid"));
1541 }
1542
1543 #[test]
1546 fn test_estimate_message_tokens_rounds_up() {
1547 let msg = Message::user("hello".to_string());
1549 assert_eq!(estimate_message_tokens(&msg), 2);
1550
1551 let msg = Message::user("abcd".to_string());
1553 assert_eq!(estimate_message_tokens(&msg), 1);
1554 }
1555
1556 #[test]
1557 fn test_estimate_message_tokens_counts_tool_calls() {
1558 let parameters = serde_json::json!({"location": "Tokyo"});
1559 let mut msg = Message::assistant(String::new());
1560 msg.tool_calls = Some(vector![ToolCall {
1561 function: ToolCallFunction {
1562 id: Some("call1".to_string()),
1563 name: "get_weather".to_string(),
1564 parameters: parameters.clone(),
1565 parse_error: None,
1566 },
1567 }]);
1568
1569 let chars = "get_weather".len() + serde_json::to_string(¶meters).unwrap().len();
1570 assert_eq!(estimate_message_tokens(&msg), (chars as u64).div_ceil(4));
1571 }
1572
1573 #[test]
1574 fn test_estimate_message_tokens_counts_thinking_blocks() {
1575 let mut msg = Message::assistant(String::new());
1576 msg.content = MessageContent::Blocks(vec![
1577 ContentBlock::Thinking {
1578 thinking: "abcd".to_string(),
1579 signature: None,
1580 redacted: false,
1581 },
1582 ContentBlock::Thinking {
1584 thinking: "wxyz".to_string(),
1585 signature: None,
1586 redacted: true,
1587 },
1588 ContentBlock::Text {
1589 text: "efgh".to_string(),
1590 },
1591 ]);
1592 assert_eq!(estimate_message_tokens(&msg), 3);
1593 }
1594
1595 #[cfg(feature = "image")]
1596 #[test]
1597 fn test_estimate_message_tokens_image_block_adds_flat_cost() {
1598 let mut msg = Message::user(String::new());
1599 msg.content = MessageContent::Blocks(vec![
1600 ContentBlock::Text {
1601 text: "abcd".to_string(),
1602 },
1603 ContentBlock::Image {
1604 data: "base64-payload-not-counted-as-chars".to_string(),
1605 mime_type: "image/png".to_string(),
1606 },
1607 ]);
1608 assert_eq!(estimate_message_tokens(&msg), 1 + 1200);
1609 }
1610
1611 #[test]
1612 fn test_estimate_context_tokens_anchors_on_latest_usage() {
1613 let mut anchored = Message::assistant("answer".to_string());
1614 anchored.usage = Some(Usage {
1615 input_tokens: 100,
1616 output_tokens: 20,
1617 cache_read_tokens: 50,
1618 cache_write_tokens: 10,
1619 });
1620 let earlier = Message::user("long history covered by the anchor".to_string());
1622 let trailing = Message::user("12345678".to_string());
1624
1625 let messages = vec![earlier, anchored, trailing];
1626 assert_eq!(estimate_context_tokens(&messages), 180 + 2);
1627 }
1628
1629 #[test]
1630 fn test_estimate_context_tokens_sums_all_without_usage() {
1631 let messages = vec![
1632 Message::user("abcd".to_string()), Message::assistant("efghijkl".to_string()), ];
1635 assert_eq!(estimate_context_tokens(&messages), 3);
1636 }
1637
1638 #[test]
1639 fn test_estimate_context_tokens_usage_on_last_message() {
1640 let mut msg = Message::assistant("whatever".to_string());
1641 msg.usage = Some(Usage {
1642 input_tokens: 7,
1643 output_tokens: 3,
1644 cache_read_tokens: 0,
1645 cache_write_tokens: 0,
1646 });
1647 let messages = vec![Message::user("earlier".to_string()), msg];
1648 assert_eq!(estimate_context_tokens(&messages), 10);
1649 }
1650}