use serde::{Deserialize, Serialize};
pub const RECORD_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Event {
pub v: u32,
pub id: String,
pub ts: i64,
pub agent: String,
pub provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<String>,
pub role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_tok: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_tok: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_tok: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_write_tok: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_est: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_sidechain: Option<bool>,
}
impl Event {
pub fn new(id: impl Into<String>, ts: i64, agent: &str, provider: &str, role: &str) -> Self {
Self {
v: RECORD_VERSION,
id: id.into(),
ts,
agent: agent.to_string(),
provider: provider.to_string(),
model: None,
project: None,
session_id: None,
turn_id: None,
role: role.to_string(),
input_tok: None,
output_tok: None,
cache_read_tok: None,
cache_write_tok: None,
duration_ms: None,
stop_reason: None,
cost_est: None,
tool_calls: Vec::new(),
is_sidechain: None,
}
}
pub fn total_tokens(&self) -> u64 {
self.input_tok.unwrap_or(0)
+ self.output_tok.unwrap_or(0)
+ self.cache_read_tok.unwrap_or(0)
+ self.cache_write_tok.unwrap_or(0)
}
pub fn has_usage(&self) -> bool {
self.input_tok.is_some()
|| self.output_tok.is_some()
|| self.cache_read_tok.is_some()
|| self.cache_write_tok.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCall {
pub tool_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_target: Option<String>,
}
impl ToolCall {
pub fn new(tool_name: impl Into<String>, tool_target: Option<String>) -> Self {
Self {
tool_name: tool_name.into(),
tool_target,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PromptRecord {
pub event_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
pub text_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IngestCursor {
pub path: String,
pub mtime: i64,
pub offset: u64,
pub adapter: String,
pub ts: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsupported_fields_serialize_as_absent_not_zero() {
let event = Event::new("abc", 1_754_300_000_000, "claude-code", "anthropic", "user");
let json = serde_json::to_string(&event).unwrap();
assert!(!json.contains("duration_ms"), "{json}");
assert!(!json.contains("cost_est"), "{json}");
assert!(!json.contains("tool_calls"), "{json}");
assert!(json.contains("\"v\":1"));
}
#[test]
fn tolerates_unknown_fields_and_missing_optionals() {
let line = r#"{"v":1,"id":"x","ts":5,"agent":"claude-code","provider":"anthropic",
"role":"assistant","future_field":{"nested":true}}"#;
let event: Event = serde_json::from_str(line).unwrap();
assert_eq!(event.id, "x");
assert_eq!(event.input_tok, None);
assert!(event.tool_calls.is_empty());
}
#[test]
fn round_trips_a_fully_populated_event() {
let mut event = Event::new("id", 1, "claude-code", "anthropic", "assistant");
event.model = Some("claude-sonnet-4-6".into());
event.input_tok = Some(412);
event.cache_write_tok = Some(0);
event.cost_est = Some(0.0412);
event.is_sidechain = Some(true);
event.tool_calls = vec![ToolCall::new("Read", Some("src/lib.rs".into()))];
let json = serde_json::to_string(&event).unwrap();
assert_eq!(serde_json::from_str::<Event>(&json).unwrap(), event);
assert!(json.contains("\"cache_write_tok\":0"));
}
}