Skip to main content

aster/conversation/
message.rs

1use crate::mcp_utils::ToolResult;
2use chrono::Utc;
3use rmcp::model::{
4    AnnotateAble, CallToolRequestParam, CallToolResult, Content, ImageContent, JsonObject,
5    PromptMessage, PromptMessageContent, PromptMessageRole, RawContent, RawImageContent,
6    RawTextContent, ResourceContents, Role, TextContent,
7};
8use serde::{Deserialize, Deserializer, Serialize};
9use std::collections::HashSet;
10use std::fmt;
11use utoipa::ToSchema;
12
13use crate::conversation::tool_result_serde;
14use crate::utils::sanitize_unicode_tags;
15
16#[derive(ToSchema)]
17pub enum ToolCallResult<T> {
18    Success { value: T },
19    Error { error: String },
20}
21
22/// Custom deserializer for MessageContent that sanitizes Unicode Tags in text content
23fn deserialize_sanitized_content<'de, D>(deserializer: D) -> Result<Vec<MessageContent>, D::Error>
24where
25    D: Deserializer<'de>,
26{
27    use serde::de::Error;
28
29    let mut raw: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
30
31    // Filter out old "conversationCompacted" messages from pre-14.0
32    raw.retain(|item| item.get("type").and_then(|v| v.as_str()) != Some("conversationCompacted"));
33
34    let mut content: Vec<MessageContent> = serde_json::from_value(serde_json::Value::Array(raw))
35        .map_err(|e| Error::custom(format!("Failed to deserialize MessageContent: {}", e)))?;
36
37    for message_content in &mut content {
38        if let MessageContent::Text(text_content) = message_content {
39            let original = &text_content.text;
40            let sanitized = sanitize_unicode_tags(original);
41            if *original != sanitized {
42                tracing::info!(
43                    original = %original,
44                    sanitized = %sanitized,
45                    removed_count = original.len() - sanitized.len(),
46                    "Unicode Tags sanitized during Message deserialization"
47                );
48                text_content.text = sanitized;
49            }
50        }
51    }
52
53    Ok(content)
54}
55
56/// Provider-specific metadata for tool requests/responses.
57/// Allows providers to store custom data without polluting the core model.
58pub type ProviderMetadata = serde_json::Map<String, serde_json::Value>;
59
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62#[derive(ToSchema)]
63pub struct ToolRequest {
64    pub id: String,
65    #[serde(with = "tool_result_serde")]
66    #[schema(value_type = Object)]
67    pub tool_call: ToolResult<CallToolRequestParam>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    #[schema(value_type = Object)]
70    pub metadata: Option<ProviderMetadata>,
71    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
72    #[schema(value_type = Object)]
73    pub tool_meta: Option<serde_json::Value>,
74}
75
76impl ToolRequest {
77    pub fn to_readable_string(&self) -> String {
78        match &self.tool_call {
79            Ok(tool_call) => {
80                format!(
81                    "Tool: {}, Args: {}",
82                    tool_call.name,
83                    serde_json::to_string_pretty(&tool_call.arguments)
84                        .unwrap_or_else(|_| "<<invalid json>>".to_string())
85                )
86            }
87            Err(e) => format!("Invalid tool call: {}", e),
88        }
89    }
90}
91
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94#[derive(ToSchema)]
95pub struct ToolResponse {
96    pub id: String,
97    #[serde(with = "tool_result_serde::call_tool_result")]
98    #[schema(value_type = Object)]
99    pub tool_result: ToolResult<CallToolResult>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    #[schema(value_type = Object)]
102    pub metadata: Option<ProviderMetadata>,
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107#[derive(ToSchema)]
108pub struct ToolConfirmationRequest {
109    pub id: String,
110    pub tool_name: String,
111    pub arguments: JsonObject,
112    pub prompt: Option<String>,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
116#[serde(tag = "actionType", rename_all = "camelCase")]
117pub enum ActionRequiredData {
118    #[serde(rename_all = "camelCase")]
119    ToolConfirmation {
120        id: String,
121        tool_name: String,
122        arguments: JsonObject,
123        prompt: Option<String>,
124    },
125    Elicitation {
126        id: String,
127        message: String,
128        requested_schema: serde_json::Value,
129    },
130    ElicitationResponse {
131        id: String,
132        user_data: serde_json::Value,
133    },
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
137#[serde(rename_all = "camelCase")]
138pub struct ActionRequired {
139    pub data: ActionRequiredData,
140}
141
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
143pub struct ThinkingContent {
144    pub thinking: String,
145    pub signature: String,
146}
147
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
149pub struct RedactedThinkingContent {
150    pub data: String,
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
154#[serde(rename_all = "camelCase")]
155pub struct FrontendToolRequest {
156    pub id: String,
157    #[serde(with = "tool_result_serde")]
158    #[schema(value_type = Object)]
159    pub tool_call: ToolResult<CallToolRequestParam>,
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
163#[serde(rename_all = "camelCase")]
164pub enum SystemNotificationType {
165    ThinkingMessage,
166    InlineMessage,
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
170#[serde(rename_all = "camelCase")]
171pub struct SystemNotificationContent {
172    pub notification_type: SystemNotificationType,
173    pub msg: String,
174}
175
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
177/// Content passed inside a message, which can be both simple content and tool content
178#[serde(tag = "type", rename_all = "camelCase")]
179pub enum MessageContent {
180    Text(TextContent),
181    Image(ImageContent),
182    ToolRequest(ToolRequest),
183    ToolResponse(ToolResponse),
184    ToolConfirmationRequest(ToolConfirmationRequest),
185    ActionRequired(ActionRequired),
186    FrontendToolRequest(FrontendToolRequest),
187    Thinking(ThinkingContent),
188    RedactedThinking(RedactedThinkingContent),
189    SystemNotification(SystemNotificationContent),
190}
191
192impl fmt::Display for MessageContent {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        match self {
195            MessageContent::Text(t) => write!(f, "{}", t.text),
196            MessageContent::Image(i) => write!(f, "[Image: {}]", i.mime_type),
197            MessageContent::ToolRequest(r) => {
198                write!(f, "[ToolRequest: {}]", r.to_readable_string())
199            }
200            MessageContent::ToolResponse(r) => write!(
201                f,
202                "[ToolResponse: {}]",
203                match &r.tool_result {
204                    Ok(result) => format!("{} content item(s)", result.content.len()),
205                    Err(e) => format!("Error: {e}"),
206                }
207            ),
208            MessageContent::ToolConfirmationRequest(r) => {
209                write!(f, "[ToolConfirmationRequest: {}]", r.tool_name)
210            }
211            MessageContent::ActionRequired(a) => match &a.data {
212                ActionRequiredData::ToolConfirmation { tool_name, .. } => {
213                    write!(f, "[ActionRequired: ToolConfirmation for {}]", tool_name)
214                }
215                ActionRequiredData::Elicitation { message, .. } => {
216                    write!(f, "[ActionRequired: Elicitation - {}]", message)
217                }
218                ActionRequiredData::ElicitationResponse { id, .. } => {
219                    write!(f, "[ActionRequired: ElicitationResponse for {}]", id)
220                }
221            },
222            MessageContent::FrontendToolRequest(r) => match &r.tool_call {
223                Ok(tool_call) => write!(f, "[FrontendToolRequest: {}]", tool_call.name),
224                Err(e) => write!(f, "[FrontendToolRequest: Error: {}]", e),
225            },
226            MessageContent::Thinking(t) => write!(f, "[Thinking: {}]", t.thinking),
227            MessageContent::RedactedThinking(_r) => write!(f, "[RedactedThinking]"),
228            MessageContent::SystemNotification(r) => {
229                write!(f, "[SystemNotification: {}]", r.msg)
230            }
231        }
232    }
233}
234
235impl MessageContent {
236    pub fn text<S: Into<String>>(text: S) -> Self {
237        MessageContent::Text(
238            RawTextContent {
239                text: text.into(),
240                meta: None,
241            }
242            .no_annotation(),
243        )
244    }
245
246    pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
247        MessageContent::Image(
248            RawImageContent {
249                data: data.into(),
250                mime_type: mime_type.into(),
251                meta: None,
252            }
253            .no_annotation(),
254        )
255    }
256
257    pub fn tool_request<S: Into<String>>(
258        id: S,
259        tool_call: ToolResult<CallToolRequestParam>,
260    ) -> Self {
261        MessageContent::ToolRequest(ToolRequest {
262            id: id.into(),
263            tool_call,
264            metadata: None,
265            tool_meta: None,
266        })
267    }
268
269    pub fn tool_request_with_metadata<S: Into<String>>(
270        id: S,
271        tool_call: ToolResult<CallToolRequestParam>,
272        metadata: Option<&ProviderMetadata>,
273    ) -> Self {
274        MessageContent::ToolRequest(ToolRequest {
275            id: id.into(),
276            tool_call,
277            metadata: metadata.cloned(),
278            tool_meta: None,
279        })
280    }
281
282    pub fn tool_response<S: Into<String>>(id: S, tool_result: ToolResult<CallToolResult>) -> Self {
283        MessageContent::ToolResponse(ToolResponse {
284            id: id.into(),
285            tool_result,
286            metadata: None,
287        })
288    }
289
290    pub fn tool_response_with_metadata<S: Into<String>>(
291        id: S,
292        tool_result: ToolResult<CallToolResult>,
293        metadata: Option<&ProviderMetadata>,
294    ) -> Self {
295        MessageContent::ToolResponse(ToolResponse {
296            id: id.into(),
297            tool_result,
298            metadata: metadata.cloned(),
299        })
300    }
301
302    pub fn action_required<S: Into<String>>(
303        id: S,
304        tool_name: String,
305        arguments: JsonObject,
306        prompt: Option<String>,
307    ) -> Self {
308        MessageContent::ActionRequired(ActionRequired {
309            data: ActionRequiredData::ToolConfirmation {
310                id: id.into(),
311                tool_name,
312                arguments,
313                prompt,
314            },
315        })
316    }
317
318    pub fn action_required_elicitation<S: Into<String>>(
319        id: S,
320        message: String,
321        requested_schema: serde_json::Value,
322    ) -> Self {
323        MessageContent::ActionRequired(ActionRequired {
324            data: ActionRequiredData::Elicitation {
325                id: id.into(),
326                message,
327                requested_schema,
328            },
329        })
330    }
331
332    pub fn action_required_elicitation_response<S: Into<String>>(
333        id: S,
334        user_data: serde_json::Value,
335    ) -> Self {
336        MessageContent::ActionRequired(ActionRequired {
337            data: ActionRequiredData::ElicitationResponse {
338                id: id.into(),
339                user_data,
340            },
341        })
342    }
343
344    pub fn thinking<S1: Into<String>, S2: Into<String>>(thinking: S1, signature: S2) -> Self {
345        MessageContent::Thinking(ThinkingContent {
346            thinking: thinking.into(),
347            signature: signature.into(),
348        })
349    }
350
351    pub fn redacted_thinking<S: Into<String>>(data: S) -> Self {
352        MessageContent::RedactedThinking(RedactedThinkingContent { data: data.into() })
353    }
354
355    pub fn frontend_tool_request<S: Into<String>>(
356        id: S,
357        tool_call: ToolResult<CallToolRequestParam>,
358    ) -> Self {
359        MessageContent::FrontendToolRequest(FrontendToolRequest {
360            id: id.into(),
361            tool_call,
362        })
363    }
364
365    pub fn system_notification<S: Into<String>>(
366        notification_type: SystemNotificationType,
367        msg: S,
368    ) -> Self {
369        MessageContent::SystemNotification(SystemNotificationContent {
370            notification_type,
371            msg: msg.into(),
372        })
373    }
374
375    pub fn as_system_notification(&self) -> Option<&SystemNotificationContent> {
376        if let MessageContent::SystemNotification(ref notification) = self {
377            Some(notification)
378        } else {
379            None
380        }
381    }
382
383    pub fn as_tool_request(&self) -> Option<&ToolRequest> {
384        if let MessageContent::ToolRequest(ref tool_request) = self {
385            Some(tool_request)
386        } else {
387            None
388        }
389    }
390
391    pub fn as_tool_response(&self) -> Option<&ToolResponse> {
392        if let MessageContent::ToolResponse(ref tool_response) = self {
393            Some(tool_response)
394        } else {
395            None
396        }
397    }
398
399    pub fn as_action_required(&self) -> Option<&ActionRequired> {
400        if let MessageContent::ActionRequired(ref action_required) = self {
401            Some(action_required)
402        } else {
403            None
404        }
405    }
406
407    pub fn as_tool_response_text(&self) -> Option<String> {
408        if let Some(tool_response) = self.as_tool_response() {
409            if let Ok(result) = &tool_response.tool_result {
410                let texts: Vec<String> = result
411                    .content
412                    .iter()
413                    .filter_map(|content| content.as_text().map(|t| t.text.to_string()))
414                    .collect();
415                if !texts.is_empty() {
416                    return Some(texts.join("\n"));
417                }
418            }
419        }
420        None
421    }
422
423    /// Get the text content if this is a TextContent variant
424    pub fn as_text(&self) -> Option<&str> {
425        match self {
426            MessageContent::Text(text) => Some(&text.text),
427            _ => None,
428        }
429    }
430
431    /// Get the thinking content if this is a ThinkingContent variant
432    pub fn as_thinking(&self) -> Option<&ThinkingContent> {
433        match self {
434            MessageContent::Thinking(thinking) => Some(thinking),
435            _ => None,
436        }
437    }
438
439    /// Get the redacted thinking content if this is a RedactedThinkingContent variant
440    pub fn as_redacted_thinking(&self) -> Option<&RedactedThinkingContent> {
441        match self {
442            MessageContent::RedactedThinking(redacted) => Some(redacted),
443            _ => None,
444        }
445    }
446}
447
448impl From<Content> for MessageContent {
449    fn from(content: Content) -> Self {
450        match content.raw {
451            RawContent::Text(text) => {
452                MessageContent::Text(text.optional_annotate(content.annotations))
453            }
454            RawContent::Image(image) => {
455                MessageContent::Image(image.optional_annotate(content.annotations))
456            }
457            RawContent::ResourceLink(_link) => MessageContent::text("[Resource link]"),
458            RawContent::Resource(resource) => {
459                let text = match &resource.resource {
460                    ResourceContents::TextResourceContents { text, .. } => text.clone(),
461                    ResourceContents::BlobResourceContents { blob, .. } => {
462                        format!("[Binary content: {}]", blob.clone())
463                    }
464                };
465                MessageContent::text(text)
466            }
467            RawContent::Audio(_) => {
468                MessageContent::text("[Audio content: not supported]".to_string())
469            }
470        }
471    }
472}
473
474impl From<PromptMessage> for Message {
475    fn from(prompt_message: PromptMessage) -> Self {
476        // Create a new message with the appropriate role
477        let message = match prompt_message.role {
478            PromptMessageRole::User => Message::user(),
479            PromptMessageRole::Assistant => Message::assistant(),
480        };
481
482        // Convert and add the content
483        let content = match prompt_message.content {
484            PromptMessageContent::Text { text } => MessageContent::text(text),
485            PromptMessageContent::Image { image } => {
486                MessageContent::image(image.data.clone(), image.mime_type.clone())
487            }
488            PromptMessageContent::ResourceLink { .. } => MessageContent::text("[Resource link]"),
489            PromptMessageContent::Resource { resource } => {
490                // For resources, convert to text content with the resource text
491                match &resource.resource {
492                    ResourceContents::TextResourceContents { text, .. } => {
493                        MessageContent::text(text.clone())
494                    }
495                    ResourceContents::BlobResourceContents { blob, .. } => {
496                        MessageContent::text(format!("[Binary content: {}]", blob.clone()))
497                    }
498                }
499            }
500        };
501
502        message.with_content(content)
503    }
504}
505
506#[derive(ToSchema, Clone, Copy, PartialEq, Serialize, Deserialize, Debug)]
507/// Metadata for message visibility
508#[serde(rename_all = "camelCase")]
509pub struct MessageMetadata {
510    /// Whether the message should be visible to the user in the UI
511    pub user_visible: bool,
512    /// Whether the message should be included in the agent's context window
513    pub agent_visible: bool,
514}
515
516impl Default for MessageMetadata {
517    fn default() -> Self {
518        MessageMetadata {
519            user_visible: true,
520            agent_visible: true,
521        }
522    }
523}
524
525impl MessageMetadata {
526    /// Create metadata for messages visible only to the agent
527    pub fn agent_only() -> Self {
528        MessageMetadata {
529            user_visible: false,
530            agent_visible: true,
531        }
532    }
533
534    /// Create metadata for messages visible only to the user
535    pub fn user_only() -> Self {
536        MessageMetadata {
537            user_visible: true,
538            agent_visible: false,
539        }
540    }
541
542    /// Create metadata for messages visible to neither user nor agent (archived)
543    pub fn invisible() -> Self {
544        MessageMetadata {
545            user_visible: false,
546            agent_visible: false,
547        }
548    }
549
550    /// Return a copy with agent_visible set to false
551    pub fn with_agent_invisible(self) -> Self {
552        Self {
553            agent_visible: false,
554            ..self
555        }
556    }
557
558    /// Return a copy with user_visible set to false
559    pub fn with_user_invisible(self) -> Self {
560        Self {
561            user_visible: false,
562            ..self
563        }
564    }
565
566    /// Return a copy with agent_visible set to true
567    pub fn with_agent_visible(self) -> Self {
568        Self {
569            agent_visible: true,
570            ..self
571        }
572    }
573
574    /// Return a copy with user_visible set to true
575    pub fn with_user_visible(self) -> Self {
576        Self {
577            user_visible: true,
578            ..self
579        }
580    }
581}
582
583#[derive(ToSchema, Clone, PartialEq, Serialize, Deserialize, Debug)]
584/// A message to or from an LLM
585#[serde(rename_all = "camelCase")]
586pub struct Message {
587    pub id: Option<String>,
588    pub role: Role,
589    pub created: i64,
590    #[serde(deserialize_with = "deserialize_sanitized_content")]
591    pub content: Vec<MessageContent>,
592    pub metadata: MessageMetadata,
593}
594
595impl Message {
596    pub fn new(role: Role, created: i64, content: Vec<MessageContent>) -> Self {
597        Message {
598            id: None,
599            role,
600            created,
601            content,
602            metadata: MessageMetadata::default(),
603        }
604    }
605    pub fn debug(&self) -> String {
606        format!("{:?}", self)
607    }
608
609    /// Create a new user message with the current timestamp
610    pub fn user() -> Self {
611        Message {
612            id: None,
613            role: Role::User,
614            created: Utc::now().timestamp(),
615            content: Vec::new(),
616            metadata: MessageMetadata::default(),
617        }
618    }
619
620    /// Create a new assistant message with the current timestamp
621    pub fn assistant() -> Self {
622        Message {
623            id: None,
624            role: Role::Assistant,
625            created: Utc::now().timestamp(),
626            content: Vec::new(),
627            metadata: MessageMetadata::default(),
628        }
629    }
630
631    pub fn with_id<S: Into<String>>(mut self, id: S) -> Self {
632        self.id = Some(id.into());
633        self
634    }
635
636    /// Add any MessageContent to the message
637    pub fn with_content(mut self, content: MessageContent) -> Self {
638        self.content.push(content);
639        self
640    }
641
642    /// Add text content to the message
643    pub fn with_text<S: Into<String>>(self, text: S) -> Self {
644        let raw_text = text.into();
645        let sanitized_text = sanitize_unicode_tags(&raw_text);
646
647        self.with_content(MessageContent::Text(
648            RawTextContent {
649                text: sanitized_text,
650                meta: None,
651            }
652            .no_annotation(),
653        ))
654    }
655
656    /// Add image content to the message
657    pub fn with_image<S: Into<String>, T: Into<String>>(self, data: S, mime_type: T) -> Self {
658        self.with_content(MessageContent::image(data, mime_type))
659    }
660
661    /// Add a tool request to the message
662    pub fn with_tool_request<S: Into<String>>(
663        self,
664        id: S,
665        tool_call: ToolResult<CallToolRequestParam>,
666    ) -> Self {
667        self.with_content(MessageContent::tool_request(id, tool_call))
668    }
669
670    pub fn with_tool_request_with_metadata<S: Into<String>>(
671        self,
672        id: S,
673        tool_call: ToolResult<CallToolRequestParam>,
674        metadata: Option<&ProviderMetadata>,
675        tool_meta: Option<serde_json::Value>,
676    ) -> Self {
677        self.with_content(MessageContent::ToolRequest(ToolRequest {
678            id: id.into(),
679            tool_call,
680            metadata: metadata.cloned(),
681            tool_meta,
682        }))
683    }
684
685    /// Add a tool response to the message
686    pub fn with_tool_response<S: Into<String>>(
687        self,
688        id: S,
689        result: ToolResult<CallToolResult>,
690    ) -> Self {
691        self.with_content(MessageContent::tool_response(id, result))
692    }
693
694    pub fn with_tool_response_with_metadata<S: Into<String>>(
695        self,
696        id: S,
697        result: ToolResult<CallToolResult>,
698        metadata: Option<&ProviderMetadata>,
699    ) -> Self {
700        self.with_content(MessageContent::tool_response_with_metadata(
701            id, result, metadata,
702        ))
703    }
704
705    /// Add an action required message for tool confirmation
706    pub fn with_action_required<S: Into<String>>(
707        self,
708        id: S,
709        tool_name: String,
710        arguments: JsonObject,
711        prompt: Option<String>,
712    ) -> Self {
713        self.with_content(MessageContent::action_required(
714            id, tool_name, arguments, prompt,
715        ))
716    }
717
718    pub fn with_frontend_tool_request<S: Into<String>>(
719        self,
720        id: S,
721        tool_call: ToolResult<CallToolRequestParam>,
722    ) -> Self {
723        self.with_content(MessageContent::frontend_tool_request(id, tool_call))
724    }
725
726    /// Add thinking content to the message
727    pub fn with_thinking<S1: Into<String>, S2: Into<String>>(
728        self,
729        thinking: S1,
730        signature: S2,
731    ) -> Self {
732        self.with_content(MessageContent::thinking(thinking, signature))
733    }
734
735    /// Add redacted thinking content to the message
736    pub fn with_redacted_thinking<S: Into<String>>(self, data: S) -> Self {
737        self.with_content(MessageContent::redacted_thinking(data))
738    }
739
740    /// Get the concatenated text content of the message, separated by newlines
741    pub fn as_concat_text(&self) -> String {
742        self.content
743            .iter()
744            .filter_map(|c| c.as_text())
745            .collect::<Vec<_>>()
746            .join("\n")
747    }
748
749    /// Check if the message is a tool call
750    pub fn is_tool_call(&self) -> bool {
751        self.content
752            .iter()
753            .any(|c| matches!(c, MessageContent::ToolRequest(_)))
754    }
755
756    /// Check if the message is a tool response
757    pub fn is_tool_response(&self) -> bool {
758        self.content
759            .iter()
760            .any(|c| matches!(c, MessageContent::ToolResponse(_)))
761    }
762
763    /// Retrieves all tool `id` from the message
764    pub fn get_tool_ids(&self) -> HashSet<&str> {
765        self.content
766            .iter()
767            .filter_map(|content| match content {
768                MessageContent::ToolRequest(req) => Some(req.id.as_str()),
769                MessageContent::ToolResponse(res) => Some(res.id.as_str()),
770                _ => None,
771            })
772            .collect()
773    }
774
775    /// Retrieves all tool `id` from ToolRequest messages
776    pub fn get_tool_request_ids(&self) -> HashSet<&str> {
777        self.content
778            .iter()
779            .filter_map(|content| {
780                if let MessageContent::ToolRequest(req) = content {
781                    Some(req.id.as_str())
782                } else {
783                    None
784                }
785            })
786            .collect()
787    }
788
789    /// Retrieves all tool `id` from ToolResponse messages
790    pub fn get_tool_response_ids(&self) -> HashSet<&str> {
791        self.content
792            .iter()
793            .filter_map(|content| {
794                if let MessageContent::ToolResponse(res) = content {
795                    Some(res.id.as_str())
796                } else {
797                    None
798                }
799            })
800            .collect()
801    }
802
803    /// Check if the message has only TextContent
804    pub fn has_only_text_content(&self) -> bool {
805        self.content
806            .iter()
807            .all(|c| matches!(c, MessageContent::Text(_)))
808    }
809
810    pub fn with_system_notification<S: Into<String>>(
811        self,
812        notification_type: SystemNotificationType,
813        msg: S,
814    ) -> Self {
815        self.with_content(MessageContent::system_notification(notification_type, msg))
816            .with_metadata(MessageMetadata::user_only())
817    }
818
819    /// Set the visibility metadata for the message
820    pub fn with_visibility(mut self, user_visible: bool, agent_visible: bool) -> Self {
821        self.metadata.user_visible = user_visible;
822        self.metadata.agent_visible = agent_visible;
823        self
824    }
825
826    /// Set the entire metadata for the message
827    pub fn with_metadata(mut self, metadata: MessageMetadata) -> Self {
828        self.metadata = metadata;
829        self
830    }
831
832    /// Mark the message as only visible to the user (not the agent)
833    pub fn user_only(mut self) -> Self {
834        self.metadata.user_visible = true;
835        self.metadata.agent_visible = false;
836        self
837    }
838
839    /// Mark the message as only visible to the agent (not the user)
840    pub fn agent_only(mut self) -> Self {
841        self.metadata.user_visible = false;
842        self.metadata.agent_visible = true;
843        self
844    }
845
846    /// Check if the message is visible to the user
847    pub fn is_user_visible(&self) -> bool {
848        self.metadata.user_visible
849    }
850
851    /// Check if the message is visible to the agent
852    pub fn is_agent_visible(&self) -> bool {
853        self.metadata.agent_visible
854    }
855}
856
857#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
858#[serde(rename_all = "camelCase")]
859pub struct TokenState {
860    pub input_tokens: i32,
861    pub output_tokens: i32,
862    pub total_tokens: i32,
863    pub accumulated_input_tokens: i32,
864    pub accumulated_output_tokens: i32,
865    pub accumulated_total_tokens: i32,
866}
867
868#[cfg(test)]
869mod tests {
870    use crate::conversation::message::{Message, MessageContent, MessageMetadata};
871    use crate::conversation::*;
872    use rmcp::model::{
873        AnnotateAble, CallToolRequestParam, PromptMessage, PromptMessageContent, PromptMessageRole,
874        RawEmbeddedResource, RawImageContent, ResourceContents,
875    };
876    use rmcp::model::{ErrorCode, ErrorData};
877    use rmcp::object;
878    use serde_json::Value;
879
880    #[test]
881    fn test_sanitize_with_text() {
882        let malicious = "Hello\u{E0041}\u{E0042}\u{E0043}world"; // Invisible "ABC"
883        let message = Message::user().with_text(malicious);
884        assert_eq!(message.as_concat_text(), "Helloworld");
885    }
886
887    #[test]
888    fn test_no_sanitize_with_text() {
889        let clean_text = "Hello world δΈ–η•Œ 🌍";
890        let message = Message::user().with_text(clean_text);
891        assert_eq!(message.as_concat_text(), clean_text);
892    }
893
894    #[test]
895    fn test_message_serialization() {
896        let message = Message::assistant()
897            .with_text("Hello, I'll help you with that.")
898            .with_tool_request(
899                "tool123",
900                Ok(CallToolRequestParam {
901                    name: "test_tool".into(),
902                    arguments: Some(object!({"param": "value"})),
903                }),
904            );
905
906        let json_str = serde_json::to_string_pretty(&message).unwrap();
907        println!("Serialized message: {}", json_str);
908
909        // Parse back to Value to check structure
910        let value: Value = serde_json::from_str(&json_str).unwrap();
911
912        // Check top-level fields
913        assert_eq!(value["role"], "assistant");
914        assert!(value["created"].is_i64());
915        assert!(value["content"].is_array());
916
917        // Check content items
918        let content = &value["content"];
919
920        // First item should be text
921        assert_eq!(content[0]["type"], "text");
922        assert_eq!(content[0]["text"], "Hello, I'll help you with that.");
923
924        // Second item should be toolRequest
925        assert_eq!(content[1]["type"], "toolRequest");
926        assert_eq!(content[1]["id"], "tool123");
927
928        // Check tool_call serialization
929        assert_eq!(content[1]["toolCall"]["status"], "success");
930        assert_eq!(content[1]["toolCall"]["value"]["name"], "test_tool");
931        assert_eq!(
932            content[1]["toolCall"]["value"]["arguments"]["param"],
933            "value"
934        );
935    }
936
937    #[test]
938    fn test_error_serialization() {
939        let message = Message::assistant().with_tool_request(
940            "tool123",
941            Err(ErrorData {
942                code: ErrorCode::INTERNAL_ERROR,
943                message: std::borrow::Cow::from("Something went wrong".to_string()),
944                data: None,
945            }),
946        );
947
948        let json_str = serde_json::to_string_pretty(&message).unwrap();
949        println!("Serialized error: {}", json_str);
950
951        // Parse back to Value to check structure
952        let value: Value = serde_json::from_str(&json_str).unwrap();
953
954        // Check tool_call serialization with error
955        let tool_call = &value["content"][0]["toolCall"];
956        assert_eq!(tool_call["status"], "error");
957        assert_eq!(tool_call["error"], "-32603: Something went wrong");
958    }
959
960    #[test]
961    fn test_deserialization() {
962        // Create a JSON string with our new format
963        let json_str = r#"{
964            "role": "assistant",
965            "created": 1740171566,
966            "content": [
967                {
968                    "type": "text",
969                    "text": "I'll help you with that."
970                },
971                {
972                    "type": "toolRequest",
973                    "id": "tool123",
974                    "toolCall": {
975                        "status": "success",
976                        "value": {
977                            "name": "test_tool",
978                            "arguments": {"param": "value"}
979                        }
980                    }
981                }
982            ],
983            "metadata": { "agentVisible": true, "userVisible": true }
984        }"#;
985
986        let message: Message = serde_json::from_str(json_str).unwrap();
987
988        assert_eq!(message.role, Role::Assistant);
989        assert_eq!(message.created, 1740171566);
990        assert_eq!(message.content.len(), 2);
991
992        // Check first content item
993        if let MessageContent::Text(text) = &message.content[0] {
994            assert_eq!(text.text, "I'll help you with that.");
995        } else {
996            panic!("Expected Text content");
997        }
998
999        // Check second content item
1000        if let MessageContent::ToolRequest(req) = &message.content[1] {
1001            assert_eq!(req.id, "tool123");
1002            if let Ok(tool_call) = &req.tool_call {
1003                assert_eq!(tool_call.name, "test_tool");
1004                assert_eq!(tool_call.arguments, Some(object!({"param": "value"})))
1005            } else {
1006                panic!("Expected successful tool call");
1007            }
1008        } else {
1009            panic!("Expected ToolRequest content");
1010        }
1011    }
1012
1013    #[test]
1014    fn test_from_prompt_message_text() {
1015        let prompt_content = PromptMessageContent::Text {
1016            text: "Hello, world!".to_string(),
1017        };
1018
1019        let prompt_message = PromptMessage {
1020            role: PromptMessageRole::User,
1021            content: prompt_content,
1022        };
1023
1024        let message = Message::from(prompt_message);
1025
1026        if let MessageContent::Text(text_content) = &message.content[0] {
1027            assert_eq!(text_content.text, "Hello, world!");
1028        } else {
1029            panic!("Expected MessageContent::Text");
1030        }
1031    }
1032
1033    #[test]
1034    fn test_from_prompt_message_image() {
1035        let prompt_content = PromptMessageContent::Image {
1036            image: RawImageContent {
1037                data: "base64data".to_string(),
1038                mime_type: "image/jpeg".to_string(),
1039                meta: None,
1040            }
1041            .no_annotation(),
1042        };
1043
1044        let prompt_message = PromptMessage {
1045            role: PromptMessageRole::User,
1046            content: prompt_content,
1047        };
1048
1049        let message = Message::from(prompt_message);
1050
1051        if let MessageContent::Image(image_content) = &message.content[0] {
1052            assert_eq!(image_content.data, "base64data");
1053            assert_eq!(image_content.mime_type, "image/jpeg");
1054        } else {
1055            panic!("Expected MessageContent::Image");
1056        }
1057    }
1058
1059    #[test]
1060    fn test_from_prompt_message_text_resource() {
1061        let resource = ResourceContents::TextResourceContents {
1062            uri: "file:///test.txt".to_string(),
1063            mime_type: Some("text/plain".to_string()),
1064            text: "Resource content".to_string(),
1065            meta: None,
1066        };
1067
1068        let prompt_content = PromptMessageContent::Resource {
1069            resource: RawEmbeddedResource {
1070                resource,
1071                meta: None,
1072            }
1073            .no_annotation(),
1074        };
1075
1076        let prompt_message = PromptMessage {
1077            role: PromptMessageRole::User,
1078            content: prompt_content,
1079        };
1080
1081        let message = Message::from(prompt_message);
1082
1083        if let MessageContent::Text(text_content) = &message.content[0] {
1084            assert_eq!(text_content.text, "Resource content");
1085        } else {
1086            panic!("Expected MessageContent::Text");
1087        }
1088    }
1089
1090    #[test]
1091    fn test_from_prompt_message_blob_resource() {
1092        let resource = ResourceContents::BlobResourceContents {
1093            uri: "file:///test.bin".to_string(),
1094            mime_type: Some("application/octet-stream".to_string()),
1095            blob: "binary_data".to_string(),
1096            meta: None,
1097        };
1098
1099        let prompt_content = PromptMessageContent::Resource {
1100            resource: RawEmbeddedResource {
1101                resource,
1102                meta: None,
1103            }
1104            .no_annotation(),
1105        };
1106
1107        let prompt_message = PromptMessage {
1108            role: PromptMessageRole::User,
1109            content: prompt_content,
1110        };
1111
1112        let message = Message::from(prompt_message);
1113
1114        if let MessageContent::Text(text_content) = &message.content[0] {
1115            assert_eq!(text_content.text, "[Binary content: binary_data]");
1116        } else {
1117            panic!("Expected MessageContent::Text");
1118        }
1119    }
1120
1121    #[test]
1122    fn test_from_prompt_message() {
1123        // Test user message conversion
1124        let prompt_message = PromptMessage {
1125            role: PromptMessageRole::User,
1126            content: PromptMessageContent::Text {
1127                text: "Hello, world!".to_string(),
1128            },
1129        };
1130
1131        let message = Message::from(prompt_message);
1132        assert_eq!(message.role, Role::User);
1133        assert_eq!(message.content.len(), 1);
1134        assert_eq!(message.as_concat_text(), "Hello, world!");
1135
1136        // Test assistant message conversion
1137        let prompt_message = PromptMessage {
1138            role: PromptMessageRole::Assistant,
1139            content: PromptMessageContent::Text {
1140                text: "I can help with that.".to_string(),
1141            },
1142        };
1143
1144        let message = Message::from(prompt_message);
1145        assert_eq!(message.role, Role::Assistant);
1146        assert_eq!(message.content.len(), 1);
1147        assert_eq!(message.as_concat_text(), "I can help with that.");
1148    }
1149
1150    #[test]
1151    fn test_message_with_text() {
1152        let message = Message::user().with_text("Hello");
1153        assert_eq!(message.as_concat_text(), "Hello");
1154    }
1155
1156    #[test]
1157    fn test_message_with_tool_request() {
1158        let tool_call = Ok(CallToolRequestParam {
1159            name: "test_tool".into(),
1160            arguments: Some(object!({})),
1161        });
1162
1163        let message = Message::assistant().with_tool_request("req1", tool_call);
1164        assert!(message.is_tool_call());
1165        assert!(!message.is_tool_response());
1166
1167        let ids = message.get_tool_ids();
1168        assert_eq!(ids.len(), 1);
1169        assert!(ids.contains("req1"));
1170    }
1171
1172    #[test]
1173    fn test_message_deserialization_sanitizes_text_content() {
1174        // Create a test string with Unicode Tags characters
1175        let malicious_text = "Hello\u{E0041}\u{E0042}\u{E0043}world";
1176        let malicious_json = format!(
1177            r#"{{
1178            "id": "test-id",
1179            "role": "user",
1180            "created": 1640995200,
1181            "content": [
1182                {{
1183                    "type": "text",
1184                    "text": "{}"
1185                }},
1186                {{
1187                    "type": "image",
1188                    "data": "base64data",
1189                    "mimeType": "image/png"
1190                }}
1191            ],
1192            "metadata": {{ "agentVisible": true, "userVisible": true }}
1193        }}"#,
1194            malicious_text
1195        );
1196
1197        let message: Message = serde_json::from_str(&malicious_json).unwrap();
1198
1199        // Text content should be sanitized
1200        assert_eq!(message.as_concat_text(), "Helloworld");
1201
1202        // Image content should be unchanged
1203        if let MessageContent::Image(img) = &message.content[1] {
1204            assert_eq!(img.data, "base64data");
1205            assert_eq!(img.mime_type, "image/png");
1206        } else {
1207            panic!("Expected ImageContent");
1208        }
1209    }
1210
1211    #[test]
1212    fn test_legitimate_unicode_preserved_during_message_deserialization() {
1213        let clean_json = r#"{
1214            "id": "test-id",
1215            "role": "user",
1216            "created": 1640995200,
1217            "content": [{
1218                "type": "text",
1219                "text": "Hello world δΈ–η•Œ 🌍"
1220            }],
1221            "metadata": { "agentVisible": true, "userVisible": true }
1222        }"#;
1223
1224        let message: Message = serde_json::from_str(clean_json).unwrap();
1225
1226        assert_eq!(message.as_concat_text(), "Hello world δΈ–η•Œ 🌍");
1227    }
1228
1229    #[test]
1230    fn test_message_metadata_defaults() {
1231        let message = Message::user().with_text("Test");
1232
1233        // By default, messages should be both user and agent visible
1234        assert!(message.is_user_visible());
1235        assert!(message.is_agent_visible());
1236    }
1237
1238    #[test]
1239    fn test_message_visibility_methods() {
1240        // Test user_only
1241        let user_only_msg = Message::user().with_text("User only").user_only();
1242        assert!(user_only_msg.is_user_visible());
1243        assert!(!user_only_msg.is_agent_visible());
1244
1245        // Test agent_only
1246        let agent_only_msg = Message::assistant().with_text("Agent only").agent_only();
1247        assert!(!agent_only_msg.is_user_visible());
1248        assert!(agent_only_msg.is_agent_visible());
1249
1250        // Test with_visibility
1251        let custom_msg = Message::user()
1252            .with_text("Custom visibility")
1253            .with_visibility(false, true);
1254        assert!(!custom_msg.is_user_visible());
1255        assert!(custom_msg.is_agent_visible());
1256    }
1257
1258    #[test]
1259    fn test_message_metadata_serialization() {
1260        let message = Message::user()
1261            .with_text("Test message")
1262            .with_visibility(false, true);
1263
1264        let json_str = serde_json::to_string(&message).unwrap();
1265        let value: Value = serde_json::from_str(&json_str).unwrap();
1266
1267        assert_eq!(value["metadata"]["userVisible"], false);
1268        assert_eq!(value["metadata"]["agentVisible"], true);
1269    }
1270
1271    #[test]
1272    fn test_message_metadata_deserialization() {
1273        // Test with explicit metadata
1274        let json_with_metadata = r#"{
1275            "role": "user",
1276            "created": 1640995200,
1277            "content": [{
1278                "type": "text",
1279                "text": "Test"
1280            }],
1281            "metadata": {
1282                "userVisible": false,
1283                "agentVisible": true
1284            }
1285        }"#;
1286
1287        let message: Message = serde_json::from_str(json_with_metadata).unwrap();
1288        assert!(!message.is_user_visible());
1289        assert!(message.is_agent_visible());
1290    }
1291
1292    #[test]
1293    fn test_message_metadata_static_methods() {
1294        // Test MessageMetadata::agent_only()
1295        let agent_only_metadata = MessageMetadata::agent_only();
1296        assert!(!agent_only_metadata.user_visible);
1297        assert!(agent_only_metadata.agent_visible);
1298
1299        // Test MessageMetadata::user_only()
1300        let user_only_metadata = MessageMetadata::user_only();
1301        assert!(user_only_metadata.user_visible);
1302        assert!(!user_only_metadata.agent_visible);
1303
1304        // Test MessageMetadata::invisible()
1305        let invisible_metadata = MessageMetadata::invisible();
1306        assert!(!invisible_metadata.user_visible);
1307        assert!(!invisible_metadata.agent_visible);
1308
1309        // Test using them with messages
1310        let agent_msg = Message::assistant()
1311            .with_text("Agent only message")
1312            .with_metadata(MessageMetadata::agent_only());
1313        assert!(!agent_msg.is_user_visible());
1314        assert!(agent_msg.is_agent_visible());
1315
1316        let user_msg = Message::user()
1317            .with_text("User only message")
1318            .with_metadata(MessageMetadata::user_only());
1319        assert!(user_msg.is_user_visible());
1320        assert!(!user_msg.is_agent_visible());
1321
1322        let invisible_msg = Message::user()
1323            .with_text("Invisible message")
1324            .with_metadata(MessageMetadata::invisible());
1325        assert!(!invisible_msg.is_user_visible());
1326        assert!(!invisible_msg.is_agent_visible());
1327    }
1328
1329    #[test]
1330    fn test_message_metadata_builder_methods() {
1331        // Test with_agent_invisible
1332        let metadata = MessageMetadata::default().with_agent_invisible();
1333        assert!(metadata.user_visible);
1334        assert!(!metadata.agent_visible);
1335
1336        // Test with_user_invisible
1337        let metadata = MessageMetadata::default().with_user_invisible();
1338        assert!(!metadata.user_visible);
1339        assert!(metadata.agent_visible);
1340
1341        // Test with_agent_visible
1342        let metadata = MessageMetadata::invisible().with_agent_visible();
1343        assert!(!metadata.user_visible);
1344        assert!(metadata.agent_visible);
1345
1346        // Test with_user_visible
1347        let metadata = MessageMetadata::invisible().with_user_visible();
1348        assert!(metadata.user_visible);
1349        assert!(!metadata.agent_visible);
1350
1351        // Test chaining
1352        let metadata = MessageMetadata::invisible()
1353            .with_user_visible()
1354            .with_agent_visible();
1355        assert!(metadata.user_visible);
1356        assert!(metadata.agent_visible);
1357    }
1358
1359    #[test]
1360    fn test_legacy_tool_response_deserialization() {
1361        let legacy_json = r#"{
1362            "role": "user",
1363            "created": 1640995200,
1364            "content": [{
1365                "type": "toolResponse",
1366                "id": "tool123",
1367                "toolResult": {
1368                    "status": "success",
1369                    "value": [
1370                        {
1371                            "type": "text",
1372                            "text": "Tool output text"
1373                        }
1374                    ]
1375                }
1376            }],
1377            "metadata": { "agentVisible": true, "userVisible": true }
1378        }"#;
1379
1380        let message: Message = serde_json::from_str(legacy_json).unwrap();
1381        assert_eq!(message.content.len(), 1);
1382
1383        if let MessageContent::ToolResponse(response) = &message.content[0] {
1384            assert_eq!(response.id, "tool123");
1385            if let Ok(result) = &response.tool_result {
1386                assert_eq!(result.content.len(), 1);
1387                assert_eq!(
1388                    result.content[0].as_text().unwrap().text,
1389                    "Tool output text"
1390                );
1391            } else {
1392                panic!("Expected successful tool result");
1393            }
1394        } else {
1395            panic!("Expected ToolResponse content");
1396        }
1397    }
1398
1399    #[test]
1400    fn test_new_tool_response_deserialization() {
1401        let new_json = r#"{
1402            "role": "user",
1403            "created": 1640995200,
1404            "content": [{
1405                "type": "toolResponse",
1406                "id": "tool456",
1407                "toolResult": {
1408                    "status": "success",
1409                    "value": {
1410                        "content": [
1411                            {
1412                                "type": "text",
1413                                "text": "New format output"
1414                            }
1415                        ],
1416                        "isError": false
1417                    }
1418                }
1419            }],
1420            "metadata": { "agentVisible": true, "userVisible": true }
1421        }"#;
1422
1423        let message: Message = serde_json::from_str(new_json).unwrap();
1424        assert_eq!(message.content.len(), 1);
1425
1426        if let MessageContent::ToolResponse(response) = &message.content[0] {
1427            assert_eq!(response.id, "tool456");
1428            if let Ok(result) = &response.tool_result {
1429                assert_eq!(result.content.len(), 1);
1430                assert_eq!(
1431                    result.content[0].as_text().unwrap().text,
1432                    "New format output"
1433                );
1434            } else {
1435                panic!("Expected successful tool result");
1436            }
1437        } else {
1438            panic!("Expected ToolResponse content");
1439        }
1440    }
1441
1442    #[test]
1443    fn test_tool_request_with_value_arguments_backward_compatibility() {
1444        struct TestCase {
1445            name: &'static str,
1446            arguments_json: &'static str,
1447            expected: Option<Value>,
1448        }
1449
1450        let test_cases = [
1451            TestCase {
1452                name: "string",
1453                arguments_json: r#""string_argument""#,
1454                expected: Some(serde_json::json!({"value": "string_argument"})),
1455            },
1456            TestCase {
1457                name: "array",
1458                arguments_json: r#"["a", "b", "c"]"#,
1459                expected: Some(serde_json::json!({"value": ["a", "b", "c"]})),
1460            },
1461            TestCase {
1462                name: "number",
1463                arguments_json: "42",
1464                expected: Some(serde_json::json!({"value": 42})),
1465            },
1466            TestCase {
1467                name: "null",
1468                arguments_json: "null",
1469                expected: None,
1470            },
1471            TestCase {
1472                name: "object",
1473                arguments_json: r#"{"key": "value", "number": 123}"#,
1474                expected: Some(serde_json::json!({"key": "value", "number": 123})),
1475            },
1476        ];
1477
1478        for tc in test_cases {
1479            let json = format!(
1480                r#"{{
1481                    "role": "assistant",
1482                    "created": 1640995200,
1483                    "content": [{{
1484                        "type": "toolRequest",
1485                        "id": "tool123",
1486                        "toolCall": {{
1487                            "status": "success",
1488                            "value": {{
1489                                "name": "test_tool",
1490                                "arguments": {}
1491                            }}
1492                        }}
1493                    }}],
1494                    "metadata": {{ "agentVisible": true, "userVisible": true }}
1495                }}"#,
1496                tc.arguments_json
1497            );
1498
1499            let message: Message = serde_json::from_str(&json)
1500                .unwrap_or_else(|e| panic!("{}: parse failed: {}", tc.name, e));
1501
1502            let MessageContent::ToolRequest(request) = &message.content[0] else {
1503                panic!("{}: expected ToolRequest content", tc.name);
1504            };
1505
1506            let Ok(tool_call) = &request.tool_call else {
1507                panic!("{}: expected successful tool call", tc.name);
1508            };
1509
1510            assert_eq!(tool_call.name, "test_tool", "{}: wrong tool name", tc.name);
1511
1512            match (&tool_call.arguments, &tc.expected) {
1513                (None, None) => {}
1514                (Some(args), Some(expected)) => {
1515                    let args_value = serde_json::to_value(args).unwrap();
1516                    assert_eq!(&args_value, expected, "{}: arguments mismatch", tc.name);
1517                }
1518                (actual, expected) => {
1519                    panic!("{}: expected {:?}, got {:?}", tc.name, expected, actual);
1520                }
1521            }
1522        }
1523    }
1524}