use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::content::{ContentBlock, ContentValidationError, validate_tool_name};
use crate::{MessageId, ProtocolTimestamp, ToolCallId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageRole {
User,
Assistant,
ToolResult,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CanonicalMessage {
User {
id: MessageId,
content: Vec<ContentBlock>,
timestamp: ProtocolTimestamp,
},
Assistant {
id: MessageId,
content: Vec<ContentBlock>,
stop_reason: StopReason,
timestamp: ProtocolTimestamp,
},
ToolResult {
id: MessageId,
tool_call_id: ToolCallId,
tool_name: String,
content: Vec<ContentBlock>,
is_error: bool,
error: Option<ToolFailure>,
timestamp: ProtocolTimestamp,
},
}
impl CanonicalMessage {
pub fn user(
id: MessageId,
content: Vec<ContentBlock>,
timestamp: ProtocolTimestamp,
) -> Result<Self, MessageValidationError> {
validate_content(&content, ContentBlock::valid_for_user)?;
Ok(Self::User {
id,
content,
timestamp,
})
}
pub fn assistant(
id: MessageId,
content: Vec<ContentBlock>,
stop_reason: StopReason,
timestamp: ProtocolTimestamp,
) -> Result<Self, MessageValidationError> {
validate_content(&content, ContentBlock::valid_for_assistant)?;
Ok(Self::Assistant {
id,
content,
stop_reason,
timestamp,
})
}
pub fn tool_result_success(
id: MessageId,
tool_call_id: ToolCallId,
tool_name: impl Into<String>,
content: Vec<ContentBlock>,
timestamp: ProtocolTimestamp,
) -> Result<Self, MessageValidationError> {
Self::tool_result(id, tool_call_id, tool_name.into(), content, None, timestamp)
}
pub fn tool_result_failure(
id: MessageId,
tool_call_id: ToolCallId,
tool_name: impl Into<String>,
content: Vec<ContentBlock>,
error: ToolFailure,
timestamp: ProtocolTimestamp,
) -> Result<Self, MessageValidationError> {
Self::tool_result(
id,
tool_call_id,
tool_name.into(),
content,
Some(error),
timestamp,
)
}
#[must_use]
pub const fn role(&self) -> MessageRole {
match self {
Self::User { .. } => MessageRole::User,
Self::Assistant { .. } => MessageRole::Assistant,
Self::ToolResult { .. } => MessageRole::ToolResult,
}
}
fn validate(&self) -> Result<(), MessageValidationError> {
match self {
Self::User { content, .. } => validate_content(content, ContentBlock::valid_for_user),
Self::Assistant { content, .. } => {
validate_content(content, ContentBlock::valid_for_assistant)
}
Self::ToolResult {
tool_name,
content,
is_error,
error,
..
} => {
validate_tool_name(tool_name)?;
validate_content(content, ContentBlock::valid_for_tool_result)?;
if *is_error != error.is_some() {
return Err(MessageValidationError::InconsistentToolFailure);
}
Ok(())
}
}
}
fn tool_result(
id: MessageId,
tool_call_id: ToolCallId,
tool_name: String,
content: Vec<ContentBlock>,
error: Option<ToolFailure>,
timestamp: ProtocolTimestamp,
) -> Result<Self, MessageValidationError> {
validate_tool_name(&tool_name)?;
validate_content(&content, ContentBlock::valid_for_tool_result)?;
Ok(Self::ToolResult {
id,
tool_call_id,
tool_name,
content,
is_error: error.is_some(),
error,
timestamp,
})
}
}
impl Serialize for CanonicalMessage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.validate().map_err(serde::ser::Error::custom)?;
SerializableCanonicalMessage::from(self).serialize(serializer)
}
}
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum SerializableCanonicalMessage<'a> {
User {
id: &'a MessageId,
content: &'a [ContentBlock],
timestamp: &'a ProtocolTimestamp,
},
Assistant {
id: &'a MessageId,
content: &'a [ContentBlock],
#[serde(rename = "stopReason")]
stop_reason: &'a StopReason,
timestamp: &'a ProtocolTimestamp,
},
ToolResult {
id: &'a MessageId,
#[serde(rename = "toolCallId")]
tool_call_id: &'a ToolCallId,
#[serde(rename = "toolName")]
tool_name: &'a str,
content: &'a [ContentBlock],
#[serde(rename = "isError")]
is_error: bool,
#[serde(skip_serializing_if = "Option::is_none")]
error: &'a Option<ToolFailure>,
timestamp: &'a ProtocolTimestamp,
},
}
impl<'a> From<&'a CanonicalMessage> for SerializableCanonicalMessage<'a> {
fn from(value: &'a CanonicalMessage) -> Self {
match value {
CanonicalMessage::User {
id,
content,
timestamp,
} => Self::User {
id,
content,
timestamp,
},
CanonicalMessage::Assistant {
id,
content,
stop_reason,
timestamp,
} => Self::Assistant {
id,
content,
stop_reason,
timestamp,
},
CanonicalMessage::ToolResult {
id,
tool_call_id,
tool_name,
content,
is_error,
error,
timestamp,
} => Self::ToolResult {
id,
tool_call_id,
tool_name,
content,
is_error: *is_error,
error,
timestamp,
},
}
}
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum RawCanonicalMessage {
User {
id: MessageId,
content: Vec<ContentBlock>,
timestamp: ProtocolTimestamp,
},
Assistant {
id: MessageId,
content: Vec<ContentBlock>,
#[serde(rename = "stopReason")]
stop_reason: StopReason,
timestamp: ProtocolTimestamp,
},
ToolResult {
id: MessageId,
#[serde(rename = "toolCallId")]
tool_call_id: ToolCallId,
#[serde(rename = "toolName")]
tool_name: String,
content: Vec<ContentBlock>,
#[serde(rename = "isError")]
is_error: bool,
#[serde(default)]
error: Option<ToolFailure>,
timestamp: ProtocolTimestamp,
},
}
impl<'de> Deserialize<'de> for CanonicalMessage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
match RawCanonicalMessage::deserialize(deserializer)? {
RawCanonicalMessage::User {
id,
content,
timestamp,
} => Self::user(id, content, timestamp),
RawCanonicalMessage::Assistant {
id,
content,
stop_reason,
timestamp,
} => Self::assistant(id, content, stop_reason, timestamp),
RawCanonicalMessage::ToolResult {
id,
tool_call_id,
tool_name,
content,
is_error,
error,
timestamp,
} => {
if is_error != error.is_some() {
return Err(serde::de::Error::custom(
"tool-result isError must match error presence",
));
}
Self::tool_result(id, tool_call_id, tool_name, content, error, timestamp)
}
}
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum StopReason {
Completed,
Length,
ToolUse,
PauseTurn,
Cancelled,
Error,
Unknown(String),
}
impl StopReason {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Completed => "completed",
Self::Length => "length",
Self::ToolUse => "tool_use",
Self::PauseTurn => "pause_turn",
Self::Cancelled => "cancelled",
Self::Error => "error",
Self::Unknown(value) => value,
}
}
#[must_use]
pub const fn is_success(&self) -> bool {
matches!(self, Self::Completed)
}
}
impl Serialize for StopReason {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if !valid_error_code(self.as_str()) {
return Err(serde::ser::Error::custom("invalid stop reason"));
}
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for StopReason {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
if !valid_error_code(&value) {
return Err(serde::de::Error::custom("invalid stop reason"));
}
Ok(match value.as_str() {
"completed" => Self::Completed,
"length" => Self::Length,
"tool_use" => Self::ToolUse,
"pause_turn" => Self::PauseTurn,
"cancelled" => Self::Cancelled,
"error" => Self::Error,
_ => Self::Unknown(value),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolFailure {
code: String,
message: String,
}
impl ToolFailure {
#[must_use]
pub fn approval_denied() -> Self {
Self {
code: "approval_denied".to_owned(),
message: "tool invocation was denied by approval".to_owned(),
}
}
pub fn new(
code: impl Into<String>,
message: impl Into<String>,
) -> Result<Self, MessageValidationError> {
let code = code.into();
let message = message.into();
if !valid_error_code(&code)
|| message.is_empty()
|| message.len() > 4096
|| message.contains('\0')
{
return Err(MessageValidationError::InvalidToolFailure);
}
Ok(Self { code, message })
}
#[must_use]
pub fn code(&self) -> &str {
&self.code
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
#[derive(Deserialize)]
struct RawToolFailure {
code: String,
message: String,
}
impl<'de> Deserialize<'de> for ToolFailure {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = RawToolFailure::deserialize(deserializer)?;
Self::new(raw.code, raw.message).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Error)]
pub enum MessageValidationError {
#[error("message content is empty or contains a block invalid for its role")]
InvalidContent,
#[error("invalid message content: {0}")]
InvalidContentValue(#[from] ContentValidationError),
#[error("tool failure code or message is invalid")]
InvalidToolFailure,
#[error("tool-result isError must match error presence")]
InconsistentToolFailure,
}
fn validate_content(
content: &[ContentBlock],
predicate: impl Fn(&ContentBlock) -> bool,
) -> Result<(), MessageValidationError> {
if content.is_empty() || content.len() > 256 || !content.iter().all(predicate) {
return Err(MessageValidationError::InvalidContent);
}
for block in content {
block.validate()?;
}
Ok(())
}
fn valid_error_code(value: &str) -> bool {
let mut bytes = value.bytes();
bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
&& value.len() <= 128
&& bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
}
impl fmt::Display for StopReason {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}