Skip to main content

fluers_core/
message.rs

1//! Conversation messages and content blocks.
2//!
3//! Mirrors `AgentMessage`, `ImageContent`, and Flue's `SignalMessage`.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::tool::ToolCall;
10/// Who authored a message in the conversation.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum Role {
14    /// System / developer instructions.
15    System,
16    /// The human user.
17    User,
18    /// The assistant / model.
19    Assistant,
20    /// A tool result returned to the model.
21    Tool,
22    /// A Flue "signal" event (lifecycle / framework-injected).
23    Signal,
24}
25
26/// A single piece of message content.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "type", rename_all = "snake_case")]
29pub enum ContentBlock {
30    /// Plain text.
31    Text {
32        /// The text body.
33        text: String,
34    },
35    /// An image attachment.
36    Image {
37        /// The image payload.
38        image: ImageContent,
39    },
40    /// A tool call issued by the model.
41    ToolUse {
42        /// The call id, used to correlate the later result.
43        id: String,
44        /// The call itself.
45        #[serde(flatten)]
46        call: ToolCall,
47    },
48    /// A tool result returned to the model.
49    ToolResult {
50        /// The call id this result corresponds to.
51        tool_use_id: String,
52        /// Serialized result content.
53        content: Value,
54    },
55}
56
57/// An image attached to a message.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ImageContent {
60    /// Media type, e.g. `image/png`.
61    #[serde(rename = "media_type")]
62    pub media_type: String,
63    /// Raw image bytes.
64    #[serde(with = "serde_base64")]
65    pub data: Vec<u8>,
66}
67
68/// Base64 (de)serialization for [`ImageContent::data`], backed by the
69/// audited `base64` crate rather than a hand-rolled codec.
70mod serde_base64 {
71    use base64::{engine::general_purpose::STANDARD, Engine};
72    use serde::{Deserialize, Deserializer, Serialize, Serializer};
73
74    pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
75        STANDARD.encode(v).serialize(s)
76    }
77
78    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
79        let s = String::deserialize(d)?;
80        STANDARD
81            .decode(s.as_bytes())
82            .map_err(serde::de::Error::custom)
83    }
84}
85
86/// A Flue "signal" message — a framework-injected lifecycle event that lives
87/// in the message stream alongside user/assistant turns.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct SignalMessage {
90    /// Always `signal`.
91    pub role: Role,
92    /// The signal type identifier.
93    #[serde(rename = "type")]
94    pub kind: String,
95    /// Optional tag name for structured signals.
96    #[serde(rename = "tag_name", skip_serializing_if = "Option::is_none")]
97    pub tag_name: Option<String>,
98    /// The signal body.
99    pub content: String,
100    /// Optional attributes.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub attributes: Option<std::collections::BTreeMap<String, String>>,
103    /// When the signal fired.
104    pub timestamp: DateTime<Utc>,
105}
106
107/// A full conversation message.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct AgentMessage {
110    /// Author role.
111    pub role: Role,
112    /// Content blocks (text / images / tool use / tool results).
113    #[serde(default, skip_serializing_if = "Vec::is_empty")]
114    pub content: Vec<ContentBlock>,
115}