use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolActivity {
pub tool_name: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub args_summary: String,
pub success: bool,
}
impl ToolActivity {
pub fn new(
tool_name: impl Into<String>,
args_summary: impl Into<String>,
success: bool,
) -> Self {
Self {
tool_name: tool_name.into(),
timestamp: chrono::Utc::now(),
args_summary: args_summary.into(),
success,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaskTokenUsage {
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_read_tokens: u64,
pub cache_write_tokens: u64,
}
impl TaskTokenUsage {
pub fn total(&self) -> u64 {
self.input_tokens + self.output_tokens + self.cache_read_tokens + self.cache_write_tokens
}
pub fn add(&mut self, other: &TaskTokenUsage) {
self.input_tokens += other.input_tokens;
self.output_tokens += other.output_tokens;
self.cache_read_tokens += other.cache_read_tokens;
self.cache_write_tokens += other.cache_write_tokens;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProgress {
pub tool_counts: HashMap<String, usize>,
pub total_tool_calls: usize,
pub token_usage: TaskTokenUsage,
pub recent_activities: Vec<ToolActivity>,
pub elapsed_ms: u64,
pub running: bool,
}
impl Default for AgentProgress {
fn default() -> Self {
Self {
tool_counts: HashMap::new(),
total_tool_calls: 0,
token_usage: TaskTokenUsage::default(),
recent_activities: Vec::new(),
elapsed_ms: 0,
running: true,
}
}
}
#[derive(Debug)]
pub struct ProgressTracker {
start_time: Instant,
tool_counts: HashMap<String, usize>,
token_usage: TaskTokenUsage,
recent_activities: VecDeque<ToolActivity>,
max_recent: usize,
active: bool,
}
impl Default for ProgressTracker {
fn default() -> Self {
Self::new(30) }
}
impl ProgressTracker {
pub fn new(max_recent: usize) -> Self {
Self {
start_time: Instant::now(),
tool_counts: HashMap::new(),
token_usage: TaskTokenUsage::default(),
recent_activities: VecDeque::with_capacity(max_recent),
max_recent,
active: true,
}
}
pub fn track_tool_call(
&mut self,
tool_name: impl Into<String>,
args_summary: impl Into<String>,
success: bool,
) {
if !self.active {
return;
}
let tool_name = tool_name.into();
*self.tool_counts.entry(tool_name.clone()).or_insert(0) += 1;
let activity = ToolActivity::new(&tool_name, args_summary, success);
self.recent_activities.push_front(activity);
while self.recent_activities.len() > self.max_recent {
self.recent_activities.pop_back();
}
}
pub fn track_tokens(&mut self, usage: TaskTokenUsage) {
if !self.active {
return;
}
self.token_usage.add(&usage);
}
pub fn progress(&self) -> AgentProgress {
AgentProgress {
tool_counts: self.tool_counts.clone(),
total_tool_calls: self.tool_counts.values().sum(),
token_usage: self.token_usage.clone(),
recent_activities: self.recent_activities.iter().cloned().collect(),
elapsed_ms: self.start_time.elapsed().as_millis() as u64,
running: self.active,
}
}
pub fn stop(&mut self) {
self.active = false;
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn reset(&mut self) {
self.start_time = Instant::now();
self.tool_counts.clear();
self.token_usage = TaskTokenUsage::default();
self.recent_activities.clear();
self.active = true;
}
pub fn get_tool_count(&self, tool_name: &str) -> usize {
self.tool_counts.get(tool_name).copied().unwrap_or(0)
}
pub fn total_tool_calls(&self) -> usize {
self.tool_counts.values().sum()
}
pub fn elapsed(&self) -> std::time::Duration {
self.start_time.elapsed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_tracker() {
let mut tracker = ProgressTracker::new(5);
tracker.track_tool_call("read", "file_path: test.txt", true);
tracker.track_tool_call("bash", "command: ls", true);
tracker.track_tool_call("read", "file_path: main.rs", true);
let progress = tracker.progress();
assert_eq!(progress.total_tool_calls, 3);
assert_eq!(tracker.get_tool_count("read"), 2);
assert_eq!(tracker.get_tool_count("bash"), 1);
assert!(progress.running);
}
#[test]
fn test_token_usage() {
let mut usage = TaskTokenUsage::default();
assert_eq!(usage.total(), 0);
usage.input_tokens = 100;
usage.output_tokens = 50;
assert_eq!(usage.total(), 150);
let other = TaskTokenUsage {
input_tokens: 50,
output_tokens: 25,
cache_read_tokens: 10,
cache_write_tokens: 5,
};
usage.add(&other);
assert_eq!(usage.total(), 240);
}
#[test]
fn test_progress_sliding_window() {
let mut tracker = ProgressTracker::new(3);
for i in 0..5 {
tracker.track_tool_call("tool", format!("arg_{}", i), true);
}
let progress = tracker.progress();
assert_eq!(progress.recent_activities.len(), 3);
assert_eq!(progress.recent_activities[0].args_summary, "arg_4");
assert_eq!(progress.recent_activities[2].args_summary, "arg_2");
}
}