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
17use everruns_provider::execution_phase::ExecutionPhase;
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[cfg_attr(feature = "openapi", derive(ToSchema))]
21#[serde(rename_all = "snake_case")]
22pub enum MessageRole {
23 System,
25 User,
27 Agent,
29 ToolResult,
31}
32
33impl std::fmt::Display for MessageRole {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 MessageRole::System => write!(f, "system"),
37 MessageRole::User => write!(f, "user"),
38 MessageRole::Agent => write!(f, "agent"),
39 MessageRole::ToolResult => write!(f, "tool_result"),
40 }
41 }
42}
43
44impl From<&str> for MessageRole {
45 fn from(s: &str) -> Self {
46 match s.to_lowercase().as_str() {
47 "system" => MessageRole::System,
48 "user" => MessageRole::User,
49 "agent" | "assistant" => MessageRole::Agent,
51 "tool_result" => MessageRole::ToolResult,
52 _ => MessageRole::User,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68#[cfg_attr(feature = "openapi", derive(ToSchema))]
69pub struct ExternalActor {
70 pub actor_id: String,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub actor_name: Option<String>,
75 pub source: String,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub metadata: Option<std::collections::HashMap<String, String>>,
80}
81
82impl ExternalActor {
83 pub fn display_label(&self) -> &str {
85 self.actor_name.as_deref().unwrap_or(&self.actor_id)
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95#[cfg_attr(feature = "openapi", derive(ToSchema))]
96pub struct ReasoningConfig {
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub effort: Option<String>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
104#[cfg_attr(feature = "openapi", derive(ToSchema))]
105pub struct Controls {
106 #[serde(skip_serializing_if = "Option::is_none")]
109 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
110 pub model_id: Option<ModelId>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
115 pub locale: Option<String>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub reasoning: Option<ReasoningConfig>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
125 pub speed: Option<String>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
131 pub verbosity: Option<String>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
138 pub error_disclosure: Option<String>,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
146 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
147 pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
148}
149
150impl Controls {
151 pub fn resolve_hints(
154 session_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
155 message_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
156 ) -> std::collections::HashMap<String, serde_json::Value> {
157 match (session_hints, message_hints) {
158 (None, None) => std::collections::HashMap::new(),
159 (Some(s), None) => s.clone(),
160 (None, Some(m)) => m.clone(),
161 (Some(s), Some(m)) => {
162 let mut merged = s.clone();
163 merged.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
164 merged
165 }
166 }
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172#[cfg_attr(feature = "openapi", derive(ToSchema))]
173pub struct Message {
174 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
176 pub id: MessageId,
177
178 pub role: MessageRole,
180
181 pub content: Vec<ContentPart>,
183
184 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub phase: Option<ExecutionPhase>,
192
193 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub thinking: Option<String>,
198
199 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub thinking_signature: Option<String>,
203
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub controls: Option<Controls>,
207
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
211 pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
212
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub external_actor: Option<ExternalActor>,
216
217 pub created_at: DateTime<Utc>,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
227#[cfg_attr(feature = "openapi", derive(ToSchema))]
228#[serde(rename_all = "snake_case")]
229pub enum ContentType {
230 Text,
231 Image,
232 ImageFile,
233 ToolCall,
234 ToolResult,
235}
236
237impl std::fmt::Display for ContentType {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 match self {
240 ContentType::Text => write!(f, "text"),
241 ContentType::Image => write!(f, "image"),
242 ContentType::ImageFile => write!(f, "image_file"),
243 ContentType::ToolCall => write!(f, "tool_call"),
244 ContentType::ToolResult => write!(f, "tool_result"),
245 }
246 }
247}
248
249impl From<&str> for ContentType {
250 fn from(s: &str) -> Self {
251 match s {
252 "image" => ContentType::Image,
253 "image_file" => ContentType::ImageFile,
254 "tool_call" => ContentType::ToolCall,
255 "tool_result" => ContentType::ToolResult,
256 _ => ContentType::Text,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
267#[cfg_attr(feature = "openapi", derive(ToSchema))]
268pub struct TextContentPart {
269 pub text: String,
270 #[serde(default, skip_serializing_if = "Vec::is_empty")]
276 pub annotations: Vec<TextAnnotation>,
277}
278
279impl TextContentPart {
280 pub fn new(text: impl Into<String>) -> Self {
281 Self {
282 text: text.into(),
283 annotations: Vec::new(),
284 }
285 }
286
287 pub fn with_annotations(mut self, annotations: Vec<TextAnnotation>) -> Self {
289 self.annotations = annotations;
290 self
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
301#[cfg_attr(feature = "openapi", derive(ToSchema))]
302pub struct TextAnnotation {
303 #[cfg_attr(feature = "openapi", schema(example = 0))]
305 pub start: usize,
306 #[cfg_attr(feature = "openapi", schema(example = 19))]
308 pub end: usize,
309 #[cfg_attr(feature = "openapi", schema(example = "citation_retrieval"))]
312 pub origin: String,
313 pub source: AnnotationSource,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
318 #[cfg_attr(feature = "openapi", schema(example = "kchk_01j9y3q8w2"))]
319 pub external_id: Option<String>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub verified: Option<VerificationVerdict>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
328#[cfg_attr(feature = "openapi", derive(ToSchema))]
329pub struct AnnotationSource {
330 #[cfg_attr(
333 feature = "openapi",
334 schema(example = "github://owner/repo@main/docs/x.md")
335 )]
336 pub uri: String,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
339 #[cfg_attr(feature = "openapi", schema(example = "Architecture Overview"))]
340 pub title: Option<String>,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
344 #[cfg_attr(
345 feature = "openapi",
346 schema(example = "The control plane owns durable state.")
347 )]
348 pub snippet: Option<String>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub location: Option<serde_json::Value>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
357#[cfg_attr(feature = "openapi", derive(ToSchema))]
358pub struct VerificationVerdict {
359 pub status: VerificationStatus,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 #[cfg_attr(feature = "openapi", schema(example = 0.92))]
364 pub score: Option<f32>,
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
369#[cfg_attr(feature = "openapi", derive(ToSchema))]
370#[cfg_attr(feature = "openapi", schema(example = "entailed"))]
371#[serde(rename_all = "snake_case")]
372pub enum VerificationStatus {
373 Entailed,
375 Unsupported,
377 Uncertain,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
383#[cfg_attr(feature = "openapi", derive(ToSchema))]
384pub struct ImageContentPart {
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub url: Option<String>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub base64: Option<String>,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub media_type: Option<String>,
391}
392
393impl ImageContentPart {
394 pub fn from_url(url: impl Into<String>) -> Self {
395 Self {
396 url: Some(url.into()),
397 base64: None,
398 media_type: None,
399 }
400 }
401
402 pub fn from_base64(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
403 Self {
404 url: None,
405 base64: Some(base64.into()),
406 media_type: Some(media_type.into()),
407 }
408 }
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
417#[cfg_attr(feature = "openapi", derive(ToSchema))]
418pub struct ImageFileContentPart {
419 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "img_01933b5a00007000800000000000001"))]
421 pub image_id: ImageId,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub filename: Option<String>,
425}
426
427impl ImageFileContentPart {
428 pub fn new(image_id: ImageId) -> Self {
429 Self {
430 image_id,
431 filename: None,
432 }
433 }
434
435 pub fn with_filename(image_id: ImageId, filename: impl Into<String>) -> Self {
436 Self {
437 image_id,
438 filename: Some(filename.into()),
439 }
440 }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
445#[cfg_attr(feature = "openapi", derive(ToSchema))]
446pub struct ToolCallContentPart {
447 pub id: String,
448 pub name: String,
449 pub arguments: serde_json::Value,
450}
451
452impl ToolCallContentPart {
453 pub fn new(
454 id: impl Into<String>,
455 name: impl Into<String>,
456 arguments: serde_json::Value,
457 ) -> Self {
458 Self {
459 id: id.into(),
460 name: name.into(),
461 arguments,
462 }
463 }
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
468#[cfg_attr(feature = "openapi", derive(ToSchema))]
469pub struct ToolResultContentPart {
470 pub tool_call_id: String,
472 #[serde(skip_serializing_if = "Option::is_none")]
473 pub result: Option<serde_json::Value>,
474 #[serde(skip_serializing_if = "Option::is_none")]
475 pub error: Option<String>,
476}
477
478impl ToolResultContentPart {
479 pub fn new(
480 tool_call_id: impl Into<String>,
481 result: Option<serde_json::Value>,
482 error: Option<String>,
483 ) -> Self {
484 Self {
485 tool_call_id: tool_call_id.into(),
486 result,
487 error,
488 }
489 }
490
491 pub fn success(tool_call_id: impl Into<String>, result: serde_json::Value) -> Self {
492 Self {
493 tool_call_id: tool_call_id.into(),
494 result: Some(result),
495 error: None,
496 }
497 }
498
499 pub fn error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
500 Self {
501 tool_call_id: tool_call_id.into(),
502 result: None,
503 error: Some(error.into()),
504 }
505 }
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
517#[cfg_attr(feature = "openapi", derive(ToSchema))]
518#[serde(tag = "type", rename_all = "snake_case")]
519pub enum ContentPart {
520 Text(TextContentPart),
522 Image(ImageContentPart),
524 ImageFile(ImageFileContentPart),
526 ToolCall(ToolCallContentPart),
528 ToolResult(ToolResultContentPart),
530}
531
532impl ContentPart {
533 pub fn text(text: impl Into<String>) -> Self {
535 ContentPart::Text(TextContentPart::new(text))
536 }
537
538 pub fn tool_result_text(value: &serde_json::Value) -> Self {
541 match value {
542 serde_json::Value::String(text) => Self::text(text.clone()),
543 other => Self::text(other.to_string()),
544 }
545 }
546
547 pub fn image_url(url: impl Into<String>) -> Self {
549 ContentPart::Image(ImageContentPart::from_url(url))
550 }
551
552 pub fn image_file(image_id: ImageId) -> Self {
554 ContentPart::ImageFile(ImageFileContentPart::new(image_id))
555 }
556
557 pub fn tool_call(
559 id: impl Into<String>,
560 name: impl Into<String>,
561 arguments: serde_json::Value,
562 ) -> Self {
563 ContentPart::ToolCall(ToolCallContentPart::new(id, name, arguments))
564 }
565
566 pub fn tool_result(
568 tool_call_id: impl Into<String>,
569 result: Option<serde_json::Value>,
570 error: Option<String>,
571 ) -> Self {
572 ContentPart::ToolResult(ToolResultContentPart::new(tool_call_id, result, error))
573 }
574
575 pub fn as_text(&self) -> Option<&str> {
577 match self {
578 ContentPart::Text(t) => Some(&t.text),
579 _ => None,
580 }
581 }
582
583 pub fn is_image_file(&self) -> bool {
585 matches!(self, ContentPart::ImageFile(_))
586 }
587
588 pub fn content_type(&self) -> ContentType {
590 match self {
591 ContentPart::Text(_) => ContentType::Text,
592 ContentPart::Image(_) => ContentType::Image,
593 ContentPart::ImageFile(_) => ContentType::ImageFile,
594 ContentPart::ToolCall(_) => ContentType::ToolCall,
595 ContentPart::ToolResult(_) => ContentType::ToolResult,
596 }
597 }
598
599 pub fn to_openai_format(&self) -> Option<serde_json::Value> {
604 match self {
605 ContentPart::Text(t) => Some(serde_json::json!({
606 "type": "text",
607 "text": t.text
608 })),
609 ContentPart::Image(img) => {
610 if let Some(url) = &img.url {
611 Some(serde_json::json!({
612 "type": "image_url",
613 "image_url": { "url": url }
614 }))
615 } else if let Some(b64) = &img.base64 {
616 let media_type = img.media_type.as_deref().unwrap_or("image/png");
617 Some(serde_json::json!({
618 "type": "image_url",
619 "image_url": { "url": format!("data:{};base64,{}", media_type, b64) }
620 }))
621 } else {
622 None
623 }
624 }
625 _ => None,
627 }
628 }
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
636#[cfg_attr(feature = "openapi", derive(ToSchema))]
637#[serde(tag = "type", rename_all = "snake_case")]
638pub enum InputContentPart {
639 Text(TextContentPart),
641 Image(ImageContentPart),
643 ImageFile(ImageFileContentPart),
645}
646
647impl From<InputContentPart> for ContentPart {
648 fn from(input: InputContentPart) -> Self {
649 match input {
650 InputContentPart::Text(t) => ContentPart::Text(t),
651 InputContentPart::Image(i) => ContentPart::Image(i),
652 InputContentPart::ImageFile(f) => ContentPart::ImageFile(f),
653 }
654 }
655}
656
657impl InputContentPart {
658 pub fn text(text: impl Into<String>) -> Self {
660 InputContentPart::Text(TextContentPart::new(text))
661 }
662
663 pub fn image_url(url: impl Into<String>) -> Self {
665 InputContentPart::Image(ImageContentPart::from_url(url))
666 }
667
668 pub fn image_file(image_id: ImageId) -> Self {
670 InputContentPart::ImageFile(ImageFileContentPart::new(image_id))
671 }
672
673 pub fn as_text(&self) -> Option<&str> {
675 match self {
676 InputContentPart::Text(t) => Some(&t.text),
677 _ => None,
678 }
679 }
680
681 pub fn content_type(&self) -> ContentType {
683 match self {
684 InputContentPart::Text(_) => ContentType::Text,
685 InputContentPart::Image(_) => ContentType::Image,
686 InputContentPart::ImageFile(_) => ContentType::ImageFile,
687 }
688 }
689}
690
691impl Message {
692 pub fn with_id(mut self, id: MessageId) -> Self {
697 self.id = id;
698 self
699 }
700
701 pub fn user(content: impl Into<String>) -> Self {
703 Self {
704 id: MessageId::new(),
705 role: MessageRole::User,
706 content: vec![ContentPart::text(content)],
707 phase: None,
708 thinking: None,
709 thinking_signature: None,
710 controls: None,
711 metadata: None,
712 external_actor: None,
713 created_at: Utc::now(),
714 }
715 }
716
717 pub fn assistant(content: impl Into<String>) -> Self {
719 Self {
720 id: MessageId::new(),
721 role: MessageRole::Agent,
722 content: vec![ContentPart::text(content)],
723 phase: None,
724 thinking: None,
725 thinking_signature: None,
726 controls: None,
727 metadata: None,
728 external_actor: None,
729 created_at: Utc::now(),
730 }
731 }
732
733 pub fn assistant_with_tools(
739 content: impl Into<String>,
740 tool_calls: Vec<crate::tool_types::ToolCall>,
741 ) -> Self {
742 let text_content = content.into();
743 let mut parts = Vec::new();
744 if !text_content.is_empty() {
746 parts.push(ContentPart::text(text_content));
747 }
748 for tc in tool_calls {
749 parts.push(ContentPart::ToolCall(ToolCallContentPart {
750 id: tc.id,
751 name: tc.name,
752 arguments: tc.arguments,
753 }));
754 }
755 Self {
756 id: MessageId::new(),
757 role: MessageRole::Agent,
758 content: parts,
759 phase: None,
760 thinking: None,
761 thinking_signature: None,
762 controls: None,
763 metadata: None,
764 external_actor: None,
765 created_at: Utc::now(),
766 }
767 }
768
769 pub fn system(content: impl Into<String>) -> Self {
771 Self {
772 id: MessageId::new(),
773 role: MessageRole::System,
774 content: vec![ContentPart::text(content)],
775 phase: None,
776 thinking: None,
777 thinking_signature: None,
778 controls: None,
779 metadata: None,
780 external_actor: None,
781 created_at: Utc::now(),
782 }
783 }
784
785 pub fn tool_result(
787 tool_call_id: impl Into<String>,
788 result: Option<serde_json::Value>,
789 error: Option<String>,
790 ) -> Self {
791 let tool_call_id = tool_call_id.into();
792 Self {
793 id: MessageId::new(),
794 role: MessageRole::ToolResult,
795 content: vec![ContentPart::ToolResult(ToolResultContentPart::new(
796 tool_call_id,
797 result,
798 error,
799 ))],
800 phase: None,
801 thinking: None,
802 thinking_signature: None,
803 controls: None,
804 metadata: None,
805 external_actor: None,
806 created_at: Utc::now(),
807 }
808 }
809
810 pub fn tool_result_with_images(
816 tool_call_id: impl Into<String>,
817 result: Option<serde_json::Value>,
818 images: Vec<everruns_provider::tool_types::ToolResultImage>,
819 ) -> Self {
820 let tool_call_id = tool_call_id.into();
821 let mut content = vec![ContentPart::ToolResult(ToolResultContentPart::new(
822 tool_call_id,
823 result,
824 None,
825 ))];
826 for img in images {
827 content.push(ContentPart::Image(ImageContentPart::from_base64(
828 img.base64,
829 img.media_type,
830 )));
831 }
832 Self {
833 id: MessageId::new(),
834 role: MessageRole::ToolResult,
835 content,
836 phase: None,
837 thinking: None,
838 thinking_signature: None,
839 controls: None,
840 metadata: None,
841 external_actor: None,
842 created_at: Utc::now(),
843 }
844 }
845
846 pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
848 self.phase = Some(phase);
849 self
850 }
851
852 pub fn tool_call_id(&self) -> Option<&str> {
856 self.content.iter().find_map(|p| match p {
857 ContentPart::ToolResult(tr) => Some(tr.tool_call_id.as_str()),
858 _ => None,
859 })
860 }
861
862 pub fn text(&self) -> Option<&str> {
864 self.content.iter().find_map(|p| p.as_text())
865 }
866
867 pub fn tool_calls(&self) -> Vec<&ToolCallContentPart> {
869 self.content
870 .iter()
871 .filter_map(|p| match p {
872 ContentPart::ToolCall(tc) => Some(tc),
873 _ => None,
874 })
875 .collect()
876 }
877
878 pub fn has_tool_calls(&self) -> bool {
880 self.content
881 .iter()
882 .any(|p| matches!(p, ContentPart::ToolCall(_)))
883 }
884
885 pub fn tool_result_content(&self) -> Option<&ToolResultContentPart> {
887 self.content.iter().find_map(|p| match p {
888 ContentPart::ToolResult(tr) => Some(tr),
889 _ => None,
890 })
891 }
892
893 pub fn content_to_llm_string(&self) -> String {
895 self.content
896 .iter()
897 .map(|part| match part {
898 ContentPart::Text(t) => t.text.clone(),
899 ContentPart::Image(_) => "[Image]".to_string(),
900 ContentPart::ImageFile(_) => "[Image File]".to_string(),
901 ContentPart::ToolCall(tc) => {
902 format!(
903 "Tool call: {} with arguments: {}",
904 tc.name,
905 serde_json::to_string(&tc.arguments).unwrap_or_default()
906 )
907 }
908 ContentPart::ToolResult(tr) => {
909 if let Some(err) = &tr.error {
910 format!("Tool error: {}", err)
911 } else if let Some(res) = &tr.result {
912 serde_json::to_string(res).unwrap_or_else(|_| "{}".to_string())
913 } else {
914 "{}".to_string()
915 }
916 }
917 })
918 .collect::<Vec<_>>()
919 .join("\n")
920 }
921
922 pub fn to_openai_format(&self) -> serde_json::Value {
931 let role = match self.role {
932 MessageRole::System => "system",
933 MessageRole::User => "user",
934 MessageRole::Agent => "assistant",
935 MessageRole::ToolResult => "tool",
936 };
937
938 if self.role == MessageRole::ToolResult {
940 let tool_call_id = self.tool_call_id().unwrap_or("");
941 let content = self
942 .content
943 .iter()
944 .find_map(|p| match p {
945 ContentPart::ToolResult(tr) => {
946 if let Some(error) = &tr.error {
947 Some(format!("Error: {}", error))
948 } else if let Some(result) = &tr.result {
949 Some(serde_json::to_string(result).unwrap_or_else(|_| "{}".to_string()))
950 } else {
951 Some("{}".to_string())
952 }
953 }
954 _ => None,
955 })
956 .unwrap_or_else(|| "{}".to_string());
957
958 return serde_json::json!({
959 "role": role,
960 "content": content,
961 "tool_call_id": tool_call_id
962 });
963 }
964
965 if self.role == MessageRole::Agent {
967 let tool_calls: Vec<serde_json::Value> = self
968 .content
969 .iter()
970 .filter_map(|p| match p {
971 ContentPart::ToolCall(tc) => Some(serde_json::json!({
972 "id": tc.id,
973 "type": "function",
974 "function": {
975 "name": tc.name,
976 "arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string())
977 }
978 })),
979 _ => None,
980 })
981 .collect();
982
983 let text_content: String = self
984 .content
985 .iter()
986 .filter_map(|p| match p {
987 ContentPart::Text(t) => Some(t.text.clone()),
988 _ => None,
989 })
990 .collect::<Vec<_>>()
991 .join("\n");
992
993 if tool_calls.is_empty() {
994 return serde_json::json!({
995 "role": role,
996 "content": text_content
997 });
998 } else {
999 let mut result = serde_json::json!({
1000 "role": role,
1001 "tool_calls": tool_calls
1002 });
1003 if !text_content.is_empty() {
1004 result["content"] = serde_json::json!(text_content);
1005 }
1006 return result;
1007 }
1008 }
1009
1010 let content = self.content_to_openai_format();
1012 serde_json::json!({
1013 "role": role,
1014 "content": content
1015 })
1016 }
1017
1018 fn content_to_openai_format(&self) -> serde_json::Value {
1020 if self.content.len() == 1
1022 && let ContentPart::Text(t) = &self.content[0]
1023 {
1024 return serde_json::json!(t.text);
1025 }
1026
1027 let parts: Vec<serde_json::Value> = self
1029 .content
1030 .iter()
1031 .filter_map(|part| part.to_openai_format())
1032 .collect();
1033
1034 if parts.is_empty() {
1035 return serde_json::json!("");
1036 }
1037
1038 if parts.len() == 1
1040 && let Some(text) = parts[0].get("text")
1041 {
1042 return text.clone();
1043 }
1044
1045 serde_json::json!(parts)
1046 }
1047}
1048
1049pub fn patch_dangling_tool_calls(messages: &[Message]) -> Vec<Message> {
1059 let mut result = Vec::new();
1060
1061 for (i, msg) in messages.iter().enumerate() {
1062 result.push(msg.clone());
1063
1064 if msg.role == MessageRole::Agent && msg.has_tool_calls() {
1066 for tc in msg.tool_calls() {
1067 let has_result = messages[(i + 1)..]
1069 .iter()
1070 .any(|m| m.role == MessageRole::ToolResult && m.tool_call_id() == Some(&tc.id));
1071
1072 if !has_result {
1073 result.push(Message::tool_result(
1074 &tc.id,
1075 None,
1076 Some(
1077 "cancelled - another message came in before it could be completed"
1078 .to_string(),
1079 ),
1080 ));
1081 }
1082 }
1083 }
1084 }
1085
1086 result
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092 use crate::tool_types::ToolCall;
1093
1094 #[test]
1095 fn test_patch_dangling_tool_calls_no_tool_calls() {
1096 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
1097 let patched = patch_dangling_tool_calls(&messages);
1098 assert_eq!(patched.len(), 2);
1099 }
1100
1101 #[test]
1102 fn test_patch_dangling_tool_calls_with_result() {
1103 let tool_call = ToolCall {
1104 id: "call_123".to_string(),
1105 name: "get_weather".to_string(),
1106 arguments: serde_json::json!({"city": "NYC"}),
1107 };
1108
1109 let messages = vec![
1110 Message::user("What's the weather?"),
1111 Message::assistant_with_tools("Let me check", vec![tool_call]),
1112 Message::tool_result("call_123", Some(serde_json::json!({"temp": 72})), None),
1113 ];
1114
1115 let patched = patch_dangling_tool_calls(&messages);
1116 assert_eq!(patched.len(), 3);
1117 }
1118
1119 #[test]
1120 fn test_patch_dangling_tool_calls_missing_result() {
1121 let tool_call = ToolCall {
1122 id: "call_456".to_string(),
1123 name: "search_web".to_string(),
1124 arguments: serde_json::json!({"query": "rust"}),
1125 };
1126
1127 let messages = vec![
1128 Message::user("Search for rust"),
1129 Message::assistant_with_tools("Searching...", vec![tool_call]),
1130 Message::user("Actually, never mind"),
1131 ];
1132
1133 let patched = patch_dangling_tool_calls(&messages);
1134 assert_eq!(patched.len(), 4);
1136 assert_eq!(patched[2].role, MessageRole::ToolResult);
1137 assert_eq!(patched[2].tool_call_id(), Some("call_456"));
1138 }
1139
1140 #[test]
1141 fn test_user_message() {
1142 let msg = Message::user("Hello");
1143 assert_eq!(msg.role, MessageRole::User);
1144 assert_eq!(msg.text(), Some("Hello"));
1145 }
1146
1147 #[test]
1148 fn test_assistant_message() {
1149 let msg = Message::assistant("Hi there!");
1150 assert_eq!(msg.role, MessageRole::Agent);
1151 assert_eq!(msg.text(), Some("Hi there!"));
1152 }
1153
1154 #[test]
1155 fn test_tool_result_message() {
1156 let msg = Message::tool_result(
1157 "call_123",
1158 Some(serde_json::json!({"result": "success"})),
1159 None,
1160 );
1161 assert_eq!(msg.role, MessageRole::ToolResult);
1162 assert_eq!(msg.tool_call_id(), Some("call_123"));
1163 }
1164
1165 #[test]
1166 fn test_assistant_with_tools_and_text() {
1167 let tool_call = ToolCall {
1168 id: "call_123".to_string(),
1169 name: "get_weather".to_string(),
1170 arguments: serde_json::json!({"location": "Tokyo"}),
1171 };
1172 let msg = Message::assistant_with_tools("Let me check the weather.", vec![tool_call]);
1173
1174 assert_eq!(msg.role, MessageRole::Agent);
1175 assert_eq!(msg.text(), Some("Let me check the weather."));
1176 assert_eq!(msg.tool_calls().len(), 1);
1177 assert_eq!(msg.tool_calls()[0].name, "get_weather");
1178 }
1179
1180 #[test]
1181 fn test_assistant_with_tools_empty_text() {
1182 let tool_call = ToolCall {
1185 id: "call_123".to_string(),
1186 name: "search".to_string(),
1187 arguments: serde_json::json!({"query": "rust"}),
1188 };
1189 let msg = Message::assistant_with_tools("", vec![tool_call]);
1190
1191 assert_eq!(msg.role, MessageRole::Agent);
1192 assert_eq!(msg.text(), None);
1194 assert_eq!(msg.tool_calls().len(), 1);
1196 assert_eq!(msg.tool_calls()[0].name, "search");
1197 assert_eq!(msg.content.len(), 1);
1199 assert!(matches!(msg.content[0], ContentPart::ToolCall(_)));
1200 }
1201
1202 #[test]
1203 fn test_assistant_with_tools_whitespace_text() {
1204 let tool_call = ToolCall {
1206 id: "call_456".to_string(),
1207 name: "fetch".to_string(),
1208 arguments: serde_json::json!({}),
1209 };
1210 let msg = Message::assistant_with_tools(" ", vec![tool_call]);
1211
1212 assert_eq!(msg.text(), Some(" "));
1214 assert_eq!(msg.content.len(), 2); }
1216
1217 #[test]
1218 fn test_assistant_with_multiple_tool_calls() {
1219 let tool_calls = vec![
1220 ToolCall {
1221 id: "call_1".to_string(),
1222 name: "search".to_string(),
1223 arguments: serde_json::json!({"q": "a"}),
1224 },
1225 ToolCall {
1226 id: "call_2".to_string(),
1227 name: "fetch".to_string(),
1228 arguments: serde_json::json!({"url": "http://example.com"}),
1229 },
1230 ];
1231 let msg = Message::assistant_with_tools("", tool_calls);
1232
1233 assert_eq!(msg.tool_calls().len(), 2);
1234 assert_eq!(msg.content.len(), 2);
1236 }
1237
1238 #[test]
1243 fn test_to_openai_format_user_message() {
1244 let msg = Message::user("Hello, world!");
1245 let converted = msg.to_openai_format();
1246
1247 assert_eq!(converted["role"], "user");
1248 assert_eq!(converted["content"], "Hello, world!");
1249 }
1250
1251 #[test]
1252 fn test_to_openai_format_system_message() {
1253 let msg = Message::system("You are a helpful assistant.");
1254 let converted = msg.to_openai_format();
1255
1256 assert_eq!(converted["role"], "system");
1257 assert_eq!(converted["content"], "You are a helpful assistant.");
1258 }
1259
1260 #[test]
1261 fn test_to_openai_format_assistant_role_mapping() {
1262 let msg = Message::assistant("Hi there!");
1264 let converted = msg.to_openai_format();
1265
1266 assert_eq!(converted["role"], "assistant");
1267 assert_eq!(converted["content"], "Hi there!");
1268 }
1269
1270 #[test]
1271 fn test_to_openai_format_assistant_with_tool_calls() {
1272 let tool_call = ToolCall {
1273 id: "call_123".to_string(),
1274 name: "get_weather".to_string(),
1275 arguments: serde_json::json!({"location": "Tokyo"}),
1276 };
1277 let msg = Message::assistant_with_tools("Let me check.", vec![tool_call]);
1278 let converted = msg.to_openai_format();
1279
1280 assert_eq!(converted["role"], "assistant");
1281 assert_eq!(converted["content"], "Let me check.");
1282
1283 let tool_calls = converted["tool_calls"].as_array().unwrap();
1284 assert_eq!(tool_calls.len(), 1);
1285 assert_eq!(tool_calls[0]["id"], "call_123");
1286 assert_eq!(tool_calls[0]["type"], "function");
1287 assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
1288 assert_eq!(
1289 tool_calls[0]["function"]["arguments"],
1290 r#"{"location":"Tokyo"}"#
1291 );
1292 }
1293
1294 #[test]
1295 fn test_to_openai_format_assistant_tool_calls_only() {
1296 let tool_call = ToolCall {
1298 id: "call_abc".to_string(),
1299 name: "search".to_string(),
1300 arguments: serde_json::json!({"query": "rust"}),
1301 };
1302 let msg = Message::assistant_with_tools("", vec![tool_call]);
1303 let converted = msg.to_openai_format();
1304
1305 assert_eq!(converted["role"], "assistant");
1306 assert!(converted.get("content").is_none());
1308 assert!(converted["tool_calls"].is_array());
1309 }
1310
1311 #[test]
1312 fn test_to_openai_format_tool_result_role_mapping() {
1313 let msg = Message::tool_result(
1315 "call_123",
1316 Some(serde_json::json!({"temperature": 72})),
1317 None,
1318 );
1319 let converted = msg.to_openai_format();
1320
1321 assert_eq!(converted["role"], "tool");
1322 assert_eq!(converted["tool_call_id"], "call_123");
1323 assert_eq!(converted["content"], r#"{"temperature":72}"#);
1324 }
1325
1326 #[test]
1327 fn test_to_openai_format_tool_result_error() {
1328 let msg = Message::tool_result("call_456", None, Some("API timeout".to_string()));
1329 let converted = msg.to_openai_format();
1330
1331 assert_eq!(converted["role"], "tool");
1332 assert_eq!(converted["tool_call_id"], "call_456");
1333 assert_eq!(converted["content"], "Error: API timeout");
1334 }
1335
1336 #[test]
1337 fn test_to_openai_format_full_conversation() {
1338 let tool_call = ToolCall {
1340 id: "call_abc".to_string(),
1341 name: "search".to_string(),
1342 arguments: serde_json::json!({"query": "rust"}),
1343 };
1344
1345 let messages = [
1346 Message::user("Search for rust"),
1347 Message::assistant_with_tools("", vec![tool_call]),
1348 Message::tool_result(
1349 "call_abc",
1350 Some(serde_json::json!({"results": ["rust-lang.org"]})),
1351 None,
1352 ),
1353 Message::assistant("Here are the search results."),
1354 ];
1355 let converted: Vec<_> = messages.iter().map(|m| m.to_openai_format()).collect();
1356
1357 assert_eq!(converted.len(), 4);
1358 assert_eq!(converted[0]["role"], "user");
1359 assert_eq!(converted[1]["role"], "assistant");
1360 assert!(converted[1]["tool_calls"].is_array());
1361 assert_eq!(converted[2]["role"], "tool");
1362 assert_eq!(converted[2]["tool_call_id"], "call_abc");
1363 assert_eq!(converted[3]["role"], "assistant");
1364 }
1365
1366 #[test]
1371 fn test_content_part_to_openai_format_text() {
1372 let part = ContentPart::text("Hello");
1373 let converted = part.to_openai_format().unwrap();
1374
1375 assert_eq!(converted["type"], "text");
1376 assert_eq!(converted["text"], "Hello");
1377 }
1378
1379 #[test]
1380 fn test_content_part_to_openai_format_image_url() {
1381 let part = ContentPart::image_url("https://example.com/img.png");
1382 let converted = part.to_openai_format().unwrap();
1383
1384 assert_eq!(converted["type"], "image_url");
1385 assert_eq!(converted["image_url"]["url"], "https://example.com/img.png");
1386 }
1387
1388 #[test]
1389 fn test_content_part_to_openai_format_image_base64() {
1390 let part = ContentPart::Image(ImageContentPart::from_base64("abc123", "image/jpeg"));
1391 let converted = part.to_openai_format().unwrap();
1392
1393 assert_eq!(converted["type"], "image_url");
1394 assert_eq!(
1395 converted["image_url"]["url"],
1396 "data:image/jpeg;base64,abc123"
1397 );
1398 }
1399
1400 #[test]
1401 fn test_content_part_to_openai_format_tool_call_returns_none() {
1402 let part = ContentPart::tool_call("call_1", "search", serde_json::json!({}));
1404 assert!(part.to_openai_format().is_none());
1405 }
1406
1407 #[test]
1408 fn test_content_part_to_openai_format_tool_result_returns_none() {
1409 let part = ContentPart::tool_result("call_1", Some(serde_json::json!({})), None);
1411 assert!(part.to_openai_format().is_none());
1412 }
1413
1414 #[test]
1415 fn test_execution_phase_from_has_tool_calls() {
1416 assert_eq!(
1417 ExecutionPhase::from_has_tool_calls(true),
1418 ExecutionPhase::Commentary
1419 );
1420 assert_eq!(
1421 ExecutionPhase::from_has_tool_calls(false),
1422 ExecutionPhase::FinalAnswer
1423 );
1424 }
1425
1426 #[test]
1427 fn test_execution_phase_refine_streamed_hint_monotonic() {
1428 use ExecutionPhase::{Commentary, FinalAnswer};
1429 assert_eq!(
1431 ExecutionPhase::refine_streamed_hint(None, Commentary),
1432 Some(Commentary)
1433 );
1434 assert_eq!(
1435 ExecutionPhase::refine_streamed_hint(None, FinalAnswer),
1436 Some(FinalAnswer)
1437 );
1438 assert_eq!(
1440 ExecutionPhase::refine_streamed_hint(Some(Commentary), FinalAnswer),
1441 Some(Commentary)
1442 );
1443 assert_eq!(
1444 ExecutionPhase::refine_streamed_hint(Some(FinalAnswer), Commentary),
1445 Some(FinalAnswer)
1446 );
1447 assert_eq!(
1450 ExecutionPhase::refine_streamed_hint(Some(Commentary), Commentary),
1451 Some(Commentary)
1452 );
1453 }
1454
1455 #[test]
1456 fn test_execution_phase_serde_roundtrip() {
1457 let commentary = ExecutionPhase::Commentary;
1458 let json = serde_json::to_string(&commentary).unwrap();
1459 assert_eq!(json, "\"commentary\"");
1460 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1461 assert_eq!(deserialized, ExecutionPhase::Commentary);
1462
1463 let final_answer = ExecutionPhase::FinalAnswer;
1464 let json = serde_json::to_string(&final_answer).unwrap();
1465 assert_eq!(json, "\"final_answer\"");
1466 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1467 assert_eq!(deserialized, ExecutionPhase::FinalAnswer);
1468 }
1469
1470 #[test]
1471 fn test_execution_phase_deserialize_legacy() {
1472 let legacy_in_progress: ExecutionPhase = serde_json::from_str("\"in_progress\"").unwrap();
1473 assert_eq!(legacy_in_progress, ExecutionPhase::Commentary);
1474
1475 let legacy_completed: ExecutionPhase = serde_json::from_str("\"completed\"").unwrap();
1476 assert_eq!(legacy_completed, ExecutionPhase::FinalAnswer);
1477 }
1478
1479 #[test]
1480 fn test_execution_phase_deserialize_unknown_fails() {
1481 let result = serde_json::from_str::<ExecutionPhase>("\"bogus\"");
1482 assert!(result.is_err());
1483 }
1484
1485 #[test]
1486 fn test_message_with_phase() {
1487 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::Commentary);
1488 assert_eq!(msg.phase, Some(ExecutionPhase::Commentary));
1489 }
1490
1491 #[test]
1492 fn test_message_phase_skipped_when_none() {
1493 let msg = Message::assistant("Hello");
1494 let json = serde_json::to_value(&msg).unwrap();
1495 assert!(json.get("phase").is_none());
1496 }
1497
1498 #[test]
1499 fn test_message_phase_included_when_set() {
1500 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::FinalAnswer);
1501 let json = serde_json::to_value(&msg).unwrap();
1502 assert_eq!(json.get("phase").unwrap(), "final_answer");
1503 }
1504
1505 #[test]
1506 fn test_resolve_hints_both_none() {
1507 let result = Controls::resolve_hints(None, None);
1508 assert!(result.is_empty());
1509 }
1510
1511 #[test]
1512 fn test_resolve_hints_session_only() {
1513 let mut session = std::collections::HashMap::new();
1514 session.insert("key1".into(), serde_json::json!("val1"));
1515 session.insert("key2".into(), serde_json::json!(42));
1516
1517 let result = Controls::resolve_hints(Some(&session), None);
1518 assert_eq!(result.len(), 2);
1519 assert_eq!(result["key1"], serde_json::json!("val1"));
1520 assert_eq!(result["key2"], serde_json::json!(42));
1521 }
1522
1523 #[test]
1524 fn test_resolve_hints_message_only() {
1525 let mut message = std::collections::HashMap::new();
1526 message.insert("key1".into(), serde_json::json!(true));
1527
1528 let result = Controls::resolve_hints(None, Some(&message));
1529 assert_eq!(result.len(), 1);
1530 assert_eq!(result["key1"], serde_json::json!(true));
1531 }
1532
1533 #[test]
1534 fn test_resolve_hints_message_overrides_session() {
1535 let mut session = std::collections::HashMap::new();
1536 session.insert("shared".into(), serde_json::json!("session_val"));
1537 session.insert("session_only".into(), serde_json::json!(1));
1538
1539 let mut message = std::collections::HashMap::new();
1540 message.insert("shared".into(), serde_json::json!("message_val"));
1541 message.insert("message_only".into(), serde_json::json!(2));
1542
1543 let result = Controls::resolve_hints(Some(&session), Some(&message));
1544 assert_eq!(result.len(), 3);
1545 assert_eq!(result["shared"], serde_json::json!("message_val"));
1546 assert_eq!(result["session_only"], serde_json::json!(1));
1547 assert_eq!(result["message_only"], serde_json::json!(2));
1548 }
1549
1550 #[test]
1551 fn test_controls_hints_serde_roundtrip() {
1552 let mut hints = std::collections::HashMap::new();
1553 hints.insert("setup_connection".into(), serde_json::json!(true));
1554 hints.insert("theme".into(), serde_json::json!("dark"));
1555
1556 let controls = Controls {
1557 hints: Some(hints),
1558 ..Default::default()
1559 };
1560
1561 let json = serde_json::to_value(&controls).unwrap();
1562 let deserialized: Controls = serde_json::from_value(json).unwrap();
1563 let h = deserialized.hints.unwrap();
1564 assert_eq!(h["setup_connection"], serde_json::json!(true));
1565 assert_eq!(h["theme"], serde_json::json!("dark"));
1566 }
1567
1568 #[test]
1569 fn tool_result_text_preserves_strings_without_json_escaping() {
1570 let value = serde_json::json!("{\n \"count\": 1\n}");
1571 assert_eq!(
1572 ContentPart::tool_result_text(&value).as_text(),
1573 Some("{\n \"count\": 1\n}")
1574 );
1575 }
1576
1577 #[test]
1578 fn tool_result_text_serializes_structured_values() {
1579 let value = serde_json::json!({"count": 1});
1580 assert_eq!(
1581 ContentPart::tool_result_text(&value).as_text(),
1582 Some("{\"count\":1}")
1583 );
1584 }
1585}