molo 0.2.3

A lightweight Rust agent framework
Documentation
//! Conversation messages.
//!
//! The message data model implements serde serialization / deserialization:
//! session persistence and cross-process transport (tool definitions and
//! messages) serialize these types directly, no manual mapping needed.
//!
//! A full conversation history is expressed as a [`Message`] sequence; each
//! message consists of [content blocks](ContentBlock) (text / images, or
//! vendor-shaped blocks passed through verbatim via [`ContentBlock::Wire`]).
//! The structured entry point of the content model is [`ContentBlock`] — the
//! shape of [`Message`] stays unchanged, and consumers just match the new
//! variant.

use serde::{Deserialize, Serialize};

/// A tool call requested by the model.
///
/// The model requests a tool call via the `tool_calls` field of
/// [`Message::Assistant`]; after the agent loop executes it, the result is
/// passed back as a [`Message::ToolResult`] message right after, paired
/// with this call by `id` (which disambiguates multiple calls to the
/// same-named tool in one turn).
///
/// # Example
///
/// Construct a tool request and pair it with the returned execution result
/// (in real flows the request is generated by the model and passed back
/// automatically after the loop executes it):
///
/// ```
/// use molo::{Message, ToolCall};
///
/// let request = Message::Assistant {
///     content: String::new(),
///     reasoning: None,
///     tool_calls: vec![ToolCall {
///         id: "call_1".into(),
///         name: "weather".into(),
///         arguments: r#"{"city":"Beijing"}"#.into(),
///     }],
/// };
/// let result = Message::tool_result("call_1", "Sunny, 23°C");
///
/// let Message::Assistant { tool_calls, .. } = &request else {
///     panic!("expected an assistant message");
/// };
/// assert_eq!(tool_calls[0].id, "call_1");
/// assert_eq!(result, Message::tool_result("call_1", "Sunny, 23°C"));
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ToolCall {
    /// Unique id of this call; the execution result is paired with it via
    /// the `id` of [`Message::ToolResult`].
    pub id: String,
    /// Tool name, matching the `name` of the tool definition
    /// [`Tool::schema`](crate::Tool::schema).
    pub name: String,
    /// Model-generated arguments (JSON text), parsed by the agent loop and
    /// handed to the tool for execution.
    pub arguments: String,
}

/// A single message in a conversation.
///
/// This type carries context uniformly between Provider / Memory / Agent:
/// what Memory stores and returns, and what Provider sends and receives,
/// are all [`Message`] sequences; Provider implementations map them to the
/// vendor's wire format.
///
/// Note: the model may request several tools in one turn, and these requests
/// **must stay in a single [`Message::Assistant`] message** — splitting them
/// across messages breaks vendor wire validation (some vendors require tool
/// results to immediately follow the assistant message carrying them, e.g.
/// DeepSeek).
///
/// # Example
///
/// Organize a conversation history with the convenience constructors (in
/// real scenarios the agent loop and Memory do this automatically):
///
/// ```
/// use molo::Message;
///
/// let history = vec![
///     Message::system("You are a helpful assistant"),
///     Message::user("How is the weather in Beijing today?"),
///     Message::assistant("Let me check the weather."),
///     Message::tool_result("call_1", "Sunny, 23°C"),
/// ];
///
/// assert_eq!(history.len(), 4);
/// assert_eq!(history[1], Message::user("How is the weather in Beijing today?"));
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Message {
    /// System instruction describing the agent's role and behavior
    /// constraints.
    System(String),
    /// User input, made of content blocks (text / images / vendor-shaped
    /// pass-through blocks).
    User(Vec<ContentBlock>),
    /// Assistant reply: text + reasoning + requested tool calls (multiple
    /// requests in one turn stay together).
    Assistant {
        /// Reply text.
        content: String,
        /// The model's reasoning; provided by thinking models (e.g.
        /// DeepSeek / Qwen3), `None` for other models.
        ///
        /// This field is a vendor extension and **must be passed back
        /// verbatim when sending the conversation history**, otherwise the
        /// API rejects the request (e.g. DeepSeek reports
        /// "The reasoning_content in the thinking mode must be passed back to the API.").
        reasoning: Option<String>,
        /// Tools requested by the model with this reply; execution results
        /// are passed back right after as [`Message::ToolResult`] messages.
        tool_calls: Vec<ToolCall>,
    },
    /// Tool execution result, passed back to the model.
    ToolResult {
        /// The id corresponding to [`ToolCall::id`].
        id: String,
        /// Text of the execution result (on failure, the error text; the
        /// model decides what to do next).
        content: String,
    },
}

/// A content block within a message.
///
/// The structured entry point of the content model: text and image blocks,
/// plus vendor-shaped pass-through blocks ([`Wire`](ContentBlock::Wire)).
///
/// # Example
///
/// ```
/// use molo::{ContentBlock, ImageContent, Message};
///
/// let msg = Message::user_blocks(vec![
///     ContentBlock::Text("What is in this picture?".into()),
///     ContentBlock::Image(ImageContent::new("image/png", vec![0x89, b'P', b'N', b'G'])),
/// ]);
///
/// assert_eq!(msg, Message::user_blocks(vec![
///     ContentBlock::Text("What is in this picture?".into()),
///     ContentBlock::Image(ImageContent::new("image/png", vec![0x89, b'P', b'N', b'G'])),
/// ]));
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ContentBlock {
    /// A text block.
    Text(String),
    /// An image block (raw bytes + MIME type). Provider implementations
    /// encode it for the wire format (e.g. the OpenAI-compatible
    /// `image_url` content block with a base64 data URL); Memory counts it
    /// as no tokens and summarizers render it as a placeholder.
    Image(ImageContent),
    /// A vendor-shaped content block passed through verbatim (e.g. the
    /// `input_audio` / `file` parts of OpenAI-compatible endpoints, whose
    /// shapes vary per vendor and keep evolving).
    ///
    /// For any modality without a typed variant, build the wire block
    /// yourself and wrap it here; the provider inserts it into the content
    /// array as-is. Whatever your endpoint accepts, this block can carry it.
    Wire(serde_json::Value),
}

/// Raw image data carried in a [`ContentBlock::Image`].
///
/// Stores the raw bytes so callers never need to base64-encode; serialization
/// (session persistence / cross-process transport) keeps the bytes as-is.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ImageContent {
    /// MIME type of the image, e.g. `image/png` / `image/jpeg`; used to build
    /// the wire data URL (`data:<mime>;base64,...`).
    pub mime_type: String,
    /// Raw image bytes (not yet base64-encoded).
    pub data: Vec<u8>,
}

impl ImageContent {
    /// Constructs from raw bytes and a MIME type.
    ///
    /// # Example
    ///
    /// ```
    /// use molo::ImageContent;
    ///
    /// let image = ImageContent::new("image/png", std::fs::read("logo.png").unwrap_or_default());
    /// assert_eq!(image.mime_type, "image/png");
    /// ```
    pub fn new(mime_type: impl Into<String>, data: Vec<u8>) -> Self {
        Self {
            mime_type: mime_type.into(),
            data,
        }
    }
}

impl Message {
    /// A system instruction message.
    pub fn system(content: impl Into<String>) -> Self {
        Self::System(content.into())
    }

    /// A plain-text user message (a single text block, equivalent to
    /// `user_blocks(vec![ContentBlock::Text(content)])`).
    pub fn user(content: impl Into<String>) -> Self {
        Self::User(vec![ContentBlock::Text(content.into())])
    }

    /// User input made of content blocks (text / images / vendor-shaped
    /// pass-through blocks).
    pub fn user_blocks(blocks: Vec<ContentBlock>) -> Self {
        Self::User(blocks)
    }

    /// An assistant reply message (no reasoning, no tool requests).
    pub fn assistant(content: impl Into<String>) -> Self {
        Self::Assistant {
            content: content.into(),
            reasoning: None,
            tool_calls: Vec::new(),
        }
    }

    /// An assistant reply with reasoning (for thinking models; pass
    /// `reasoning` back verbatim when sending history, otherwise the API
    /// rejects the request).
    pub fn assistant_with_reasoning(
        content: impl Into<String>,
        reasoning: impl Into<String>,
    ) -> Self {
        Self::Assistant {
            content: content.into(),
            reasoning: Some(reasoning.into()),
            tool_calls: Vec::new(),
        }
    }

    /// A tool execution result passed back to the model; `id` corresponds to
    /// the [`ToolCall::id`] in the [`Message::Assistant`] message that
    /// carried the request.
    ///
    /// # Example
    ///
    /// ```
    /// use molo::Message;
    ///
    /// let result = Message::tool_result("call_1", "Sunny, 23°C");
    /// assert_eq!(result, Message::ToolResult {
    ///     id: "call_1".into(),
    ///     content: "Sunny, 23°C".into(),
    /// });
    /// ```
    pub fn tool_result(id: impl Into<String>, content: impl Into<String>) -> Self {
        Self::ToolResult {
            id: id.into(),
            content: content.into(),
        }
    }
}