Skip to main content

molo_core/
message.rs

1//! Conversation messages.
2//!
3//! The message data model implements serde serialization / deserialization:
4//! session persistence and cross-process transport (tool definitions and
5//! messages) serialize these types directly, no manual mapping needed.
6//!
7//! A full conversation history is expressed as a [`Message`] sequence; each
8//! message consists of [content blocks](ContentBlock) (text / images, or
9//! vendor-shaped blocks passed through verbatim via [`ContentBlock::Wire`]).
10//! The structured entry point of the content model is [`ContentBlock`] — the
11//! shape of [`Message`] stays unchanged, and consumers just match the new
12//! variant.
13
14use serde::{Deserialize, Serialize};
15
16/// A tool call requested by the model.
17///
18/// The model requests a tool call via the `tool_calls` field of
19/// [`Message::Assistant`]; after the agent loop executes it, the result is
20/// passed back as a [`Message::ToolResult`] message right after, paired
21/// with this call by `id` (which disambiguates multiple calls to the
22/// same-named tool in one turn).
23///
24/// # Example
25///
26/// Construct a tool request and pair it with the returned execution result
27/// (in real flows the request is generated by the model and passed back
28/// automatically after the loop executes it):
29///
30/// ```
31/// # extern crate molo_core as molo;
32/// use molo::{Message, ToolCall};
33///
34/// let request = Message::Assistant {
35///     content: String::new(),
36///     reasoning: None,
37///     tool_calls: vec![ToolCall {
38///         id: "call_1".into(),
39///         name: "weather".into(),
40///         arguments: r#"{"city":"Beijing"}"#.into(),
41///     }],
42/// };
43/// let result = Message::tool_result("call_1", "Sunny, 23°C");
44///
45/// let Message::Assistant { tool_calls, .. } = &request else {
46///     panic!("expected an assistant message");
47/// };
48/// assert_eq!(tool_calls[0].id, "call_1");
49/// assert_eq!(result, Message::tool_result("call_1", "Sunny, 23°C"));
50/// ```
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub struct ToolCall {
53    /// Unique id of this call; the execution result is paired with it via
54    /// the `id` of [`Message::ToolResult`].
55    pub id: String,
56    /// Tool name, matching the `name` of the tool definition
57    /// [`Tool::schema`](crate::Tool::schema).
58    pub name: String,
59    /// Model-generated arguments (JSON text), parsed by the agent loop and
60    /// handed to the tool for execution.
61    pub arguments: String,
62}
63
64/// A single message in a conversation.
65///
66/// This type carries context uniformly between Provider / Memory / Agent:
67/// what Memory stores and returns, and what Provider sends and receives,
68/// are all [`Message`] sequences; Provider implementations map them to the
69/// vendor's wire format.
70///
71/// Note: the model may request several tools in one turn, and these requests
72/// **must stay in a single [`Message::Assistant`] message** — splitting them
73/// across messages breaks vendor wire validation (some vendors require tool
74/// results to immediately follow the assistant message carrying them, e.g.
75/// DeepSeek).
76///
77/// # Example
78///
79/// Organize a conversation history with the convenience constructors (in
80/// real scenarios the agent loop and Memory do this automatically):
81///
82/// ```
83/// # extern crate molo_core as molo;
84/// use molo::Message;
85///
86/// let history = vec![
87///     Message::system("You are a helpful assistant"),
88///     Message::user("How is the weather in Beijing today?"),
89///     Message::assistant("Let me check the weather."),
90///     Message::tool_result("call_1", "Sunny, 23°C"),
91/// ];
92///
93/// assert_eq!(history.len(), 4);
94/// assert_eq!(history[1], Message::user("How is the weather in Beijing today?"));
95/// ```
96#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
97pub enum Message {
98    /// System instruction describing the agent's role and behavior
99    /// constraints.
100    System(String),
101    /// User input, made of content blocks (text / images / vendor-shaped
102    /// pass-through blocks).
103    User(Vec<ContentBlock>),
104    /// Assistant reply: text + reasoning + requested tool calls (multiple
105    /// requests in one turn stay together).
106    Assistant {
107        /// Reply text.
108        content: String,
109        /// The model's reasoning; provided by thinking models (e.g.
110        /// DeepSeek / Qwen3), `None` for other models.
111        ///
112        /// This field is a vendor extension and **must be passed back
113        /// verbatim when sending the conversation history**, otherwise the
114        /// API rejects the request (e.g. DeepSeek reports
115        /// "The reasoning_content in the thinking mode must be passed back to the API.").
116        reasoning: Option<String>,
117        /// Tools requested by the model with this reply; execution results
118        /// are passed back right after as [`Message::ToolResult`] messages.
119        tool_calls: Vec<ToolCall>,
120    },
121    /// Tool execution result, passed back to the model.
122    ToolResult {
123        /// The id corresponding to [`ToolCall::id`].
124        id: String,
125        /// Text of the execution result (on failure, the error text; the
126        /// model decides what to do next).
127        content: String,
128    },
129}
130
131/// A content block within a message.
132///
133/// The structured entry point of the content model: text and image blocks,
134/// plus vendor-shaped pass-through blocks ([`Wire`](ContentBlock::Wire)).
135///
136/// # Example
137///
138/// ```
139/// # extern crate molo_core as molo;
140/// use molo::{ContentBlock, ImageContent, Message};
141///
142/// let msg = Message::user_blocks(vec![
143///     ContentBlock::Text("What is in this picture?".into()),
144///     ContentBlock::Image(ImageContent::new("image/png", vec![0x89, b'P', b'N', b'G'])),
145/// ]);
146///
147/// assert_eq!(msg, Message::user_blocks(vec![
148///     ContentBlock::Text("What is in this picture?".into()),
149///     ContentBlock::Image(ImageContent::new("image/png", vec![0x89, b'P', b'N', b'G'])),
150/// ]));
151/// ```
152#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
153pub enum ContentBlock {
154    /// A text block.
155    Text(String),
156    /// An image block (raw bytes + MIME type). Provider implementations
157    /// encode it for the wire format (e.g. the OpenAI-compatible
158    /// `image_url` content block with a base64 data URL); Memory counts it
159    /// as no tokens and summarizers render it as a placeholder.
160    Image(ImageContent),
161    /// A vendor-shaped content block passed through verbatim (e.g. the
162    /// `input_audio` / `file` parts of OpenAI-compatible endpoints, whose
163    /// shapes vary per vendor and keep evolving).
164    ///
165    /// For any modality without a typed variant, build the wire block
166    /// yourself and wrap it here; the provider inserts it into the content
167    /// array as-is. Whatever your endpoint accepts, this block can carry it.
168    Wire(serde_json::Value),
169}
170
171/// Raw image data carried in a [`ContentBlock::Image`].
172///
173/// Stores the raw bytes so callers never need to base64-encode; serialization
174/// (session persistence / cross-process transport) keeps the bytes as-is.
175#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
176pub struct ImageContent {
177    /// MIME type of the image, e.g. `image/png` / `image/jpeg`; used to build
178    /// the wire data URL (`data:<mime>;base64,...`).
179    pub mime_type: String,
180    /// Raw image bytes (not yet base64-encoded).
181    pub data: Vec<u8>,
182}
183
184impl ImageContent {
185    /// Constructs from raw bytes and a MIME type.
186    ///
187    /// # Example
188    ///
189    /// ```
190    /// # extern crate molo_core as molo;
191    /// use molo::ImageContent;
192    ///
193    /// let image = ImageContent::new("image/png", std::fs::read("logo.png").unwrap_or_default());
194    /// assert_eq!(image.mime_type, "image/png");
195    /// ```
196    pub fn new(mime_type: impl Into<String>, data: Vec<u8>) -> Self {
197        Self {
198            mime_type: mime_type.into(),
199            data,
200        }
201    }
202}
203
204impl Message {
205    /// A system instruction message.
206    pub fn system(content: impl Into<String>) -> Self {
207        Self::System(content.into())
208    }
209
210    /// A plain-text user message (a single text block, equivalent to
211    /// `user_blocks(vec![ContentBlock::Text(content)])`).
212    pub fn user(content: impl Into<String>) -> Self {
213        Self::User(vec![ContentBlock::Text(content.into())])
214    }
215
216    /// User input made of content blocks (text / images / vendor-shaped
217    /// pass-through blocks).
218    pub fn user_blocks(blocks: Vec<ContentBlock>) -> Self {
219        Self::User(blocks)
220    }
221
222    /// An assistant reply message (no reasoning, no tool requests).
223    pub fn assistant(content: impl Into<String>) -> Self {
224        Self::Assistant {
225            content: content.into(),
226            reasoning: None,
227            tool_calls: Vec::new(),
228        }
229    }
230
231    /// An assistant reply with reasoning (for thinking models; pass
232    /// `reasoning` back verbatim when sending history, otherwise the API
233    /// rejects the request).
234    pub fn assistant_with_reasoning(
235        content: impl Into<String>,
236        reasoning: impl Into<String>,
237    ) -> Self {
238        Self::Assistant {
239            content: content.into(),
240            reasoning: Some(reasoning.into()),
241            tool_calls: Vec::new(),
242        }
243    }
244
245    /// A tool execution result passed back to the model; `id` corresponds to
246    /// the [`ToolCall::id`] in the [`Message::Assistant`] message that
247    /// carried the request.
248    ///
249    /// # Example
250    ///
251    /// ```
252    /// # extern crate molo_core as molo;
253    /// use molo::Message;
254    ///
255    /// let result = Message::tool_result("call_1", "Sunny, 23°C");
256    /// assert_eq!(result, Message::ToolResult {
257    ///     id: "call_1".into(),
258    ///     content: "Sunny, 23°C".into(),
259    /// });
260    /// ```
261    pub fn tool_result(id: impl Into<String>, content: impl Into<String>) -> Self {
262        Self::ToolResult {
263            id: id.into(),
264            content: content.into(),
265        }
266    }
267}