use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
User,
Assistant,
Tool,
System,
}
impl MessageRole {
pub const fn as_str(self) -> &'static str {
match self {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::Tool => "tool",
MessageRole::System => "system",
}
}
}
impl std::fmt::Display for MessageRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayType {
User,
AssistantText,
ToolCallRequest,
ToolResult,
System,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallItem {
pub id: String,
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageData {
pub base64: String,
pub media_type: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DisplayHint {
#[default]
Normal,
Draft,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: MessageRole,
#[serde(default)]
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallItem>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip)]
pub images: Option<Vec<ImageData>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_name: Option<String>,
#[serde(skip)]
pub recipient_name: Option<String>,
#[serde(skip)]
pub display_hint: DisplayHint,
}
impl ChatMessage {
pub fn text(role: MessageRole, content: impl Into<String>) -> Self {
Self {
role,
content: content.into(),
tool_calls: None,
tool_call_id: None,
images: None,
reasoning_content: None,
sender_name: None,
recipient_name: None,
display_hint: DisplayHint::Normal,
}
}
pub fn with_sender(mut self, name: impl Into<String>) -> Self {
self.sender_name = Some(name.into());
self
}
pub fn with_recipient(mut self, name: impl Into<String>) -> Self {
self.recipient_name = Some(name.into());
self
}
pub fn with_display_hint(mut self, hint: DisplayHint) -> Self {
self.display_hint = hint;
self
}
pub fn display_type(&self) -> DisplayType {
match self.role {
MessageRole::User => DisplayType::User,
MessageRole::System => DisplayType::System,
MessageRole::Assistant => {
if self.tool_calls.is_some() {
DisplayType::ToolCallRequest
} else {
DisplayType::AssistantText
}
}
MessageRole::Tool => DisplayType::ToolResult,
}
}
}
pub(super) fn is_zero_u64(v: &u64) -> bool {
*v == 0
}
pub fn current_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SessionEvent {
Msg {
#[serde(flatten)]
message: ChatMessage,
#[serde(default, skip_serializing_if = "is_zero_u64")]
timestamp_ms: u64,
},
Clear,
Restore { messages: Vec<ChatMessage> },
Metrics { metrics: SessionMetrics },
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionMetrics {
pub total_llm_calls: u32,
pub total_tool_calls: u32,
pub total_input_tokens: u64,
pub total_output_tokens: u64,
pub estimated_context_tokens_peak: usize,
pub auto_compact_count: u32,
pub micro_compact_count: u32,
pub skill_loads: Vec<String>,
pub ttft_ms_per_call: Vec<u64>,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub total_llm_elapsed_ms: u64,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub total_tool_elapsed_ms: u64,
pub session_start_ms: u64,
pub session_end_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionOp {
pub op: SessionOpKind,
pub timestamp_ms: u64,
pub is_error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SessionOpKind {
Edit {
path: String,
},
Write {
path: String,
},
Bash {
command: String,
},
}
impl SessionEvent {
pub fn msg(message: ChatMessage) -> Self {
Self::Msg {
message,
timestamp_ms: current_millis(),
}
}
}