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