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}