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 image_url(url: impl Into<String>) -> Self {
542 ContentPart::Image(ImageContentPart::from_url(url))
543 }
544
545 pub fn image_file(image_id: ImageId) -> Self {
547 ContentPart::ImageFile(ImageFileContentPart::new(image_id))
548 }
549
550 pub fn tool_call(
552 id: impl Into<String>,
553 name: impl Into<String>,
554 arguments: serde_json::Value,
555 ) -> Self {
556 ContentPart::ToolCall(ToolCallContentPart::new(id, name, arguments))
557 }
558
559 pub fn tool_result(
561 tool_call_id: impl Into<String>,
562 result: Option<serde_json::Value>,
563 error: Option<String>,
564 ) -> Self {
565 ContentPart::ToolResult(ToolResultContentPart::new(tool_call_id, result, error))
566 }
567
568 pub fn as_text(&self) -> Option<&str> {
570 match self {
571 ContentPart::Text(t) => Some(&t.text),
572 _ => None,
573 }
574 }
575
576 pub fn is_image_file(&self) -> bool {
578 matches!(self, ContentPart::ImageFile(_))
579 }
580
581 pub fn content_type(&self) -> ContentType {
583 match self {
584 ContentPart::Text(_) => ContentType::Text,
585 ContentPart::Image(_) => ContentType::Image,
586 ContentPart::ImageFile(_) => ContentType::ImageFile,
587 ContentPart::ToolCall(_) => ContentType::ToolCall,
588 ContentPart::ToolResult(_) => ContentType::ToolResult,
589 }
590 }
591
592 pub fn to_openai_format(&self) -> Option<serde_json::Value> {
597 match self {
598 ContentPart::Text(t) => Some(serde_json::json!({
599 "type": "text",
600 "text": t.text
601 })),
602 ContentPart::Image(img) => {
603 if let Some(url) = &img.url {
604 Some(serde_json::json!({
605 "type": "image_url",
606 "image_url": { "url": url }
607 }))
608 } else if let Some(b64) = &img.base64 {
609 let media_type = img.media_type.as_deref().unwrap_or("image/png");
610 Some(serde_json::json!({
611 "type": "image_url",
612 "image_url": { "url": format!("data:{};base64,{}", media_type, b64) }
613 }))
614 } else {
615 None
616 }
617 }
618 _ => None,
620 }
621 }
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
629#[cfg_attr(feature = "openapi", derive(ToSchema))]
630#[serde(tag = "type", rename_all = "snake_case")]
631pub enum InputContentPart {
632 Text(TextContentPart),
634 Image(ImageContentPart),
636 ImageFile(ImageFileContentPart),
638}
639
640impl From<InputContentPart> for ContentPart {
641 fn from(input: InputContentPart) -> Self {
642 match input {
643 InputContentPart::Text(t) => ContentPart::Text(t),
644 InputContentPart::Image(i) => ContentPart::Image(i),
645 InputContentPart::ImageFile(f) => ContentPart::ImageFile(f),
646 }
647 }
648}
649
650impl InputContentPart {
651 pub fn text(text: impl Into<String>) -> Self {
653 InputContentPart::Text(TextContentPart::new(text))
654 }
655
656 pub fn image_url(url: impl Into<String>) -> Self {
658 InputContentPart::Image(ImageContentPart::from_url(url))
659 }
660
661 pub fn image_file(image_id: ImageId) -> Self {
663 InputContentPart::ImageFile(ImageFileContentPart::new(image_id))
664 }
665
666 pub fn as_text(&self) -> Option<&str> {
668 match self {
669 InputContentPart::Text(t) => Some(&t.text),
670 _ => None,
671 }
672 }
673
674 pub fn content_type(&self) -> ContentType {
676 match self {
677 InputContentPart::Text(_) => ContentType::Text,
678 InputContentPart::Image(_) => ContentType::Image,
679 InputContentPart::ImageFile(_) => ContentType::ImageFile,
680 }
681 }
682}
683
684impl Message {
685 pub fn with_id(mut self, id: MessageId) -> Self {
690 self.id = id;
691 self
692 }
693
694 pub fn user(content: impl Into<String>) -> Self {
696 Self {
697 id: MessageId::new(),
698 role: MessageRole::User,
699 content: vec![ContentPart::text(content)],
700 phase: None,
701 thinking: None,
702 thinking_signature: None,
703 controls: None,
704 metadata: None,
705 external_actor: None,
706 created_at: Utc::now(),
707 }
708 }
709
710 pub fn assistant(content: impl Into<String>) -> Self {
712 Self {
713 id: MessageId::new(),
714 role: MessageRole::Agent,
715 content: vec![ContentPart::text(content)],
716 phase: None,
717 thinking: None,
718 thinking_signature: None,
719 controls: None,
720 metadata: None,
721 external_actor: None,
722 created_at: Utc::now(),
723 }
724 }
725
726 pub fn assistant_with_tools(
732 content: impl Into<String>,
733 tool_calls: Vec<crate::tool_types::ToolCall>,
734 ) -> Self {
735 let text_content = content.into();
736 let mut parts = Vec::new();
737 if !text_content.is_empty() {
739 parts.push(ContentPart::text(text_content));
740 }
741 for tc in tool_calls {
742 parts.push(ContentPart::ToolCall(ToolCallContentPart {
743 id: tc.id,
744 name: tc.name,
745 arguments: tc.arguments,
746 }));
747 }
748 Self {
749 id: MessageId::new(),
750 role: MessageRole::Agent,
751 content: parts,
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 system(content: impl Into<String>) -> Self {
764 Self {
765 id: MessageId::new(),
766 role: MessageRole::System,
767 content: vec![ContentPart::text(content)],
768 phase: None,
769 thinking: None,
770 thinking_signature: None,
771 controls: None,
772 metadata: None,
773 external_actor: None,
774 created_at: Utc::now(),
775 }
776 }
777
778 pub fn tool_result(
780 tool_call_id: impl Into<String>,
781 result: Option<serde_json::Value>,
782 error: Option<String>,
783 ) -> Self {
784 let tool_call_id = tool_call_id.into();
785 Self {
786 id: MessageId::new(),
787 role: MessageRole::ToolResult,
788 content: vec![ContentPart::ToolResult(ToolResultContentPart::new(
789 tool_call_id,
790 result,
791 error,
792 ))],
793 phase: None,
794 thinking: None,
795 thinking_signature: None,
796 controls: None,
797 metadata: None,
798 external_actor: None,
799 created_at: Utc::now(),
800 }
801 }
802
803 pub fn tool_result_with_images(
809 tool_call_id: impl Into<String>,
810 result: Option<serde_json::Value>,
811 images: Vec<crate::tools::ToolResultImage>,
812 ) -> Self {
813 let tool_call_id = tool_call_id.into();
814 let mut content = vec![ContentPart::ToolResult(ToolResultContentPart::new(
815 tool_call_id,
816 result,
817 None,
818 ))];
819 for img in images {
820 content.push(ContentPart::Image(ImageContentPart::from_base64(
821 img.base64,
822 img.media_type,
823 )));
824 }
825 Self {
826 id: MessageId::new(),
827 role: MessageRole::ToolResult,
828 content,
829 phase: None,
830 thinking: None,
831 thinking_signature: None,
832 controls: None,
833 metadata: None,
834 external_actor: None,
835 created_at: Utc::now(),
836 }
837 }
838
839 pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
841 self.phase = Some(phase);
842 self
843 }
844
845 pub fn tool_call_id(&self) -> Option<&str> {
849 self.content.iter().find_map(|p| match p {
850 ContentPart::ToolResult(tr) => Some(tr.tool_call_id.as_str()),
851 _ => None,
852 })
853 }
854
855 pub fn text(&self) -> Option<&str> {
857 self.content.iter().find_map(|p| p.as_text())
858 }
859
860 pub fn tool_calls(&self) -> Vec<&ToolCallContentPart> {
862 self.content
863 .iter()
864 .filter_map(|p| match p {
865 ContentPart::ToolCall(tc) => Some(tc),
866 _ => None,
867 })
868 .collect()
869 }
870
871 pub fn has_tool_calls(&self) -> bool {
873 self.content
874 .iter()
875 .any(|p| matches!(p, ContentPart::ToolCall(_)))
876 }
877
878 pub fn tool_result_content(&self) -> Option<&ToolResultContentPart> {
880 self.content.iter().find_map(|p| match p {
881 ContentPart::ToolResult(tr) => Some(tr),
882 _ => None,
883 })
884 }
885
886 pub fn content_to_llm_string(&self) -> String {
888 self.content
889 .iter()
890 .map(|part| match part {
891 ContentPart::Text(t) => t.text.clone(),
892 ContentPart::Image(_) => "[Image]".to_string(),
893 ContentPart::ImageFile(_) => "[Image File]".to_string(),
894 ContentPart::ToolCall(tc) => {
895 format!(
896 "Tool call: {} with arguments: {}",
897 tc.name,
898 serde_json::to_string(&tc.arguments).unwrap_or_default()
899 )
900 }
901 ContentPart::ToolResult(tr) => {
902 if let Some(err) = &tr.error {
903 format!("Tool error: {}", err)
904 } else if let Some(res) = &tr.result {
905 serde_json::to_string(res).unwrap_or_else(|_| "{}".to_string())
906 } else {
907 "{}".to_string()
908 }
909 }
910 })
911 .collect::<Vec<_>>()
912 .join("\n")
913 }
914
915 pub fn to_openai_format(&self) -> serde_json::Value {
924 let role = match self.role {
925 MessageRole::System => "system",
926 MessageRole::User => "user",
927 MessageRole::Agent => "assistant",
928 MessageRole::ToolResult => "tool",
929 };
930
931 if self.role == MessageRole::ToolResult {
933 let tool_call_id = self.tool_call_id().unwrap_or("");
934 let content = self
935 .content
936 .iter()
937 .find_map(|p| match p {
938 ContentPart::ToolResult(tr) => {
939 if let Some(error) = &tr.error {
940 Some(format!("Error: {}", error))
941 } else if let Some(result) = &tr.result {
942 Some(serde_json::to_string(result).unwrap_or_else(|_| "{}".to_string()))
943 } else {
944 Some("{}".to_string())
945 }
946 }
947 _ => None,
948 })
949 .unwrap_or_else(|| "{}".to_string());
950
951 return serde_json::json!({
952 "role": role,
953 "content": content,
954 "tool_call_id": tool_call_id
955 });
956 }
957
958 if self.role == MessageRole::Agent {
960 let tool_calls: Vec<serde_json::Value> = self
961 .content
962 .iter()
963 .filter_map(|p| match p {
964 ContentPart::ToolCall(tc) => Some(serde_json::json!({
965 "id": tc.id,
966 "type": "function",
967 "function": {
968 "name": tc.name,
969 "arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string())
970 }
971 })),
972 _ => None,
973 })
974 .collect();
975
976 let text_content: String = self
977 .content
978 .iter()
979 .filter_map(|p| match p {
980 ContentPart::Text(t) => Some(t.text.clone()),
981 _ => None,
982 })
983 .collect::<Vec<_>>()
984 .join("\n");
985
986 if tool_calls.is_empty() {
987 return serde_json::json!({
988 "role": role,
989 "content": text_content
990 });
991 } else {
992 let mut result = serde_json::json!({
993 "role": role,
994 "tool_calls": tool_calls
995 });
996 if !text_content.is_empty() {
997 result["content"] = serde_json::json!(text_content);
998 }
999 return result;
1000 }
1001 }
1002
1003 let content = self.content_to_openai_format();
1005 serde_json::json!({
1006 "role": role,
1007 "content": content
1008 })
1009 }
1010
1011 fn content_to_openai_format(&self) -> serde_json::Value {
1013 if self.content.len() == 1
1015 && let ContentPart::Text(t) = &self.content[0]
1016 {
1017 return serde_json::json!(t.text);
1018 }
1019
1020 let parts: Vec<serde_json::Value> = self
1022 .content
1023 .iter()
1024 .filter_map(|part| part.to_openai_format())
1025 .collect();
1026
1027 if parts.is_empty() {
1028 return serde_json::json!("");
1029 }
1030
1031 if parts.len() == 1
1033 && let Some(text) = parts[0].get("text")
1034 {
1035 return text.clone();
1036 }
1037
1038 serde_json::json!(parts)
1039 }
1040}
1041
1042pub fn patch_dangling_tool_calls(messages: &[Message]) -> Vec<Message> {
1052 let mut result = Vec::new();
1053
1054 for (i, msg) in messages.iter().enumerate() {
1055 result.push(msg.clone());
1056
1057 if msg.role == MessageRole::Agent && msg.has_tool_calls() {
1059 for tc in msg.tool_calls() {
1060 let has_result = messages[(i + 1)..]
1062 .iter()
1063 .any(|m| m.role == MessageRole::ToolResult && m.tool_call_id() == Some(&tc.id));
1064
1065 if !has_result {
1066 result.push(Message::tool_result(
1067 &tc.id,
1068 None,
1069 Some(
1070 "cancelled - another message came in before it could be completed"
1071 .to_string(),
1072 ),
1073 ));
1074 }
1075 }
1076 }
1077 }
1078
1079 result
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085 use crate::tool_types::ToolCall;
1086
1087 #[test]
1088 fn test_patch_dangling_tool_calls_no_tool_calls() {
1089 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
1090 let patched = patch_dangling_tool_calls(&messages);
1091 assert_eq!(patched.len(), 2);
1092 }
1093
1094 #[test]
1095 fn test_patch_dangling_tool_calls_with_result() {
1096 let tool_call = ToolCall {
1097 id: "call_123".to_string(),
1098 name: "get_weather".to_string(),
1099 arguments: serde_json::json!({"city": "NYC"}),
1100 };
1101
1102 let messages = vec![
1103 Message::user("What's the weather?"),
1104 Message::assistant_with_tools("Let me check", vec![tool_call]),
1105 Message::tool_result("call_123", Some(serde_json::json!({"temp": 72})), None),
1106 ];
1107
1108 let patched = patch_dangling_tool_calls(&messages);
1109 assert_eq!(patched.len(), 3);
1110 }
1111
1112 #[test]
1113 fn test_patch_dangling_tool_calls_missing_result() {
1114 let tool_call = ToolCall {
1115 id: "call_456".to_string(),
1116 name: "search_web".to_string(),
1117 arguments: serde_json::json!({"query": "rust"}),
1118 };
1119
1120 let messages = vec![
1121 Message::user("Search for rust"),
1122 Message::assistant_with_tools("Searching...", vec![tool_call]),
1123 Message::user("Actually, never mind"),
1124 ];
1125
1126 let patched = patch_dangling_tool_calls(&messages);
1127 assert_eq!(patched.len(), 4);
1129 assert_eq!(patched[2].role, MessageRole::ToolResult);
1130 assert_eq!(patched[2].tool_call_id(), Some("call_456"));
1131 }
1132
1133 #[test]
1134 fn test_user_message() {
1135 let msg = Message::user("Hello");
1136 assert_eq!(msg.role, MessageRole::User);
1137 assert_eq!(msg.text(), Some("Hello"));
1138 }
1139
1140 #[test]
1141 fn test_assistant_message() {
1142 let msg = Message::assistant("Hi there!");
1143 assert_eq!(msg.role, MessageRole::Agent);
1144 assert_eq!(msg.text(), Some("Hi there!"));
1145 }
1146
1147 #[test]
1148 fn test_tool_result_message() {
1149 let msg = Message::tool_result(
1150 "call_123",
1151 Some(serde_json::json!({"result": "success"})),
1152 None,
1153 );
1154 assert_eq!(msg.role, MessageRole::ToolResult);
1155 assert_eq!(msg.tool_call_id(), Some("call_123"));
1156 }
1157
1158 #[test]
1159 fn test_assistant_with_tools_and_text() {
1160 let tool_call = ToolCall {
1161 id: "call_123".to_string(),
1162 name: "get_weather".to_string(),
1163 arguments: serde_json::json!({"location": "Tokyo"}),
1164 };
1165 let msg = Message::assistant_with_tools("Let me check the weather.", vec![tool_call]);
1166
1167 assert_eq!(msg.role, MessageRole::Agent);
1168 assert_eq!(msg.text(), Some("Let me check the weather."));
1169 assert_eq!(msg.tool_calls().len(), 1);
1170 assert_eq!(msg.tool_calls()[0].name, "get_weather");
1171 }
1172
1173 #[test]
1174 fn test_assistant_with_tools_empty_text() {
1175 let tool_call = ToolCall {
1178 id: "call_123".to_string(),
1179 name: "search".to_string(),
1180 arguments: serde_json::json!({"query": "rust"}),
1181 };
1182 let msg = Message::assistant_with_tools("", vec![tool_call]);
1183
1184 assert_eq!(msg.role, MessageRole::Agent);
1185 assert_eq!(msg.text(), None);
1187 assert_eq!(msg.tool_calls().len(), 1);
1189 assert_eq!(msg.tool_calls()[0].name, "search");
1190 assert_eq!(msg.content.len(), 1);
1192 assert!(matches!(msg.content[0], ContentPart::ToolCall(_)));
1193 }
1194
1195 #[test]
1196 fn test_assistant_with_tools_whitespace_text() {
1197 let tool_call = ToolCall {
1199 id: "call_456".to_string(),
1200 name: "fetch".to_string(),
1201 arguments: serde_json::json!({}),
1202 };
1203 let msg = Message::assistant_with_tools(" ", vec![tool_call]);
1204
1205 assert_eq!(msg.text(), Some(" "));
1207 assert_eq!(msg.content.len(), 2); }
1209
1210 #[test]
1211 fn test_assistant_with_multiple_tool_calls() {
1212 let tool_calls = vec![
1213 ToolCall {
1214 id: "call_1".to_string(),
1215 name: "search".to_string(),
1216 arguments: serde_json::json!({"q": "a"}),
1217 },
1218 ToolCall {
1219 id: "call_2".to_string(),
1220 name: "fetch".to_string(),
1221 arguments: serde_json::json!({"url": "http://example.com"}),
1222 },
1223 ];
1224 let msg = Message::assistant_with_tools("", tool_calls);
1225
1226 assert_eq!(msg.tool_calls().len(), 2);
1227 assert_eq!(msg.content.len(), 2);
1229 }
1230
1231 #[test]
1236 fn test_to_openai_format_user_message() {
1237 let msg = Message::user("Hello, world!");
1238 let converted = msg.to_openai_format();
1239
1240 assert_eq!(converted["role"], "user");
1241 assert_eq!(converted["content"], "Hello, world!");
1242 }
1243
1244 #[test]
1245 fn test_to_openai_format_system_message() {
1246 let msg = Message::system("You are a helpful assistant.");
1247 let converted = msg.to_openai_format();
1248
1249 assert_eq!(converted["role"], "system");
1250 assert_eq!(converted["content"], "You are a helpful assistant.");
1251 }
1252
1253 #[test]
1254 fn test_to_openai_format_assistant_role_mapping() {
1255 let msg = Message::assistant("Hi there!");
1257 let converted = msg.to_openai_format();
1258
1259 assert_eq!(converted["role"], "assistant");
1260 assert_eq!(converted["content"], "Hi there!");
1261 }
1262
1263 #[test]
1264 fn test_to_openai_format_assistant_with_tool_calls() {
1265 let tool_call = ToolCall {
1266 id: "call_123".to_string(),
1267 name: "get_weather".to_string(),
1268 arguments: serde_json::json!({"location": "Tokyo"}),
1269 };
1270 let msg = Message::assistant_with_tools("Let me check.", vec![tool_call]);
1271 let converted = msg.to_openai_format();
1272
1273 assert_eq!(converted["role"], "assistant");
1274 assert_eq!(converted["content"], "Let me check.");
1275
1276 let tool_calls = converted["tool_calls"].as_array().unwrap();
1277 assert_eq!(tool_calls.len(), 1);
1278 assert_eq!(tool_calls[0]["id"], "call_123");
1279 assert_eq!(tool_calls[0]["type"], "function");
1280 assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
1281 assert_eq!(
1282 tool_calls[0]["function"]["arguments"],
1283 r#"{"location":"Tokyo"}"#
1284 );
1285 }
1286
1287 #[test]
1288 fn test_to_openai_format_assistant_tool_calls_only() {
1289 let tool_call = ToolCall {
1291 id: "call_abc".to_string(),
1292 name: "search".to_string(),
1293 arguments: serde_json::json!({"query": "rust"}),
1294 };
1295 let msg = Message::assistant_with_tools("", vec![tool_call]);
1296 let converted = msg.to_openai_format();
1297
1298 assert_eq!(converted["role"], "assistant");
1299 assert!(converted.get("content").is_none());
1301 assert!(converted["tool_calls"].is_array());
1302 }
1303
1304 #[test]
1305 fn test_to_openai_format_tool_result_role_mapping() {
1306 let msg = Message::tool_result(
1308 "call_123",
1309 Some(serde_json::json!({"temperature": 72})),
1310 None,
1311 );
1312 let converted = msg.to_openai_format();
1313
1314 assert_eq!(converted["role"], "tool");
1315 assert_eq!(converted["tool_call_id"], "call_123");
1316 assert_eq!(converted["content"], r#"{"temperature":72}"#);
1317 }
1318
1319 #[test]
1320 fn test_to_openai_format_tool_result_error() {
1321 let msg = Message::tool_result("call_456", None, Some("API timeout".to_string()));
1322 let converted = msg.to_openai_format();
1323
1324 assert_eq!(converted["role"], "tool");
1325 assert_eq!(converted["tool_call_id"], "call_456");
1326 assert_eq!(converted["content"], "Error: API timeout");
1327 }
1328
1329 #[test]
1330 fn test_to_openai_format_full_conversation() {
1331 let tool_call = ToolCall {
1333 id: "call_abc".to_string(),
1334 name: "search".to_string(),
1335 arguments: serde_json::json!({"query": "rust"}),
1336 };
1337
1338 let messages = [
1339 Message::user("Search for rust"),
1340 Message::assistant_with_tools("", vec![tool_call]),
1341 Message::tool_result(
1342 "call_abc",
1343 Some(serde_json::json!({"results": ["rust-lang.org"]})),
1344 None,
1345 ),
1346 Message::assistant("Here are the search results."),
1347 ];
1348 let converted: Vec<_> = messages.iter().map(|m| m.to_openai_format()).collect();
1349
1350 assert_eq!(converted.len(), 4);
1351 assert_eq!(converted[0]["role"], "user");
1352 assert_eq!(converted[1]["role"], "assistant");
1353 assert!(converted[1]["tool_calls"].is_array());
1354 assert_eq!(converted[2]["role"], "tool");
1355 assert_eq!(converted[2]["tool_call_id"], "call_abc");
1356 assert_eq!(converted[3]["role"], "assistant");
1357 }
1358
1359 #[test]
1364 fn test_content_part_to_openai_format_text() {
1365 let part = ContentPart::text("Hello");
1366 let converted = part.to_openai_format().unwrap();
1367
1368 assert_eq!(converted["type"], "text");
1369 assert_eq!(converted["text"], "Hello");
1370 }
1371
1372 #[test]
1373 fn test_content_part_to_openai_format_image_url() {
1374 let part = ContentPart::image_url("https://example.com/img.png");
1375 let converted = part.to_openai_format().unwrap();
1376
1377 assert_eq!(converted["type"], "image_url");
1378 assert_eq!(converted["image_url"]["url"], "https://example.com/img.png");
1379 }
1380
1381 #[test]
1382 fn test_content_part_to_openai_format_image_base64() {
1383 let part = ContentPart::Image(ImageContentPart::from_base64("abc123", "image/jpeg"));
1384 let converted = part.to_openai_format().unwrap();
1385
1386 assert_eq!(converted["type"], "image_url");
1387 assert_eq!(
1388 converted["image_url"]["url"],
1389 "data:image/jpeg;base64,abc123"
1390 );
1391 }
1392
1393 #[test]
1394 fn test_content_part_to_openai_format_tool_call_returns_none() {
1395 let part = ContentPart::tool_call("call_1", "search", serde_json::json!({}));
1397 assert!(part.to_openai_format().is_none());
1398 }
1399
1400 #[test]
1401 fn test_content_part_to_openai_format_tool_result_returns_none() {
1402 let part = ContentPart::tool_result("call_1", Some(serde_json::json!({})), None);
1404 assert!(part.to_openai_format().is_none());
1405 }
1406
1407 #[test]
1408 fn test_execution_phase_from_has_tool_calls() {
1409 assert_eq!(
1410 ExecutionPhase::from_has_tool_calls(true),
1411 ExecutionPhase::Commentary
1412 );
1413 assert_eq!(
1414 ExecutionPhase::from_has_tool_calls(false),
1415 ExecutionPhase::FinalAnswer
1416 );
1417 }
1418
1419 #[test]
1420 fn test_execution_phase_refine_streamed_hint_monotonic() {
1421 use ExecutionPhase::{Commentary, FinalAnswer};
1422 assert_eq!(
1424 ExecutionPhase::refine_streamed_hint(None, Commentary),
1425 Some(Commentary)
1426 );
1427 assert_eq!(
1428 ExecutionPhase::refine_streamed_hint(None, FinalAnswer),
1429 Some(FinalAnswer)
1430 );
1431 assert_eq!(
1433 ExecutionPhase::refine_streamed_hint(Some(Commentary), FinalAnswer),
1434 Some(Commentary)
1435 );
1436 assert_eq!(
1437 ExecutionPhase::refine_streamed_hint(Some(FinalAnswer), Commentary),
1438 Some(FinalAnswer)
1439 );
1440 assert_eq!(
1443 ExecutionPhase::refine_streamed_hint(Some(Commentary), Commentary),
1444 Some(Commentary)
1445 );
1446 }
1447
1448 #[test]
1449 fn test_execution_phase_serde_roundtrip() {
1450 let commentary = ExecutionPhase::Commentary;
1451 let json = serde_json::to_string(&commentary).unwrap();
1452 assert_eq!(json, "\"commentary\"");
1453 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1454 assert_eq!(deserialized, ExecutionPhase::Commentary);
1455
1456 let final_answer = ExecutionPhase::FinalAnswer;
1457 let json = serde_json::to_string(&final_answer).unwrap();
1458 assert_eq!(json, "\"final_answer\"");
1459 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1460 assert_eq!(deserialized, ExecutionPhase::FinalAnswer);
1461 }
1462
1463 #[test]
1464 fn test_execution_phase_deserialize_legacy() {
1465 let legacy_in_progress: ExecutionPhase = serde_json::from_str("\"in_progress\"").unwrap();
1466 assert_eq!(legacy_in_progress, ExecutionPhase::Commentary);
1467
1468 let legacy_completed: ExecutionPhase = serde_json::from_str("\"completed\"").unwrap();
1469 assert_eq!(legacy_completed, ExecutionPhase::FinalAnswer);
1470 }
1471
1472 #[test]
1473 fn test_execution_phase_deserialize_unknown_fails() {
1474 let result = serde_json::from_str::<ExecutionPhase>("\"bogus\"");
1475 assert!(result.is_err());
1476 }
1477
1478 #[test]
1479 fn test_message_with_phase() {
1480 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::Commentary);
1481 assert_eq!(msg.phase, Some(ExecutionPhase::Commentary));
1482 }
1483
1484 #[test]
1485 fn test_message_phase_skipped_when_none() {
1486 let msg = Message::assistant("Hello");
1487 let json = serde_json::to_value(&msg).unwrap();
1488 assert!(json.get("phase").is_none());
1489 }
1490
1491 #[test]
1492 fn test_message_phase_included_when_set() {
1493 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::FinalAnswer);
1494 let json = serde_json::to_value(&msg).unwrap();
1495 assert_eq!(json.get("phase").unwrap(), "final_answer");
1496 }
1497
1498 #[test]
1499 fn test_resolve_hints_both_none() {
1500 let result = Controls::resolve_hints(None, None);
1501 assert!(result.is_empty());
1502 }
1503
1504 #[test]
1505 fn test_resolve_hints_session_only() {
1506 let mut session = std::collections::HashMap::new();
1507 session.insert("key1".into(), serde_json::json!("val1"));
1508 session.insert("key2".into(), serde_json::json!(42));
1509
1510 let result = Controls::resolve_hints(Some(&session), None);
1511 assert_eq!(result.len(), 2);
1512 assert_eq!(result["key1"], serde_json::json!("val1"));
1513 assert_eq!(result["key2"], serde_json::json!(42));
1514 }
1515
1516 #[test]
1517 fn test_resolve_hints_message_only() {
1518 let mut message = std::collections::HashMap::new();
1519 message.insert("key1".into(), serde_json::json!(true));
1520
1521 let result = Controls::resolve_hints(None, Some(&message));
1522 assert_eq!(result.len(), 1);
1523 assert_eq!(result["key1"], serde_json::json!(true));
1524 }
1525
1526 #[test]
1527 fn test_resolve_hints_message_overrides_session() {
1528 let mut session = std::collections::HashMap::new();
1529 session.insert("shared".into(), serde_json::json!("session_val"));
1530 session.insert("session_only".into(), serde_json::json!(1));
1531
1532 let mut message = std::collections::HashMap::new();
1533 message.insert("shared".into(), serde_json::json!("message_val"));
1534 message.insert("message_only".into(), serde_json::json!(2));
1535
1536 let result = Controls::resolve_hints(Some(&session), Some(&message));
1537 assert_eq!(result.len(), 3);
1538 assert_eq!(result["shared"], serde_json::json!("message_val"));
1539 assert_eq!(result["session_only"], serde_json::json!(1));
1540 assert_eq!(result["message_only"], serde_json::json!(2));
1541 }
1542
1543 #[test]
1544 fn test_controls_hints_serde_roundtrip() {
1545 let mut hints = std::collections::HashMap::new();
1546 hints.insert("setup_connection".into(), serde_json::json!(true));
1547 hints.insert("theme".into(), serde_json::json!("dark"));
1548
1549 let controls = Controls {
1550 hints: Some(hints),
1551 ..Default::default()
1552 };
1553
1554 let json = serde_json::to_value(&controls).unwrap();
1555 let deserialized: Controls = serde_json::from_value(json).unwrap();
1556 let h = deserialized.hints.unwrap();
1557 assert_eq!(h["setup_connection"], serde_json::json!(true));
1558 assert_eq!(h["theme"], serde_json::json!("dark"));
1559 }
1560}