1use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use crate::typed_id::{ImageId, MessageId, ModelId};
13
14#[cfg(feature = "openapi")]
15use utoipa::ToSchema;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[cfg_attr(feature = "openapi", derive(ToSchema))]
35pub enum ExecutionPhase {
36 Commentary,
39 FinalAnswer,
41}
42
43impl ExecutionPhase {
44 pub fn from_has_tool_calls(has_tool_calls: bool) -> Self {
46 if has_tool_calls {
47 Self::Commentary
48 } else {
49 Self::FinalAnswer
50 }
51 }
52
53 pub fn from_provider_str(s: &str) -> Option<Self> {
56 match s {
57 "commentary" | "in_progress" => Some(Self::Commentary),
58 "final_answer" | "completed" => Some(Self::FinalAnswer),
59 _ => None,
60 }
61 }
62
63 pub fn as_provider_str(&self) -> &'static str {
65 match self {
66 Self::Commentary => "commentary",
67 Self::FinalAnswer => "final_answer",
68 }
69 }
70}
71
72impl std::fmt::Display for ExecutionPhase {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.write_str(self.as_provider_str())
75 }
76}
77
78impl Serialize for ExecutionPhase {
79 fn serialize<S: serde::Serializer>(
80 &self,
81 serializer: S,
82 ) -> std::result::Result<S::Ok, S::Error> {
83 serializer.serialize_str(self.as_provider_str())
84 }
85}
86
87impl<'de> Deserialize<'de> for ExecutionPhase {
88 fn deserialize<D: serde::Deserializer<'de>>(
89 deserializer: D,
90 ) -> std::result::Result<Self, D::Error> {
91 let s = String::deserialize(deserializer)?;
92 match s.as_str() {
93 "commentary" | "in_progress" => Ok(Self::Commentary),
94 "final_answer" | "completed" => Ok(Self::FinalAnswer),
95 other => Err(serde::de::Error::unknown_variant(
96 other,
97 &["commentary", "final_answer", "in_progress", "completed"],
98 )),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105#[cfg_attr(feature = "openapi", derive(ToSchema))]
106#[serde(rename_all = "snake_case")]
107pub enum MessageRole {
108 System,
110 User,
112 Agent,
114 ToolResult,
116}
117
118impl std::fmt::Display for MessageRole {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 match self {
121 MessageRole::System => write!(f, "system"),
122 MessageRole::User => write!(f, "user"),
123 MessageRole::Agent => write!(f, "agent"),
124 MessageRole::ToolResult => write!(f, "tool_result"),
125 }
126 }
127}
128
129impl From<&str> for MessageRole {
130 fn from(s: &str) -> Self {
131 match s.to_lowercase().as_str() {
132 "system" => MessageRole::System,
133 "user" => MessageRole::User,
134 "agent" | "assistant" => MessageRole::Agent,
136 "tool_result" => MessageRole::ToolResult,
137 _ => MessageRole::User,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
153#[cfg_attr(feature = "openapi", derive(ToSchema))]
154pub struct ExternalActor {
155 pub actor_id: String,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub actor_name: Option<String>,
160 pub source: String,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub metadata: Option<std::collections::HashMap<String, String>>,
165}
166
167impl ExternalActor {
168 pub fn display_label(&self) -> &str {
170 self.actor_name.as_deref().unwrap_or(&self.actor_id)
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
180#[cfg_attr(feature = "openapi", derive(ToSchema))]
181pub struct ReasoningConfig {
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub effort: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
189#[cfg_attr(feature = "openapi", derive(ToSchema))]
190pub struct Controls {
191 #[serde(skip_serializing_if = "Option::is_none")]
194 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
195 pub model_id: Option<ModelId>,
196
197 #[serde(skip_serializing_if = "Option::is_none")]
200 pub locale: Option<String>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub reasoning: Option<ReasoningConfig>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
210 pub speed: Option<String>,
211
212 #[serde(skip_serializing_if = "Option::is_none")]
216 pub verbosity: Option<String>,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
223 pub error_disclosure: Option<String>,
224
225 #[serde(default, skip_serializing_if = "Option::is_none")]
231 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
232 pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
233}
234
235impl Controls {
236 pub fn resolve_hints(
239 session_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
240 message_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
241 ) -> std::collections::HashMap<String, serde_json::Value> {
242 match (session_hints, message_hints) {
243 (None, None) => std::collections::HashMap::new(),
244 (Some(s), None) => s.clone(),
245 (None, Some(m)) => m.clone(),
246 (Some(s), Some(m)) => {
247 let mut merged = s.clone();
248 merged.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
249 merged
250 }
251 }
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257#[cfg_attr(feature = "openapi", derive(ToSchema))]
258pub struct Message {
259 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
261 pub id: MessageId,
262
263 pub role: MessageRole,
265
266 pub content: Vec<ContentPart>,
268
269 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub phase: Option<ExecutionPhase>,
277
278 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub thinking: Option<String>,
283
284 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub thinking_signature: Option<String>,
288
289 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub controls: Option<Controls>,
292
293 #[serde(default, skip_serializing_if = "Option::is_none")]
295 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
296 pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
297
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub external_actor: Option<ExternalActor>,
301
302 pub created_at: DateTime<Utc>,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
312#[cfg_attr(feature = "openapi", derive(ToSchema))]
313#[serde(rename_all = "snake_case")]
314pub enum ContentType {
315 Text,
316 Image,
317 ImageFile,
318 ToolCall,
319 ToolResult,
320}
321
322impl std::fmt::Display for ContentType {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 match self {
325 ContentType::Text => write!(f, "text"),
326 ContentType::Image => write!(f, "image"),
327 ContentType::ImageFile => write!(f, "image_file"),
328 ContentType::ToolCall => write!(f, "tool_call"),
329 ContentType::ToolResult => write!(f, "tool_result"),
330 }
331 }
332}
333
334impl From<&str> for ContentType {
335 fn from(s: &str) -> Self {
336 match s {
337 "image" => ContentType::Image,
338 "image_file" => ContentType::ImageFile,
339 "tool_call" => ContentType::ToolCall,
340 "tool_result" => ContentType::ToolResult,
341 _ => ContentType::Text,
342 }
343 }
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
352#[cfg_attr(feature = "openapi", derive(ToSchema))]
353pub struct TextContentPart {
354 pub text: String,
355}
356
357impl TextContentPart {
358 pub fn new(text: impl Into<String>) -> Self {
359 Self { text: text.into() }
360 }
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
365#[cfg_attr(feature = "openapi", derive(ToSchema))]
366pub struct ImageContentPart {
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub url: Option<String>,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub base64: Option<String>,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub media_type: Option<String>,
373}
374
375impl ImageContentPart {
376 pub fn from_url(url: impl Into<String>) -> Self {
377 Self {
378 url: Some(url.into()),
379 base64: None,
380 media_type: None,
381 }
382 }
383
384 pub fn from_base64(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
385 Self {
386 url: None,
387 base64: Some(base64.into()),
388 media_type: Some(media_type.into()),
389 }
390 }
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
399#[cfg_attr(feature = "openapi", derive(ToSchema))]
400pub struct ImageFileContentPart {
401 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "img_01933b5a00007000800000000000001"))]
403 pub image_id: ImageId,
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub filename: Option<String>,
407}
408
409impl ImageFileContentPart {
410 pub fn new(image_id: ImageId) -> Self {
411 Self {
412 image_id,
413 filename: None,
414 }
415 }
416
417 pub fn with_filename(image_id: ImageId, filename: impl Into<String>) -> Self {
418 Self {
419 image_id,
420 filename: Some(filename.into()),
421 }
422 }
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
427#[cfg_attr(feature = "openapi", derive(ToSchema))]
428pub struct ToolCallContentPart {
429 pub id: String,
430 pub name: String,
431 pub arguments: serde_json::Value,
432}
433
434impl ToolCallContentPart {
435 pub fn new(
436 id: impl Into<String>,
437 name: impl Into<String>,
438 arguments: serde_json::Value,
439 ) -> Self {
440 Self {
441 id: id.into(),
442 name: name.into(),
443 arguments,
444 }
445 }
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
450#[cfg_attr(feature = "openapi", derive(ToSchema))]
451pub struct ToolResultContentPart {
452 pub tool_call_id: String,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 pub result: Option<serde_json::Value>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 pub error: Option<String>,
458}
459
460impl ToolResultContentPart {
461 pub fn new(
462 tool_call_id: impl Into<String>,
463 result: Option<serde_json::Value>,
464 error: Option<String>,
465 ) -> Self {
466 Self {
467 tool_call_id: tool_call_id.into(),
468 result,
469 error,
470 }
471 }
472
473 pub fn success(tool_call_id: impl Into<String>, result: serde_json::Value) -> Self {
474 Self {
475 tool_call_id: tool_call_id.into(),
476 result: Some(result),
477 error: None,
478 }
479 }
480
481 pub fn error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
482 Self {
483 tool_call_id: tool_call_id.into(),
484 result: None,
485 error: Some(error.into()),
486 }
487 }
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
499#[cfg_attr(feature = "openapi", derive(ToSchema))]
500#[serde(tag = "type", rename_all = "snake_case")]
501pub enum ContentPart {
502 Text(TextContentPart),
504 Image(ImageContentPart),
506 ImageFile(ImageFileContentPart),
508 ToolCall(ToolCallContentPart),
510 ToolResult(ToolResultContentPart),
512}
513
514impl ContentPart {
515 pub fn text(text: impl Into<String>) -> Self {
517 ContentPart::Text(TextContentPart::new(text))
518 }
519
520 pub fn image_url(url: impl Into<String>) -> Self {
522 ContentPart::Image(ImageContentPart::from_url(url))
523 }
524
525 pub fn image_file(image_id: ImageId) -> Self {
527 ContentPart::ImageFile(ImageFileContentPart::new(image_id))
528 }
529
530 pub fn tool_call(
532 id: impl Into<String>,
533 name: impl Into<String>,
534 arguments: serde_json::Value,
535 ) -> Self {
536 ContentPart::ToolCall(ToolCallContentPart::new(id, name, arguments))
537 }
538
539 pub fn tool_result(
541 tool_call_id: impl Into<String>,
542 result: Option<serde_json::Value>,
543 error: Option<String>,
544 ) -> Self {
545 ContentPart::ToolResult(ToolResultContentPart::new(tool_call_id, result, error))
546 }
547
548 pub fn as_text(&self) -> Option<&str> {
550 match self {
551 ContentPart::Text(t) => Some(&t.text),
552 _ => None,
553 }
554 }
555
556 pub fn is_image_file(&self) -> bool {
558 matches!(self, ContentPart::ImageFile(_))
559 }
560
561 pub fn content_type(&self) -> ContentType {
563 match self {
564 ContentPart::Text(_) => ContentType::Text,
565 ContentPart::Image(_) => ContentType::Image,
566 ContentPart::ImageFile(_) => ContentType::ImageFile,
567 ContentPart::ToolCall(_) => ContentType::ToolCall,
568 ContentPart::ToolResult(_) => ContentType::ToolResult,
569 }
570 }
571
572 pub fn to_openai_format(&self) -> Option<serde_json::Value> {
577 match self {
578 ContentPart::Text(t) => Some(serde_json::json!({
579 "type": "text",
580 "text": t.text
581 })),
582 ContentPart::Image(img) => {
583 if let Some(url) = &img.url {
584 Some(serde_json::json!({
585 "type": "image_url",
586 "image_url": { "url": url }
587 }))
588 } else if let Some(b64) = &img.base64 {
589 let media_type = img.media_type.as_deref().unwrap_or("image/png");
590 Some(serde_json::json!({
591 "type": "image_url",
592 "image_url": { "url": format!("data:{};base64,{}", media_type, b64) }
593 }))
594 } else {
595 None
596 }
597 }
598 _ => None,
600 }
601 }
602}
603
604#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
609#[cfg_attr(feature = "openapi", derive(ToSchema))]
610#[serde(tag = "type", rename_all = "snake_case")]
611pub enum InputContentPart {
612 Text(TextContentPart),
614 Image(ImageContentPart),
616 ImageFile(ImageFileContentPart),
618}
619
620impl From<InputContentPart> for ContentPart {
621 fn from(input: InputContentPart) -> Self {
622 match input {
623 InputContentPart::Text(t) => ContentPart::Text(t),
624 InputContentPart::Image(i) => ContentPart::Image(i),
625 InputContentPart::ImageFile(f) => ContentPart::ImageFile(f),
626 }
627 }
628}
629
630impl InputContentPart {
631 pub fn text(text: impl Into<String>) -> Self {
633 InputContentPart::Text(TextContentPart::new(text))
634 }
635
636 pub fn image_url(url: impl Into<String>) -> Self {
638 InputContentPart::Image(ImageContentPart::from_url(url))
639 }
640
641 pub fn image_file(image_id: ImageId) -> Self {
643 InputContentPart::ImageFile(ImageFileContentPart::new(image_id))
644 }
645
646 pub fn as_text(&self) -> Option<&str> {
648 match self {
649 InputContentPart::Text(t) => Some(&t.text),
650 _ => None,
651 }
652 }
653
654 pub fn content_type(&self) -> ContentType {
656 match self {
657 InputContentPart::Text(_) => ContentType::Text,
658 InputContentPart::Image(_) => ContentType::Image,
659 InputContentPart::ImageFile(_) => ContentType::ImageFile,
660 }
661 }
662}
663
664impl Message {
665 pub fn user(content: impl Into<String>) -> Self {
667 Self {
668 id: MessageId::new(),
669 role: MessageRole::User,
670 content: vec![ContentPart::text(content)],
671 phase: None,
672 thinking: None,
673 thinking_signature: None,
674 controls: None,
675 metadata: None,
676 external_actor: None,
677 created_at: Utc::now(),
678 }
679 }
680
681 pub fn assistant(content: impl Into<String>) -> Self {
683 Self {
684 id: MessageId::new(),
685 role: MessageRole::Agent,
686 content: vec![ContentPart::text(content)],
687 phase: None,
688 thinking: None,
689 thinking_signature: None,
690 controls: None,
691 metadata: None,
692 external_actor: None,
693 created_at: Utc::now(),
694 }
695 }
696
697 pub fn assistant_with_tools(
703 content: impl Into<String>,
704 tool_calls: Vec<crate::tool_types::ToolCall>,
705 ) -> Self {
706 let text_content = content.into();
707 let mut parts = Vec::new();
708 if !text_content.is_empty() {
710 parts.push(ContentPart::text(text_content));
711 }
712 for tc in tool_calls {
713 parts.push(ContentPart::ToolCall(ToolCallContentPart {
714 id: tc.id,
715 name: tc.name,
716 arguments: tc.arguments,
717 }));
718 }
719 Self {
720 id: MessageId::new(),
721 role: MessageRole::Agent,
722 content: parts,
723 phase: None,
724 thinking: None,
725 thinking_signature: None,
726 controls: None,
727 metadata: None,
728 external_actor: None,
729 created_at: Utc::now(),
730 }
731 }
732
733 pub fn system(content: impl Into<String>) -> Self {
735 Self {
736 id: MessageId::new(),
737 role: MessageRole::System,
738 content: vec![ContentPart::text(content)],
739 phase: None,
740 thinking: None,
741 thinking_signature: None,
742 controls: None,
743 metadata: None,
744 external_actor: None,
745 created_at: Utc::now(),
746 }
747 }
748
749 pub fn tool_result(
751 tool_call_id: impl Into<String>,
752 result: Option<serde_json::Value>,
753 error: Option<String>,
754 ) -> Self {
755 let tool_call_id = tool_call_id.into();
756 Self {
757 id: MessageId::new(),
758 role: MessageRole::ToolResult,
759 content: vec![ContentPart::ToolResult(ToolResultContentPart::new(
760 tool_call_id,
761 result,
762 error,
763 ))],
764 phase: None,
765 thinking: None,
766 thinking_signature: None,
767 controls: None,
768 metadata: None,
769 external_actor: None,
770 created_at: Utc::now(),
771 }
772 }
773
774 pub fn tool_result_with_images(
780 tool_call_id: impl Into<String>,
781 result: Option<serde_json::Value>,
782 images: Vec<crate::tools::ToolResultImage>,
783 ) -> Self {
784 let tool_call_id = tool_call_id.into();
785 let mut content = vec![ContentPart::ToolResult(ToolResultContentPart::new(
786 tool_call_id,
787 result,
788 None,
789 ))];
790 for img in images {
791 content.push(ContentPart::Image(ImageContentPart::from_base64(
792 img.base64,
793 img.media_type,
794 )));
795 }
796 Self {
797 id: MessageId::new(),
798 role: MessageRole::ToolResult,
799 content,
800 phase: None,
801 thinking: None,
802 thinking_signature: None,
803 controls: None,
804 metadata: None,
805 external_actor: None,
806 created_at: Utc::now(),
807 }
808 }
809
810 pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
812 self.phase = Some(phase);
813 self
814 }
815
816 pub fn tool_call_id(&self) -> Option<&str> {
820 self.content.iter().find_map(|p| match p {
821 ContentPart::ToolResult(tr) => Some(tr.tool_call_id.as_str()),
822 _ => None,
823 })
824 }
825
826 pub fn text(&self) -> Option<&str> {
828 self.content.iter().find_map(|p| p.as_text())
829 }
830
831 pub fn tool_calls(&self) -> Vec<&ToolCallContentPart> {
833 self.content
834 .iter()
835 .filter_map(|p| match p {
836 ContentPart::ToolCall(tc) => Some(tc),
837 _ => None,
838 })
839 .collect()
840 }
841
842 pub fn has_tool_calls(&self) -> bool {
844 self.content
845 .iter()
846 .any(|p| matches!(p, ContentPart::ToolCall(_)))
847 }
848
849 pub fn tool_result_content(&self) -> Option<&ToolResultContentPart> {
851 self.content.iter().find_map(|p| match p {
852 ContentPart::ToolResult(tr) => Some(tr),
853 _ => None,
854 })
855 }
856
857 pub fn content_to_llm_string(&self) -> String {
859 self.content
860 .iter()
861 .map(|part| match part {
862 ContentPart::Text(t) => t.text.clone(),
863 ContentPart::Image(_) => "[Image]".to_string(),
864 ContentPart::ImageFile(_) => "[Image File]".to_string(),
865 ContentPart::ToolCall(tc) => {
866 format!(
867 "Tool call: {} with arguments: {}",
868 tc.name,
869 serde_json::to_string(&tc.arguments).unwrap_or_default()
870 )
871 }
872 ContentPart::ToolResult(tr) => {
873 if let Some(err) = &tr.error {
874 format!("Tool error: {}", err)
875 } else if let Some(res) = &tr.result {
876 serde_json::to_string(res).unwrap_or_else(|_| "{}".to_string())
877 } else {
878 "{}".to_string()
879 }
880 }
881 })
882 .collect::<Vec<_>>()
883 .join("\n")
884 }
885
886 pub fn to_openai_format(&self) -> serde_json::Value {
895 let role = match self.role {
896 MessageRole::System => "system",
897 MessageRole::User => "user",
898 MessageRole::Agent => "assistant",
899 MessageRole::ToolResult => "tool",
900 };
901
902 if self.role == MessageRole::ToolResult {
904 let tool_call_id = self.tool_call_id().unwrap_or("");
905 let content = self
906 .content
907 .iter()
908 .find_map(|p| match p {
909 ContentPart::ToolResult(tr) => {
910 if let Some(error) = &tr.error {
911 Some(format!("Error: {}", error))
912 } else if let Some(result) = &tr.result {
913 Some(serde_json::to_string(result).unwrap_or_else(|_| "{}".to_string()))
914 } else {
915 Some("{}".to_string())
916 }
917 }
918 _ => None,
919 })
920 .unwrap_or_else(|| "{}".to_string());
921
922 return serde_json::json!({
923 "role": role,
924 "content": content,
925 "tool_call_id": tool_call_id
926 });
927 }
928
929 if self.role == MessageRole::Agent {
931 let tool_calls: Vec<serde_json::Value> = self
932 .content
933 .iter()
934 .filter_map(|p| match p {
935 ContentPart::ToolCall(tc) => Some(serde_json::json!({
936 "id": tc.id,
937 "type": "function",
938 "function": {
939 "name": tc.name,
940 "arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string())
941 }
942 })),
943 _ => None,
944 })
945 .collect();
946
947 let text_content: String = self
948 .content
949 .iter()
950 .filter_map(|p| match p {
951 ContentPart::Text(t) => Some(t.text.clone()),
952 _ => None,
953 })
954 .collect::<Vec<_>>()
955 .join("\n");
956
957 if tool_calls.is_empty() {
958 return serde_json::json!({
959 "role": role,
960 "content": text_content
961 });
962 } else {
963 let mut result = serde_json::json!({
964 "role": role,
965 "tool_calls": tool_calls
966 });
967 if !text_content.is_empty() {
968 result["content"] = serde_json::json!(text_content);
969 }
970 return result;
971 }
972 }
973
974 let content = self.content_to_openai_format();
976 serde_json::json!({
977 "role": role,
978 "content": content
979 })
980 }
981
982 fn content_to_openai_format(&self) -> serde_json::Value {
984 if self.content.len() == 1
986 && let ContentPart::Text(t) = &self.content[0]
987 {
988 return serde_json::json!(t.text);
989 }
990
991 let parts: Vec<serde_json::Value> = self
993 .content
994 .iter()
995 .filter_map(|part| part.to_openai_format())
996 .collect();
997
998 if parts.is_empty() {
999 return serde_json::json!("");
1000 }
1001
1002 if parts.len() == 1
1004 && let Some(text) = parts[0].get("text")
1005 {
1006 return text.clone();
1007 }
1008
1009 serde_json::json!(parts)
1010 }
1011}
1012
1013pub fn patch_dangling_tool_calls(messages: &[Message]) -> Vec<Message> {
1023 let mut result = Vec::new();
1024
1025 for (i, msg) in messages.iter().enumerate() {
1026 result.push(msg.clone());
1027
1028 if msg.role == MessageRole::Agent && msg.has_tool_calls() {
1030 for tc in msg.tool_calls() {
1031 let has_result = messages[(i + 1)..]
1033 .iter()
1034 .any(|m| m.role == MessageRole::ToolResult && m.tool_call_id() == Some(&tc.id));
1035
1036 if !has_result {
1037 result.push(Message::tool_result(
1038 &tc.id,
1039 None,
1040 Some(
1041 "cancelled - another message came in before it could be completed"
1042 .to_string(),
1043 ),
1044 ));
1045 }
1046 }
1047 }
1048 }
1049
1050 result
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056 use crate::tool_types::ToolCall;
1057
1058 #[test]
1059 fn test_patch_dangling_tool_calls_no_tool_calls() {
1060 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
1061 let patched = patch_dangling_tool_calls(&messages);
1062 assert_eq!(patched.len(), 2);
1063 }
1064
1065 #[test]
1066 fn test_patch_dangling_tool_calls_with_result() {
1067 let tool_call = ToolCall {
1068 id: "call_123".to_string(),
1069 name: "get_weather".to_string(),
1070 arguments: serde_json::json!({"city": "NYC"}),
1071 };
1072
1073 let messages = vec![
1074 Message::user("What's the weather?"),
1075 Message::assistant_with_tools("Let me check", vec![tool_call]),
1076 Message::tool_result("call_123", Some(serde_json::json!({"temp": 72})), None),
1077 ];
1078
1079 let patched = patch_dangling_tool_calls(&messages);
1080 assert_eq!(patched.len(), 3);
1081 }
1082
1083 #[test]
1084 fn test_patch_dangling_tool_calls_missing_result() {
1085 let tool_call = ToolCall {
1086 id: "call_456".to_string(),
1087 name: "search_web".to_string(),
1088 arguments: serde_json::json!({"query": "rust"}),
1089 };
1090
1091 let messages = vec![
1092 Message::user("Search for rust"),
1093 Message::assistant_with_tools("Searching...", vec![tool_call]),
1094 Message::user("Actually, never mind"),
1095 ];
1096
1097 let patched = patch_dangling_tool_calls(&messages);
1098 assert_eq!(patched.len(), 4);
1100 assert_eq!(patched[2].role, MessageRole::ToolResult);
1101 assert_eq!(patched[2].tool_call_id(), Some("call_456"));
1102 }
1103
1104 #[test]
1105 fn test_user_message() {
1106 let msg = Message::user("Hello");
1107 assert_eq!(msg.role, MessageRole::User);
1108 assert_eq!(msg.text(), Some("Hello"));
1109 }
1110
1111 #[test]
1112 fn test_assistant_message() {
1113 let msg = Message::assistant("Hi there!");
1114 assert_eq!(msg.role, MessageRole::Agent);
1115 assert_eq!(msg.text(), Some("Hi there!"));
1116 }
1117
1118 #[test]
1119 fn test_tool_result_message() {
1120 let msg = Message::tool_result(
1121 "call_123",
1122 Some(serde_json::json!({"result": "success"})),
1123 None,
1124 );
1125 assert_eq!(msg.role, MessageRole::ToolResult);
1126 assert_eq!(msg.tool_call_id(), Some("call_123"));
1127 }
1128
1129 #[test]
1130 fn test_assistant_with_tools_and_text() {
1131 let tool_call = ToolCall {
1132 id: "call_123".to_string(),
1133 name: "get_weather".to_string(),
1134 arguments: serde_json::json!({"location": "Tokyo"}),
1135 };
1136 let msg = Message::assistant_with_tools("Let me check the weather.", vec![tool_call]);
1137
1138 assert_eq!(msg.role, MessageRole::Agent);
1139 assert_eq!(msg.text(), Some("Let me check the weather."));
1140 assert_eq!(msg.tool_calls().len(), 1);
1141 assert_eq!(msg.tool_calls()[0].name, "get_weather");
1142 }
1143
1144 #[test]
1145 fn test_assistant_with_tools_empty_text() {
1146 let tool_call = ToolCall {
1149 id: "call_123".to_string(),
1150 name: "search".to_string(),
1151 arguments: serde_json::json!({"query": "rust"}),
1152 };
1153 let msg = Message::assistant_with_tools("", vec![tool_call]);
1154
1155 assert_eq!(msg.role, MessageRole::Agent);
1156 assert_eq!(msg.text(), None);
1158 assert_eq!(msg.tool_calls().len(), 1);
1160 assert_eq!(msg.tool_calls()[0].name, "search");
1161 assert_eq!(msg.content.len(), 1);
1163 assert!(matches!(msg.content[0], ContentPart::ToolCall(_)));
1164 }
1165
1166 #[test]
1167 fn test_assistant_with_tools_whitespace_text() {
1168 let tool_call = ToolCall {
1170 id: "call_456".to_string(),
1171 name: "fetch".to_string(),
1172 arguments: serde_json::json!({}),
1173 };
1174 let msg = Message::assistant_with_tools(" ", vec![tool_call]);
1175
1176 assert_eq!(msg.text(), Some(" "));
1178 assert_eq!(msg.content.len(), 2); }
1180
1181 #[test]
1182 fn test_assistant_with_multiple_tool_calls() {
1183 let tool_calls = vec![
1184 ToolCall {
1185 id: "call_1".to_string(),
1186 name: "search".to_string(),
1187 arguments: serde_json::json!({"q": "a"}),
1188 },
1189 ToolCall {
1190 id: "call_2".to_string(),
1191 name: "fetch".to_string(),
1192 arguments: serde_json::json!({"url": "http://example.com"}),
1193 },
1194 ];
1195 let msg = Message::assistant_with_tools("", tool_calls);
1196
1197 assert_eq!(msg.tool_calls().len(), 2);
1198 assert_eq!(msg.content.len(), 2);
1200 }
1201
1202 #[test]
1207 fn test_to_openai_format_user_message() {
1208 let msg = Message::user("Hello, world!");
1209 let converted = msg.to_openai_format();
1210
1211 assert_eq!(converted["role"], "user");
1212 assert_eq!(converted["content"], "Hello, world!");
1213 }
1214
1215 #[test]
1216 fn test_to_openai_format_system_message() {
1217 let msg = Message::system("You are a helpful assistant.");
1218 let converted = msg.to_openai_format();
1219
1220 assert_eq!(converted["role"], "system");
1221 assert_eq!(converted["content"], "You are a helpful assistant.");
1222 }
1223
1224 #[test]
1225 fn test_to_openai_format_assistant_role_mapping() {
1226 let msg = Message::assistant("Hi there!");
1228 let converted = msg.to_openai_format();
1229
1230 assert_eq!(converted["role"], "assistant");
1231 assert_eq!(converted["content"], "Hi there!");
1232 }
1233
1234 #[test]
1235 fn test_to_openai_format_assistant_with_tool_calls() {
1236 let tool_call = ToolCall {
1237 id: "call_123".to_string(),
1238 name: "get_weather".to_string(),
1239 arguments: serde_json::json!({"location": "Tokyo"}),
1240 };
1241 let msg = Message::assistant_with_tools("Let me check.", vec![tool_call]);
1242 let converted = msg.to_openai_format();
1243
1244 assert_eq!(converted["role"], "assistant");
1245 assert_eq!(converted["content"], "Let me check.");
1246
1247 let tool_calls = converted["tool_calls"].as_array().unwrap();
1248 assert_eq!(tool_calls.len(), 1);
1249 assert_eq!(tool_calls[0]["id"], "call_123");
1250 assert_eq!(tool_calls[0]["type"], "function");
1251 assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
1252 assert_eq!(
1253 tool_calls[0]["function"]["arguments"],
1254 r#"{"location":"Tokyo"}"#
1255 );
1256 }
1257
1258 #[test]
1259 fn test_to_openai_format_assistant_tool_calls_only() {
1260 let tool_call = ToolCall {
1262 id: "call_abc".to_string(),
1263 name: "search".to_string(),
1264 arguments: serde_json::json!({"query": "rust"}),
1265 };
1266 let msg = Message::assistant_with_tools("", vec![tool_call]);
1267 let converted = msg.to_openai_format();
1268
1269 assert_eq!(converted["role"], "assistant");
1270 assert!(converted.get("content").is_none());
1272 assert!(converted["tool_calls"].is_array());
1273 }
1274
1275 #[test]
1276 fn test_to_openai_format_tool_result_role_mapping() {
1277 let msg = Message::tool_result(
1279 "call_123",
1280 Some(serde_json::json!({"temperature": 72})),
1281 None,
1282 );
1283 let converted = msg.to_openai_format();
1284
1285 assert_eq!(converted["role"], "tool");
1286 assert_eq!(converted["tool_call_id"], "call_123");
1287 assert_eq!(converted["content"], r#"{"temperature":72}"#);
1288 }
1289
1290 #[test]
1291 fn test_to_openai_format_tool_result_error() {
1292 let msg = Message::tool_result("call_456", None, Some("API timeout".to_string()));
1293 let converted = msg.to_openai_format();
1294
1295 assert_eq!(converted["role"], "tool");
1296 assert_eq!(converted["tool_call_id"], "call_456");
1297 assert_eq!(converted["content"], "Error: API timeout");
1298 }
1299
1300 #[test]
1301 fn test_to_openai_format_full_conversation() {
1302 let tool_call = ToolCall {
1304 id: "call_abc".to_string(),
1305 name: "search".to_string(),
1306 arguments: serde_json::json!({"query": "rust"}),
1307 };
1308
1309 let messages = [
1310 Message::user("Search for rust"),
1311 Message::assistant_with_tools("", vec![tool_call]),
1312 Message::tool_result(
1313 "call_abc",
1314 Some(serde_json::json!({"results": ["rust-lang.org"]})),
1315 None,
1316 ),
1317 Message::assistant("Here are the search results."),
1318 ];
1319 let converted: Vec<_> = messages.iter().map(|m| m.to_openai_format()).collect();
1320
1321 assert_eq!(converted.len(), 4);
1322 assert_eq!(converted[0]["role"], "user");
1323 assert_eq!(converted[1]["role"], "assistant");
1324 assert!(converted[1]["tool_calls"].is_array());
1325 assert_eq!(converted[2]["role"], "tool");
1326 assert_eq!(converted[2]["tool_call_id"], "call_abc");
1327 assert_eq!(converted[3]["role"], "assistant");
1328 }
1329
1330 #[test]
1335 fn test_content_part_to_openai_format_text() {
1336 let part = ContentPart::text("Hello");
1337 let converted = part.to_openai_format().unwrap();
1338
1339 assert_eq!(converted["type"], "text");
1340 assert_eq!(converted["text"], "Hello");
1341 }
1342
1343 #[test]
1344 fn test_content_part_to_openai_format_image_url() {
1345 let part = ContentPart::image_url("https://example.com/img.png");
1346 let converted = part.to_openai_format().unwrap();
1347
1348 assert_eq!(converted["type"], "image_url");
1349 assert_eq!(converted["image_url"]["url"], "https://example.com/img.png");
1350 }
1351
1352 #[test]
1353 fn test_content_part_to_openai_format_image_base64() {
1354 let part = ContentPart::Image(ImageContentPart::from_base64("abc123", "image/jpeg"));
1355 let converted = part.to_openai_format().unwrap();
1356
1357 assert_eq!(converted["type"], "image_url");
1358 assert_eq!(
1359 converted["image_url"]["url"],
1360 "data:image/jpeg;base64,abc123"
1361 );
1362 }
1363
1364 #[test]
1365 fn test_content_part_to_openai_format_tool_call_returns_none() {
1366 let part = ContentPart::tool_call("call_1", "search", serde_json::json!({}));
1368 assert!(part.to_openai_format().is_none());
1369 }
1370
1371 #[test]
1372 fn test_content_part_to_openai_format_tool_result_returns_none() {
1373 let part = ContentPart::tool_result("call_1", Some(serde_json::json!({})), None);
1375 assert!(part.to_openai_format().is_none());
1376 }
1377
1378 #[test]
1379 fn test_execution_phase_from_has_tool_calls() {
1380 assert_eq!(
1381 ExecutionPhase::from_has_tool_calls(true),
1382 ExecutionPhase::Commentary
1383 );
1384 assert_eq!(
1385 ExecutionPhase::from_has_tool_calls(false),
1386 ExecutionPhase::FinalAnswer
1387 );
1388 }
1389
1390 #[test]
1391 fn test_execution_phase_from_provider_str() {
1392 assert_eq!(
1393 ExecutionPhase::from_provider_str("commentary"),
1394 Some(ExecutionPhase::Commentary)
1395 );
1396 assert_eq!(
1397 ExecutionPhase::from_provider_str("final_answer"),
1398 Some(ExecutionPhase::FinalAnswer)
1399 );
1400 assert_eq!(
1402 ExecutionPhase::from_provider_str("in_progress"),
1403 Some(ExecutionPhase::Commentary)
1404 );
1405 assert_eq!(
1406 ExecutionPhase::from_provider_str("completed"),
1407 Some(ExecutionPhase::FinalAnswer)
1408 );
1409 assert_eq!(ExecutionPhase::from_provider_str("unknown"), None);
1410 }
1411
1412 #[test]
1413 fn test_execution_phase_serde_roundtrip() {
1414 let commentary = ExecutionPhase::Commentary;
1415 let json = serde_json::to_string(&commentary).unwrap();
1416 assert_eq!(json, "\"commentary\"");
1417 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1418 assert_eq!(deserialized, ExecutionPhase::Commentary);
1419
1420 let final_answer = ExecutionPhase::FinalAnswer;
1421 let json = serde_json::to_string(&final_answer).unwrap();
1422 assert_eq!(json, "\"final_answer\"");
1423 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1424 assert_eq!(deserialized, ExecutionPhase::FinalAnswer);
1425 }
1426
1427 #[test]
1428 fn test_execution_phase_deserialize_legacy() {
1429 let legacy_in_progress: ExecutionPhase = serde_json::from_str("\"in_progress\"").unwrap();
1430 assert_eq!(legacy_in_progress, ExecutionPhase::Commentary);
1431
1432 let legacy_completed: ExecutionPhase = serde_json::from_str("\"completed\"").unwrap();
1433 assert_eq!(legacy_completed, ExecutionPhase::FinalAnswer);
1434 }
1435
1436 #[test]
1437 fn test_execution_phase_deserialize_unknown_fails() {
1438 let result = serde_json::from_str::<ExecutionPhase>("\"bogus\"");
1439 assert!(result.is_err());
1440 }
1441
1442 #[test]
1443 fn test_message_with_phase() {
1444 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::Commentary);
1445 assert_eq!(msg.phase, Some(ExecutionPhase::Commentary));
1446 }
1447
1448 #[test]
1449 fn test_message_phase_skipped_when_none() {
1450 let msg = Message::assistant("Hello");
1451 let json = serde_json::to_value(&msg).unwrap();
1452 assert!(json.get("phase").is_none());
1453 }
1454
1455 #[test]
1456 fn test_message_phase_included_when_set() {
1457 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::FinalAnswer);
1458 let json = serde_json::to_value(&msg).unwrap();
1459 assert_eq!(json.get("phase").unwrap(), "final_answer");
1460 }
1461
1462 #[test]
1463 fn test_resolve_hints_both_none() {
1464 let result = Controls::resolve_hints(None, None);
1465 assert!(result.is_empty());
1466 }
1467
1468 #[test]
1469 fn test_resolve_hints_session_only() {
1470 let mut session = std::collections::HashMap::new();
1471 session.insert("key1".into(), serde_json::json!("val1"));
1472 session.insert("key2".into(), serde_json::json!(42));
1473
1474 let result = Controls::resolve_hints(Some(&session), None);
1475 assert_eq!(result.len(), 2);
1476 assert_eq!(result["key1"], serde_json::json!("val1"));
1477 assert_eq!(result["key2"], serde_json::json!(42));
1478 }
1479
1480 #[test]
1481 fn test_resolve_hints_message_only() {
1482 let mut message = std::collections::HashMap::new();
1483 message.insert("key1".into(), serde_json::json!(true));
1484
1485 let result = Controls::resolve_hints(None, Some(&message));
1486 assert_eq!(result.len(), 1);
1487 assert_eq!(result["key1"], serde_json::json!(true));
1488 }
1489
1490 #[test]
1491 fn test_resolve_hints_message_overrides_session() {
1492 let mut session = std::collections::HashMap::new();
1493 session.insert("shared".into(), serde_json::json!("session_val"));
1494 session.insert("session_only".into(), serde_json::json!(1));
1495
1496 let mut message = std::collections::HashMap::new();
1497 message.insert("shared".into(), serde_json::json!("message_val"));
1498 message.insert("message_only".into(), serde_json::json!(2));
1499
1500 let result = Controls::resolve_hints(Some(&session), Some(&message));
1501 assert_eq!(result.len(), 3);
1502 assert_eq!(result["shared"], serde_json::json!("message_val"));
1503 assert_eq!(result["session_only"], serde_json::json!(1));
1504 assert_eq!(result["message_only"], serde_json::json!(2));
1505 }
1506
1507 #[test]
1508 fn test_controls_hints_serde_roundtrip() {
1509 let mut hints = std::collections::HashMap::new();
1510 hints.insert("setup_connection".into(), serde_json::json!(true));
1511 hints.insert("theme".into(), serde_json::json!("dark"));
1512
1513 let controls = Controls {
1514 hints: Some(hints),
1515 ..Default::default()
1516 };
1517
1518 let json = serde_json::to_value(&controls).unwrap();
1519 let deserialized: Controls = serde_json::from_value(json).unwrap();
1520 let h = deserialized.hints.unwrap();
1521 assert_eq!(h["setup_connection"], serde_json::json!(true));
1522 assert_eq!(h["theme"], serde_json::json!("dark"));
1523 }
1524}