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