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