1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Role {
10 System,
11 User,
12 Assistant,
13 Tool,
14}
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Visibility {
20 #[default]
22 All,
23 Internal,
25}
26
27impl Visibility {
28 pub fn is_default(&self) -> bool {
30 *self == Visibility::All
31 }
32}
33
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
36pub struct MessageMetadata {
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub run_id: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub step_index: Option<u32>,
43}
44
45pub fn gen_message_id() -> String {
47 uuid::Uuid::now_v7().to_string()
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Message {
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub id: Option<String>,
56 pub role: Role,
57 pub content: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub tool_calls: Option<Vec<ToolCall>>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub tool_call_id: Option<String>,
64 #[serde(default, skip_serializing_if = "Visibility::is_default")]
67 pub visibility: Visibility,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub metadata: Option<MessageMetadata>,
71}
72
73impl Message {
74 pub fn system(content: impl Into<String>) -> Self {
76 Self {
77 id: Some(gen_message_id()),
78 role: Role::System,
79 content: content.into(),
80 tool_calls: None,
81 tool_call_id: None,
82 visibility: Visibility::All,
83 metadata: None,
84 }
85 }
86
87 pub fn internal_system(content: impl Into<String>) -> Self {
92 Self {
93 id: Some(gen_message_id()),
94 role: Role::System,
95 content: content.into(),
96 tool_calls: None,
97 tool_call_id: None,
98 visibility: Visibility::Internal,
99 metadata: None,
100 }
101 }
102
103 pub fn user(content: impl Into<String>) -> Self {
105 Self {
106 id: Some(gen_message_id()),
107 role: Role::User,
108 content: content.into(),
109 tool_calls: None,
110 tool_call_id: None,
111 visibility: Visibility::All,
112 metadata: None,
113 }
114 }
115
116 pub fn assistant(content: impl Into<String>) -> Self {
118 Self {
119 id: Some(gen_message_id()),
120 role: Role::Assistant,
121 content: content.into(),
122 tool_calls: None,
123 tool_call_id: None,
124 visibility: Visibility::All,
125 metadata: None,
126 }
127 }
128
129 pub fn assistant_with_tool_calls(content: impl Into<String>, calls: Vec<ToolCall>) -> Self {
131 Self {
132 id: Some(gen_message_id()),
133 role: Role::Assistant,
134 content: content.into(),
135 tool_calls: if calls.is_empty() { None } else { Some(calls) },
136 tool_call_id: None,
137 visibility: Visibility::All,
138 metadata: None,
139 }
140 }
141
142 pub fn tool(call_id: impl Into<String>, content: impl Into<String>) -> Self {
144 Self {
145 id: Some(gen_message_id()),
146 role: Role::Tool,
147 content: content.into(),
148 tool_calls: None,
149 tool_call_id: Some(call_id.into()),
150 visibility: Visibility::All,
151 metadata: None,
152 }
153 }
154
155 #[must_use]
157 pub fn with_id(mut self, id: String) -> Self {
158 self.id = Some(id);
159 self
160 }
161
162 #[must_use]
164 pub fn with_metadata(mut self, metadata: MessageMetadata) -> Self {
165 self.metadata = Some(metadata);
166 self
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct ToolCall {
173 pub id: String,
175 pub name: String,
177 pub arguments: Value,
179}
180
181impl ToolCall {
182 pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: Value) -> Self {
184 Self {
185 id: id.into(),
186 name: name.into(),
187 arguments,
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use serde_json::json;
196
197 #[test]
198 fn test_user_message() {
199 let msg = Message::user("Hello");
200 assert_eq!(msg.role, Role::User);
201 assert_eq!(msg.content, "Hello");
202 assert!(msg.id.is_some());
203 assert!(msg.tool_calls.is_none());
204 assert!(msg.tool_call_id.is_none());
205 assert!(msg.metadata.is_none());
206 }
207
208 #[test]
209 fn test_all_constructors_generate_uuid_v7_id() {
210 let msgs = vec![
211 Message::system("sys"),
212 Message::internal_system("internal"),
213 Message::user("usr"),
214 Message::assistant("asst"),
215 Message::assistant_with_tool_calls("tc", vec![]),
216 Message::tool("c1", "result"),
217 ];
218 for msg in &msgs {
219 let id = msg.id.as_ref().expect("message should have an id");
220 assert_eq!(id.len(), 36, "id should be UUID format: {}", id);
222 assert_eq!(&id[14..15], "7", "UUID version should be 7: {}", id);
223 }
224 let ids: std::collections::HashSet<&str> =
226 msgs.iter().map(|m| m.id.as_deref().unwrap()).collect();
227 assert_eq!(ids.len(), msgs.len());
228 }
229
230 #[test]
231 fn test_assistant_with_tool_calls() {
232 let calls = vec![ToolCall::new("call_1", "search", json!({"query": "rust"}))];
233 let msg = Message::assistant_with_tool_calls("Let me search", calls);
234
235 assert_eq!(msg.role, Role::Assistant);
236 assert_eq!(msg.content, "Let me search");
237 assert!(msg.tool_calls.is_some());
238 assert_eq!(msg.tool_calls.as_ref().unwrap().len(), 1);
239 }
240
241 #[test]
242 fn test_tool_message() {
243 let msg = Message::tool("call_1", "Result: 42");
244
245 assert_eq!(msg.role, Role::Tool);
246 assert_eq!(msg.content, "Result: 42");
247 assert_eq!(msg.tool_call_id.as_deref(), Some("call_1"));
248 }
249
250 #[test]
251 fn test_message_serialization() {
252 let msg = Message::user("test");
253 let json = serde_json::to_string(&msg).unwrap();
254 assert!(json.contains("\"role\":\"user\""));
255 assert!(!json.contains("tool_calls"));
257 assert!(!json.contains("tool_call_id"));
258 assert!(!json.contains("metadata"));
259 }
260
261 #[test]
262 fn test_message_with_metadata_serialization() {
263 let msg = Message::user("test").with_metadata(MessageMetadata {
264 run_id: Some("run-1".to_string()),
265 step_index: Some(3),
266 });
267 let json = serde_json::to_string(&msg).unwrap();
268 assert!(json.contains("\"run_id\":\"run-1\""));
269 assert!(json.contains("\"step_index\":3"));
270
271 let parsed: Message = serde_json::from_str(&json).unwrap();
273 let meta = parsed.metadata.unwrap();
274 assert_eq!(meta.run_id.as_deref(), Some("run-1"));
275 assert_eq!(meta.step_index, Some(3));
276 }
277
278 #[test]
279 fn test_message_without_metadata_deserializes() {
280 let json = r#"{"id":"abc","role":"user","content":"hello"}"#;
282 let msg: Message = serde_json::from_str(json).unwrap();
283 assert!(msg.metadata.is_none());
284 assert_eq!(msg.visibility, Visibility::All);
285 }
286
287 #[test]
288 fn test_tool_call_serialization() {
289 let call = ToolCall::new("id_1", "calculator", json!({"expr": "2+2"}));
290 let json = serde_json::to_string(&call).unwrap();
291 let parsed: ToolCall = serde_json::from_str(&json).unwrap();
292
293 assert_eq!(parsed.id, "id_1");
294 assert_eq!(parsed.name, "calculator");
295 assert_eq!(parsed.arguments["expr"], "2+2");
296 }
297
298 #[test]
299 fn test_with_id_overrides_auto_generated() {
300 let msg = Message::user("hi").with_id("custom-id".to_string());
301 assert_eq!(msg.id.as_deref(), Some("custom-id"));
302 }
303
304 #[test]
305 fn test_gen_message_id_is_public_and_uuid_v7() {
306 let id = gen_message_id();
307 assert_eq!(id.len(), 36);
308 assert_eq!(&id[14..15], "7");
309 }
310}