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 pub fn refine_streamed_hint(current: Option<Self>, incoming: Self) -> Option<Self> {
80 Some(current.unwrap_or(incoming))
82 }
83}
84
85impl std::fmt::Display for ExecutionPhase {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.write_str(self.as_provider_str())
88 }
89}
90
91impl Serialize for ExecutionPhase {
92 fn serialize<S: serde::Serializer>(
93 &self,
94 serializer: S,
95 ) -> std::result::Result<S::Ok, S::Error> {
96 serializer.serialize_str(self.as_provider_str())
97 }
98}
99
100impl<'de> Deserialize<'de> for ExecutionPhase {
101 fn deserialize<D: serde::Deserializer<'de>>(
102 deserializer: D,
103 ) -> std::result::Result<Self, D::Error> {
104 let s = String::deserialize(deserializer)?;
105 match s.as_str() {
106 "commentary" | "in_progress" => Ok(Self::Commentary),
107 "final_answer" | "completed" => Ok(Self::FinalAnswer),
108 other => Err(serde::de::Error::unknown_variant(
109 other,
110 &["commentary", "final_answer", "in_progress", "completed"],
111 )),
112 }
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[cfg_attr(feature = "openapi", derive(ToSchema))]
119#[serde(rename_all = "snake_case")]
120pub enum MessageRole {
121 System,
123 User,
125 Agent,
127 ToolResult,
129}
130
131impl std::fmt::Display for MessageRole {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 match self {
134 MessageRole::System => write!(f, "system"),
135 MessageRole::User => write!(f, "user"),
136 MessageRole::Agent => write!(f, "agent"),
137 MessageRole::ToolResult => write!(f, "tool_result"),
138 }
139 }
140}
141
142impl From<&str> for MessageRole {
143 fn from(s: &str) -> Self {
144 match s.to_lowercase().as_str() {
145 "system" => MessageRole::System,
146 "user" => MessageRole::User,
147 "agent" | "assistant" => MessageRole::Agent,
149 "tool_result" => MessageRole::ToolResult,
150 _ => MessageRole::User,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
166#[cfg_attr(feature = "openapi", derive(ToSchema))]
167pub struct ExternalActor {
168 pub actor_id: String,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub actor_name: Option<String>,
173 pub source: String,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub metadata: Option<std::collections::HashMap<String, String>>,
178}
179
180impl ExternalActor {
181 pub fn display_label(&self) -> &str {
183 self.actor_name.as_deref().unwrap_or(&self.actor_id)
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
193#[cfg_attr(feature = "openapi", derive(ToSchema))]
194pub struct ReasoningConfig {
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub effort: Option<String>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
202#[cfg_attr(feature = "openapi", derive(ToSchema))]
203pub struct Controls {
204 #[serde(skip_serializing_if = "Option::is_none")]
207 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
208 pub model_id: Option<ModelId>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
213 pub locale: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub reasoning: Option<ReasoningConfig>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
223 pub speed: Option<String>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
229 pub verbosity: Option<String>,
230
231 #[serde(skip_serializing_if = "Option::is_none")]
236 pub error_disclosure: Option<String>,
237
238 #[serde(default, skip_serializing_if = "Option::is_none")]
244 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
245 pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
246}
247
248impl Controls {
249 pub fn resolve_hints(
252 session_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
253 message_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
254 ) -> std::collections::HashMap<String, serde_json::Value> {
255 match (session_hints, message_hints) {
256 (None, None) => std::collections::HashMap::new(),
257 (Some(s), None) => s.clone(),
258 (None, Some(m)) => m.clone(),
259 (Some(s), Some(m)) => {
260 let mut merged = s.clone();
261 merged.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
262 merged
263 }
264 }
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270#[cfg_attr(feature = "openapi", derive(ToSchema))]
271pub struct Message {
272 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
274 pub id: MessageId,
275
276 pub role: MessageRole,
278
279 pub content: Vec<ContentPart>,
281
282 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub phase: Option<ExecutionPhase>,
290
291 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub thinking: Option<String>,
296
297 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub thinking_signature: Option<String>,
301
302 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub controls: Option<Controls>,
305
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
309 pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
310
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub external_actor: Option<ExternalActor>,
314
315 pub created_at: DateTime<Utc>,
317}
318
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
325#[cfg_attr(feature = "openapi", derive(ToSchema))]
326#[serde(rename_all = "snake_case")]
327pub enum ContentType {
328 Text,
329 Image,
330 ImageFile,
331 ToolCall,
332 ToolResult,
333}
334
335impl std::fmt::Display for ContentType {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 match self {
338 ContentType::Text => write!(f, "text"),
339 ContentType::Image => write!(f, "image"),
340 ContentType::ImageFile => write!(f, "image_file"),
341 ContentType::ToolCall => write!(f, "tool_call"),
342 ContentType::ToolResult => write!(f, "tool_result"),
343 }
344 }
345}
346
347impl From<&str> for ContentType {
348 fn from(s: &str) -> Self {
349 match s {
350 "image" => ContentType::Image,
351 "image_file" => ContentType::ImageFile,
352 "tool_call" => ContentType::ToolCall,
353 "tool_result" => ContentType::ToolResult,
354 _ => ContentType::Text,
355 }
356 }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
365#[cfg_attr(feature = "openapi", derive(ToSchema))]
366pub struct TextContentPart {
367 pub text: String,
368}
369
370impl TextContentPart {
371 pub fn new(text: impl Into<String>) -> Self {
372 Self { text: text.into() }
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
378#[cfg_attr(feature = "openapi", derive(ToSchema))]
379pub struct ImageContentPart {
380 #[serde(skip_serializing_if = "Option::is_none")]
381 pub url: Option<String>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub base64: Option<String>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub media_type: Option<String>,
386}
387
388impl ImageContentPart {
389 pub fn from_url(url: impl Into<String>) -> Self {
390 Self {
391 url: Some(url.into()),
392 base64: None,
393 media_type: None,
394 }
395 }
396
397 pub fn from_base64(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
398 Self {
399 url: None,
400 base64: Some(base64.into()),
401 media_type: Some(media_type.into()),
402 }
403 }
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
412#[cfg_attr(feature = "openapi", derive(ToSchema))]
413pub struct ImageFileContentPart {
414 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "img_01933b5a00007000800000000000001"))]
416 pub image_id: ImageId,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub filename: Option<String>,
420}
421
422impl ImageFileContentPart {
423 pub fn new(image_id: ImageId) -> Self {
424 Self {
425 image_id,
426 filename: None,
427 }
428 }
429
430 pub fn with_filename(image_id: ImageId, filename: impl Into<String>) -> Self {
431 Self {
432 image_id,
433 filename: Some(filename.into()),
434 }
435 }
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
440#[cfg_attr(feature = "openapi", derive(ToSchema))]
441pub struct ToolCallContentPart {
442 pub id: String,
443 pub name: String,
444 pub arguments: serde_json::Value,
445}
446
447impl ToolCallContentPart {
448 pub fn new(
449 id: impl Into<String>,
450 name: impl Into<String>,
451 arguments: serde_json::Value,
452 ) -> Self {
453 Self {
454 id: id.into(),
455 name: name.into(),
456 arguments,
457 }
458 }
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
463#[cfg_attr(feature = "openapi", derive(ToSchema))]
464pub struct ToolResultContentPart {
465 pub tool_call_id: String,
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub result: Option<serde_json::Value>,
469 #[serde(skip_serializing_if = "Option::is_none")]
470 pub error: Option<String>,
471}
472
473impl ToolResultContentPart {
474 pub fn new(
475 tool_call_id: impl Into<String>,
476 result: Option<serde_json::Value>,
477 error: Option<String>,
478 ) -> Self {
479 Self {
480 tool_call_id: tool_call_id.into(),
481 result,
482 error,
483 }
484 }
485
486 pub fn success(tool_call_id: impl Into<String>, result: serde_json::Value) -> Self {
487 Self {
488 tool_call_id: tool_call_id.into(),
489 result: Some(result),
490 error: None,
491 }
492 }
493
494 pub fn error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
495 Self {
496 tool_call_id: tool_call_id.into(),
497 result: None,
498 error: Some(error.into()),
499 }
500 }
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
512#[cfg_attr(feature = "openapi", derive(ToSchema))]
513#[serde(tag = "type", rename_all = "snake_case")]
514pub enum ContentPart {
515 Text(TextContentPart),
517 Image(ImageContentPart),
519 ImageFile(ImageFileContentPart),
521 ToolCall(ToolCallContentPart),
523 ToolResult(ToolResultContentPart),
525}
526
527impl ContentPart {
528 pub fn text(text: impl Into<String>) -> Self {
530 ContentPart::Text(TextContentPart::new(text))
531 }
532
533 pub fn image_url(url: impl Into<String>) -> Self {
535 ContentPart::Image(ImageContentPart::from_url(url))
536 }
537
538 pub fn image_file(image_id: ImageId) -> Self {
540 ContentPart::ImageFile(ImageFileContentPart::new(image_id))
541 }
542
543 pub fn tool_call(
545 id: impl Into<String>,
546 name: impl Into<String>,
547 arguments: serde_json::Value,
548 ) -> Self {
549 ContentPart::ToolCall(ToolCallContentPart::new(id, name, arguments))
550 }
551
552 pub fn tool_result(
554 tool_call_id: impl Into<String>,
555 result: Option<serde_json::Value>,
556 error: Option<String>,
557 ) -> Self {
558 ContentPart::ToolResult(ToolResultContentPart::new(tool_call_id, result, error))
559 }
560
561 pub fn as_text(&self) -> Option<&str> {
563 match self {
564 ContentPart::Text(t) => Some(&t.text),
565 _ => None,
566 }
567 }
568
569 pub fn is_image_file(&self) -> bool {
571 matches!(self, ContentPart::ImageFile(_))
572 }
573
574 pub fn content_type(&self) -> ContentType {
576 match self {
577 ContentPart::Text(_) => ContentType::Text,
578 ContentPart::Image(_) => ContentType::Image,
579 ContentPart::ImageFile(_) => ContentType::ImageFile,
580 ContentPart::ToolCall(_) => ContentType::ToolCall,
581 ContentPart::ToolResult(_) => ContentType::ToolResult,
582 }
583 }
584
585 pub fn to_openai_format(&self) -> Option<serde_json::Value> {
590 match self {
591 ContentPart::Text(t) => Some(serde_json::json!({
592 "type": "text",
593 "text": t.text
594 })),
595 ContentPart::Image(img) => {
596 if let Some(url) = &img.url {
597 Some(serde_json::json!({
598 "type": "image_url",
599 "image_url": { "url": url }
600 }))
601 } else if let Some(b64) = &img.base64 {
602 let media_type = img.media_type.as_deref().unwrap_or("image/png");
603 Some(serde_json::json!({
604 "type": "image_url",
605 "image_url": { "url": format!("data:{};base64,{}", media_type, b64) }
606 }))
607 } else {
608 None
609 }
610 }
611 _ => None,
613 }
614 }
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
622#[cfg_attr(feature = "openapi", derive(ToSchema))]
623#[serde(tag = "type", rename_all = "snake_case")]
624pub enum InputContentPart {
625 Text(TextContentPart),
627 Image(ImageContentPart),
629 ImageFile(ImageFileContentPart),
631}
632
633impl From<InputContentPart> for ContentPart {
634 fn from(input: InputContentPart) -> Self {
635 match input {
636 InputContentPart::Text(t) => ContentPart::Text(t),
637 InputContentPart::Image(i) => ContentPart::Image(i),
638 InputContentPart::ImageFile(f) => ContentPart::ImageFile(f),
639 }
640 }
641}
642
643impl InputContentPart {
644 pub fn text(text: impl Into<String>) -> Self {
646 InputContentPart::Text(TextContentPart::new(text))
647 }
648
649 pub fn image_url(url: impl Into<String>) -> Self {
651 InputContentPart::Image(ImageContentPart::from_url(url))
652 }
653
654 pub fn image_file(image_id: ImageId) -> Self {
656 InputContentPart::ImageFile(ImageFileContentPart::new(image_id))
657 }
658
659 pub fn as_text(&self) -> Option<&str> {
661 match self {
662 InputContentPart::Text(t) => Some(&t.text),
663 _ => None,
664 }
665 }
666
667 pub fn content_type(&self) -> ContentType {
669 match self {
670 InputContentPart::Text(_) => ContentType::Text,
671 InputContentPart::Image(_) => ContentType::Image,
672 InputContentPart::ImageFile(_) => ContentType::ImageFile,
673 }
674 }
675}
676
677impl Message {
678 pub fn user(content: impl Into<String>) -> Self {
680 Self {
681 id: MessageId::new(),
682 role: MessageRole::User,
683 content: vec![ContentPart::text(content)],
684 phase: None,
685 thinking: None,
686 thinking_signature: None,
687 controls: None,
688 metadata: None,
689 external_actor: None,
690 created_at: Utc::now(),
691 }
692 }
693
694 pub fn assistant(content: impl Into<String>) -> Self {
696 Self {
697 id: MessageId::new(),
698 role: MessageRole::Agent,
699 content: vec![ContentPart::text(content)],
700 phase: None,
701 thinking: None,
702 thinking_signature: None,
703 controls: None,
704 metadata: None,
705 external_actor: None,
706 created_at: Utc::now(),
707 }
708 }
709
710 pub fn assistant_with_tools(
716 content: impl Into<String>,
717 tool_calls: Vec<crate::tool_types::ToolCall>,
718 ) -> Self {
719 let text_content = content.into();
720 let mut parts = Vec::new();
721 if !text_content.is_empty() {
723 parts.push(ContentPart::text(text_content));
724 }
725 for tc in tool_calls {
726 parts.push(ContentPart::ToolCall(ToolCallContentPart {
727 id: tc.id,
728 name: tc.name,
729 arguments: tc.arguments,
730 }));
731 }
732 Self {
733 id: MessageId::new(),
734 role: MessageRole::Agent,
735 content: parts,
736 phase: None,
737 thinking: None,
738 thinking_signature: None,
739 controls: None,
740 metadata: None,
741 external_actor: None,
742 created_at: Utc::now(),
743 }
744 }
745
746 pub fn system(content: impl Into<String>) -> Self {
748 Self {
749 id: MessageId::new(),
750 role: MessageRole::System,
751 content: vec![ContentPart::text(content)],
752 phase: None,
753 thinking: None,
754 thinking_signature: None,
755 controls: None,
756 metadata: None,
757 external_actor: None,
758 created_at: Utc::now(),
759 }
760 }
761
762 pub fn tool_result(
764 tool_call_id: impl Into<String>,
765 result: Option<serde_json::Value>,
766 error: Option<String>,
767 ) -> Self {
768 let tool_call_id = tool_call_id.into();
769 Self {
770 id: MessageId::new(),
771 role: MessageRole::ToolResult,
772 content: vec![ContentPart::ToolResult(ToolResultContentPart::new(
773 tool_call_id,
774 result,
775 error,
776 ))],
777 phase: None,
778 thinking: None,
779 thinking_signature: None,
780 controls: None,
781 metadata: None,
782 external_actor: None,
783 created_at: Utc::now(),
784 }
785 }
786
787 pub fn tool_result_with_images(
793 tool_call_id: impl Into<String>,
794 result: Option<serde_json::Value>,
795 images: Vec<crate::tools::ToolResultImage>,
796 ) -> Self {
797 let tool_call_id = tool_call_id.into();
798 let mut content = vec![ContentPart::ToolResult(ToolResultContentPart::new(
799 tool_call_id,
800 result,
801 None,
802 ))];
803 for img in images {
804 content.push(ContentPart::Image(ImageContentPart::from_base64(
805 img.base64,
806 img.media_type,
807 )));
808 }
809 Self {
810 id: MessageId::new(),
811 role: MessageRole::ToolResult,
812 content,
813 phase: None,
814 thinking: None,
815 thinking_signature: None,
816 controls: None,
817 metadata: None,
818 external_actor: None,
819 created_at: Utc::now(),
820 }
821 }
822
823 pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
825 self.phase = Some(phase);
826 self
827 }
828
829 pub fn tool_call_id(&self) -> Option<&str> {
833 self.content.iter().find_map(|p| match p {
834 ContentPart::ToolResult(tr) => Some(tr.tool_call_id.as_str()),
835 _ => None,
836 })
837 }
838
839 pub fn text(&self) -> Option<&str> {
841 self.content.iter().find_map(|p| p.as_text())
842 }
843
844 pub fn tool_calls(&self) -> Vec<&ToolCallContentPart> {
846 self.content
847 .iter()
848 .filter_map(|p| match p {
849 ContentPart::ToolCall(tc) => Some(tc),
850 _ => None,
851 })
852 .collect()
853 }
854
855 pub fn has_tool_calls(&self) -> bool {
857 self.content
858 .iter()
859 .any(|p| matches!(p, ContentPart::ToolCall(_)))
860 }
861
862 pub fn tool_result_content(&self) -> Option<&ToolResultContentPart> {
864 self.content.iter().find_map(|p| match p {
865 ContentPart::ToolResult(tr) => Some(tr),
866 _ => None,
867 })
868 }
869
870 pub fn content_to_llm_string(&self) -> String {
872 self.content
873 .iter()
874 .map(|part| match part {
875 ContentPart::Text(t) => t.text.clone(),
876 ContentPart::Image(_) => "[Image]".to_string(),
877 ContentPart::ImageFile(_) => "[Image File]".to_string(),
878 ContentPart::ToolCall(tc) => {
879 format!(
880 "Tool call: {} with arguments: {}",
881 tc.name,
882 serde_json::to_string(&tc.arguments).unwrap_or_default()
883 )
884 }
885 ContentPart::ToolResult(tr) => {
886 if let Some(err) = &tr.error {
887 format!("Tool error: {}", err)
888 } else if let Some(res) = &tr.result {
889 serde_json::to_string(res).unwrap_or_else(|_| "{}".to_string())
890 } else {
891 "{}".to_string()
892 }
893 }
894 })
895 .collect::<Vec<_>>()
896 .join("\n")
897 }
898
899 pub fn to_openai_format(&self) -> serde_json::Value {
908 let role = match self.role {
909 MessageRole::System => "system",
910 MessageRole::User => "user",
911 MessageRole::Agent => "assistant",
912 MessageRole::ToolResult => "tool",
913 };
914
915 if self.role == MessageRole::ToolResult {
917 let tool_call_id = self.tool_call_id().unwrap_or("");
918 let content = self
919 .content
920 .iter()
921 .find_map(|p| match p {
922 ContentPart::ToolResult(tr) => {
923 if let Some(error) = &tr.error {
924 Some(format!("Error: {}", error))
925 } else if let Some(result) = &tr.result {
926 Some(serde_json::to_string(result).unwrap_or_else(|_| "{}".to_string()))
927 } else {
928 Some("{}".to_string())
929 }
930 }
931 _ => None,
932 })
933 .unwrap_or_else(|| "{}".to_string());
934
935 return serde_json::json!({
936 "role": role,
937 "content": content,
938 "tool_call_id": tool_call_id
939 });
940 }
941
942 if self.role == MessageRole::Agent {
944 let tool_calls: Vec<serde_json::Value> = self
945 .content
946 .iter()
947 .filter_map(|p| match p {
948 ContentPart::ToolCall(tc) => Some(serde_json::json!({
949 "id": tc.id,
950 "type": "function",
951 "function": {
952 "name": tc.name,
953 "arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string())
954 }
955 })),
956 _ => None,
957 })
958 .collect();
959
960 let text_content: String = self
961 .content
962 .iter()
963 .filter_map(|p| match p {
964 ContentPart::Text(t) => Some(t.text.clone()),
965 _ => None,
966 })
967 .collect::<Vec<_>>()
968 .join("\n");
969
970 if tool_calls.is_empty() {
971 return serde_json::json!({
972 "role": role,
973 "content": text_content
974 });
975 } else {
976 let mut result = serde_json::json!({
977 "role": role,
978 "tool_calls": tool_calls
979 });
980 if !text_content.is_empty() {
981 result["content"] = serde_json::json!(text_content);
982 }
983 return result;
984 }
985 }
986
987 let content = self.content_to_openai_format();
989 serde_json::json!({
990 "role": role,
991 "content": content
992 })
993 }
994
995 fn content_to_openai_format(&self) -> serde_json::Value {
997 if self.content.len() == 1
999 && let ContentPart::Text(t) = &self.content[0]
1000 {
1001 return serde_json::json!(t.text);
1002 }
1003
1004 let parts: Vec<serde_json::Value> = self
1006 .content
1007 .iter()
1008 .filter_map(|part| part.to_openai_format())
1009 .collect();
1010
1011 if parts.is_empty() {
1012 return serde_json::json!("");
1013 }
1014
1015 if parts.len() == 1
1017 && let Some(text) = parts[0].get("text")
1018 {
1019 return text.clone();
1020 }
1021
1022 serde_json::json!(parts)
1023 }
1024}
1025
1026pub fn patch_dangling_tool_calls(messages: &[Message]) -> Vec<Message> {
1036 let mut result = Vec::new();
1037
1038 for (i, msg) in messages.iter().enumerate() {
1039 result.push(msg.clone());
1040
1041 if msg.role == MessageRole::Agent && msg.has_tool_calls() {
1043 for tc in msg.tool_calls() {
1044 let has_result = messages[(i + 1)..]
1046 .iter()
1047 .any(|m| m.role == MessageRole::ToolResult && m.tool_call_id() == Some(&tc.id));
1048
1049 if !has_result {
1050 result.push(Message::tool_result(
1051 &tc.id,
1052 None,
1053 Some(
1054 "cancelled - another message came in before it could be completed"
1055 .to_string(),
1056 ),
1057 ));
1058 }
1059 }
1060 }
1061 }
1062
1063 result
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use crate::tool_types::ToolCall;
1070
1071 #[test]
1072 fn test_patch_dangling_tool_calls_no_tool_calls() {
1073 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
1074 let patched = patch_dangling_tool_calls(&messages);
1075 assert_eq!(patched.len(), 2);
1076 }
1077
1078 #[test]
1079 fn test_patch_dangling_tool_calls_with_result() {
1080 let tool_call = ToolCall {
1081 id: "call_123".to_string(),
1082 name: "get_weather".to_string(),
1083 arguments: serde_json::json!({"city": "NYC"}),
1084 };
1085
1086 let messages = vec![
1087 Message::user("What's the weather?"),
1088 Message::assistant_with_tools("Let me check", vec![tool_call]),
1089 Message::tool_result("call_123", Some(serde_json::json!({"temp": 72})), None),
1090 ];
1091
1092 let patched = patch_dangling_tool_calls(&messages);
1093 assert_eq!(patched.len(), 3);
1094 }
1095
1096 #[test]
1097 fn test_patch_dangling_tool_calls_missing_result() {
1098 let tool_call = ToolCall {
1099 id: "call_456".to_string(),
1100 name: "search_web".to_string(),
1101 arguments: serde_json::json!({"query": "rust"}),
1102 };
1103
1104 let messages = vec![
1105 Message::user("Search for rust"),
1106 Message::assistant_with_tools("Searching...", vec![tool_call]),
1107 Message::user("Actually, never mind"),
1108 ];
1109
1110 let patched = patch_dangling_tool_calls(&messages);
1111 assert_eq!(patched.len(), 4);
1113 assert_eq!(patched[2].role, MessageRole::ToolResult);
1114 assert_eq!(patched[2].tool_call_id(), Some("call_456"));
1115 }
1116
1117 #[test]
1118 fn test_user_message() {
1119 let msg = Message::user("Hello");
1120 assert_eq!(msg.role, MessageRole::User);
1121 assert_eq!(msg.text(), Some("Hello"));
1122 }
1123
1124 #[test]
1125 fn test_assistant_message() {
1126 let msg = Message::assistant("Hi there!");
1127 assert_eq!(msg.role, MessageRole::Agent);
1128 assert_eq!(msg.text(), Some("Hi there!"));
1129 }
1130
1131 #[test]
1132 fn test_tool_result_message() {
1133 let msg = Message::tool_result(
1134 "call_123",
1135 Some(serde_json::json!({"result": "success"})),
1136 None,
1137 );
1138 assert_eq!(msg.role, MessageRole::ToolResult);
1139 assert_eq!(msg.tool_call_id(), Some("call_123"));
1140 }
1141
1142 #[test]
1143 fn test_assistant_with_tools_and_text() {
1144 let tool_call = ToolCall {
1145 id: "call_123".to_string(),
1146 name: "get_weather".to_string(),
1147 arguments: serde_json::json!({"location": "Tokyo"}),
1148 };
1149 let msg = Message::assistant_with_tools("Let me check the weather.", vec![tool_call]);
1150
1151 assert_eq!(msg.role, MessageRole::Agent);
1152 assert_eq!(msg.text(), Some("Let me check the weather."));
1153 assert_eq!(msg.tool_calls().len(), 1);
1154 assert_eq!(msg.tool_calls()[0].name, "get_weather");
1155 }
1156
1157 #[test]
1158 fn test_assistant_with_tools_empty_text() {
1159 let tool_call = ToolCall {
1162 id: "call_123".to_string(),
1163 name: "search".to_string(),
1164 arguments: serde_json::json!({"query": "rust"}),
1165 };
1166 let msg = Message::assistant_with_tools("", vec![tool_call]);
1167
1168 assert_eq!(msg.role, MessageRole::Agent);
1169 assert_eq!(msg.text(), None);
1171 assert_eq!(msg.tool_calls().len(), 1);
1173 assert_eq!(msg.tool_calls()[0].name, "search");
1174 assert_eq!(msg.content.len(), 1);
1176 assert!(matches!(msg.content[0], ContentPart::ToolCall(_)));
1177 }
1178
1179 #[test]
1180 fn test_assistant_with_tools_whitespace_text() {
1181 let tool_call = ToolCall {
1183 id: "call_456".to_string(),
1184 name: "fetch".to_string(),
1185 arguments: serde_json::json!({}),
1186 };
1187 let msg = Message::assistant_with_tools(" ", vec![tool_call]);
1188
1189 assert_eq!(msg.text(), Some(" "));
1191 assert_eq!(msg.content.len(), 2); }
1193
1194 #[test]
1195 fn test_assistant_with_multiple_tool_calls() {
1196 let tool_calls = vec![
1197 ToolCall {
1198 id: "call_1".to_string(),
1199 name: "search".to_string(),
1200 arguments: serde_json::json!({"q": "a"}),
1201 },
1202 ToolCall {
1203 id: "call_2".to_string(),
1204 name: "fetch".to_string(),
1205 arguments: serde_json::json!({"url": "http://example.com"}),
1206 },
1207 ];
1208 let msg = Message::assistant_with_tools("", tool_calls);
1209
1210 assert_eq!(msg.tool_calls().len(), 2);
1211 assert_eq!(msg.content.len(), 2);
1213 }
1214
1215 #[test]
1220 fn test_to_openai_format_user_message() {
1221 let msg = Message::user("Hello, world!");
1222 let converted = msg.to_openai_format();
1223
1224 assert_eq!(converted["role"], "user");
1225 assert_eq!(converted["content"], "Hello, world!");
1226 }
1227
1228 #[test]
1229 fn test_to_openai_format_system_message() {
1230 let msg = Message::system("You are a helpful assistant.");
1231 let converted = msg.to_openai_format();
1232
1233 assert_eq!(converted["role"], "system");
1234 assert_eq!(converted["content"], "You are a helpful assistant.");
1235 }
1236
1237 #[test]
1238 fn test_to_openai_format_assistant_role_mapping() {
1239 let msg = Message::assistant("Hi there!");
1241 let converted = msg.to_openai_format();
1242
1243 assert_eq!(converted["role"], "assistant");
1244 assert_eq!(converted["content"], "Hi there!");
1245 }
1246
1247 #[test]
1248 fn test_to_openai_format_assistant_with_tool_calls() {
1249 let tool_call = ToolCall {
1250 id: "call_123".to_string(),
1251 name: "get_weather".to_string(),
1252 arguments: serde_json::json!({"location": "Tokyo"}),
1253 };
1254 let msg = Message::assistant_with_tools("Let me check.", vec![tool_call]);
1255 let converted = msg.to_openai_format();
1256
1257 assert_eq!(converted["role"], "assistant");
1258 assert_eq!(converted["content"], "Let me check.");
1259
1260 let tool_calls = converted["tool_calls"].as_array().unwrap();
1261 assert_eq!(tool_calls.len(), 1);
1262 assert_eq!(tool_calls[0]["id"], "call_123");
1263 assert_eq!(tool_calls[0]["type"], "function");
1264 assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
1265 assert_eq!(
1266 tool_calls[0]["function"]["arguments"],
1267 r#"{"location":"Tokyo"}"#
1268 );
1269 }
1270
1271 #[test]
1272 fn test_to_openai_format_assistant_tool_calls_only() {
1273 let tool_call = ToolCall {
1275 id: "call_abc".to_string(),
1276 name: "search".to_string(),
1277 arguments: serde_json::json!({"query": "rust"}),
1278 };
1279 let msg = Message::assistant_with_tools("", vec![tool_call]);
1280 let converted = msg.to_openai_format();
1281
1282 assert_eq!(converted["role"], "assistant");
1283 assert!(converted.get("content").is_none());
1285 assert!(converted["tool_calls"].is_array());
1286 }
1287
1288 #[test]
1289 fn test_to_openai_format_tool_result_role_mapping() {
1290 let msg = Message::tool_result(
1292 "call_123",
1293 Some(serde_json::json!({"temperature": 72})),
1294 None,
1295 );
1296 let converted = msg.to_openai_format();
1297
1298 assert_eq!(converted["role"], "tool");
1299 assert_eq!(converted["tool_call_id"], "call_123");
1300 assert_eq!(converted["content"], r#"{"temperature":72}"#);
1301 }
1302
1303 #[test]
1304 fn test_to_openai_format_tool_result_error() {
1305 let msg = Message::tool_result("call_456", None, Some("API timeout".to_string()));
1306 let converted = msg.to_openai_format();
1307
1308 assert_eq!(converted["role"], "tool");
1309 assert_eq!(converted["tool_call_id"], "call_456");
1310 assert_eq!(converted["content"], "Error: API timeout");
1311 }
1312
1313 #[test]
1314 fn test_to_openai_format_full_conversation() {
1315 let tool_call = ToolCall {
1317 id: "call_abc".to_string(),
1318 name: "search".to_string(),
1319 arguments: serde_json::json!({"query": "rust"}),
1320 };
1321
1322 let messages = [
1323 Message::user("Search for rust"),
1324 Message::assistant_with_tools("", vec![tool_call]),
1325 Message::tool_result(
1326 "call_abc",
1327 Some(serde_json::json!({"results": ["rust-lang.org"]})),
1328 None,
1329 ),
1330 Message::assistant("Here are the search results."),
1331 ];
1332 let converted: Vec<_> = messages.iter().map(|m| m.to_openai_format()).collect();
1333
1334 assert_eq!(converted.len(), 4);
1335 assert_eq!(converted[0]["role"], "user");
1336 assert_eq!(converted[1]["role"], "assistant");
1337 assert!(converted[1]["tool_calls"].is_array());
1338 assert_eq!(converted[2]["role"], "tool");
1339 assert_eq!(converted[2]["tool_call_id"], "call_abc");
1340 assert_eq!(converted[3]["role"], "assistant");
1341 }
1342
1343 #[test]
1348 fn test_content_part_to_openai_format_text() {
1349 let part = ContentPart::text("Hello");
1350 let converted = part.to_openai_format().unwrap();
1351
1352 assert_eq!(converted["type"], "text");
1353 assert_eq!(converted["text"], "Hello");
1354 }
1355
1356 #[test]
1357 fn test_content_part_to_openai_format_image_url() {
1358 let part = ContentPart::image_url("https://example.com/img.png");
1359 let converted = part.to_openai_format().unwrap();
1360
1361 assert_eq!(converted["type"], "image_url");
1362 assert_eq!(converted["image_url"]["url"], "https://example.com/img.png");
1363 }
1364
1365 #[test]
1366 fn test_content_part_to_openai_format_image_base64() {
1367 let part = ContentPart::Image(ImageContentPart::from_base64("abc123", "image/jpeg"));
1368 let converted = part.to_openai_format().unwrap();
1369
1370 assert_eq!(converted["type"], "image_url");
1371 assert_eq!(
1372 converted["image_url"]["url"],
1373 "data:image/jpeg;base64,abc123"
1374 );
1375 }
1376
1377 #[test]
1378 fn test_content_part_to_openai_format_tool_call_returns_none() {
1379 let part = ContentPart::tool_call("call_1", "search", serde_json::json!({}));
1381 assert!(part.to_openai_format().is_none());
1382 }
1383
1384 #[test]
1385 fn test_content_part_to_openai_format_tool_result_returns_none() {
1386 let part = ContentPart::tool_result("call_1", Some(serde_json::json!({})), None);
1388 assert!(part.to_openai_format().is_none());
1389 }
1390
1391 #[test]
1392 fn test_execution_phase_from_has_tool_calls() {
1393 assert_eq!(
1394 ExecutionPhase::from_has_tool_calls(true),
1395 ExecutionPhase::Commentary
1396 );
1397 assert_eq!(
1398 ExecutionPhase::from_has_tool_calls(false),
1399 ExecutionPhase::FinalAnswer
1400 );
1401 }
1402
1403 #[test]
1404 fn test_execution_phase_refine_streamed_hint_monotonic() {
1405 use ExecutionPhase::{Commentary, FinalAnswer};
1406 assert_eq!(
1408 ExecutionPhase::refine_streamed_hint(None, Commentary),
1409 Some(Commentary)
1410 );
1411 assert_eq!(
1412 ExecutionPhase::refine_streamed_hint(None, FinalAnswer),
1413 Some(FinalAnswer)
1414 );
1415 assert_eq!(
1417 ExecutionPhase::refine_streamed_hint(Some(Commentary), FinalAnswer),
1418 Some(Commentary)
1419 );
1420 assert_eq!(
1421 ExecutionPhase::refine_streamed_hint(Some(FinalAnswer), Commentary),
1422 Some(FinalAnswer)
1423 );
1424 assert_eq!(
1427 ExecutionPhase::refine_streamed_hint(Some(Commentary), Commentary),
1428 Some(Commentary)
1429 );
1430 }
1431
1432 #[test]
1433 fn test_execution_phase_from_provider_str() {
1434 assert_eq!(
1435 ExecutionPhase::from_provider_str("commentary"),
1436 Some(ExecutionPhase::Commentary)
1437 );
1438 assert_eq!(
1439 ExecutionPhase::from_provider_str("final_answer"),
1440 Some(ExecutionPhase::FinalAnswer)
1441 );
1442 assert_eq!(
1444 ExecutionPhase::from_provider_str("in_progress"),
1445 Some(ExecutionPhase::Commentary)
1446 );
1447 assert_eq!(
1448 ExecutionPhase::from_provider_str("completed"),
1449 Some(ExecutionPhase::FinalAnswer)
1450 );
1451 assert_eq!(ExecutionPhase::from_provider_str("unknown"), None);
1452 }
1453
1454 #[test]
1455 fn test_execution_phase_serde_roundtrip() {
1456 let commentary = ExecutionPhase::Commentary;
1457 let json = serde_json::to_string(&commentary).unwrap();
1458 assert_eq!(json, "\"commentary\"");
1459 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1460 assert_eq!(deserialized, ExecutionPhase::Commentary);
1461
1462 let final_answer = ExecutionPhase::FinalAnswer;
1463 let json = serde_json::to_string(&final_answer).unwrap();
1464 assert_eq!(json, "\"final_answer\"");
1465 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1466 assert_eq!(deserialized, ExecutionPhase::FinalAnswer);
1467 }
1468
1469 #[test]
1470 fn test_execution_phase_deserialize_legacy() {
1471 let legacy_in_progress: ExecutionPhase = serde_json::from_str("\"in_progress\"").unwrap();
1472 assert_eq!(legacy_in_progress, ExecutionPhase::Commentary);
1473
1474 let legacy_completed: ExecutionPhase = serde_json::from_str("\"completed\"").unwrap();
1475 assert_eq!(legacy_completed, ExecutionPhase::FinalAnswer);
1476 }
1477
1478 #[test]
1479 fn test_execution_phase_deserialize_unknown_fails() {
1480 let result = serde_json::from_str::<ExecutionPhase>("\"bogus\"");
1481 assert!(result.is_err());
1482 }
1483
1484 #[test]
1485 fn test_message_with_phase() {
1486 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::Commentary);
1487 assert_eq!(msg.phase, Some(ExecutionPhase::Commentary));
1488 }
1489
1490 #[test]
1491 fn test_message_phase_skipped_when_none() {
1492 let msg = Message::assistant("Hello");
1493 let json = serde_json::to_value(&msg).unwrap();
1494 assert!(json.get("phase").is_none());
1495 }
1496
1497 #[test]
1498 fn test_message_phase_included_when_set() {
1499 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::FinalAnswer);
1500 let json = serde_json::to_value(&msg).unwrap();
1501 assert_eq!(json.get("phase").unwrap(), "final_answer");
1502 }
1503
1504 #[test]
1505 fn test_resolve_hints_both_none() {
1506 let result = Controls::resolve_hints(None, None);
1507 assert!(result.is_empty());
1508 }
1509
1510 #[test]
1511 fn test_resolve_hints_session_only() {
1512 let mut session = std::collections::HashMap::new();
1513 session.insert("key1".into(), serde_json::json!("val1"));
1514 session.insert("key2".into(), serde_json::json!(42));
1515
1516 let result = Controls::resolve_hints(Some(&session), None);
1517 assert_eq!(result.len(), 2);
1518 assert_eq!(result["key1"], serde_json::json!("val1"));
1519 assert_eq!(result["key2"], serde_json::json!(42));
1520 }
1521
1522 #[test]
1523 fn test_resolve_hints_message_only() {
1524 let mut message = std::collections::HashMap::new();
1525 message.insert("key1".into(), serde_json::json!(true));
1526
1527 let result = Controls::resolve_hints(None, Some(&message));
1528 assert_eq!(result.len(), 1);
1529 assert_eq!(result["key1"], serde_json::json!(true));
1530 }
1531
1532 #[test]
1533 fn test_resolve_hints_message_overrides_session() {
1534 let mut session = std::collections::HashMap::new();
1535 session.insert("shared".into(), serde_json::json!("session_val"));
1536 session.insert("session_only".into(), serde_json::json!(1));
1537
1538 let mut message = std::collections::HashMap::new();
1539 message.insert("shared".into(), serde_json::json!("message_val"));
1540 message.insert("message_only".into(), serde_json::json!(2));
1541
1542 let result = Controls::resolve_hints(Some(&session), Some(&message));
1543 assert_eq!(result.len(), 3);
1544 assert_eq!(result["shared"], serde_json::json!("message_val"));
1545 assert_eq!(result["session_only"], serde_json::json!(1));
1546 assert_eq!(result["message_only"], serde_json::json!(2));
1547 }
1548
1549 #[test]
1550 fn test_controls_hints_serde_roundtrip() {
1551 let mut hints = std::collections::HashMap::new();
1552 hints.insert("setup_connection".into(), serde_json::json!(true));
1553 hints.insert("theme".into(), serde_json::json!("dark"));
1554
1555 let controls = Controls {
1556 hints: Some(hints),
1557 ..Default::default()
1558 };
1559
1560 let json = serde_json::to_value(&controls).unwrap();
1561 let deserialized: Controls = serde_json::from_value(json).unwrap();
1562 let h = deserialized.hints.unwrap();
1563 assert_eq!(h["setup_connection"], serde_json::json!(true));
1564 assert_eq!(h["theme"], serde_json::json!("dark"));
1565 }
1566}