Skip to main content

objectiveai_sdk/agent/completions/message/
mod.rs

1//! Message types for agent completions.
2//!
3//! Messages represent the conversation history sent to the model. Each message
4//! has a role (system, user, assistant, tool, or developer) and content.
5
6mod assistant_message;
7mod developer_message;
8mod file_content;
9mod pipe_ack;
10mod rich_content;
11mod simple_content;
12mod system_message;
13mod tool_message;
14mod user_message;
15
16pub use assistant_message::*;
17pub use developer_message::*;
18pub use file_content::*;
19pub use pipe_ack::*;
20pub use rich_content::*;
21pub use simple_content::*;
22pub use system_message::*;
23pub use tool_message::*;
24pub use user_message::*;
25
26#[cfg(test)]
27mod assistant_message_tests;
28#[cfg(all(test, feature = "mcp"))]
29mod rich_content_tests;
30
31use crate::functions;
32use functions::expression::{ExpressionError, FromStarlarkValue};
33use schemars::JsonSchema;
34use serde::{Deserialize, Serialize};
35use starlark::values::dict::DictRef as StarlarkDictRef;
36use starlark::values::{UnpackValue, Value as StarlarkValue};
37
38/// Utilities for working with message prompts.
39pub mod prompt {
40    use super::Message;
41    use schemars::JsonSchema;
42
43    /// Returns whether two messages are the same chainable role
44    /// (developer, user, or system) and have compatible names
45    /// (at most one has a name, or both have the same name).
46    fn is_chain(a: &Message, b: &Message) -> bool {
47        match (a, b) {
48            (Message::Developer(a), Message::Developer(b)) => {
49                !a.has_name() || !b.has_name() || a.name == b.name
50            }
51            (Message::System(a), Message::System(b)) => {
52                !a.has_name() || !b.has_name() || a.name == b.name
53            }
54            (Message::User(a), Message::User(b)) => {
55                !a.has_name() || !b.has_name() || a.name == b.name
56            }
57            _ => false,
58        }
59    }
60
61    /// Pushes `other` into `target` (same-role merge).
62    fn push(target: &mut Message, other: &Message) {
63        match (target, other) {
64            (Message::Developer(t), Message::Developer(o)) => t.push(o),
65            (Message::System(t), Message::System(o)) => t.push(o),
66            (Message::User(t), Message::User(o)) => t.push(o),
67            _ => unreachable!(),
68        }
69    }
70
71    /// Prepares a list of messages by normalizing each one, then
72    /// merging chains of consecutive same-role developer/user/system
73    /// messages (only when at most one message in the chain has a name).
74    pub fn prepare(messages: &mut Vec<Message>) {
75        messages.iter_mut().for_each(Message::prepare);
76
77        // scan for any chain to avoid allocation if none exist
78        let has_chain = messages.windows(2).any(|w| is_chain(&w[0], &w[1]));
79        if !has_chain {
80            return;
81        }
82
83        let mut merged = Vec::with_capacity(messages.len());
84        for msg in messages.drain(..) {
85            if let Some(last) = merged.last_mut() {
86                if is_chain(last, &msg) {
87                    push(last, &msg);
88                    continue;
89                }
90            }
91            merged.push(msg);
92        }
93        *messages = merged;
94
95        // re-prepare after merging
96        prepare(messages);
97    }
98
99    /// Computes a content-addressed ID for a list of messages.
100    pub fn id(messages: &[Message]) -> String {
101        let mut hasher = twox_hash::XxHash3_128::with_seed(0);
102        hasher.write(serde_json::to_string(messages).unwrap().as_bytes());
103        format!("{:0>22}", base62::encode(hasher.finish_128()))
104    }
105}
106
107/// A message in the conversation.
108#[derive(
109    Debug,
110    Clone,
111    PartialEq,
112    Serialize,
113    Deserialize,
114    JsonSchema,
115    arbitrary::Arbitrary,
116)]
117#[serde(tag = "role")]
118#[schemars(rename = "agent.completions.message.Message")]
119pub enum Message {
120    /// A developer message (similar to system, but from the developer).
121    #[schemars(title = "Developer")]
122    #[serde(rename = "developer")]
123    Developer(DeveloperMessage),
124    /// A system message setting context or instructions.
125    #[schemars(title = "System")]
126    #[serde(rename = "system")]
127    System(SystemMessage),
128    /// A user message from the end user.
129    #[schemars(title = "User")]
130    #[serde(rename = "user")]
131    User(UserMessage),
132    /// An assistant message (model's previous response).
133    #[schemars(title = "Assistant")]
134    #[serde(rename = "assistant")]
135    Assistant(AssistantMessage),
136    /// A tool message containing the result of a tool call.
137    #[schemars(title = "Tool")]
138    #[serde(rename = "tool")]
139    Tool(ToolMessage),
140}
141
142impl Message {
143    /// Prepares the message for sending by normalizing its content.
144    ///
145    /// This method consolidates consecutive text parts, removes empty parts,
146    /// and normalizes optional fields (setting empty strings to `None`).
147    pub fn prepare(&mut self) {
148        match self {
149            Message::Developer(msg) => msg.prepare(),
150            Message::System(msg) => msg.prepare(),
151            Message::User(msg) => msg.prepare(),
152            Message::Assistant(msg) => msg.prepare(),
153            Message::Tool(msg) => msg.prepare(),
154        }
155    }
156
157}
158
159impl FromStarlarkValue for Message {
160    fn from_starlark_value(
161        value: &StarlarkValue,
162    ) -> Result<Self, ExpressionError> {
163        let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
164            ExpressionError::StarlarkConversionError(
165                "Message: expected dict".into(),
166            )
167        })?;
168        // First pass: find the role
169        let mut role = None;
170        for (k, v) in dict.iter() {
171            if let Ok(Some("role")) = <&str as UnpackValue>::unpack_value(k) {
172                role = Some(
173                    <&str as UnpackValue>::unpack_value(v)
174                        .map_err(|e| {
175                            ExpressionError::StarlarkConversionError(
176                                e.to_string(),
177                            )
178                        })?
179                        .ok_or_else(|| {
180                            ExpressionError::StarlarkConversionError(
181                                "Message: expected string role".into(),
182                            )
183                        })?,
184                );
185                break;
186            }
187        }
188        let role = role.ok_or_else(|| {
189            ExpressionError::StarlarkConversionError(
190                "Message: missing role".into(),
191            )
192        })?;
193        match role {
194            "developer" => DeveloperMessage::from_starlark_value(value)
195                .map(Message::Developer),
196            "system" => {
197                SystemMessage::from_starlark_value(value).map(Message::System)
198            }
199            "user" => {
200                UserMessage::from_starlark_value(value).map(Message::User)
201            }
202            "assistant" => AssistantMessage::from_starlark_value(value)
203                .map(Message::Assistant),
204            "tool" => {
205                ToolMessage::from_starlark_value(value).map(Message::Tool)
206            }
207            _ => Err(ExpressionError::StarlarkConversionError(format!(
208                "Message: unknown role: {}",
209                role
210            ))),
211        }
212    }
213}
214
215/// A message with expressions for dynamic content.
216///
217/// This is the expression variant of [`Message`] used in function definitions
218/// where message content can be computed from the function input at runtime.
219/// Supports both JMESPath and Starlark expressions.
220#[derive(
221    Debug,
222    Clone,
223    PartialEq,
224    Serialize,
225    Deserialize,
226    JsonSchema,
227    arbitrary::Arbitrary,
228)]
229#[serde(tag = "role")]
230#[schemars(rename = "agent.completions.message.MessageExpression")]
231pub enum MessageExpression {
232    #[schemars(title = "Developer")]
233    #[serde(rename = "developer")]
234    Developer(DeveloperMessageExpression),
235    #[schemars(title = "System")]
236    #[serde(rename = "system")]
237    System(SystemMessageExpression),
238    #[schemars(title = "User")]
239    #[serde(rename = "user")]
240    User(UserMessageExpression),
241    #[schemars(title = "Assistant")]
242    #[serde(rename = "assistant")]
243    Assistant(AssistantMessageExpression),
244    #[schemars(title = "Tool")]
245    #[serde(rename = "tool")]
246    Tool(ToolMessageExpression),
247}
248
249impl MessageExpression {
250    /// Compiles the expression into a concrete [`Message`].
251    ///
252    /// Evaluates all expressions (JMESPath or Starlark) using the provided
253    /// parameters and returns the resulting message.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if any expression evaluation fails.
258    pub fn compile(
259        self,
260        params: &functions::expression::Params,
261    ) -> Result<Message, functions::expression::ExpressionError> {
262        match self {
263            MessageExpression::Developer(msg) => {
264                msg.compile(params).map(Message::Developer)
265            }
266            MessageExpression::System(msg) => {
267                msg.compile(params).map(Message::System)
268            }
269            MessageExpression::User(msg) => {
270                msg.compile(params).map(Message::User)
271            }
272            MessageExpression::Assistant(msg) => {
273                msg.compile(params).map(Message::Assistant)
274            }
275            MessageExpression::Tool(msg) => {
276                msg.compile(params).map(Message::Tool)
277            }
278        }
279    }
280}
281
282impl FromStarlarkValue for MessageExpression {
283    fn from_starlark_value(
284        value: &StarlarkValue,
285    ) -> Result<Self, ExpressionError> {
286        let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
287            ExpressionError::StarlarkConversionError(
288                "MessageExpression: expected dict".into(),
289            )
290        })?;
291        // First pass: find the role
292        let mut role = None;
293        for (k, v) in dict.iter() {
294            if let Ok(Some("role")) = <&str as UnpackValue>::unpack_value(k) {
295                role = Some(
296                    <&str as UnpackValue>::unpack_value(v)
297                        .map_err(|e| {
298                            ExpressionError::StarlarkConversionError(
299                                e.to_string(),
300                            )
301                        })?
302                        .ok_or_else(|| {
303                            ExpressionError::StarlarkConversionError(
304                                "MessageExpression: expected string role"
305                                    .into(),
306                            )
307                        })?,
308                );
309                break;
310            }
311        }
312        let role = role.ok_or_else(|| {
313            ExpressionError::StarlarkConversionError(
314                "MessageExpression: missing role".into(),
315            )
316        })?;
317        match role {
318            "developer" => {
319                DeveloperMessageExpression::from_starlark_value(value)
320                    .map(MessageExpression::Developer)
321            }
322            "system" => SystemMessageExpression::from_starlark_value(value)
323                .map(MessageExpression::System),
324            "user" => UserMessageExpression::from_starlark_value(value)
325                .map(MessageExpression::User),
326            "assistant" => {
327                AssistantMessageExpression::from_starlark_value(value)
328                    .map(MessageExpression::Assistant)
329            }
330            "tool" => ToolMessageExpression::from_starlark_value(value)
331                .map(MessageExpression::Tool),
332            _ => Err(ExpressionError::StarlarkConversionError(format!(
333                "MessageExpression: unknown role: {}",
334                role
335            ))),
336        }
337    }
338}
339
340crate::functions::expression::impl_from_special_unsupported!(MessageExpression,);
341
342impl crate::functions::expression::FromSpecial
343    for Vec<crate::functions::expression::WithExpression<MessageExpression>>
344{
345    fn from_special(
346        _special: &crate::functions::expression::Special,
347        _params: &crate::functions::expression::Params,
348    ) -> Result<Self, crate::functions::expression::ExpressionError> {
349        Err(crate::functions::expression::ExpressionError::UnsupportedSpecial)
350    }
351}