pub mod attachment;
pub mod message;
pub mod metadata;
pub mod progress;
pub mod root;
pub mod system;
pub mod tool_result;
pub use attachment::{AttachmentBlock, AttachmentType};
pub use message::{ContentBlock, MessageContent};
pub use metadata::EventMetadata;
pub use progress::{ProgressData, ProgressEvent};
pub use root::{
AgentNameEvent, AiTitleEvent, AssistantMessage, AtisLatchEvent, BridgeSessionEvent,
CacheCreation, CustomTitleEvent, FileHistoryDeltaEvent, FileHistorySnapshot, LastPromptEvent,
ModeEvent, OriginInfo, PermissionModeEvent, QueueOperation, RootAttachmentEvent, SessionEvent,
SessionSummary, Snapshot, TokenUsage, UserTurnKind,
};
pub use system::{CompactMetadata, SystemEvent};
pub use tool_result::ToolUseResult;
pub use root::QueueOperation as QueueOperationEvent;
pub use root::{AssistantEvent as AssistantMessageEvent, UserEvent as UserMessageEvent};
impl SessionEvent {
pub fn extract_tags(&self) -> Vec<String> {
match self {
Self::User(e) => {
let mut tags = vec!["user".to_string()];
if e.metadata.user_type.as_deref() == Some("external") {
tags.push("prompt".to_string());
}
if e.tool_use_result.is_some() {
tags.push("tool_result".to_string());
}
tags
}
Self::Assistant(e) => {
let mut tags = vec!["assistant".to_string()];
tags.push(e.message.model.clone());
for block in &e.message.content {
match block {
ContentBlock::Text(_) if !tags.contains(&"text".to_string()) => {
tags.push("text".to_string());
}
ContentBlock::Text(_) => {}
ContentBlock::ToolUse(tool) => {
tags.push("tool_use".to_string());
tags.push(tool.name.clone());
}
_ => {}
}
}
tags
}
Self::Progress(e) => {
let mut tags = vec!["progress".to_string()];
match &e.data {
ProgressData::BashProgress(_) => {
tags.push("bash_progress".to_string());
}
ProgressData::AgentProgress(agent) => {
tags.push("agent_progress".to_string());
tags.push(agent.agent_id.clone());
if let Some(slug) = &e.metadata.slug {
tags.push(slug.clone());
}
}
ProgressData::HookProgress(hook) => {
tags.push("hook_progress".to_string());
tags.push(hook.hook_name.clone());
}
ProgressData::QueryUpdate(_) => {
tags.push("query_update".to_string());
}
ProgressData::SearchResultsReceived(_) => {
tags.push("search_results".to_string());
}
ProgressData::WaitingForTask(task) => {
tags.push("waiting_for_task".to_string());
tags.push(task.task_type.clone());
}
ProgressData::Unknown => {
tags.push("unknown_progress".to_string());
}
}
tags
}
Self::System(e) => {
let mut tags = vec!["system".to_string()];
if let Some(subtype) = &e.subtype {
tags.push(subtype.clone());
if e.is_compact_boundary() {
if let Some(meta) = &e.compact_metadata {
tags.push(meta.trigger.clone());
}
}
}
tags
}
Self::FileSnapshot(_) => vec!["file_snapshot".to_string()],
Self::QueueOperation(e) => {
vec!["queue_operation".to_string(), e.operation.clone()]
}
Self::Summary(_) => vec!["summary".to_string()],
Self::Attachment(e) => {
vec!["attachment".to_string(), e.attachment.type_name().to_string()]
}
Self::CustomTitle(_) => vec!["custom_title".to_string()],
Self::AiTitle(_) => vec!["ai_title".to_string()],
Self::LastPrompt(_) => vec!["last_prompt".to_string()],
Self::BridgeSession(_) => vec!["bridge_session".to_string()],
Self::AtisLatch(_) => vec!["atis_latch".to_string()],
Self::Mode(e) => vec!["mode".to_string(), e.mode.clone()],
Self::PermissionMode(e) => {
vec!["permission_mode".to_string(), e.permission_mode.clone()]
}
Self::AgentName(_) => vec!["agent_name".to_string()],
Self::FileHistoryDelta(_) => vec!["file_history_delta".to_string()],
Self::Unknown => vec!["unknown".to_string()],
}
}
pub fn is_context_relevant(&self) -> bool {
match self {
Self::Progress(e) => matches!(e.data, ProgressData::AgentProgress(_)),
Self::User(e) => e.metadata.user_type.as_deref() == Some("external"),
Self::Assistant(e) => e
.message
.content
.iter()
.any(|block| matches!(block, ContentBlock::Text(_))),
Self::System(e) => e.is_compact_boundary(),
_ => false,
}
}
}
impl root::TokenUsage {
pub fn total(&self) -> u64 {
self.input_tokens + self.output_tokens
}
pub fn total_input(&self) -> u64 {
self.input_tokens + self.cache_creation_input_tokens + self.cache_read_input_tokens
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn effective_input(&self) -> f64 {
(self.input_tokens + self.cache_creation_input_tokens) as f64
+ (self.cache_read_input_tokens as f64 * 0.1)
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn calculate_cost(&self, model: &str) -> Option<f64> {
let (input_cost, output_cost) = match normalize_model_name(model) {
"haiku" => (1.0 / 1_000_000.0, 5.0 / 1_000_000.0),
"sonnet" => (3.0 / 1_000_000.0, 15.0 / 1_000_000.0),
"opus" => (15.0 / 1_000_000.0, 75.0 / 1_000_000.0),
_ => return None,
};
let cache_write_cost = input_cost * 1.25;
let cache_read_cost = input_cost * 0.1;
Some(
(self.input_tokens as f64 * input_cost)
+ (self.output_tokens as f64 * output_cost)
+ (self.cache_creation_input_tokens as f64 * cache_write_cost)
+ (self.cache_read_input_tokens as f64 * cache_read_cost),
)
}
#[must_use]
pub fn format_cost(&self, model: &str) -> String {
match self.calculate_cost(model) {
Some(cost) => {
if cost < 0.01 {
format!("${cost:.4}")
} else {
format!("${cost:.2}")
}
}
None => "Unknown model".to_string(),
}
}
#[deprecated(since = "0.1.0", note = "Use calculate_cost(model) instead")]
pub fn estimated_cost(&self) -> f64 {
self.calculate_cost("sonnet").unwrap_or(0.0)
}
}
fn normalize_model_name(model: &str) -> &str {
let lower = model.to_lowercase();
if lower.contains("haiku") {
"haiku"
} else if lower.contains("sonnet") {
"sonnet"
} else if lower.contains("opus") {
"opus"
} else {
"unknown"
}
}