1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use thiserror::Error;
5
6use crate::content::{ContentBlock, ContentValidationError, validate_tool_name};
7use crate::{MessageId, ProtocolTimestamp, ToolCallId};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum MessageRole {
12 User,
14 Assistant,
16 ToolResult,
18}
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum CanonicalMessage {
23 User {
25 id: MessageId,
27 content: Vec<ContentBlock>,
29 timestamp: ProtocolTimestamp,
31 },
32 Assistant {
34 id: MessageId,
36 content: Vec<ContentBlock>,
38 stop_reason: StopReason,
40 timestamp: ProtocolTimestamp,
42 },
43 ToolResult {
45 id: MessageId,
47 tool_call_id: ToolCallId,
49 tool_name: String,
51 content: Vec<ContentBlock>,
53 is_error: bool,
55 error: Option<ToolFailure>,
57 timestamp: ProtocolTimestamp,
59 },
60}
61
62impl CanonicalMessage {
63 pub fn user(
70 id: MessageId,
71 content: Vec<ContentBlock>,
72 timestamp: ProtocolTimestamp,
73 ) -> Result<Self, MessageValidationError> {
74 validate_content(&content, ContentBlock::valid_for_user)?;
75 Ok(Self::User {
76 id,
77 content,
78 timestamp,
79 })
80 }
81
82 pub fn assistant(
89 id: MessageId,
90 content: Vec<ContentBlock>,
91 stop_reason: StopReason,
92 timestamp: ProtocolTimestamp,
93 ) -> Result<Self, MessageValidationError> {
94 validate_content(&content, ContentBlock::valid_for_assistant)?;
95 Ok(Self::Assistant {
96 id,
97 content,
98 stop_reason,
99 timestamp,
100 })
101 }
102
103 pub fn tool_result_success(
109 id: MessageId,
110 tool_call_id: ToolCallId,
111 tool_name: impl Into<String>,
112 content: Vec<ContentBlock>,
113 timestamp: ProtocolTimestamp,
114 ) -> Result<Self, MessageValidationError> {
115 Self::tool_result(id, tool_call_id, tool_name.into(), content, None, timestamp)
116 }
117
118 pub fn tool_result_failure(
124 id: MessageId,
125 tool_call_id: ToolCallId,
126 tool_name: impl Into<String>,
127 content: Vec<ContentBlock>,
128 error: ToolFailure,
129 timestamp: ProtocolTimestamp,
130 ) -> Result<Self, MessageValidationError> {
131 Self::tool_result(
132 id,
133 tool_call_id,
134 tool_name.into(),
135 content,
136 Some(error),
137 timestamp,
138 )
139 }
140
141 #[must_use]
143 pub const fn role(&self) -> MessageRole {
144 match self {
145 Self::User { .. } => MessageRole::User,
146 Self::Assistant { .. } => MessageRole::Assistant,
147 Self::ToolResult { .. } => MessageRole::ToolResult,
148 }
149 }
150
151 fn validate(&self) -> Result<(), MessageValidationError> {
152 match self {
153 Self::User { content, .. } => validate_content(content, ContentBlock::valid_for_user),
154 Self::Assistant { content, .. } => {
155 validate_content(content, ContentBlock::valid_for_assistant)
156 }
157 Self::ToolResult {
158 tool_name,
159 content,
160 is_error,
161 error,
162 ..
163 } => {
164 validate_tool_name(tool_name)?;
165 validate_content(content, ContentBlock::valid_for_tool_result)?;
166 if *is_error != error.is_some() {
167 return Err(MessageValidationError::InconsistentToolFailure);
168 }
169 Ok(())
170 }
171 }
172 }
173
174 fn tool_result(
175 id: MessageId,
176 tool_call_id: ToolCallId,
177 tool_name: String,
178 content: Vec<ContentBlock>,
179 error: Option<ToolFailure>,
180 timestamp: ProtocolTimestamp,
181 ) -> Result<Self, MessageValidationError> {
182 validate_tool_name(&tool_name)?;
183 validate_content(&content, ContentBlock::valid_for_tool_result)?;
184 Ok(Self::ToolResult {
185 id,
186 tool_call_id,
187 tool_name,
188 content,
189 is_error: error.is_some(),
190 error,
191 timestamp,
192 })
193 }
194}
195
196impl Serialize for CanonicalMessage {
197 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198 where
199 S: Serializer,
200 {
201 self.validate().map_err(serde::ser::Error::custom)?;
202 SerializableCanonicalMessage::from(self).serialize(serializer)
203 }
204}
205
206#[derive(Serialize)]
207#[serde(tag = "type", rename_all = "snake_case")]
208enum SerializableCanonicalMessage<'a> {
209 User {
210 id: &'a MessageId,
211 content: &'a [ContentBlock],
212 timestamp: &'a ProtocolTimestamp,
213 },
214 Assistant {
215 id: &'a MessageId,
216 content: &'a [ContentBlock],
217 #[serde(rename = "stopReason")]
218 stop_reason: &'a StopReason,
219 timestamp: &'a ProtocolTimestamp,
220 },
221 ToolResult {
222 id: &'a MessageId,
223 #[serde(rename = "toolCallId")]
224 tool_call_id: &'a ToolCallId,
225 #[serde(rename = "toolName")]
226 tool_name: &'a str,
227 content: &'a [ContentBlock],
228 #[serde(rename = "isError")]
229 is_error: bool,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 error: &'a Option<ToolFailure>,
232 timestamp: &'a ProtocolTimestamp,
233 },
234}
235
236impl<'a> From<&'a CanonicalMessage> for SerializableCanonicalMessage<'a> {
237 fn from(value: &'a CanonicalMessage) -> Self {
238 match value {
239 CanonicalMessage::User {
240 id,
241 content,
242 timestamp,
243 } => Self::User {
244 id,
245 content,
246 timestamp,
247 },
248 CanonicalMessage::Assistant {
249 id,
250 content,
251 stop_reason,
252 timestamp,
253 } => Self::Assistant {
254 id,
255 content,
256 stop_reason,
257 timestamp,
258 },
259 CanonicalMessage::ToolResult {
260 id,
261 tool_call_id,
262 tool_name,
263 content,
264 is_error,
265 error,
266 timestamp,
267 } => Self::ToolResult {
268 id,
269 tool_call_id,
270 tool_name,
271 content,
272 is_error: *is_error,
273 error,
274 timestamp,
275 },
276 }
277 }
278}
279
280#[derive(Deserialize)]
281#[serde(tag = "type", rename_all = "snake_case")]
282enum RawCanonicalMessage {
283 User {
284 id: MessageId,
285 content: Vec<ContentBlock>,
286 timestamp: ProtocolTimestamp,
287 },
288 Assistant {
289 id: MessageId,
290 content: Vec<ContentBlock>,
291 #[serde(rename = "stopReason")]
292 stop_reason: StopReason,
293 timestamp: ProtocolTimestamp,
294 },
295 ToolResult {
296 id: MessageId,
297 #[serde(rename = "toolCallId")]
298 tool_call_id: ToolCallId,
299 #[serde(rename = "toolName")]
300 tool_name: String,
301 content: Vec<ContentBlock>,
302 #[serde(rename = "isError")]
303 is_error: bool,
304 #[serde(default)]
305 error: Option<ToolFailure>,
306 timestamp: ProtocolTimestamp,
307 },
308}
309
310impl<'de> Deserialize<'de> for CanonicalMessage {
311 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312 where
313 D: Deserializer<'de>,
314 {
315 match RawCanonicalMessage::deserialize(deserializer)? {
316 RawCanonicalMessage::User {
317 id,
318 content,
319 timestamp,
320 } => Self::user(id, content, timestamp),
321 RawCanonicalMessage::Assistant {
322 id,
323 content,
324 stop_reason,
325 timestamp,
326 } => Self::assistant(id, content, stop_reason, timestamp),
327 RawCanonicalMessage::ToolResult {
328 id,
329 tool_call_id,
330 tool_name,
331 content,
332 is_error,
333 error,
334 timestamp,
335 } => {
336 if is_error != error.is_some() {
337 return Err(serde::de::Error::custom(
338 "tool-result isError must match error presence",
339 ));
340 }
341 Self::tool_result(id, tool_call_id, tool_name, content, error, timestamp)
342 }
343 }
344 .map_err(serde::de::Error::custom)
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Hash)]
350pub enum StopReason {
351 Completed,
353 Length,
355 ToolUse,
357 PauseTurn,
359 Cancelled,
361 Error,
363 Unknown(String),
365}
366
367impl StopReason {
368 #[must_use]
370 pub fn as_str(&self) -> &str {
371 match self {
372 Self::Completed => "completed",
373 Self::Length => "length",
374 Self::ToolUse => "tool_use",
375 Self::PauseTurn => "pause_turn",
376 Self::Cancelled => "cancelled",
377 Self::Error => "error",
378 Self::Unknown(value) => value,
379 }
380 }
381
382 #[must_use]
384 pub const fn is_success(&self) -> bool {
385 matches!(self, Self::Completed)
386 }
387}
388
389impl Serialize for StopReason {
390 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
391 where
392 S: Serializer,
393 {
394 if !valid_error_code(self.as_str()) {
395 return Err(serde::ser::Error::custom("invalid stop reason"));
396 }
397 serializer.serialize_str(self.as_str())
398 }
399}
400
401impl<'de> Deserialize<'de> for StopReason {
402 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403 where
404 D: Deserializer<'de>,
405 {
406 let value = String::deserialize(deserializer)?;
407 if !valid_error_code(&value) {
408 return Err(serde::de::Error::custom("invalid stop reason"));
409 }
410 Ok(match value.as_str() {
411 "completed" => Self::Completed,
412 "length" => Self::Length,
413 "tool_use" => Self::ToolUse,
414 "pause_turn" => Self::PauseTurn,
415 "cancelled" => Self::Cancelled,
416 "error" => Self::Error,
417 _ => Self::Unknown(value),
418 })
419 }
420}
421
422#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
424#[serde(rename_all = "camelCase")]
425pub struct ToolFailure {
426 code: String,
427 message: String,
428}
429
430impl ToolFailure {
431 #[must_use]
433 pub fn approval_denied() -> Self {
434 Self {
435 code: "approval_denied".to_owned(),
436 message: "tool invocation was denied by approval".to_owned(),
437 }
438 }
439
440 pub fn new(
447 code: impl Into<String>,
448 message: impl Into<String>,
449 ) -> Result<Self, MessageValidationError> {
450 let code = code.into();
451 let message = message.into();
452 if !valid_error_code(&code)
453 || message.is_empty()
454 || message.len() > 4096
455 || message.contains('\0')
456 {
457 return Err(MessageValidationError::InvalidToolFailure);
458 }
459 Ok(Self { code, message })
460 }
461
462 #[must_use]
464 pub fn code(&self) -> &str {
465 &self.code
466 }
467
468 #[must_use]
470 pub fn message(&self) -> &str {
471 &self.message
472 }
473}
474
475#[derive(Deserialize)]
476struct RawToolFailure {
477 code: String,
478 message: String,
479}
480
481impl<'de> Deserialize<'de> for ToolFailure {
482 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
483 where
484 D: Deserializer<'de>,
485 {
486 let raw = RawToolFailure::deserialize(deserializer)?;
487 Self::new(raw.code, raw.message).map_err(serde::de::Error::custom)
488 }
489}
490
491#[derive(Debug, Error)]
493pub enum MessageValidationError {
494 #[error("message content is empty or contains a block invalid for its role")]
496 InvalidContent,
497 #[error("invalid message content: {0}")]
499 InvalidContentValue(#[from] ContentValidationError),
500 #[error("tool failure code or message is invalid")]
502 InvalidToolFailure,
503 #[error("tool-result isError must match error presence")]
505 InconsistentToolFailure,
506}
507
508fn validate_content(
509 content: &[ContentBlock],
510 predicate: impl Fn(&ContentBlock) -> bool,
511) -> Result<(), MessageValidationError> {
512 if content.is_empty() || content.len() > 256 || !content.iter().all(predicate) {
513 return Err(MessageValidationError::InvalidContent);
514 }
515 for block in content {
516 block.validate()?;
517 }
518 Ok(())
519}
520
521fn valid_error_code(value: &str) -> bool {
522 let mut bytes = value.bytes();
523 bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
524 && value.len() <= 128
525 && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
526}
527
528impl fmt::Display for StopReason {
529 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
530 formatter.write_str(self.as_str())
531 }
532}