use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct TokenUsage {
pub input_tokens: u64,
pub output_tokens: u64,
pub cached_input_tokens: u64,
pub cache_creation_tokens: u64,
pub reasoning_tokens: u64,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
pub struct LatencyStatistics {
pub count: u64,
pub total_ms: u64,
pub mean_ms: Option<f64>,
pub p50_ms: Option<u64>,
pub p95_ms: Option<u64>,
pub max_ms: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct HarnessTraceSummary {
pub turns: u64,
pub steps: u64,
pub tool_calls: u64,
pub tool_counts: BTreeMap<String, u64>,
pub error_categories: BTreeMap<String, u64>,
pub latency: LatencyStatistics,
pub output_bytes: u64,
pub repeated_calls: u64,
pub repeated_tool_counts: BTreeMap<String, u64>,
pub token_usage: TokenUsage,
pub malformed_lines: u64,
pub unrecognized_lines: u64,
}
impl HarnessTraceSummary {
pub fn merge(&mut self, other: &Self) {
self.turns = self.turns.saturating_add(other.turns);
self.steps = self.steps.saturating_add(other.steps);
self.tool_calls = self.tool_calls.saturating_add(other.tool_calls);
self.output_bytes = self.output_bytes.saturating_add(other.output_bytes);
self.repeated_calls = self.repeated_calls.saturating_add(other.repeated_calls);
self.malformed_lines = self.malformed_lines.saturating_add(other.malformed_lines);
self.unrecognized_lines = self.unrecognized_lines.saturating_add(other.unrecognized_lines);
for (tool, count) in &other.tool_counts {
let entry = self.tool_counts.entry(tool.clone()).or_default();
*entry = entry.saturating_add(*count);
}
for (tool, count) in &other.repeated_tool_counts {
let entry = self.repeated_tool_counts.entry(tool.clone()).or_default();
*entry = entry.saturating_add(*count);
}
for (category, count) in &other.error_categories {
let entry = self.error_categories.entry(category.clone()).or_default();
*entry = entry.saturating_add(*count);
}
let previous_latency_count = self.latency.count;
let combined_count = previous_latency_count.saturating_add(other.latency.count);
self.latency.total_ms = self.latency.total_ms.saturating_add(other.latency.total_ms);
self.latency.count = combined_count;
self.latency.mean_ms = (combined_count > 0).then_some(self.latency.total_ms as f64 / combined_count as f64);
self.latency.max_ms = match (self.latency.max_ms, other.latency.max_ms) {
(Some(left), Some(right)) => Some(left.max(right)),
(left, right) => left.or(right),
};
if previous_latency_count > 0 && other.latency.count > 0 {
self.latency.p50_ms = None;
self.latency.p95_ms = None;
} else if previous_latency_count == 0 {
self.latency.p50_ms = other.latency.p50_ms;
self.latency.p95_ms = other.latency.p95_ms;
}
self.token_usage.input_tokens = self.token_usage.input_tokens.saturating_add(other.token_usage.input_tokens);
self.token_usage.output_tokens = self.token_usage.output_tokens.saturating_add(other.token_usage.output_tokens);
self.token_usage.cached_input_tokens = self
.token_usage
.cached_input_tokens
.saturating_add(other.token_usage.cached_input_tokens);
self.token_usage.cache_creation_tokens = self
.token_usage
.cache_creation_tokens
.saturating_add(other.token_usage.cache_creation_tokens);
self.token_usage.reasoning_tokens = self
.token_usage
.reasoning_tokens
.saturating_add(other.token_usage.reasoning_tokens);
}
}