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