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}
273
274impl TextContentPart {
275 pub fn new(text: impl Into<String>) -> Self {
276 Self { text: text.into() }
277 }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
282#[cfg_attr(feature = "openapi", derive(ToSchema))]
283pub struct ImageContentPart {
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub url: Option<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub base64: Option<String>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub media_type: Option<String>,
290}
291
292impl ImageContentPart {
293 pub fn from_url(url: impl Into<String>) -> Self {
294 Self {
295 url: Some(url.into()),
296 base64: None,
297 media_type: None,
298 }
299 }
300
301 pub fn from_base64(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
302 Self {
303 url: None,
304 base64: Some(base64.into()),
305 media_type: Some(media_type.into()),
306 }
307 }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
316#[cfg_attr(feature = "openapi", derive(ToSchema))]
317pub struct ImageFileContentPart {
318 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "img_01933b5a00007000800000000000001"))]
320 pub image_id: ImageId,
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub filename: Option<String>,
324}
325
326impl ImageFileContentPart {
327 pub fn new(image_id: ImageId) -> Self {
328 Self {
329 image_id,
330 filename: None,
331 }
332 }
333
334 pub fn with_filename(image_id: ImageId, filename: impl Into<String>) -> Self {
335 Self {
336 image_id,
337 filename: Some(filename.into()),
338 }
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
344#[cfg_attr(feature = "openapi", derive(ToSchema))]
345pub struct ToolCallContentPart {
346 pub id: String,
347 pub name: String,
348 pub arguments: serde_json::Value,
349}
350
351impl ToolCallContentPart {
352 pub fn new(
353 id: impl Into<String>,
354 name: impl Into<String>,
355 arguments: serde_json::Value,
356 ) -> Self {
357 Self {
358 id: id.into(),
359 name: name.into(),
360 arguments,
361 }
362 }
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
367#[cfg_attr(feature = "openapi", derive(ToSchema))]
368pub struct ToolResultContentPart {
369 pub tool_call_id: String,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub result: Option<serde_json::Value>,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub error: Option<String>,
375}
376
377impl ToolResultContentPart {
378 pub fn new(
379 tool_call_id: impl Into<String>,
380 result: Option<serde_json::Value>,
381 error: Option<String>,
382 ) -> Self {
383 Self {
384 tool_call_id: tool_call_id.into(),
385 result,
386 error,
387 }
388 }
389
390 pub fn success(tool_call_id: impl Into<String>, result: serde_json::Value) -> Self {
391 Self {
392 tool_call_id: tool_call_id.into(),
393 result: Some(result),
394 error: None,
395 }
396 }
397
398 pub fn error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
399 Self {
400 tool_call_id: tool_call_id.into(),
401 result: None,
402 error: Some(error.into()),
403 }
404 }
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
416#[cfg_attr(feature = "openapi", derive(ToSchema))]
417#[serde(tag = "type", rename_all = "snake_case")]
418pub enum ContentPart {
419 Text(TextContentPart),
421 Image(ImageContentPart),
423 ImageFile(ImageFileContentPart),
425 ToolCall(ToolCallContentPart),
427 ToolResult(ToolResultContentPart),
429}
430
431impl ContentPart {
432 pub fn text(text: impl Into<String>) -> Self {
434 ContentPart::Text(TextContentPart::new(text))
435 }
436
437 pub fn image_url(url: impl Into<String>) -> Self {
439 ContentPart::Image(ImageContentPart::from_url(url))
440 }
441
442 pub fn image_file(image_id: ImageId) -> Self {
444 ContentPart::ImageFile(ImageFileContentPart::new(image_id))
445 }
446
447 pub fn tool_call(
449 id: impl Into<String>,
450 name: impl Into<String>,
451 arguments: serde_json::Value,
452 ) -> Self {
453 ContentPart::ToolCall(ToolCallContentPart::new(id, name, arguments))
454 }
455
456 pub fn tool_result(
458 tool_call_id: impl Into<String>,
459 result: Option<serde_json::Value>,
460 error: Option<String>,
461 ) -> Self {
462 ContentPart::ToolResult(ToolResultContentPart::new(tool_call_id, result, error))
463 }
464
465 pub fn as_text(&self) -> Option<&str> {
467 match self {
468 ContentPart::Text(t) => Some(&t.text),
469 _ => None,
470 }
471 }
472
473 pub fn is_image_file(&self) -> bool {
475 matches!(self, ContentPart::ImageFile(_))
476 }
477
478 pub fn content_type(&self) -> ContentType {
480 match self {
481 ContentPart::Text(_) => ContentType::Text,
482 ContentPart::Image(_) => ContentType::Image,
483 ContentPart::ImageFile(_) => ContentType::ImageFile,
484 ContentPart::ToolCall(_) => ContentType::ToolCall,
485 ContentPart::ToolResult(_) => ContentType::ToolResult,
486 }
487 }
488
489 pub fn to_openai_format(&self) -> Option<serde_json::Value> {
494 match self {
495 ContentPart::Text(t) => Some(serde_json::json!({
496 "type": "text",
497 "text": t.text
498 })),
499 ContentPart::Image(img) => {
500 if let Some(url) = &img.url {
501 Some(serde_json::json!({
502 "type": "image_url",
503 "image_url": { "url": url }
504 }))
505 } else if let Some(b64) = &img.base64 {
506 let media_type = img.media_type.as_deref().unwrap_or("image/png");
507 Some(serde_json::json!({
508 "type": "image_url",
509 "image_url": { "url": format!("data:{};base64,{}", media_type, b64) }
510 }))
511 } else {
512 None
513 }
514 }
515 _ => None,
517 }
518 }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
526#[cfg_attr(feature = "openapi", derive(ToSchema))]
527#[serde(tag = "type", rename_all = "snake_case")]
528pub enum InputContentPart {
529 Text(TextContentPart),
531 Image(ImageContentPart),
533 ImageFile(ImageFileContentPart),
535}
536
537impl From<InputContentPart> for ContentPart {
538 fn from(input: InputContentPart) -> Self {
539 match input {
540 InputContentPart::Text(t) => ContentPart::Text(t),
541 InputContentPart::Image(i) => ContentPart::Image(i),
542 InputContentPart::ImageFile(f) => ContentPart::ImageFile(f),
543 }
544 }
545}
546
547impl InputContentPart {
548 pub fn text(text: impl Into<String>) -> Self {
550 InputContentPart::Text(TextContentPart::new(text))
551 }
552
553 pub fn image_url(url: impl Into<String>) -> Self {
555 InputContentPart::Image(ImageContentPart::from_url(url))
556 }
557
558 pub fn image_file(image_id: ImageId) -> Self {
560 InputContentPart::ImageFile(ImageFileContentPart::new(image_id))
561 }
562
563 pub fn as_text(&self) -> Option<&str> {
565 match self {
566 InputContentPart::Text(t) => Some(&t.text),
567 _ => None,
568 }
569 }
570
571 pub fn content_type(&self) -> ContentType {
573 match self {
574 InputContentPart::Text(_) => ContentType::Text,
575 InputContentPart::Image(_) => ContentType::Image,
576 InputContentPart::ImageFile(_) => ContentType::ImageFile,
577 }
578 }
579}
580
581impl Message {
582 pub fn with_id(mut self, id: MessageId) -> Self {
587 self.id = id;
588 self
589 }
590
591 pub fn user(content: impl Into<String>) -> Self {
593 Self {
594 id: MessageId::new(),
595 role: MessageRole::User,
596 content: vec![ContentPart::text(content)],
597 phase: None,
598 thinking: None,
599 thinking_signature: None,
600 controls: None,
601 metadata: None,
602 external_actor: None,
603 created_at: Utc::now(),
604 }
605 }
606
607 pub fn assistant(content: impl Into<String>) -> Self {
609 Self {
610 id: MessageId::new(),
611 role: MessageRole::Agent,
612 content: vec![ContentPart::text(content)],
613 phase: None,
614 thinking: None,
615 thinking_signature: None,
616 controls: None,
617 metadata: None,
618 external_actor: None,
619 created_at: Utc::now(),
620 }
621 }
622
623 pub fn assistant_with_tools(
629 content: impl Into<String>,
630 tool_calls: Vec<crate::tool_types::ToolCall>,
631 ) -> Self {
632 let text_content = content.into();
633 let mut parts = Vec::new();
634 if !text_content.is_empty() {
636 parts.push(ContentPart::text(text_content));
637 }
638 for tc in tool_calls {
639 parts.push(ContentPart::ToolCall(ToolCallContentPart {
640 id: tc.id,
641 name: tc.name,
642 arguments: tc.arguments,
643 }));
644 }
645 Self {
646 id: MessageId::new(),
647 role: MessageRole::Agent,
648 content: parts,
649 phase: None,
650 thinking: None,
651 thinking_signature: None,
652 controls: None,
653 metadata: None,
654 external_actor: None,
655 created_at: Utc::now(),
656 }
657 }
658
659 pub fn system(content: impl Into<String>) -> Self {
661 Self {
662 id: MessageId::new(),
663 role: MessageRole::System,
664 content: vec![ContentPart::text(content)],
665 phase: None,
666 thinking: None,
667 thinking_signature: None,
668 controls: None,
669 metadata: None,
670 external_actor: None,
671 created_at: Utc::now(),
672 }
673 }
674
675 pub fn tool_result(
677 tool_call_id: impl Into<String>,
678 result: Option<serde_json::Value>,
679 error: Option<String>,
680 ) -> Self {
681 let tool_call_id = tool_call_id.into();
682 Self {
683 id: MessageId::new(),
684 role: MessageRole::ToolResult,
685 content: vec![ContentPart::ToolResult(ToolResultContentPart::new(
686 tool_call_id,
687 result,
688 error,
689 ))],
690 phase: None,
691 thinking: None,
692 thinking_signature: None,
693 controls: None,
694 metadata: None,
695 external_actor: None,
696 created_at: Utc::now(),
697 }
698 }
699
700 pub fn tool_result_with_images(
706 tool_call_id: impl Into<String>,
707 result: Option<serde_json::Value>,
708 images: Vec<crate::tools::ToolResultImage>,
709 ) -> Self {
710 let tool_call_id = tool_call_id.into();
711 let mut content = vec![ContentPart::ToolResult(ToolResultContentPart::new(
712 tool_call_id,
713 result,
714 None,
715 ))];
716 for img in images {
717 content.push(ContentPart::Image(ImageContentPart::from_base64(
718 img.base64,
719 img.media_type,
720 )));
721 }
722 Self {
723 id: MessageId::new(),
724 role: MessageRole::ToolResult,
725 content,
726 phase: None,
727 thinking: None,
728 thinking_signature: None,
729 controls: None,
730 metadata: None,
731 external_actor: None,
732 created_at: Utc::now(),
733 }
734 }
735
736 pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
738 self.phase = Some(phase);
739 self
740 }
741
742 pub fn tool_call_id(&self) -> Option<&str> {
746 self.content.iter().find_map(|p| match p {
747 ContentPart::ToolResult(tr) => Some(tr.tool_call_id.as_str()),
748 _ => None,
749 })
750 }
751
752 pub fn text(&self) -> Option<&str> {
754 self.content.iter().find_map(|p| p.as_text())
755 }
756
757 pub fn tool_calls(&self) -> Vec<&ToolCallContentPart> {
759 self.content
760 .iter()
761 .filter_map(|p| match p {
762 ContentPart::ToolCall(tc) => Some(tc),
763 _ => None,
764 })
765 .collect()
766 }
767
768 pub fn has_tool_calls(&self) -> bool {
770 self.content
771 .iter()
772 .any(|p| matches!(p, ContentPart::ToolCall(_)))
773 }
774
775 pub fn tool_result_content(&self) -> Option<&ToolResultContentPart> {
777 self.content.iter().find_map(|p| match p {
778 ContentPart::ToolResult(tr) => Some(tr),
779 _ => None,
780 })
781 }
782
783 pub fn content_to_llm_string(&self) -> String {
785 self.content
786 .iter()
787 .map(|part| match part {
788 ContentPart::Text(t) => t.text.clone(),
789 ContentPart::Image(_) => "[Image]".to_string(),
790 ContentPart::ImageFile(_) => "[Image File]".to_string(),
791 ContentPart::ToolCall(tc) => {
792 format!(
793 "Tool call: {} with arguments: {}",
794 tc.name,
795 serde_json::to_string(&tc.arguments).unwrap_or_default()
796 )
797 }
798 ContentPart::ToolResult(tr) => {
799 if let Some(err) = &tr.error {
800 format!("Tool error: {}", err)
801 } else if let Some(res) = &tr.result {
802 serde_json::to_string(res).unwrap_or_else(|_| "{}".to_string())
803 } else {
804 "{}".to_string()
805 }
806 }
807 })
808 .collect::<Vec<_>>()
809 .join("\n")
810 }
811
812 pub fn to_openai_format(&self) -> serde_json::Value {
821 let role = match self.role {
822 MessageRole::System => "system",
823 MessageRole::User => "user",
824 MessageRole::Agent => "assistant",
825 MessageRole::ToolResult => "tool",
826 };
827
828 if self.role == MessageRole::ToolResult {
830 let tool_call_id = self.tool_call_id().unwrap_or("");
831 let content = self
832 .content
833 .iter()
834 .find_map(|p| match p {
835 ContentPart::ToolResult(tr) => {
836 if let Some(error) = &tr.error {
837 Some(format!("Error: {}", error))
838 } else if let Some(result) = &tr.result {
839 Some(serde_json::to_string(result).unwrap_or_else(|_| "{}".to_string()))
840 } else {
841 Some("{}".to_string())
842 }
843 }
844 _ => None,
845 })
846 .unwrap_or_else(|| "{}".to_string());
847
848 return serde_json::json!({
849 "role": role,
850 "content": content,
851 "tool_call_id": tool_call_id
852 });
853 }
854
855 if self.role == MessageRole::Agent {
857 let tool_calls: Vec<serde_json::Value> = self
858 .content
859 .iter()
860 .filter_map(|p| match p {
861 ContentPart::ToolCall(tc) => Some(serde_json::json!({
862 "id": tc.id,
863 "type": "function",
864 "function": {
865 "name": tc.name,
866 "arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string())
867 }
868 })),
869 _ => None,
870 })
871 .collect();
872
873 let text_content: String = self
874 .content
875 .iter()
876 .filter_map(|p| match p {
877 ContentPart::Text(t) => Some(t.text.clone()),
878 _ => None,
879 })
880 .collect::<Vec<_>>()
881 .join("\n");
882
883 if tool_calls.is_empty() {
884 return serde_json::json!({
885 "role": role,
886 "content": text_content
887 });
888 } else {
889 let mut result = serde_json::json!({
890 "role": role,
891 "tool_calls": tool_calls
892 });
893 if !text_content.is_empty() {
894 result["content"] = serde_json::json!(text_content);
895 }
896 return result;
897 }
898 }
899
900 let content = self.content_to_openai_format();
902 serde_json::json!({
903 "role": role,
904 "content": content
905 })
906 }
907
908 fn content_to_openai_format(&self) -> serde_json::Value {
910 if self.content.len() == 1
912 && let ContentPart::Text(t) = &self.content[0]
913 {
914 return serde_json::json!(t.text);
915 }
916
917 let parts: Vec<serde_json::Value> = self
919 .content
920 .iter()
921 .filter_map(|part| part.to_openai_format())
922 .collect();
923
924 if parts.is_empty() {
925 return serde_json::json!("");
926 }
927
928 if parts.len() == 1
930 && let Some(text) = parts[0].get("text")
931 {
932 return text.clone();
933 }
934
935 serde_json::json!(parts)
936 }
937}
938
939pub fn patch_dangling_tool_calls(messages: &[Message]) -> Vec<Message> {
949 let mut result = Vec::new();
950
951 for (i, msg) in messages.iter().enumerate() {
952 result.push(msg.clone());
953
954 if msg.role == MessageRole::Agent && msg.has_tool_calls() {
956 for tc in msg.tool_calls() {
957 let has_result = messages[(i + 1)..]
959 .iter()
960 .any(|m| m.role == MessageRole::ToolResult && m.tool_call_id() == Some(&tc.id));
961
962 if !has_result {
963 result.push(Message::tool_result(
964 &tc.id,
965 None,
966 Some(
967 "cancelled - another message came in before it could be completed"
968 .to_string(),
969 ),
970 ));
971 }
972 }
973 }
974 }
975
976 result
977}
978
979#[cfg(test)]
980mod tests {
981 use super::*;
982 use crate::tool_types::ToolCall;
983
984 #[test]
985 fn test_patch_dangling_tool_calls_no_tool_calls() {
986 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
987 let patched = patch_dangling_tool_calls(&messages);
988 assert_eq!(patched.len(), 2);
989 }
990
991 #[test]
992 fn test_patch_dangling_tool_calls_with_result() {
993 let tool_call = ToolCall {
994 id: "call_123".to_string(),
995 name: "get_weather".to_string(),
996 arguments: serde_json::json!({"city": "NYC"}),
997 };
998
999 let messages = vec![
1000 Message::user("What's the weather?"),
1001 Message::assistant_with_tools("Let me check", vec![tool_call]),
1002 Message::tool_result("call_123", Some(serde_json::json!({"temp": 72})), None),
1003 ];
1004
1005 let patched = patch_dangling_tool_calls(&messages);
1006 assert_eq!(patched.len(), 3);
1007 }
1008
1009 #[test]
1010 fn test_patch_dangling_tool_calls_missing_result() {
1011 let tool_call = ToolCall {
1012 id: "call_456".to_string(),
1013 name: "search_web".to_string(),
1014 arguments: serde_json::json!({"query": "rust"}),
1015 };
1016
1017 let messages = vec![
1018 Message::user("Search for rust"),
1019 Message::assistant_with_tools("Searching...", vec![tool_call]),
1020 Message::user("Actually, never mind"),
1021 ];
1022
1023 let patched = patch_dangling_tool_calls(&messages);
1024 assert_eq!(patched.len(), 4);
1026 assert_eq!(patched[2].role, MessageRole::ToolResult);
1027 assert_eq!(patched[2].tool_call_id(), Some("call_456"));
1028 }
1029
1030 #[test]
1031 fn test_user_message() {
1032 let msg = Message::user("Hello");
1033 assert_eq!(msg.role, MessageRole::User);
1034 assert_eq!(msg.text(), Some("Hello"));
1035 }
1036
1037 #[test]
1038 fn test_assistant_message() {
1039 let msg = Message::assistant("Hi there!");
1040 assert_eq!(msg.role, MessageRole::Agent);
1041 assert_eq!(msg.text(), Some("Hi there!"));
1042 }
1043
1044 #[test]
1045 fn test_tool_result_message() {
1046 let msg = Message::tool_result(
1047 "call_123",
1048 Some(serde_json::json!({"result": "success"})),
1049 None,
1050 );
1051 assert_eq!(msg.role, MessageRole::ToolResult);
1052 assert_eq!(msg.tool_call_id(), Some("call_123"));
1053 }
1054
1055 #[test]
1056 fn test_assistant_with_tools_and_text() {
1057 let tool_call = ToolCall {
1058 id: "call_123".to_string(),
1059 name: "get_weather".to_string(),
1060 arguments: serde_json::json!({"location": "Tokyo"}),
1061 };
1062 let msg = Message::assistant_with_tools("Let me check the weather.", vec![tool_call]);
1063
1064 assert_eq!(msg.role, MessageRole::Agent);
1065 assert_eq!(msg.text(), Some("Let me check the weather."));
1066 assert_eq!(msg.tool_calls().len(), 1);
1067 assert_eq!(msg.tool_calls()[0].name, "get_weather");
1068 }
1069
1070 #[test]
1071 fn test_assistant_with_tools_empty_text() {
1072 let tool_call = ToolCall {
1075 id: "call_123".to_string(),
1076 name: "search".to_string(),
1077 arguments: serde_json::json!({"query": "rust"}),
1078 };
1079 let msg = Message::assistant_with_tools("", vec![tool_call]);
1080
1081 assert_eq!(msg.role, MessageRole::Agent);
1082 assert_eq!(msg.text(), None);
1084 assert_eq!(msg.tool_calls().len(), 1);
1086 assert_eq!(msg.tool_calls()[0].name, "search");
1087 assert_eq!(msg.content.len(), 1);
1089 assert!(matches!(msg.content[0], ContentPart::ToolCall(_)));
1090 }
1091
1092 #[test]
1093 fn test_assistant_with_tools_whitespace_text() {
1094 let tool_call = ToolCall {
1096 id: "call_456".to_string(),
1097 name: "fetch".to_string(),
1098 arguments: serde_json::json!({}),
1099 };
1100 let msg = Message::assistant_with_tools(" ", vec![tool_call]);
1101
1102 assert_eq!(msg.text(), Some(" "));
1104 assert_eq!(msg.content.len(), 2); }
1106
1107 #[test]
1108 fn test_assistant_with_multiple_tool_calls() {
1109 let tool_calls = vec![
1110 ToolCall {
1111 id: "call_1".to_string(),
1112 name: "search".to_string(),
1113 arguments: serde_json::json!({"q": "a"}),
1114 },
1115 ToolCall {
1116 id: "call_2".to_string(),
1117 name: "fetch".to_string(),
1118 arguments: serde_json::json!({"url": "http://example.com"}),
1119 },
1120 ];
1121 let msg = Message::assistant_with_tools("", tool_calls);
1122
1123 assert_eq!(msg.tool_calls().len(), 2);
1124 assert_eq!(msg.content.len(), 2);
1126 }
1127
1128 #[test]
1133 fn test_to_openai_format_user_message() {
1134 let msg = Message::user("Hello, world!");
1135 let converted = msg.to_openai_format();
1136
1137 assert_eq!(converted["role"], "user");
1138 assert_eq!(converted["content"], "Hello, world!");
1139 }
1140
1141 #[test]
1142 fn test_to_openai_format_system_message() {
1143 let msg = Message::system("You are a helpful assistant.");
1144 let converted = msg.to_openai_format();
1145
1146 assert_eq!(converted["role"], "system");
1147 assert_eq!(converted["content"], "You are a helpful assistant.");
1148 }
1149
1150 #[test]
1151 fn test_to_openai_format_assistant_role_mapping() {
1152 let msg = Message::assistant("Hi there!");
1154 let converted = msg.to_openai_format();
1155
1156 assert_eq!(converted["role"], "assistant");
1157 assert_eq!(converted["content"], "Hi there!");
1158 }
1159
1160 #[test]
1161 fn test_to_openai_format_assistant_with_tool_calls() {
1162 let tool_call = ToolCall {
1163 id: "call_123".to_string(),
1164 name: "get_weather".to_string(),
1165 arguments: serde_json::json!({"location": "Tokyo"}),
1166 };
1167 let msg = Message::assistant_with_tools("Let me check.", vec![tool_call]);
1168 let converted = msg.to_openai_format();
1169
1170 assert_eq!(converted["role"], "assistant");
1171 assert_eq!(converted["content"], "Let me check.");
1172
1173 let tool_calls = converted["tool_calls"].as_array().unwrap();
1174 assert_eq!(tool_calls.len(), 1);
1175 assert_eq!(tool_calls[0]["id"], "call_123");
1176 assert_eq!(tool_calls[0]["type"], "function");
1177 assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
1178 assert_eq!(
1179 tool_calls[0]["function"]["arguments"],
1180 r#"{"location":"Tokyo"}"#
1181 );
1182 }
1183
1184 #[test]
1185 fn test_to_openai_format_assistant_tool_calls_only() {
1186 let tool_call = ToolCall {
1188 id: "call_abc".to_string(),
1189 name: "search".to_string(),
1190 arguments: serde_json::json!({"query": "rust"}),
1191 };
1192 let msg = Message::assistant_with_tools("", vec![tool_call]);
1193 let converted = msg.to_openai_format();
1194
1195 assert_eq!(converted["role"], "assistant");
1196 assert!(converted.get("content").is_none());
1198 assert!(converted["tool_calls"].is_array());
1199 }
1200
1201 #[test]
1202 fn test_to_openai_format_tool_result_role_mapping() {
1203 let msg = Message::tool_result(
1205 "call_123",
1206 Some(serde_json::json!({"temperature": 72})),
1207 None,
1208 );
1209 let converted = msg.to_openai_format();
1210
1211 assert_eq!(converted["role"], "tool");
1212 assert_eq!(converted["tool_call_id"], "call_123");
1213 assert_eq!(converted["content"], r#"{"temperature":72}"#);
1214 }
1215
1216 #[test]
1217 fn test_to_openai_format_tool_result_error() {
1218 let msg = Message::tool_result("call_456", None, Some("API timeout".to_string()));
1219 let converted = msg.to_openai_format();
1220
1221 assert_eq!(converted["role"], "tool");
1222 assert_eq!(converted["tool_call_id"], "call_456");
1223 assert_eq!(converted["content"], "Error: API timeout");
1224 }
1225
1226 #[test]
1227 fn test_to_openai_format_full_conversation() {
1228 let tool_call = ToolCall {
1230 id: "call_abc".to_string(),
1231 name: "search".to_string(),
1232 arguments: serde_json::json!({"query": "rust"}),
1233 };
1234
1235 let messages = [
1236 Message::user("Search for rust"),
1237 Message::assistant_with_tools("", vec![tool_call]),
1238 Message::tool_result(
1239 "call_abc",
1240 Some(serde_json::json!({"results": ["rust-lang.org"]})),
1241 None,
1242 ),
1243 Message::assistant("Here are the search results."),
1244 ];
1245 let converted: Vec<_> = messages.iter().map(|m| m.to_openai_format()).collect();
1246
1247 assert_eq!(converted.len(), 4);
1248 assert_eq!(converted[0]["role"], "user");
1249 assert_eq!(converted[1]["role"], "assistant");
1250 assert!(converted[1]["tool_calls"].is_array());
1251 assert_eq!(converted[2]["role"], "tool");
1252 assert_eq!(converted[2]["tool_call_id"], "call_abc");
1253 assert_eq!(converted[3]["role"], "assistant");
1254 }
1255
1256 #[test]
1261 fn test_content_part_to_openai_format_text() {
1262 let part = ContentPart::text("Hello");
1263 let converted = part.to_openai_format().unwrap();
1264
1265 assert_eq!(converted["type"], "text");
1266 assert_eq!(converted["text"], "Hello");
1267 }
1268
1269 #[test]
1270 fn test_content_part_to_openai_format_image_url() {
1271 let part = ContentPart::image_url("https://example.com/img.png");
1272 let converted = part.to_openai_format().unwrap();
1273
1274 assert_eq!(converted["type"], "image_url");
1275 assert_eq!(converted["image_url"]["url"], "https://example.com/img.png");
1276 }
1277
1278 #[test]
1279 fn test_content_part_to_openai_format_image_base64() {
1280 let part = ContentPart::Image(ImageContentPart::from_base64("abc123", "image/jpeg"));
1281 let converted = part.to_openai_format().unwrap();
1282
1283 assert_eq!(converted["type"], "image_url");
1284 assert_eq!(
1285 converted["image_url"]["url"],
1286 "data:image/jpeg;base64,abc123"
1287 );
1288 }
1289
1290 #[test]
1291 fn test_content_part_to_openai_format_tool_call_returns_none() {
1292 let part = ContentPart::tool_call("call_1", "search", serde_json::json!({}));
1294 assert!(part.to_openai_format().is_none());
1295 }
1296
1297 #[test]
1298 fn test_content_part_to_openai_format_tool_result_returns_none() {
1299 let part = ContentPart::tool_result("call_1", Some(serde_json::json!({})), None);
1301 assert!(part.to_openai_format().is_none());
1302 }
1303
1304 #[test]
1305 fn test_execution_phase_from_has_tool_calls() {
1306 assert_eq!(
1307 ExecutionPhase::from_has_tool_calls(true),
1308 ExecutionPhase::Commentary
1309 );
1310 assert_eq!(
1311 ExecutionPhase::from_has_tool_calls(false),
1312 ExecutionPhase::FinalAnswer
1313 );
1314 }
1315
1316 #[test]
1317 fn test_execution_phase_refine_streamed_hint_monotonic() {
1318 use ExecutionPhase::{Commentary, FinalAnswer};
1319 assert_eq!(
1321 ExecutionPhase::refine_streamed_hint(None, Commentary),
1322 Some(Commentary)
1323 );
1324 assert_eq!(
1325 ExecutionPhase::refine_streamed_hint(None, FinalAnswer),
1326 Some(FinalAnswer)
1327 );
1328 assert_eq!(
1330 ExecutionPhase::refine_streamed_hint(Some(Commentary), FinalAnswer),
1331 Some(Commentary)
1332 );
1333 assert_eq!(
1334 ExecutionPhase::refine_streamed_hint(Some(FinalAnswer), Commentary),
1335 Some(FinalAnswer)
1336 );
1337 assert_eq!(
1340 ExecutionPhase::refine_streamed_hint(Some(Commentary), Commentary),
1341 Some(Commentary)
1342 );
1343 }
1344
1345 #[test]
1346 fn test_execution_phase_serde_roundtrip() {
1347 let commentary = ExecutionPhase::Commentary;
1348 let json = serde_json::to_string(&commentary).unwrap();
1349 assert_eq!(json, "\"commentary\"");
1350 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1351 assert_eq!(deserialized, ExecutionPhase::Commentary);
1352
1353 let final_answer = ExecutionPhase::FinalAnswer;
1354 let json = serde_json::to_string(&final_answer).unwrap();
1355 assert_eq!(json, "\"final_answer\"");
1356 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1357 assert_eq!(deserialized, ExecutionPhase::FinalAnswer);
1358 }
1359
1360 #[test]
1361 fn test_execution_phase_deserialize_legacy() {
1362 let legacy_in_progress: ExecutionPhase = serde_json::from_str("\"in_progress\"").unwrap();
1363 assert_eq!(legacy_in_progress, ExecutionPhase::Commentary);
1364
1365 let legacy_completed: ExecutionPhase = serde_json::from_str("\"completed\"").unwrap();
1366 assert_eq!(legacy_completed, ExecutionPhase::FinalAnswer);
1367 }
1368
1369 #[test]
1370 fn test_execution_phase_deserialize_unknown_fails() {
1371 let result = serde_json::from_str::<ExecutionPhase>("\"bogus\"");
1372 assert!(result.is_err());
1373 }
1374
1375 #[test]
1376 fn test_message_with_phase() {
1377 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::Commentary);
1378 assert_eq!(msg.phase, Some(ExecutionPhase::Commentary));
1379 }
1380
1381 #[test]
1382 fn test_message_phase_skipped_when_none() {
1383 let msg = Message::assistant("Hello");
1384 let json = serde_json::to_value(&msg).unwrap();
1385 assert!(json.get("phase").is_none());
1386 }
1387
1388 #[test]
1389 fn test_message_phase_included_when_set() {
1390 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::FinalAnswer);
1391 let json = serde_json::to_value(&msg).unwrap();
1392 assert_eq!(json.get("phase").unwrap(), "final_answer");
1393 }
1394
1395 #[test]
1396 fn test_resolve_hints_both_none() {
1397 let result = Controls::resolve_hints(None, None);
1398 assert!(result.is_empty());
1399 }
1400
1401 #[test]
1402 fn test_resolve_hints_session_only() {
1403 let mut session = std::collections::HashMap::new();
1404 session.insert("key1".into(), serde_json::json!("val1"));
1405 session.insert("key2".into(), serde_json::json!(42));
1406
1407 let result = Controls::resolve_hints(Some(&session), None);
1408 assert_eq!(result.len(), 2);
1409 assert_eq!(result["key1"], serde_json::json!("val1"));
1410 assert_eq!(result["key2"], serde_json::json!(42));
1411 }
1412
1413 #[test]
1414 fn test_resolve_hints_message_only() {
1415 let mut message = std::collections::HashMap::new();
1416 message.insert("key1".into(), serde_json::json!(true));
1417
1418 let result = Controls::resolve_hints(None, Some(&message));
1419 assert_eq!(result.len(), 1);
1420 assert_eq!(result["key1"], serde_json::json!(true));
1421 }
1422
1423 #[test]
1424 fn test_resolve_hints_message_overrides_session() {
1425 let mut session = std::collections::HashMap::new();
1426 session.insert("shared".into(), serde_json::json!("session_val"));
1427 session.insert("session_only".into(), serde_json::json!(1));
1428
1429 let mut message = std::collections::HashMap::new();
1430 message.insert("shared".into(), serde_json::json!("message_val"));
1431 message.insert("message_only".into(), serde_json::json!(2));
1432
1433 let result = Controls::resolve_hints(Some(&session), Some(&message));
1434 assert_eq!(result.len(), 3);
1435 assert_eq!(result["shared"], serde_json::json!("message_val"));
1436 assert_eq!(result["session_only"], serde_json::json!(1));
1437 assert_eq!(result["message_only"], serde_json::json!(2));
1438 }
1439
1440 #[test]
1441 fn test_controls_hints_serde_roundtrip() {
1442 let mut hints = std::collections::HashMap::new();
1443 hints.insert("setup_connection".into(), serde_json::json!(true));
1444 hints.insert("theme".into(), serde_json::json!("dark"));
1445
1446 let controls = Controls {
1447 hints: Some(hints),
1448 ..Default::default()
1449 };
1450
1451 let json = serde_json::to_value(&controls).unwrap();
1452 let deserialized: Controls = serde_json::from_value(json).unwrap();
1453 let h = deserialized.hints.unwrap();
1454 assert_eq!(h["setup_connection"], serde_json::json!(true));
1455 assert_eq!(h["theme"], serde_json::json!("dark"));
1456 }
1457}