Skip to main content

steer_core/app/conversation/
message.rs

1//! Message types for conversation representation.
2//!
3//! This module contains the core message types used throughout the application:
4//! - `Message` - The main message struct with metadata
5//! - `MessageData` - Role-specific content (User, Assistant, Tool)
6//! - Content types for each role
7
8use 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/// Role in the conversation
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy, Display)]
16pub enum Role {
17    User,
18    Assistant,
19    Tool,
20}
21
22/// Content that can be sent by a user
23#[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/// Different types of thought content from AI models
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(tag = "thought_type")]
58pub enum ThoughtContent {
59    /// Simple thought content (e.g., from Gemini)
60    #[serde(rename = "simple")]
61    Simple { text: String },
62    /// Claude-style thinking with signature
63    #[serde(rename = "signed")]
64    Signed { text: String, signature: String },
65    /// Claude-style redacted thinking
66    #[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    /// Extract displayable text from any thought type
86    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/// Content that can be sent by an assistant
96#[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/// A message in the conversation, with role-specific content
121#[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    /// Helper to get current timestamp
158    pub fn current_timestamp() -> u64 {
159        SystemTime::now()
160            .duration_since(UNIX_EPOCH)
161            .unwrap_or_default()
162            .as_secs()
163    }
164
165    /// Helper to generate unique IDs
166    pub fn generate_id(prefix: &str, _timestamp: u64) -> String {
167        use uuid::Uuid;
168        format!("{}_{}", prefix, Uuid::now_v7())
169    }
170
171    /// Extract text content from the message
172    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    /// Get a string representation of the message content
195    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                // This is a simplified representation. The TUI will have a more detailed view.
234                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}