steer_core/app/conversation/
message.rs1use serde::{Deserialize, Serialize};
9use std::time::{SystemTime, UNIX_EPOCH};
10use steer_tools::ToolCall;
11pub use steer_tools::result::ToolResult;
12use strum_macros::Display;
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy, Display)]
16pub enum Role {
17 User,
18 Assistant,
19 Tool,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(tag = "source", rename_all = "snake_case")]
24pub enum ImageSource {
25 SessionFile { relative_path: String },
26 DataUrl { data_url: String },
27 Url { url: String },
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct ImageContent {
32 pub mime_type: String,
33 pub source: ImageSource,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub width: Option<u32>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub height: Option<u32>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub bytes: Option<u64>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub sha256: Option<String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46#[serde(tag = "type", rename_all = "snake_case")]
47pub enum UserContent {
48 Text {
49 text: String,
50 },
51 Image {
52 image: ImageContent,
53 },
54 CommandExecution {
55 command: String,
56 stdout: String,
57 stderr: String,
58 exit_code: i32,
59 },
60}
61
62impl UserContent {
63 pub fn format_command_execution_as_xml(
64 command: &str,
65 stdout: &str,
66 stderr: &str,
67 exit_code: i32,
68 ) -> String {
69 format!(
70 r"<executed_command>
71 <command>{command}</command>
72 <stdout>{stdout}</stdout>
73 <stderr>{stderr}</stderr>
74 <exit_code>{exit_code}</exit_code>
75</executed_command>"
76 )
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
82#[serde(tag = "thought_type")]
83pub enum ThoughtContent {
84 #[serde(rename = "simple")]
86 Simple { text: String },
87 #[serde(rename = "signed")]
89 Signed { text: String, signature: String },
90 #[serde(rename = "redacted")]
92 Redacted { data: String },
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
96#[serde(transparent)]
97pub struct ThoughtSignature(String);
98
99impl ThoughtSignature {
100 pub fn new(value: impl Into<String>) -> Self {
101 Self(value.into())
102 }
103
104 pub fn as_str(&self) -> &str {
105 &self.0
106 }
107}
108
109impl ThoughtContent {
110 pub fn display_text(&self) -> String {
112 match self {
113 ThoughtContent::Simple { text } => text.clone(),
114 ThoughtContent::Signed { text, .. } => text.clone(),
115 ThoughtContent::Redacted { .. } => "[Redacted Thinking]".to_string(),
116 }
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122#[serde(tag = "type", rename_all = "snake_case")]
123pub enum AssistantContent {
124 Text {
125 text: String,
126 },
127 Image {
128 image: ImageContent,
129 },
130 ToolCall {
131 tool_call: ToolCall,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 thought_signature: Option<ThoughtSignature>,
134 },
135 Thought {
136 thought: ThoughtContent,
137 },
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct Message {
142 pub timestamp: u64,
143 pub id: String,
144 pub parent_message_id: Option<String>,
145 pub data: MessageData,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(tag = "role", rename_all = "lowercase")]
151pub enum MessageData {
152 User {
153 content: Vec<UserContent>,
154 },
155 Assistant {
156 content: Vec<AssistantContent>,
157 },
158 Tool {
159 tool_use_id: String,
160 result: ToolResult,
161 },
162}
163
164impl Message {
165 pub fn role(&self) -> Role {
166 match &self.data {
167 MessageData::User { .. } => Role::User,
168 MessageData::Assistant { .. } => Role::Assistant,
169 MessageData::Tool { .. } => Role::Tool,
170 }
171 }
172
173 pub fn id(&self) -> &str {
174 &self.id
175 }
176
177 pub fn timestamp(&self) -> u64 {
178 self.timestamp
179 }
180
181 pub fn parent_message_id(&self) -> Option<&str> {
182 self.parent_message_id.as_deref()
183 }
184
185 pub fn current_timestamp() -> u64 {
187 SystemTime::now()
188 .duration_since(UNIX_EPOCH)
189 .unwrap_or_default()
190 .as_secs()
191 }
192
193 pub fn generate_id(prefix: &str, _timestamp: u64) -> String {
195 use uuid::Uuid;
196 format!("{}_{}", prefix, Uuid::now_v7())
197 }
198
199 pub fn extract_text(&self) -> String {
201 match &self.data {
202 MessageData::User { content } => content
203 .iter()
204 .map(|c| match c {
205 UserContent::Text { text } => text.clone(),
206 UserContent::Image { .. } => "[Image]".to_string(),
207 UserContent::CommandExecution { stdout, .. } => stdout.clone(),
208 })
209 .collect::<Vec<_>>()
210 .join("\n"),
211 MessageData::Assistant { content } => content
212 .iter()
213 .map(|c| match c {
214 AssistantContent::Text { text } => text.clone(),
215 AssistantContent::Image { .. } => "[Image]".to_string(),
216 AssistantContent::ToolCall { .. } | AssistantContent::Thought { .. } => {
217 String::new()
218 }
219 })
220 .filter(|line| !line.is_empty())
221 .collect::<Vec<_>>()
222 .join("\n"),
223 MessageData::Tool { result, .. } => result.llm_format(),
224 }
225 }
226
227 pub fn content_string(&self) -> String {
229 match &self.data {
230 MessageData::User { content } => content
231 .iter()
232 .map(|c| match c {
233 UserContent::Text { text } => text.clone(),
234 UserContent::Image { image } => {
235 format!("[Image: {}]", image.mime_type)
236 }
237 UserContent::CommandExecution {
238 command,
239 stdout,
240 stderr,
241 exit_code,
242 } => {
243 let mut output = format!("$ {command}\n{stdout}");
244 if *exit_code != 0 {
245 output.push_str(&format!("\nExit code: {exit_code}"));
246 }
247 if !stderr.is_empty() {
248 output.push_str(&format!("\nError: {stderr}"));
249 }
250 output
251 }
252 })
253 .collect::<Vec<_>>()
254 .join("\n"),
255 MessageData::Assistant { content } => content
256 .iter()
257 .map(|c| match c {
258 AssistantContent::Text { text } => text.clone(),
259 AssistantContent::Image { image } => {
260 format!("[Image: {}]", image.mime_type)
261 }
262 AssistantContent::ToolCall { tool_call, .. } => {
263 format!("[Tool Call: {}]", tool_call.name)
264 }
265 AssistantContent::Thought { thought } => {
266 format!("[Thought: {}]", thought.display_text())
267 }
268 })
269 .collect::<Vec<_>>()
270 .join("\n"),
271 MessageData::Tool { result, .. } => {
272 let result_type = match result {
274 ToolResult::Search(_) => "Search Result",
275 ToolResult::FileList(_) => "File List",
276 ToolResult::FileContent(_) => "File Content",
277 ToolResult::Edit(_) => "Edit Result",
278 ToolResult::Bash(_) => "Bash Result",
279 ToolResult::Glob(_) => "Glob Result",
280 ToolResult::TodoRead(_) => "Todo List",
281 ToolResult::TodoWrite(_) => "Todo Update",
282 ToolResult::Fetch(_) => "Fetch Result",
283 ToolResult::Agent(_) => "Agent Result",
284 ToolResult::External(_) => "External Tool Result",
285 ToolResult::Error(_) => "Error",
286 };
287 format!("[Tool Result: {result_type}]")
288 }
289 }
290 }
291}