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