Skip to main content

orion_core/
messages.rs

1use serde::{Deserialize, Serialize};
2
3/// Role in a conversation.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum Role {
7    /// System / developer instruction that frames the conversation.
8    System,
9    /// A message from the end user.
10    User,
11    /// A message from the model.
12    Assistant,
13    /// An assistant turn that requested one or more tool calls.
14    ToolCall,
15    /// The result of executing a tool, fed back to the model.
16    ToolResult,
17}
18
19/// A tool invocation requested by the assistant.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ToolCall {
22    /// Unique id linking this call to its [`ToolResult`].
23    pub id: String,
24    /// Name of the tool to invoke.
25    pub name: String,
26    /// Arguments to pass to the tool, as a JSON value.
27    pub arguments: serde_json::Value,
28}
29
30/// Result of executing a tool.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ToolResult {
33    /// Id of the [`ToolCall`] this result answers.
34    pub tool_call_id: String,
35    /// Name of the tool that produced this result.
36    pub tool_name: String,
37    /// The tool's output (or the error message when `is_error`).
38    pub content: String,
39    /// Whether the tool failed.
40    pub is_error: bool,
41}
42
43/// A single message in the conversation.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Message {
46    /// Stable identifier for the message (used to address pins and tool results).
47    pub id: String,
48    /// Who produced the message.
49    pub role: Role,
50    /// The message text.
51    pub content: String,
52    /// Creation time as a Unix timestamp in milliseconds.
53    pub timestamp: i64,
54
55    /// Tool calls made by the assistant (only when role = Assistant).
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub tool_calls: Vec<ToolCall>,
58
59    /// Tool execution result (only when role = ToolResult).
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub tool_result: Option<ToolResult>,
62
63    /// Token count for this message (populated after tokenization).
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub token_count: Option<u32>,
66
67    /// Whether this message is pinned. Pinned messages always survive context
68    /// pruning, regardless of the token budget or prune strategy.
69    #[serde(default, skip_serializing_if = "is_false")]
70    pub pinned: bool,
71}
72
73fn is_false(value: &bool) -> bool {
74    !*value
75}
76
77impl Message {
78    /// Build a system message.
79    ///
80    /// ```
81    /// use orion_core::Message;
82    /// let sys = Message::system("msg-1", "You are helpful.");
83    /// assert_eq!(sys.content, "You are helpful.");
84    /// ```
85    pub fn system(id: impl Into<String>, content: impl Into<String>) -> Self {
86        Self {
87            id: id.into(),
88            role: Role::System,
89            content: content.into(),
90            timestamp: chrono::Utc::now().timestamp_millis(),
91            tool_calls: vec![],
92            tool_result: None,
93            token_count: None,
94            pinned: false,
95        }
96    }
97
98    /// Build a user message.
99    pub fn user(id: impl Into<String>, content: impl Into<String>) -> Self {
100        Self {
101            id: id.into(),
102            role: Role::User,
103            content: content.into(),
104            timestamp: chrono::Utc::now().timestamp_millis(),
105            tool_calls: vec![],
106            tool_result: None,
107            token_count: None,
108            pinned: false,
109        }
110    }
111
112    /// Build an assistant message.
113    pub fn assistant(id: impl Into<String>, content: impl Into<String>) -> Self {
114        Self {
115            id: id.into(),
116            role: Role::Assistant,
117            content: content.into(),
118            timestamp: chrono::Utc::now().timestamp_millis(),
119            tool_calls: vec![],
120            tool_result: None,
121            token_count: None,
122            pinned: false,
123        }
124    }
125
126    /// Build a tool-result message, linking it back to the assistant's
127    /// [`ToolCall`] via `tool_call_id`.
128    ///
129    /// ```
130    /// use orion_core::Message;
131    /// let result = Message::tool_result(
132    ///     "msg-4",            // message id
133    ///     "call-1",           // tool_call_id (links to the assistant's request)
134    ///     "read_file",        // tool name
135    ///     "file contents...", // result content
136    ///     false,              // is_error
137    /// );
138    /// assert!(result.tool_result.is_some());
139    /// ```
140    pub fn tool_result(
141        id: impl Into<String>,
142        tool_call_id: impl Into<String>,
143        tool_name: impl Into<String>,
144        content: impl Into<String>,
145        is_error: bool,
146    ) -> Self {
147        let tool_call_id = tool_call_id.into();
148        let tool_name = tool_name.into();
149        let content = content.into();
150        Self {
151            id: id.into(),
152            role: Role::ToolResult,
153            content: content.clone(),
154            timestamp: chrono::Utc::now().timestamp_millis(),
155            tool_calls: vec![],
156            tool_result: Some(ToolResult {
157                tool_call_id,
158                tool_name,
159                content,
160                is_error,
161            }),
162            token_count: None,
163            pinned: false,
164        }
165    }
166
167    /// Mark this message as pinned (survives context pruning). Builder-style.
168    pub fn pinned(mut self) -> Self {
169        self.pinned = true;
170        self
171    }
172}