1use serde::{Deserialize, Serialize};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum MessageRole {
8 User,
9 Assistant,
10 Tool,
11 System,
12}
13
14impl MessageRole {
15 pub const fn as_str(self) -> &'static str {
17 match self {
18 MessageRole::User => "user",
19 MessageRole::Assistant => "assistant",
20 MessageRole::Tool => "tool",
21 MessageRole::System => "system",
22 }
23 }
24}
25
26impl std::fmt::Display for MessageRole {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.write_str(self.as_str())
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DisplayType {
38 User,
40 AssistantText,
42 ToolCallRequest,
44 ToolResult,
46 System,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ToolCallItem {
53 pub id: String,
54 pub name: String,
55 pub arguments: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ImageData {
61 pub base64: String,
63 pub media_type: String,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub enum DisplayHint {
70 #[default]
72 Normal,
73 Draft,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ChatMessage {
80 pub role: MessageRole,
81 #[serde(default)]
83 pub content: String,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub tool_calls: Option<Vec<ToolCallItem>>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub tool_call_id: Option<String>,
90 #[serde(skip)]
92 pub images: Option<Vec<ImageData>>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub reasoning_content: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
100 pub sender_name: Option<String>,
101 #[serde(skip)]
105 pub recipient_name: Option<String>,
106 #[serde(skip)]
109 pub display_hint: DisplayHint,
110}
111
112impl ChatMessage {
113 pub fn text(role: MessageRole, content: impl Into<String>) -> Self {
115 Self {
116 role,
117 content: content.into(),
118 tool_calls: None,
119 tool_call_id: None,
120 images: None,
121 reasoning_content: None,
122 sender_name: None,
123 recipient_name: None,
124 display_hint: DisplayHint::Normal,
125 }
126 }
127
128 pub fn with_sender(mut self, name: impl Into<String>) -> Self {
132 self.sender_name = Some(name.into());
133 self
134 }
135
136 pub fn with_recipient(mut self, name: impl Into<String>) -> Self {
140 self.recipient_name = Some(name.into());
141 self
142 }
143
144 pub fn with_display_hint(mut self, hint: DisplayHint) -> Self {
146 self.display_hint = hint;
147 self
148 }
149
150 pub fn display_type(&self) -> DisplayType {
155 match self.role {
156 MessageRole::User => DisplayType::User,
157 MessageRole::System => DisplayType::System,
158 MessageRole::Assistant => {
159 if self.tool_calls.is_some() {
160 DisplayType::ToolCallRequest
161 } else {
162 DisplayType::AssistantText
163 }
164 }
165 MessageRole::Tool => DisplayType::ToolResult,
166 }
167 }
168}
169
170pub(super) fn is_zero_u64(v: &u64) -> bool {
171 *v == 0
172}
173
174pub fn current_millis() -> u64 {
176 SystemTime::now()
177 .duration_since(UNIX_EPOCH)
178 .map(|d| d.as_millis() as u64)
179 .unwrap_or(0)
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(tag = "type", rename_all = "snake_case")]
185pub enum SessionEvent {
186 Msg {
188 #[serde(flatten)]
189 message: ChatMessage,
190 #[serde(default, skip_serializing_if = "is_zero_u64")]
192 timestamp_ms: u64,
193 },
194 Clear,
196 Restore { messages: Vec<ChatMessage> },
198 Metrics { metrics: SessionMetrics },
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize, Default)]
204pub struct SessionMetrics {
205 pub total_llm_calls: u32,
207 pub total_tool_calls: u32,
209 pub total_input_tokens: u64,
211 pub total_output_tokens: u64,
213 pub estimated_context_tokens_peak: usize,
215 pub auto_compact_count: u32,
217 pub micro_compact_count: u32,
219 pub skill_loads: Vec<String>,
221 pub ttft_ms_per_call: Vec<u64>,
223 #[serde(default, skip_serializing_if = "is_zero_u64")]
225 pub total_llm_elapsed_ms: u64,
226 #[serde(default, skip_serializing_if = "is_zero_u64")]
228 pub total_tool_elapsed_ms: u64,
229 pub session_start_ms: u64,
231 pub session_end_ms: u64,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct SessionOp {
238 pub op: SessionOpKind,
240 pub timestamp_ms: u64,
242 pub is_error: bool,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(tag = "kind", rename_all = "snake_case")]
249pub enum SessionOpKind {
250 Edit {
252 path: String,
254 },
255 Write {
257 path: String,
259 },
260 Bash {
262 command: String,
264 },
265}
266
267impl SessionEvent {
268 pub fn msg(message: ChatMessage) -> Self {
270 Self::Msg {
271 message,
272 timestamp_ms: current_millis(),
273 }
274 }
275}