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")]
217 pub error_disclosure: Option<String>,
218
219 #[serde(default, skip_serializing_if = "Option::is_none")]
225 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
226 pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
227}
228
229impl Controls {
230 pub fn resolve_hints(
233 session_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
234 message_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
235 ) -> std::collections::HashMap<String, serde_json::Value> {
236 match (session_hints, message_hints) {
237 (None, None) => std::collections::HashMap::new(),
238 (Some(s), None) => s.clone(),
239 (None, Some(m)) => m.clone(),
240 (Some(s), Some(m)) => {
241 let mut merged = s.clone();
242 merged.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
243 merged
244 }
245 }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251#[cfg_attr(feature = "openapi", derive(ToSchema))]
252pub struct Message {
253 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
255 pub id: MessageId,
256
257 pub role: MessageRole,
259
260 pub content: Vec<ContentPart>,
262
263 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub phase: Option<ExecutionPhase>,
271
272 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub thinking: Option<String>,
277
278 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub thinking_signature: Option<String>,
282
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub controls: Option<Controls>,
286
287 #[serde(default, skip_serializing_if = "Option::is_none")]
289 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
290 pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
291
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub external_actor: Option<ExternalActor>,
295
296 pub created_at: DateTime<Utc>,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306#[cfg_attr(feature = "openapi", derive(ToSchema))]
307#[serde(rename_all = "snake_case")]
308pub enum ContentType {
309 Text,
310 Image,
311 ImageFile,
312 ToolCall,
313 ToolResult,
314}
315
316impl std::fmt::Display for ContentType {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 match self {
319 ContentType::Text => write!(f, "text"),
320 ContentType::Image => write!(f, "image"),
321 ContentType::ImageFile => write!(f, "image_file"),
322 ContentType::ToolCall => write!(f, "tool_call"),
323 ContentType::ToolResult => write!(f, "tool_result"),
324 }
325 }
326}
327
328impl From<&str> for ContentType {
329 fn from(s: &str) -> Self {
330 match s {
331 "image" => ContentType::Image,
332 "image_file" => ContentType::ImageFile,
333 "tool_call" => ContentType::ToolCall,
334 "tool_result" => ContentType::ToolResult,
335 _ => ContentType::Text,
336 }
337 }
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
346#[cfg_attr(feature = "openapi", derive(ToSchema))]
347pub struct TextContentPart {
348 pub text: String,
349}
350
351impl TextContentPart {
352 pub fn new(text: impl Into<String>) -> Self {
353 Self { text: text.into() }
354 }
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
359#[cfg_attr(feature = "openapi", derive(ToSchema))]
360pub struct ImageContentPart {
361 #[serde(skip_serializing_if = "Option::is_none")]
362 pub url: Option<String>,
363 #[serde(skip_serializing_if = "Option::is_none")]
364 pub base64: Option<String>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub media_type: Option<String>,
367}
368
369impl ImageContentPart {
370 pub fn from_url(url: impl Into<String>) -> Self {
371 Self {
372 url: Some(url.into()),
373 base64: None,
374 media_type: None,
375 }
376 }
377
378 pub fn from_base64(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
379 Self {
380 url: None,
381 base64: Some(base64.into()),
382 media_type: Some(media_type.into()),
383 }
384 }
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
393#[cfg_attr(feature = "openapi", derive(ToSchema))]
394pub struct ImageFileContentPart {
395 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "img_01933b5a00007000800000000000001"))]
397 pub image_id: ImageId,
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub filename: Option<String>,
401}
402
403impl ImageFileContentPart {
404 pub fn new(image_id: ImageId) -> Self {
405 Self {
406 image_id,
407 filename: None,
408 }
409 }
410
411 pub fn with_filename(image_id: ImageId, filename: impl Into<String>) -> Self {
412 Self {
413 image_id,
414 filename: Some(filename.into()),
415 }
416 }
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
421#[cfg_attr(feature = "openapi", derive(ToSchema))]
422pub struct ToolCallContentPart {
423 pub id: String,
424 pub name: String,
425 pub arguments: serde_json::Value,
426}
427
428impl ToolCallContentPart {
429 pub fn new(
430 id: impl Into<String>,
431 name: impl Into<String>,
432 arguments: serde_json::Value,
433 ) -> Self {
434 Self {
435 id: id.into(),
436 name: name.into(),
437 arguments,
438 }
439 }
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
444#[cfg_attr(feature = "openapi", derive(ToSchema))]
445pub struct ToolResultContentPart {
446 pub tool_call_id: String,
448 #[serde(skip_serializing_if = "Option::is_none")]
449 pub result: Option<serde_json::Value>,
450 #[serde(skip_serializing_if = "Option::is_none")]
451 pub error: Option<String>,
452}
453
454impl ToolResultContentPart {
455 pub fn new(
456 tool_call_id: impl Into<String>,
457 result: Option<serde_json::Value>,
458 error: Option<String>,
459 ) -> Self {
460 Self {
461 tool_call_id: tool_call_id.into(),
462 result,
463 error,
464 }
465 }
466
467 pub fn success(tool_call_id: impl Into<String>, result: serde_json::Value) -> Self {
468 Self {
469 tool_call_id: tool_call_id.into(),
470 result: Some(result),
471 error: None,
472 }
473 }
474
475 pub fn error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
476 Self {
477 tool_call_id: tool_call_id.into(),
478 result: None,
479 error: Some(error.into()),
480 }
481 }
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
493#[cfg_attr(feature = "openapi", derive(ToSchema))]
494#[serde(tag = "type", rename_all = "snake_case")]
495pub enum ContentPart {
496 Text(TextContentPart),
498 Image(ImageContentPart),
500 ImageFile(ImageFileContentPart),
502 ToolCall(ToolCallContentPart),
504 ToolResult(ToolResultContentPart),
506}
507
508impl ContentPart {
509 pub fn text(text: impl Into<String>) -> Self {
511 ContentPart::Text(TextContentPart::new(text))
512 }
513
514 pub fn image_url(url: impl Into<String>) -> Self {
516 ContentPart::Image(ImageContentPart::from_url(url))
517 }
518
519 pub fn image_file(image_id: ImageId) -> Self {
521 ContentPart::ImageFile(ImageFileContentPart::new(image_id))
522 }
523
524 pub fn tool_call(
526 id: impl Into<String>,
527 name: impl Into<String>,
528 arguments: serde_json::Value,
529 ) -> Self {
530 ContentPart::ToolCall(ToolCallContentPart::new(id, name, arguments))
531 }
532
533 pub fn tool_result(
535 tool_call_id: impl Into<String>,
536 result: Option<serde_json::Value>,
537 error: Option<String>,
538 ) -> Self {
539 ContentPart::ToolResult(ToolResultContentPart::new(tool_call_id, result, error))
540 }
541
542 pub fn as_text(&self) -> Option<&str> {
544 match self {
545 ContentPart::Text(t) => Some(&t.text),
546 _ => None,
547 }
548 }
549
550 pub fn is_image_file(&self) -> bool {
552 matches!(self, ContentPart::ImageFile(_))
553 }
554
555 pub fn content_type(&self) -> ContentType {
557 match self {
558 ContentPart::Text(_) => ContentType::Text,
559 ContentPart::Image(_) => ContentType::Image,
560 ContentPart::ImageFile(_) => ContentType::ImageFile,
561 ContentPart::ToolCall(_) => ContentType::ToolCall,
562 ContentPart::ToolResult(_) => ContentType::ToolResult,
563 }
564 }
565
566 pub fn to_openai_format(&self) -> Option<serde_json::Value> {
571 match self {
572 ContentPart::Text(t) => Some(serde_json::json!({
573 "type": "text",
574 "text": t.text
575 })),
576 ContentPart::Image(img) => {
577 if let Some(url) = &img.url {
578 Some(serde_json::json!({
579 "type": "image_url",
580 "image_url": { "url": url }
581 }))
582 } else if let Some(b64) = &img.base64 {
583 let media_type = img.media_type.as_deref().unwrap_or("image/png");
584 Some(serde_json::json!({
585 "type": "image_url",
586 "image_url": { "url": format!("data:{};base64,{}", media_type, b64) }
587 }))
588 } else {
589 None
590 }
591 }
592 _ => None,
594 }
595 }
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
603#[cfg_attr(feature = "openapi", derive(ToSchema))]
604#[serde(tag = "type", rename_all = "snake_case")]
605pub enum InputContentPart {
606 Text(TextContentPart),
608 Image(ImageContentPart),
610 ImageFile(ImageFileContentPart),
612}
613
614impl From<InputContentPart> for ContentPart {
615 fn from(input: InputContentPart) -> Self {
616 match input {
617 InputContentPart::Text(t) => ContentPart::Text(t),
618 InputContentPart::Image(i) => ContentPart::Image(i),
619 InputContentPart::ImageFile(f) => ContentPart::ImageFile(f),
620 }
621 }
622}
623
624impl InputContentPart {
625 pub fn text(text: impl Into<String>) -> Self {
627 InputContentPart::Text(TextContentPart::new(text))
628 }
629
630 pub fn image_url(url: impl Into<String>) -> Self {
632 InputContentPart::Image(ImageContentPart::from_url(url))
633 }
634
635 pub fn image_file(image_id: ImageId) -> Self {
637 InputContentPart::ImageFile(ImageFileContentPart::new(image_id))
638 }
639
640 pub fn as_text(&self) -> Option<&str> {
642 match self {
643 InputContentPart::Text(t) => Some(&t.text),
644 _ => None,
645 }
646 }
647
648 pub fn content_type(&self) -> ContentType {
650 match self {
651 InputContentPart::Text(_) => ContentType::Text,
652 InputContentPart::Image(_) => ContentType::Image,
653 InputContentPart::ImageFile(_) => ContentType::ImageFile,
654 }
655 }
656}
657
658impl Message {
659 pub fn user(content: impl Into<String>) -> Self {
661 Self {
662 id: MessageId::new(),
663 role: MessageRole::User,
664 content: vec![ContentPart::text(content)],
665 phase: None,
666 thinking: None,
667 thinking_signature: None,
668 controls: None,
669 metadata: None,
670 external_actor: None,
671 created_at: Utc::now(),
672 }
673 }
674
675 pub fn assistant(content: impl Into<String>) -> Self {
677 Self {
678 id: MessageId::new(),
679 role: MessageRole::Agent,
680 content: vec![ContentPart::text(content)],
681 phase: None,
682 thinking: None,
683 thinking_signature: None,
684 controls: None,
685 metadata: None,
686 external_actor: None,
687 created_at: Utc::now(),
688 }
689 }
690
691 pub fn assistant_with_tools(
697 content: impl Into<String>,
698 tool_calls: Vec<crate::tool_types::ToolCall>,
699 ) -> Self {
700 let text_content = content.into();
701 let mut parts = Vec::new();
702 if !text_content.is_empty() {
704 parts.push(ContentPart::text(text_content));
705 }
706 for tc in tool_calls {
707 parts.push(ContentPart::ToolCall(ToolCallContentPart {
708 id: tc.id,
709 name: tc.name,
710 arguments: tc.arguments,
711 }));
712 }
713 Self {
714 id: MessageId::new(),
715 role: MessageRole::Agent,
716 content: parts,
717 phase: None,
718 thinking: None,
719 thinking_signature: None,
720 controls: None,
721 metadata: None,
722 external_actor: None,
723 created_at: Utc::now(),
724 }
725 }
726
727 pub fn system(content: impl Into<String>) -> Self {
729 Self {
730 id: MessageId::new(),
731 role: MessageRole::System,
732 content: vec![ContentPart::text(content)],
733 phase: None,
734 thinking: None,
735 thinking_signature: None,
736 controls: None,
737 metadata: None,
738 external_actor: None,
739 created_at: Utc::now(),
740 }
741 }
742
743 pub fn tool_result(
745 tool_call_id: impl Into<String>,
746 result: Option<serde_json::Value>,
747 error: Option<String>,
748 ) -> Self {
749 let tool_call_id = tool_call_id.into();
750 Self {
751 id: MessageId::new(),
752 role: MessageRole::ToolResult,
753 content: vec![ContentPart::ToolResult(ToolResultContentPart::new(
754 tool_call_id,
755 result,
756 error,
757 ))],
758 phase: None,
759 thinking: None,
760 thinking_signature: None,
761 controls: None,
762 metadata: None,
763 external_actor: None,
764 created_at: Utc::now(),
765 }
766 }
767
768 pub fn tool_result_with_images(
774 tool_call_id: impl Into<String>,
775 result: Option<serde_json::Value>,
776 images: Vec<crate::tools::ToolResultImage>,
777 ) -> Self {
778 let tool_call_id = tool_call_id.into();
779 let mut content = vec![ContentPart::ToolResult(ToolResultContentPart::new(
780 tool_call_id,
781 result,
782 None,
783 ))];
784 for img in images {
785 content.push(ContentPart::Image(ImageContentPart::from_base64(
786 img.base64,
787 img.media_type,
788 )));
789 }
790 Self {
791 id: MessageId::new(),
792 role: MessageRole::ToolResult,
793 content,
794 phase: None,
795 thinking: None,
796 thinking_signature: None,
797 controls: None,
798 metadata: None,
799 external_actor: None,
800 created_at: Utc::now(),
801 }
802 }
803
804 pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
806 self.phase = Some(phase);
807 self
808 }
809
810 pub fn tool_call_id(&self) -> Option<&str> {
814 self.content.iter().find_map(|p| match p {
815 ContentPart::ToolResult(tr) => Some(tr.tool_call_id.as_str()),
816 _ => None,
817 })
818 }
819
820 pub fn text(&self) -> Option<&str> {
822 self.content.iter().find_map(|p| p.as_text())
823 }
824
825 pub fn tool_calls(&self) -> Vec<&ToolCallContentPart> {
827 self.content
828 .iter()
829 .filter_map(|p| match p {
830 ContentPart::ToolCall(tc) => Some(tc),
831 _ => None,
832 })
833 .collect()
834 }
835
836 pub fn has_tool_calls(&self) -> bool {
838 self.content
839 .iter()
840 .any(|p| matches!(p, ContentPart::ToolCall(_)))
841 }
842
843 pub fn tool_result_content(&self) -> Option<&ToolResultContentPart> {
845 self.content.iter().find_map(|p| match p {
846 ContentPart::ToolResult(tr) => Some(tr),
847 _ => None,
848 })
849 }
850
851 pub fn content_to_llm_string(&self) -> String {
853 self.content
854 .iter()
855 .map(|part| match part {
856 ContentPart::Text(t) => t.text.clone(),
857 ContentPart::Image(_) => "[Image]".to_string(),
858 ContentPart::ImageFile(_) => "[Image File]".to_string(),
859 ContentPart::ToolCall(tc) => {
860 format!(
861 "Tool call: {} with arguments: {}",
862 tc.name,
863 serde_json::to_string(&tc.arguments).unwrap_or_default()
864 )
865 }
866 ContentPart::ToolResult(tr) => {
867 if let Some(err) = &tr.error {
868 format!("Tool error: {}", err)
869 } else if let Some(res) = &tr.result {
870 serde_json::to_string(res).unwrap_or_else(|_| "{}".to_string())
871 } else {
872 "{}".to_string()
873 }
874 }
875 })
876 .collect::<Vec<_>>()
877 .join("\n")
878 }
879
880 pub fn to_openai_format(&self) -> serde_json::Value {
889 let role = match self.role {
890 MessageRole::System => "system",
891 MessageRole::User => "user",
892 MessageRole::Agent => "assistant",
893 MessageRole::ToolResult => "tool",
894 };
895
896 if self.role == MessageRole::ToolResult {
898 let tool_call_id = self.tool_call_id().unwrap_or("");
899 let content = self
900 .content
901 .iter()
902 .find_map(|p| match p {
903 ContentPart::ToolResult(tr) => {
904 if let Some(error) = &tr.error {
905 Some(format!("Error: {}", error))
906 } else if let Some(result) = &tr.result {
907 Some(serde_json::to_string(result).unwrap_or_else(|_| "{}".to_string()))
908 } else {
909 Some("{}".to_string())
910 }
911 }
912 _ => None,
913 })
914 .unwrap_or_else(|| "{}".to_string());
915
916 return serde_json::json!({
917 "role": role,
918 "content": content,
919 "tool_call_id": tool_call_id
920 });
921 }
922
923 if self.role == MessageRole::Agent {
925 let tool_calls: Vec<serde_json::Value> = self
926 .content
927 .iter()
928 .filter_map(|p| match p {
929 ContentPart::ToolCall(tc) => Some(serde_json::json!({
930 "id": tc.id,
931 "type": "function",
932 "function": {
933 "name": tc.name,
934 "arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string())
935 }
936 })),
937 _ => None,
938 })
939 .collect();
940
941 let text_content: String = self
942 .content
943 .iter()
944 .filter_map(|p| match p {
945 ContentPart::Text(t) => Some(t.text.clone()),
946 _ => None,
947 })
948 .collect::<Vec<_>>()
949 .join("\n");
950
951 if tool_calls.is_empty() {
952 return serde_json::json!({
953 "role": role,
954 "content": text_content
955 });
956 } else {
957 let mut result = serde_json::json!({
958 "role": role,
959 "tool_calls": tool_calls
960 });
961 if !text_content.is_empty() {
962 result["content"] = serde_json::json!(text_content);
963 }
964 return result;
965 }
966 }
967
968 let content = self.content_to_openai_format();
970 serde_json::json!({
971 "role": role,
972 "content": content
973 })
974 }
975
976 fn content_to_openai_format(&self) -> serde_json::Value {
978 if self.content.len() == 1
980 && let ContentPart::Text(t) = &self.content[0]
981 {
982 return serde_json::json!(t.text);
983 }
984
985 let parts: Vec<serde_json::Value> = self
987 .content
988 .iter()
989 .filter_map(|part| part.to_openai_format())
990 .collect();
991
992 if parts.is_empty() {
993 return serde_json::json!("");
994 }
995
996 if parts.len() == 1
998 && let Some(text) = parts[0].get("text")
999 {
1000 return text.clone();
1001 }
1002
1003 serde_json::json!(parts)
1004 }
1005}
1006
1007pub fn patch_dangling_tool_calls(messages: &[Message]) -> Vec<Message> {
1017 let mut result = Vec::new();
1018
1019 for (i, msg) in messages.iter().enumerate() {
1020 result.push(msg.clone());
1021
1022 if msg.role == MessageRole::Agent && msg.has_tool_calls() {
1024 for tc in msg.tool_calls() {
1025 let has_result = messages[(i + 1)..]
1027 .iter()
1028 .any(|m| m.role == MessageRole::ToolResult && m.tool_call_id() == Some(&tc.id));
1029
1030 if !has_result {
1031 result.push(Message::tool_result(
1032 &tc.id,
1033 None,
1034 Some(
1035 "cancelled - another message came in before it could be completed"
1036 .to_string(),
1037 ),
1038 ));
1039 }
1040 }
1041 }
1042 }
1043
1044 result
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050 use crate::tool_types::ToolCall;
1051
1052 #[test]
1053 fn test_patch_dangling_tool_calls_no_tool_calls() {
1054 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
1055 let patched = patch_dangling_tool_calls(&messages);
1056 assert_eq!(patched.len(), 2);
1057 }
1058
1059 #[test]
1060 fn test_patch_dangling_tool_calls_with_result() {
1061 let tool_call = ToolCall {
1062 id: "call_123".to_string(),
1063 name: "get_weather".to_string(),
1064 arguments: serde_json::json!({"city": "NYC"}),
1065 };
1066
1067 let messages = vec![
1068 Message::user("What's the weather?"),
1069 Message::assistant_with_tools("Let me check", vec![tool_call]),
1070 Message::tool_result("call_123", Some(serde_json::json!({"temp": 72})), None),
1071 ];
1072
1073 let patched = patch_dangling_tool_calls(&messages);
1074 assert_eq!(patched.len(), 3);
1075 }
1076
1077 #[test]
1078 fn test_patch_dangling_tool_calls_missing_result() {
1079 let tool_call = ToolCall {
1080 id: "call_456".to_string(),
1081 name: "search_web".to_string(),
1082 arguments: serde_json::json!({"query": "rust"}),
1083 };
1084
1085 let messages = vec![
1086 Message::user("Search for rust"),
1087 Message::assistant_with_tools("Searching...", vec![tool_call]),
1088 Message::user("Actually, never mind"),
1089 ];
1090
1091 let patched = patch_dangling_tool_calls(&messages);
1092 assert_eq!(patched.len(), 4);
1094 assert_eq!(patched[2].role, MessageRole::ToolResult);
1095 assert_eq!(patched[2].tool_call_id(), Some("call_456"));
1096 }
1097
1098 #[test]
1099 fn test_user_message() {
1100 let msg = Message::user("Hello");
1101 assert_eq!(msg.role, MessageRole::User);
1102 assert_eq!(msg.text(), Some("Hello"));
1103 }
1104
1105 #[test]
1106 fn test_assistant_message() {
1107 let msg = Message::assistant("Hi there!");
1108 assert_eq!(msg.role, MessageRole::Agent);
1109 assert_eq!(msg.text(), Some("Hi there!"));
1110 }
1111
1112 #[test]
1113 fn test_tool_result_message() {
1114 let msg = Message::tool_result(
1115 "call_123",
1116 Some(serde_json::json!({"result": "success"})),
1117 None,
1118 );
1119 assert_eq!(msg.role, MessageRole::ToolResult);
1120 assert_eq!(msg.tool_call_id(), Some("call_123"));
1121 }
1122
1123 #[test]
1124 fn test_assistant_with_tools_and_text() {
1125 let tool_call = ToolCall {
1126 id: "call_123".to_string(),
1127 name: "get_weather".to_string(),
1128 arguments: serde_json::json!({"location": "Tokyo"}),
1129 };
1130 let msg = Message::assistant_with_tools("Let me check the weather.", vec![tool_call]);
1131
1132 assert_eq!(msg.role, MessageRole::Agent);
1133 assert_eq!(msg.text(), Some("Let me check the weather."));
1134 assert_eq!(msg.tool_calls().len(), 1);
1135 assert_eq!(msg.tool_calls()[0].name, "get_weather");
1136 }
1137
1138 #[test]
1139 fn test_assistant_with_tools_empty_text() {
1140 let tool_call = ToolCall {
1143 id: "call_123".to_string(),
1144 name: "search".to_string(),
1145 arguments: serde_json::json!({"query": "rust"}),
1146 };
1147 let msg = Message::assistant_with_tools("", vec![tool_call]);
1148
1149 assert_eq!(msg.role, MessageRole::Agent);
1150 assert_eq!(msg.text(), None);
1152 assert_eq!(msg.tool_calls().len(), 1);
1154 assert_eq!(msg.tool_calls()[0].name, "search");
1155 assert_eq!(msg.content.len(), 1);
1157 assert!(matches!(msg.content[0], ContentPart::ToolCall(_)));
1158 }
1159
1160 #[test]
1161 fn test_assistant_with_tools_whitespace_text() {
1162 let tool_call = ToolCall {
1164 id: "call_456".to_string(),
1165 name: "fetch".to_string(),
1166 arguments: serde_json::json!({}),
1167 };
1168 let msg = Message::assistant_with_tools(" ", vec![tool_call]);
1169
1170 assert_eq!(msg.text(), Some(" "));
1172 assert_eq!(msg.content.len(), 2); }
1174
1175 #[test]
1176 fn test_assistant_with_multiple_tool_calls() {
1177 let tool_calls = vec![
1178 ToolCall {
1179 id: "call_1".to_string(),
1180 name: "search".to_string(),
1181 arguments: serde_json::json!({"q": "a"}),
1182 },
1183 ToolCall {
1184 id: "call_2".to_string(),
1185 name: "fetch".to_string(),
1186 arguments: serde_json::json!({"url": "http://example.com"}),
1187 },
1188 ];
1189 let msg = Message::assistant_with_tools("", tool_calls);
1190
1191 assert_eq!(msg.tool_calls().len(), 2);
1192 assert_eq!(msg.content.len(), 2);
1194 }
1195
1196 #[test]
1201 fn test_to_openai_format_user_message() {
1202 let msg = Message::user("Hello, world!");
1203 let converted = msg.to_openai_format();
1204
1205 assert_eq!(converted["role"], "user");
1206 assert_eq!(converted["content"], "Hello, world!");
1207 }
1208
1209 #[test]
1210 fn test_to_openai_format_system_message() {
1211 let msg = Message::system("You are a helpful assistant.");
1212 let converted = msg.to_openai_format();
1213
1214 assert_eq!(converted["role"], "system");
1215 assert_eq!(converted["content"], "You are a helpful assistant.");
1216 }
1217
1218 #[test]
1219 fn test_to_openai_format_assistant_role_mapping() {
1220 let msg = Message::assistant("Hi there!");
1222 let converted = msg.to_openai_format();
1223
1224 assert_eq!(converted["role"], "assistant");
1225 assert_eq!(converted["content"], "Hi there!");
1226 }
1227
1228 #[test]
1229 fn test_to_openai_format_assistant_with_tool_calls() {
1230 let tool_call = ToolCall {
1231 id: "call_123".to_string(),
1232 name: "get_weather".to_string(),
1233 arguments: serde_json::json!({"location": "Tokyo"}),
1234 };
1235 let msg = Message::assistant_with_tools("Let me check.", vec![tool_call]);
1236 let converted = msg.to_openai_format();
1237
1238 assert_eq!(converted["role"], "assistant");
1239 assert_eq!(converted["content"], "Let me check.");
1240
1241 let tool_calls = converted["tool_calls"].as_array().unwrap();
1242 assert_eq!(tool_calls.len(), 1);
1243 assert_eq!(tool_calls[0]["id"], "call_123");
1244 assert_eq!(tool_calls[0]["type"], "function");
1245 assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
1246 assert_eq!(
1247 tool_calls[0]["function"]["arguments"],
1248 r#"{"location":"Tokyo"}"#
1249 );
1250 }
1251
1252 #[test]
1253 fn test_to_openai_format_assistant_tool_calls_only() {
1254 let tool_call = ToolCall {
1256 id: "call_abc".to_string(),
1257 name: "search".to_string(),
1258 arguments: serde_json::json!({"query": "rust"}),
1259 };
1260 let msg = Message::assistant_with_tools("", vec![tool_call]);
1261 let converted = msg.to_openai_format();
1262
1263 assert_eq!(converted["role"], "assistant");
1264 assert!(converted.get("content").is_none());
1266 assert!(converted["tool_calls"].is_array());
1267 }
1268
1269 #[test]
1270 fn test_to_openai_format_tool_result_role_mapping() {
1271 let msg = Message::tool_result(
1273 "call_123",
1274 Some(serde_json::json!({"temperature": 72})),
1275 None,
1276 );
1277 let converted = msg.to_openai_format();
1278
1279 assert_eq!(converted["role"], "tool");
1280 assert_eq!(converted["tool_call_id"], "call_123");
1281 assert_eq!(converted["content"], r#"{"temperature":72}"#);
1282 }
1283
1284 #[test]
1285 fn test_to_openai_format_tool_result_error() {
1286 let msg = Message::tool_result("call_456", None, Some("API timeout".to_string()));
1287 let converted = msg.to_openai_format();
1288
1289 assert_eq!(converted["role"], "tool");
1290 assert_eq!(converted["tool_call_id"], "call_456");
1291 assert_eq!(converted["content"], "Error: API timeout");
1292 }
1293
1294 #[test]
1295 fn test_to_openai_format_full_conversation() {
1296 let tool_call = ToolCall {
1298 id: "call_abc".to_string(),
1299 name: "search".to_string(),
1300 arguments: serde_json::json!({"query": "rust"}),
1301 };
1302
1303 let messages = [
1304 Message::user("Search for rust"),
1305 Message::assistant_with_tools("", vec![tool_call]),
1306 Message::tool_result(
1307 "call_abc",
1308 Some(serde_json::json!({"results": ["rust-lang.org"]})),
1309 None,
1310 ),
1311 Message::assistant("Here are the search results."),
1312 ];
1313 let converted: Vec<_> = messages.iter().map(|m| m.to_openai_format()).collect();
1314
1315 assert_eq!(converted.len(), 4);
1316 assert_eq!(converted[0]["role"], "user");
1317 assert_eq!(converted[1]["role"], "assistant");
1318 assert!(converted[1]["tool_calls"].is_array());
1319 assert_eq!(converted[2]["role"], "tool");
1320 assert_eq!(converted[2]["tool_call_id"], "call_abc");
1321 assert_eq!(converted[3]["role"], "assistant");
1322 }
1323
1324 #[test]
1329 fn test_content_part_to_openai_format_text() {
1330 let part = ContentPart::text("Hello");
1331 let converted = part.to_openai_format().unwrap();
1332
1333 assert_eq!(converted["type"], "text");
1334 assert_eq!(converted["text"], "Hello");
1335 }
1336
1337 #[test]
1338 fn test_content_part_to_openai_format_image_url() {
1339 let part = ContentPart::image_url("https://example.com/img.png");
1340 let converted = part.to_openai_format().unwrap();
1341
1342 assert_eq!(converted["type"], "image_url");
1343 assert_eq!(converted["image_url"]["url"], "https://example.com/img.png");
1344 }
1345
1346 #[test]
1347 fn test_content_part_to_openai_format_image_base64() {
1348 let part = ContentPart::Image(ImageContentPart::from_base64("abc123", "image/jpeg"));
1349 let converted = part.to_openai_format().unwrap();
1350
1351 assert_eq!(converted["type"], "image_url");
1352 assert_eq!(
1353 converted["image_url"]["url"],
1354 "data:image/jpeg;base64,abc123"
1355 );
1356 }
1357
1358 #[test]
1359 fn test_content_part_to_openai_format_tool_call_returns_none() {
1360 let part = ContentPart::tool_call("call_1", "search", serde_json::json!({}));
1362 assert!(part.to_openai_format().is_none());
1363 }
1364
1365 #[test]
1366 fn test_content_part_to_openai_format_tool_result_returns_none() {
1367 let part = ContentPart::tool_result("call_1", Some(serde_json::json!({})), None);
1369 assert!(part.to_openai_format().is_none());
1370 }
1371
1372 #[test]
1373 fn test_execution_phase_from_has_tool_calls() {
1374 assert_eq!(
1375 ExecutionPhase::from_has_tool_calls(true),
1376 ExecutionPhase::Commentary
1377 );
1378 assert_eq!(
1379 ExecutionPhase::from_has_tool_calls(false),
1380 ExecutionPhase::FinalAnswer
1381 );
1382 }
1383
1384 #[test]
1385 fn test_execution_phase_from_provider_str() {
1386 assert_eq!(
1387 ExecutionPhase::from_provider_str("commentary"),
1388 Some(ExecutionPhase::Commentary)
1389 );
1390 assert_eq!(
1391 ExecutionPhase::from_provider_str("final_answer"),
1392 Some(ExecutionPhase::FinalAnswer)
1393 );
1394 assert_eq!(
1396 ExecutionPhase::from_provider_str("in_progress"),
1397 Some(ExecutionPhase::Commentary)
1398 );
1399 assert_eq!(
1400 ExecutionPhase::from_provider_str("completed"),
1401 Some(ExecutionPhase::FinalAnswer)
1402 );
1403 assert_eq!(ExecutionPhase::from_provider_str("unknown"), None);
1404 }
1405
1406 #[test]
1407 fn test_execution_phase_serde_roundtrip() {
1408 let commentary = ExecutionPhase::Commentary;
1409 let json = serde_json::to_string(&commentary).unwrap();
1410 assert_eq!(json, "\"commentary\"");
1411 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1412 assert_eq!(deserialized, ExecutionPhase::Commentary);
1413
1414 let final_answer = ExecutionPhase::FinalAnswer;
1415 let json = serde_json::to_string(&final_answer).unwrap();
1416 assert_eq!(json, "\"final_answer\"");
1417 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1418 assert_eq!(deserialized, ExecutionPhase::FinalAnswer);
1419 }
1420
1421 #[test]
1422 fn test_execution_phase_deserialize_legacy() {
1423 let legacy_in_progress: ExecutionPhase = serde_json::from_str("\"in_progress\"").unwrap();
1424 assert_eq!(legacy_in_progress, ExecutionPhase::Commentary);
1425
1426 let legacy_completed: ExecutionPhase = serde_json::from_str("\"completed\"").unwrap();
1427 assert_eq!(legacy_completed, ExecutionPhase::FinalAnswer);
1428 }
1429
1430 #[test]
1431 fn test_execution_phase_deserialize_unknown_fails() {
1432 let result = serde_json::from_str::<ExecutionPhase>("\"bogus\"");
1433 assert!(result.is_err());
1434 }
1435
1436 #[test]
1437 fn test_message_with_phase() {
1438 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::Commentary);
1439 assert_eq!(msg.phase, Some(ExecutionPhase::Commentary));
1440 }
1441
1442 #[test]
1443 fn test_message_phase_skipped_when_none() {
1444 let msg = Message::assistant("Hello");
1445 let json = serde_json::to_value(&msg).unwrap();
1446 assert!(json.get("phase").is_none());
1447 }
1448
1449 #[test]
1450 fn test_message_phase_included_when_set() {
1451 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::FinalAnswer);
1452 let json = serde_json::to_value(&msg).unwrap();
1453 assert_eq!(json.get("phase").unwrap(), "final_answer");
1454 }
1455
1456 #[test]
1457 fn test_resolve_hints_both_none() {
1458 let result = Controls::resolve_hints(None, None);
1459 assert!(result.is_empty());
1460 }
1461
1462 #[test]
1463 fn test_resolve_hints_session_only() {
1464 let mut session = std::collections::HashMap::new();
1465 session.insert("key1".into(), serde_json::json!("val1"));
1466 session.insert("key2".into(), serde_json::json!(42));
1467
1468 let result = Controls::resolve_hints(Some(&session), None);
1469 assert_eq!(result.len(), 2);
1470 assert_eq!(result["key1"], serde_json::json!("val1"));
1471 assert_eq!(result["key2"], serde_json::json!(42));
1472 }
1473
1474 #[test]
1475 fn test_resolve_hints_message_only() {
1476 let mut message = std::collections::HashMap::new();
1477 message.insert("key1".into(), serde_json::json!(true));
1478
1479 let result = Controls::resolve_hints(None, Some(&message));
1480 assert_eq!(result.len(), 1);
1481 assert_eq!(result["key1"], serde_json::json!(true));
1482 }
1483
1484 #[test]
1485 fn test_resolve_hints_message_overrides_session() {
1486 let mut session = std::collections::HashMap::new();
1487 session.insert("shared".into(), serde_json::json!("session_val"));
1488 session.insert("session_only".into(), serde_json::json!(1));
1489
1490 let mut message = std::collections::HashMap::new();
1491 message.insert("shared".into(), serde_json::json!("message_val"));
1492 message.insert("message_only".into(), serde_json::json!(2));
1493
1494 let result = Controls::resolve_hints(Some(&session), Some(&message));
1495 assert_eq!(result.len(), 3);
1496 assert_eq!(result["shared"], serde_json::json!("message_val"));
1497 assert_eq!(result["session_only"], serde_json::json!(1));
1498 assert_eq!(result["message_only"], serde_json::json!(2));
1499 }
1500
1501 #[test]
1502 fn test_controls_hints_serde_roundtrip() {
1503 let mut hints = std::collections::HashMap::new();
1504 hints.insert("setup_connection".into(), serde_json::json!(true));
1505 hints.insert("theme".into(), serde_json::json!("dark"));
1506
1507 let controls = Controls {
1508 hints: Some(hints),
1509 ..Default::default()
1510 };
1511
1512 let json = serde_json::to_value(&controls).unwrap();
1513 let deserialized: Controls = serde_json::from_value(json).unwrap();
1514 let h = deserialized.hints.unwrap();
1515 assert_eq!(h["setup_connection"], serde_json::json!(true));
1516 assert_eq!(h["theme"], serde_json::json!("dark"));
1517 }
1518}