1use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13pub struct Session {
14 pub path: PathBuf,
16 pub format: String,
18 pub metadata: SessionMetadata,
20 pub turns: Vec<Turn>,
22}
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27pub struct SessionMetadata {
28 pub session_id: Option<String>,
30 pub timestamp: Option<String>,
32 pub provider: Option<String>,
34 pub model: Option<String>,
36 pub project: Option<String>,
38}
39
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
42#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43pub struct Turn {
44 pub messages: Vec<Message>,
46 pub token_usage: Option<TokenUsage>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53pub struct Message {
54 pub role: Role,
56 pub content: Vec<ContentBlock>,
58 pub timestamp: Option<String>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
65#[serde(rename_all = "lowercase")]
66pub enum Role {
67 User,
68 Assistant,
69 System,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
75#[serde(tag = "type", rename_all = "snake_case")]
76pub enum ContentBlock {
77 Text { text: String },
79 ToolUse {
81 id: String,
82 name: String,
83 input: serde_json::Value,
84 },
85 ToolResult {
87 tool_use_id: String,
88 content: String,
89 is_error: bool,
90 },
91 Thinking { text: String },
93}
94
95#[derive(Debug, Clone, Default, Serialize, Deserialize)]
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98pub struct TokenUsage {
99 pub input: u64,
101 pub output: u64,
103 pub cache_read: Option<u64>,
105 pub cache_create: Option<u64>,
107}
108
109impl Session {
110 pub fn new(path: PathBuf, format: impl Into<String>) -> Self {
112 Self {
113 path,
114 format: format.into(),
115 metadata: SessionMetadata::default(),
116 turns: Vec::new(),
117 }
118 }
119
120 pub fn message_count(&self) -> usize {
122 self.turns.iter().map(|t| t.messages.len()).sum()
123 }
124
125 pub fn messages_by_role(&self, role: Role) -> usize {
127 self.turns
128 .iter()
129 .flat_map(|t| &t.messages)
130 .filter(|m| m.role == role)
131 .count()
132 }
133
134 pub fn tool_uses(&self) -> impl Iterator<Item = (&str, &serde_json::Value)> {
136 self.turns.iter().flat_map(|t| &t.messages).flat_map(|m| {
137 m.content.iter().filter_map(|block| match block {
138 ContentBlock::ToolUse { name, input, .. } => Some((name.as_str(), input)),
139 _ => None,
140 })
141 })
142 }
143
144 pub fn tool_results(&self) -> impl Iterator<Item = (&str, bool)> {
146 self.turns.iter().flat_map(|t| &t.messages).flat_map(|m| {
147 m.content.iter().filter_map(|block| match block {
148 ContentBlock::ToolResult {
149 content, is_error, ..
150 } => Some((content.as_str(), *is_error)),
151 _ => None,
152 })
153 })
154 }
155
156 pub fn total_tokens(&self) -> TokenUsage {
158 let mut total = TokenUsage::default();
159 for turn in &self.turns {
160 if let Some(usage) = &turn.token_usage {
161 total.input += usage.input;
162 total.output += usage.output;
163 if let Some(cache_read) = usage.cache_read {
164 *total.cache_read.get_or_insert(0) += cache_read;
165 }
166 if let Some(cache_create) = usage.cache_create {
167 *total.cache_create.get_or_insert(0) += cache_create;
168 }
169 }
170 }
171 total
172 }
173}
174
175impl std::fmt::Display for Role {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 match self {
178 Role::User => write!(f, "user"),
179 Role::Assistant => write!(f, "assistant"),
180 Role::System => write!(f, "system"),
181 }
182 }
183}