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)]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum UserContent {
26 Text {
27 text: String,
28 },
29 CommandExecution {
30 command: String,
31 stdout: String,
32 stderr: String,
33 exit_code: i32,
34 },
35}
36
37impl UserContent {
38 pub fn format_command_execution_as_xml(
39 command: &str,
40 stdout: &str,
41 stderr: &str,
42 exit_code: i32,
43 ) -> String {
44 format!(
45 r"<executed_command>
46 <command>{command}</command>
47 <stdout>{stdout}</stdout>
48 <stderr>{stderr}</stderr>
49 <exit_code>{exit_code}</exit_code>
50</executed_command>"
51 )
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(tag = "thought_type")]
58pub enum ThoughtContent {
59 #[serde(rename = "simple")]
61 Simple { text: String },
62 #[serde(rename = "signed")]
64 Signed { text: String, signature: String },
65 #[serde(rename = "redacted")]
67 Redacted { data: String },
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
71#[serde(transparent)]
72pub struct ThoughtSignature(String);
73
74impl ThoughtSignature {
75 pub fn new(value: impl Into<String>) -> Self {
76 Self(value.into())
77 }
78
79 pub fn as_str(&self) -> &str {
80 &self.0
81 }
82}
83
84impl ThoughtContent {
85 pub fn display_text(&self) -> String {
87 match self {
88 ThoughtContent::Simple { text } => text.clone(),
89 ThoughtContent::Signed { text, .. } => text.clone(),
90 ThoughtContent::Redacted { .. } => "[Redacted Thinking]".to_string(),
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97#[serde(tag = "type", rename_all = "snake_case")]
98pub enum AssistantContent {
99 Text {
100 text: String,
101 },
102 ToolCall {
103 tool_call: ToolCall,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 thought_signature: Option<ThoughtSignature>,
106 },
107 Thought {
108 thought: ThoughtContent,
109 },
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct Message {
114 pub timestamp: u64,
115 pub id: String,
116 pub parent_message_id: Option<String>,
117 pub data: MessageData,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(tag = "role", rename_all = "lowercase")]
123pub enum MessageData {
124 User {
125 content: Vec<UserContent>,
126 },
127 Assistant {
128 content: Vec<AssistantContent>,
129 },
130 Tool {
131 tool_use_id: String,
132 result: ToolResult,
133 },
134}
135
136impl Message {
137 pub fn role(&self) -> Role {
138 match &self.data {
139 MessageData::User { .. } => Role::User,
140 MessageData::Assistant { .. } => Role::Assistant,
141 MessageData::Tool { .. } => Role::Tool,
142 }
143 }
144
145 pub fn id(&self) -> &str {
146 &self.id
147 }
148
149 pub fn timestamp(&self) -> u64 {
150 self.timestamp
151 }
152
153 pub fn parent_message_id(&self) -> Option<&str> {
154 self.parent_message_id.as_deref()
155 }
156
157 pub fn current_timestamp() -> u64 {
159 SystemTime::now()
160 .duration_since(UNIX_EPOCH)
161 .unwrap_or_default()
162 .as_secs()
163 }
164
165 pub fn generate_id(prefix: &str, _timestamp: u64) -> String {
167 use uuid::Uuid;
168 format!("{}_{}", prefix, Uuid::now_v7())
169 }
170
171 pub fn extract_text(&self) -> String {
173 match &self.data {
174 MessageData::User { content } => content
175 .iter()
176 .map(|c| match c {
177 UserContent::Text { text } => text.clone(),
178 UserContent::CommandExecution { stdout, .. } => stdout.clone(),
179 })
180 .collect::<Vec<_>>()
181 .join("\n"),
182 MessageData::Assistant { content } => content
183 .iter()
184 .filter_map(|c| match c {
185 AssistantContent::Text { text } => Some(text.clone()),
186 _ => None,
187 })
188 .collect::<Vec<_>>()
189 .join("\n"),
190 MessageData::Tool { result, .. } => result.llm_format(),
191 }
192 }
193
194 pub fn content_string(&self) -> String {
196 match &self.data {
197 MessageData::User { content } => content
198 .iter()
199 .map(|c| match c {
200 UserContent::Text { text } => text.clone(),
201 UserContent::CommandExecution {
202 command,
203 stdout,
204 stderr,
205 exit_code,
206 } => {
207 let mut output = format!("$ {command}\n{stdout}");
208 if *exit_code != 0 {
209 output.push_str(&format!("\nExit code: {exit_code}"));
210 }
211 if !stderr.is_empty() {
212 output.push_str(&format!("\nError: {stderr}"));
213 }
214 output
215 }
216 })
217 .collect::<Vec<_>>()
218 .join("\n"),
219 MessageData::Assistant { content } => content
220 .iter()
221 .map(|c| match c {
222 AssistantContent::Text { text } => text.clone(),
223 AssistantContent::ToolCall { tool_call, .. } => {
224 format!("[Tool Call: {}]", tool_call.name)
225 }
226 AssistantContent::Thought { thought } => {
227 format!("[Thought: {}]", thought.display_text())
228 }
229 })
230 .collect::<Vec<_>>()
231 .join("\n"),
232 MessageData::Tool { result, .. } => {
233 let result_type = match result {
235 ToolResult::Search(_) => "Search Result",
236 ToolResult::FileList(_) => "File List",
237 ToolResult::FileContent(_) => "File Content",
238 ToolResult::Edit(_) => "Edit Result",
239 ToolResult::Bash(_) => "Bash Result",
240 ToolResult::Glob(_) => "Glob Result",
241 ToolResult::TodoRead(_) => "Todo List",
242 ToolResult::TodoWrite(_) => "Todo Update",
243 ToolResult::Fetch(_) => "Fetch Result",
244 ToolResult::Agent(_) => "Agent Result",
245 ToolResult::External(_) => "External Tool Result",
246 ToolResult::Error(_) => "Error",
247 };
248 format!("[Tool Result: {result_type}]")
249 }
250 }
251 }
252}