use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionRef {
pub path: PathBuf,
pub agent: String,
pub project: Option<String>,
pub size_bytes: u64,
pub modified: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub agent: String,
pub project: Option<String>,
pub model: Option<String>,
pub parent_session: Option<String>,
pub started_at: Option<Timestamp>,
pub ended_at: Option<Timestamp>,
pub events: Vec<Event>,
pub sub_agents: Vec<SubAgent>,
pub skipped_lines: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EventKind {
User,
Assistant,
ToolCall,
ToolResult,
System,
Compaction,
SubAgentSpawn,
Attachment,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Event {
pub kind: EventKind,
pub ts: Option<Timestamp>,
pub request_id: Option<String>,
pub model: Option<String>,
pub usage: Option<Usage>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
pub sidechain: bool,
pub content_summary: Option<String>,
pub content_chars: u64,
#[serde(default, skip_serializing_if = "is_zero")]
pub thinking_chars: u64,
pub has_thinking: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attachment_kind: Option<String>,
#[serde(default, skip_serializing_if = "is_zero")]
pub item_count: u64,
}
fn is_zero(n: &u64) -> bool {
*n == 0
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Usage {
pub input: Option<u64>,
pub output: Option<u64>,
pub cache_creation: Option<u64>,
pub cache_creation_5m: Option<u64>,
pub cache_creation_1h: Option<u64>,
pub cache_read: Option<u64>,
pub thinking: Option<u64>,
}
impl Usage {
pub fn is_empty(&self) -> bool {
self.input.is_none()
&& self.output.is_none()
&& self.cache_creation.is_none()
&& self.cache_read.is_none()
&& self.thinking.is_none()
}
pub fn known_total(&self) -> u64 {
self.input.unwrap_or(0)
+ self.output.unwrap_or(0)
+ self.cache_creation.unwrap_or(0)
+ self.cache_read.unwrap_or(0)
+ self.thinking.unwrap_or(0)
}
pub fn merge_max(self, other: Usage) -> Usage {
fn max_opt(a: Option<u64>, b: Option<u64>) -> Option<u64> {
match (a, b) {
(Some(x), Some(y)) => Some(x.max(y)),
(x, None) => x,
(None, y) => y,
}
}
Usage {
input: max_opt(self.input, other.input),
output: max_opt(self.output, other.output),
cache_creation: max_opt(self.cache_creation, other.cache_creation),
cache_creation_5m: max_opt(self.cache_creation_5m, other.cache_creation_5m),
cache_creation_1h: max_opt(self.cache_creation_1h, other.cache_creation_1h),
cache_read: max_opt(self.cache_read, other.cache_read),
thinking: max_opt(self.thinking, other.thinking),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub input_bytes: u64,
pub server: Option<String>,
}
impl ToolCall {
pub fn server_from_name(name: &str) -> Option<String> {
let rest = name.strip_prefix("mcp__")?;
rest.split("__").next().map(str::to_string)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SubAgent {
pub tool_call_id: String,
pub agent_type: Option<String>,
pub description: Option<String>,
pub ts: Option<Timestamp>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_max_treats_none_as_unknown() {
let a = Usage {
input: Some(100),
output: None,
..Usage::default()
};
let b = Usage {
input: Some(40),
output: Some(7),
..Usage::default()
};
let m = a.merge_max(b);
assert_eq!(m.input, Some(100));
assert_eq!(m.output, Some(7));
assert_eq!(m.cache_read, None, "unknown stays unknown, not zero");
}
#[test]
fn mcp_server_is_parsed_from_tool_name() {
assert_eq!(
ToolCall::server_from_name("mcp__github__search_issues").as_deref(),
Some("github")
);
assert_eq!(ToolCall::server_from_name("Read"), None);
}
}