Skip to main content

agent_base/types/
message.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
5pub enum MessageRole {
6    System,
7    User,
8    Assistant,
9    Tool,
10}
11
12impl MessageRole {
13    pub fn as_str(&self) -> &'static str {
14        match self {
15            MessageRole::System => "system",
16            MessageRole::User => "user",
17            MessageRole::Assistant => "assistant",
18            MessageRole::Tool => "tool",
19        }
20    }
21
22    pub fn as_api_role(&self) -> &'static str {
23        self.as_str()
24    }
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct Message {
29    pub role: MessageRole,
30    pub content: String,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub enum ChatMessage {
35    System {
36        content: String,
37        /// 临时消息:turn 结束后从内存清理,持久化时跳过。
38        #[serde(default, skip_serializing)]
39        ephemeral: bool,
40    },
41    User {
42        content: String,
43        #[serde(default, skip_serializing_if = "Vec::is_empty")]
44        images: Vec<ImageAttachment>,
45        /// 临时消息:turn 结束后从内存清理,持久化时跳过。
46        #[serde(default, skip_serializing)]
47        ephemeral: bool,
48    },
49    Assistant {
50        content: Option<String>,
51        reasoning_content: Option<String>,
52        tool_calls: Option<Vec<ToolCallMessage>>,
53    },
54    Tool {
55        tool_call_id: String,
56        content: String,
57    },
58    /// Application-defined message type for extensibility.
59    ///
60    /// Consumers can inject custom message types (e.g., artifacts, notifications)
61    /// into the conversation transcript. Custom messages are preserved in the
62    /// transcript but filtered out by the default `convert_to_llm` callback
63    /// before being sent to the LLM provider.
64    Custom {
65        role: String,
66        data: serde_json::Value,
67    },
68}
69
70#[derive(Clone, Debug, Serialize, Deserialize)]
71pub struct ToolCallMessage {
72    pub id: String,
73    pub name: String,
74    pub arguments: String,
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
78pub enum ImageAttachment {
79    Url {
80        url: String,
81        #[serde(default, skip_serializing_if = "Option::is_none")]
82        detail: Option<ImageDetail>,
83    },
84    Base64 {
85        data: String,
86        #[serde(default, skip_serializing_if = "Option::is_none")]
87        media_type: Option<String>,
88        #[serde(default, skip_serializing_if = "Option::is_none")]
89        detail: Option<ImageDetail>,
90    },
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize)]
94pub enum ImageDetail {
95    Low,
96    High,
97    Auto,
98}
99
100impl ChatMessage {
101    pub fn system(content: impl Into<String>) -> Self {
102        Self::System {
103            content: content.into(),
104            ephemeral: false,
105        }
106    }
107
108    /// 创建临时 system 消息:turn 结束后自动清理,不持久化。
109    pub fn system_ephemeral(content: impl Into<String>) -> Self {
110        Self::System {
111            content: content.into(),
112            ephemeral: true,
113        }
114    }
115
116    pub fn user(content: impl Into<String>) -> Self {
117        Self::User {
118            content: content.into(),
119            images: Vec::new(),
120            ephemeral: false,
121        }
122    }
123
124    /// 创建临时 user 消息:turn 结束后自动清理,不持久化。
125    pub fn user_ephemeral(content: impl Into<String>) -> Self {
126        Self::User {
127            content: content.into(),
128            images: Vec::new(),
129            ephemeral: true,
130        }
131    }
132
133    pub fn user_with_images(content: impl Into<String>, images: Vec<ImageAttachment>) -> Self {
134        Self::User {
135            content: content.into(),
136            images,
137            ephemeral: false,
138        }
139    }
140
141    /// 是否为临时消息(turn 结束后自动清理,不持久化)。
142    pub fn is_ephemeral(&self) -> bool {
143        match self {
144            Self::System { ephemeral, .. } => *ephemeral,
145            Self::User { ephemeral, .. } => *ephemeral,
146            _ => false,
147        }
148    }
149
150    pub fn assistant(content: impl Into<String>) -> Self {
151        Self::Assistant {
152            content: Some(content.into()),
153            reasoning_content: None,
154            tool_calls: None,
155        }
156    }
157
158    pub fn assistant_with_reasoning(
159        content: impl Into<String>,
160        reasoning: impl Into<String>,
161    ) -> Self {
162        Self::Assistant {
163            content: Some(content.into()),
164            reasoning_content: Some(reasoning.into()),
165            tool_calls: None,
166        }
167    }
168
169    pub fn assistant_tool_call(
170        tool_call_id: impl Into<String>,
171        tool_name: impl Into<String>,
172        arguments: impl Into<String>,
173    ) -> Self {
174        Self::Assistant {
175            content: None,
176            reasoning_content: None,
177            tool_calls: Some(vec![ToolCallMessage {
178                id: tool_call_id.into(),
179                name: tool_name.into(),
180                arguments: arguments.into(),
181            }]),
182        }
183    }
184
185    pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
186        Self::Tool {
187            tool_call_id: tool_call_id.into(),
188            content: content.into(),
189        }
190    }
191}
192
193impl From<&ChatMessage> for Message {
194    fn from(cm: &ChatMessage) -> Self {
195        match cm {
196            ChatMessage::System { content, .. } => Message {
197                role: MessageRole::System,
198                content: content.clone(),
199            },
200            ChatMessage::User { content, .. } => Message {
201                role: MessageRole::User,
202                content: content.clone(),
203            },
204            ChatMessage::Assistant { content, .. } => Message {
205                role: MessageRole::Assistant,
206                content: content.clone().unwrap_or_default(),
207            },
208            ChatMessage::Tool { content, .. } => Message {
209                role: MessageRole::Tool,
210                content: content.clone(),
211            },
212            ChatMessage::Custom { role: _, data } => Message {
213                role: MessageRole::User,
214                content: data.to_string(),
215            },
216        }
217    }
218}
219
220/// Callback that transforms the message list before it is sent to the LLM.
221///
222/// The default implementation filters out [`ChatMessage::Custom`] variants because
223/// most providers don't understand application-specific message types. Consumers
224/// can override this to inject custom serialization logic for their message types.
225pub type ConvertToLlmFn = Arc<dyn Fn(&[ChatMessage]) -> Vec<ChatMessage> + Send + Sync>;
226
227/// Default conversion that strips [`ChatMessage::Custom`] messages.
228pub fn default_convert_to_llm(messages: &[ChatMessage]) -> Vec<ChatMessage> {
229    messages
230        .iter()
231        .filter(|m| !matches!(m, ChatMessage::Custom { .. }))
232        .cloned()
233        .collect()
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_default_convert_to_llm_filters_custom() {
242        let messages = vec![
243            ChatMessage::system("You are a helpful assistant."),
244            ChatMessage::user("Hello"),
245            ChatMessage::Custom {
246                role: "artifact".to_string(),
247                data: serde_json::json!({"id": "abc123"}),
248            },
249            ChatMessage::assistant("Hi there!"),
250            ChatMessage::Custom {
251                role: "notification".to_string(),
252                data: serde_json::json!({"level": "info"}),
253            },
254            ChatMessage::tool("call_1", "result"),
255        ];
256
257        let filtered = default_convert_to_llm(&messages);
258
259        // Custom messages are removed, regular messages are preserved
260        assert_eq!(filtered.len(), 4);
261        assert!(matches!(filtered[0], ChatMessage::System { .. }));
262        assert!(matches!(filtered[1], ChatMessage::User { .. }));
263        assert!(matches!(filtered[2], ChatMessage::Assistant { .. }));
264        assert!(matches!(filtered[3], ChatMessage::Tool { .. }));
265    }
266
267    #[test]
268    fn test_default_convert_to_llm_no_custom() {
269        let messages = vec![
270            ChatMessage::system("sys"),
271            ChatMessage::user("usr"),
272            ChatMessage::assistant("asst"),
273        ];
274
275        let filtered = default_convert_to_llm(&messages);
276        assert_eq!(filtered.len(), 3);
277    }
278
279    #[test]
280    fn test_custom_convert_to_llm_preserves_selected() {
281        let messages = vec![
282            ChatMessage::system("sys"),
283            ChatMessage::Custom {
284                role: "artifact".to_string(),
285                data: serde_json::json!({"id": "x"}),
286            },
287            ChatMessage::user("usr"),
288        ];
289
290        // Custom callback that preserves only artifacts, not notifications
291        let convert = |msgs: &[ChatMessage]| -> Vec<ChatMessage> {
292            msgs.iter()
293                .filter(|m| match m {
294                    ChatMessage::Custom { role, .. } => role == "artifact",
295                    _ => true,
296                })
297                .cloned()
298                .collect()
299        };
300
301        let filtered = convert(&messages);
302        assert_eq!(filtered.len(), 3);
303        assert!(matches!(filtered[0], ChatMessage::System { .. }));
304        assert!(matches!(filtered[1], ChatMessage::Custom { .. }));
305        assert!(matches!(filtered[2], ChatMessage::User { .. }));
306
307        // With a notification-only message
308        let messages2 = vec![
309            ChatMessage::system("sys"),
310            ChatMessage::Custom {
311                role: "notification".to_string(),
312                data: serde_json::json!({"level": "info"}),
313            },
314        ];
315        let filtered2 = convert(&messages2);
316        assert_eq!(filtered2.len(), 1);
317        assert!(matches!(filtered2[0], ChatMessage::System { .. }));
318    }
319
320    #[test]
321    fn test_custom_message_is_ephemeral_false() {
322        let custom = ChatMessage::Custom {
323            role: "artifact".to_string(),
324            data: serde_json::json!({}),
325        };
326        assert!(!custom.is_ephemeral());
327    }
328
329    #[test]
330    fn test_custom_message_serialization_roundtrip() {
331        let custom = ChatMessage::Custom {
332            role: "artifact".to_string(),
333            data: serde_json::json!({"id": "test-123", "content": "hello"}),
334        };
335        let json_str = serde_json::to_string(&custom).unwrap();
336        let deserialized: ChatMessage = serde_json::from_str(&json_str).unwrap();
337        match deserialized {
338            ChatMessage::Custom { role, data } => {
339                assert_eq!(role, "artifact");
340                assert_eq!(data["id"], "test-123");
341                assert_eq!(data["content"], "hello");
342            }
343            _ => panic!("Expected Custom variant"),
344        }
345    }
346}