1use crate::error::{ParseError, ToolResult};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct TextBlock {
9 pub text: String,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct ThinkingBlock {
15 pub thinking: String,
16 pub redacted: Option<String>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub struct ImageSource {
23 pub data: String,
25 pub media_type: String,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct ToolCall {
32 pub id: String,
33 pub name: String,
34 pub arguments: serde_json::Value,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40#[serde(tag = "type", rename_all = "snake_case")]
41pub enum ContentBlock {
42 Text(TextBlock),
43 Thinking(ThinkingBlock),
44 Image { source: ImageSource },
45 ToolCall(ToolCall),
46}
47
48impl ContentBlock {
49 pub fn text(s: String) -> Self {
50 ContentBlock::Text(TextBlock { text: s })
51 }
52
53 pub fn as_text(&self) -> Option<&str> {
54 match self {
55 ContentBlock::Text(block) => Some(&block.text),
56 _ => None,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(tag = "type", rename_all = "snake_case")]
64pub enum Message {
65 System {
66 content: Vec<ContentBlock>,
67 },
68 User {
69 content: Vec<ContentBlock>,
70 },
71 Assistant {
72 content: Vec<ContentBlock>,
73 },
74 ToolResult {
75 tool_call_id: String,
76 is_error: bool,
78 content: Vec<ContentBlock>,
79 },
80}
81
82impl Message {
83 pub fn content(&self) -> &Vec<ContentBlock> {
85 match self {
86 Message::System { content }
87 | Message::User { content }
88 | Message::Assistant { content }
89 | Message::ToolResult { content, .. } => content,
90 }
91 }
92
93 pub fn tool_call_id(&self) -> String {
95 match self {
96 Message::ToolResult { tool_call_id, .. } => tool_call_id.clone(),
97 _ => String::new(),
98 }
99 }
100
101 pub fn is_tool_error(&self) -> bool {
103 matches!(self, Message::ToolResult { is_error: true, .. })
104 }
105
106 pub fn tool_result(call: &ToolCall, result: &ToolResult) -> Self {
111 let (content_str, is_error) = match result {
112 Ok(s) => (s.clone(), false),
113 Err(e) => (format!("tool error: {e}"), true),
114 };
115 Message::ToolResult {
116 tool_call_id: call.id.clone(),
117 is_error,
118 content: text_block(content_str),
119 }
120 }
121
122 pub fn validate(&self) -> Result<(), ParseError> {
130 match self {
131 Message::ToolResult {
132 tool_call_id,
133 is_error: _,
134 content,
135 } => {
136 if tool_call_id.is_empty() {
137 return Err(ParseError {
138 detail: "ToolResult.tool_call_id must not be empty".into(),
139 });
140 }
141 for block in content {
142 match block {
143 ContentBlock::ToolCall(_) => {
144 return Err(ParseError {
145 detail: "ToolResult must not contain ToolCall blocks".into(),
146 });
147 }
148 ContentBlock::Thinking(_) => {
149 return Err(ParseError {
150 detail: "ToolResult must not contain Thinking blocks".into(),
151 });
152 }
153 _ => {}
154 }
155 }
156 }
157 Message::Assistant { content } => {
158 for block in content {
159 if let ContentBlock::ToolCall(tc) = block
160 && tc.id.is_empty()
161 {
162 return Err(ParseError {
163 detail: "Assistant ToolCall.id must not be empty".into(),
164 });
165 }
166 }
167 }
168 Message::User { content } => {
169 for block in content {
170 if let ContentBlock::Thinking(_) = block {
171 return Err(ParseError {
172 detail: "User must not contain Thinking blocks".into(),
173 });
174 }
175 }
176 }
177 Message::System { .. } => {}
178 }
179 Ok(())
180 }
181
182 pub fn extract_tool_calls(&self) -> Vec<ToolCall> {
184 match self {
185 Message::Assistant { content } => content
186 .iter()
187 .filter_map(|b| {
188 if let ContentBlock::ToolCall(tc) = b {
189 Some(tc.clone())
190 } else {
191 None
192 }
193 })
194 .collect(),
195 _ => Vec::new(),
196 }
197 }
198}
199
200pub fn text_block(s: String) -> Vec<ContentBlock> {
202 vec![ContentBlock::text(s)]
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_content_block_text() {
211 let block = ContentBlock::text("hello".to_string());
212 assert_eq!(block.as_text(), Some("hello"));
213 }
214
215 #[test]
216 fn test_content_block_tool_call_no_as_text() {
217 let block = ContentBlock::ToolCall(ToolCall {
218 id: "1".into(),
219 name: "test".into(),
220 arguments: serde_json::json!({}),
221 });
222 assert_eq!(block.as_text(), None);
223 }
224
225 #[test]
226 fn test_message_content() {
227 let msg = Message::User {
228 content: text_block("hello world".to_string()),
229 };
230 assert_eq!(msg.content().len(), 1);
231 assert_eq!(msg.content()[0].as_text(), Some("hello world"));
232 }
233
234 #[test]
235 fn test_message_extract_tool_calls() {
236 let tc = ToolCall {
237 id: "1".into(),
238 name: "test".into(),
239 arguments: serde_json::json!({}),
240 };
241 let msg = Message::Assistant {
242 content: vec![ContentBlock::ToolCall(tc.clone())],
243 };
244 let calls = msg.extract_tool_calls();
245 assert_eq!(calls.len(), 1);
246 assert_eq!(calls[0].name, "test");
247 }
248
249 #[test]
252 fn test_validate_user_ok() {
253 let msg = Message::User {
254 content: text_block("hello".to_string()),
255 };
256 assert!(msg.validate().is_ok());
257 }
258
259 #[test]
260 fn test_validate_user_reject_thinking() {
261 let msg = Message::User {
262 content: vec![ContentBlock::Thinking(ThinkingBlock {
263 thinking: "hmm".into(),
264 redacted: None,
265 })],
266 };
267 assert!(matches!(msg.validate(), Err(ParseError { .. })));
268 }
269
270 #[test]
271 fn test_validate_assistant_ok() {
272 let msg = Message::Assistant {
273 content: text_block("hi".to_string()),
274 };
275 assert!(msg.validate().is_ok());
276 }
277
278 #[test]
279 fn test_validate_assistant_tool_call_empty_id() {
280 let msg = Message::Assistant {
281 content: vec![ContentBlock::ToolCall(ToolCall {
282 id: String::new(),
283 name: "test".into(),
284 arguments: serde_json::json!({}),
285 })],
286 };
287 assert!(matches!(msg.validate(), Err(ParseError { .. })));
288 }
289
290 #[test]
291 fn test_validate_tool_result_ok() {
292 let msg = Message::ToolResult {
293 tool_call_id: "call_1".to_string(),
294 is_error: false,
295 content: text_block("ok".to_string()),
296 };
297 assert!(msg.validate().is_ok());
298 }
299
300 #[test]
301 fn test_validate_tool_result_empty_id() {
302 let msg = Message::ToolResult {
303 tool_call_id: String::new(),
304 is_error: false,
305 content: text_block("ok".to_string()),
306 };
307 assert!(matches!(msg.validate(), Err(ParseError { .. })));
308 }
309
310 #[test]
311 fn test_validate_tool_result_reject_tool_call() {
312 let msg = Message::ToolResult {
313 tool_call_id: "call_1".to_string(),
314 is_error: false,
315 content: vec![ContentBlock::ToolCall(ToolCall {
316 id: "x".into(),
317 name: "y".into(),
318 arguments: serde_json::json!({}),
319 })],
320 };
321 assert!(matches!(msg.validate(), Err(ParseError { .. })));
322 }
323
324 #[test]
325 fn test_validate_tool_result_reject_thinking() {
326 let msg = Message::ToolResult {
327 tool_call_id: "call_1".to_string(),
328 is_error: false,
329 content: vec![ContentBlock::Thinking(ThinkingBlock {
330 thinking: "hmm".into(),
331 redacted: None,
332 })],
333 };
334 assert!(matches!(msg.validate(), Err(ParseError { .. })));
335 }
336
337 #[test]
338 fn test_validate_system_ok() {
339 let msg = Message::System {
340 content: text_block("you are helpful".to_string()),
341 };
342 assert!(msg.validate().is_ok());
343 }
344}