Skip to main content

tea_protocol/
message.rs

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/// Role of a canonical message.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum MessageRole {
12    /// User-authored input.
13    User,
14    /// Model-authored output.
15    Assistant,
16    /// Result of a tool call.
17    ToolResult,
18}
19
20/// A provider-neutral conversation message.
21#[derive(Debug, Clone, PartialEq)]
22pub enum CanonicalMessage {
23    /// User-authored input.
24    User {
25        /// Stable message identifier.
26        id: MessageId,
27        /// User-visible text or images.
28        content: Vec<ContentBlock>,
29        /// Message creation time.
30        timestamp: ProtocolTimestamp,
31    },
32    /// Model-authored output.
33    Assistant {
34        /// Stable message identifier.
35        id: MessageId,
36        /// Text, thinking, and tool calls.
37        content: Vec<ContentBlock>,
38        /// Normalized completion reason.
39        stop_reason: StopReason,
40        /// Message completion time.
41        timestamp: ProtocolTimestamp,
42    },
43    /// Result returned for a tool call.
44    ToolResult {
45        /// Stable message identifier.
46        id: MessageId,
47        /// Tool call receiving this result.
48        tool_call_id: ToolCallId,
49        /// Registered tool name.
50        tool_name: String,
51        /// Text or image result content.
52        content: Vec<ContentBlock>,
53        /// Whether execution failed.
54        is_error: bool,
55        /// Machine-readable failure details, present only on failure.
56        error: Option<ToolFailure>,
57        /// Result completion time.
58        timestamp: ProtocolTimestamp,
59    },
60}
61
62impl CanonicalMessage {
63    /// Creates a validated user message.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error when content is empty, too numerous, or contains a
68    /// block that is invalid for the user role.
69    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    /// Creates a validated assistant message.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error when content is empty, too numerous, or contains a
87    /// block that is invalid for the assistant role.
88    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    /// Creates a successful tool-result message.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error when the tool name or result content is invalid.
108    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    /// Creates a failed tool-result message.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error when the tool name or result content is invalid.
123    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    /// Returns the message role.
142    #[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/// Normalized assistant stop reason with forward-compatible unknown values.
349#[derive(Debug, Clone, PartialEq, Eq, Hash)]
350pub enum StopReason {
351    /// Model completed normally.
352    Completed,
353    /// Model reached an output limit.
354    Length,
355    /// Model requested one or more tools.
356    ToolUse,
357    /// Provider paused a server-side tool loop and requires transcript replay.
358    PauseTurn,
359    /// Operation was cancelled.
360    Cancelled,
361    /// Provider or runtime failed.
362    Error,
363    /// A future provider-neutral value not known by this crate.
364    Unknown(String),
365}
366
367impl StopReason {
368    /// Returns the serialized reason.
369    #[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    /// Returns whether this reason represents normal completion.
383    #[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/// Machine-readable failure returned by a tool.
423#[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    /// Creates the canonical model-visible approval-denied failure.
432    #[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    /// Creates validated tool failure details.
441    ///
442    /// # Errors
443    ///
444    /// Returns an error when the code is not canonical snake case or the
445    /// technical message is empty, oversized, or contains a null character.
446    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    /// Returns the machine-readable failure code.
463    #[must_use]
464    pub fn code(&self) -> &str {
465        &self.code
466    }
467
468    /// Returns the English technical diagnostic message.
469    #[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/// Error returned when validating canonical messages.
492#[derive(Debug, Error)]
493pub enum MessageValidationError {
494    /// The content array is empty or contains a block invalid for the role.
495    #[error("message content is empty or contains a block invalid for its role")]
496    InvalidContent,
497    /// Content or tool-name validation failed.
498    #[error("invalid message content: {0}")]
499    InvalidContentValue(#[from] ContentValidationError),
500    /// Tool failure code or message is invalid.
501    #[error("tool failure code or message is invalid")]
502    InvalidToolFailure,
503    /// Tool-result error flag does not match failure presence.
504    #[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}