1use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, VecDeque};
8use std::time::Instant;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ToolActivity {
13 pub tool_name: String,
15 pub timestamp: chrono::DateTime<chrono::Utc>,
17 pub args_summary: String,
19 pub success: bool,
21}
22
23impl ToolActivity {
24 pub fn new(
26 tool_name: impl Into<String>,
27 args_summary: impl Into<String>,
28 success: bool,
29 ) -> Self {
30 Self {
31 tool_name: tool_name.into(),
32 timestamp: chrono::Utc::now(),
33 args_summary: args_summary.into(),
34 success,
35 }
36 }
37}
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41pub struct TaskTokenUsage {
42 pub input_tokens: u64,
44 pub output_tokens: u64,
46 pub cache_read_tokens: u64,
48 pub cache_write_tokens: u64,
50}
51
52impl TaskTokenUsage {
53 pub fn total(&self) -> u64 {
55 self.input_tokens + self.output_tokens + self.cache_read_tokens + self.cache_write_tokens
56 }
57
58 pub fn add(&mut self, other: &TaskTokenUsage) {
60 self.input_tokens += other.input_tokens;
61 self.output_tokens += other.output_tokens;
62 self.cache_read_tokens += other.cache_read_tokens;
63 self.cache_write_tokens += other.cache_write_tokens;
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct AgentProgress {
70 pub tool_counts: HashMap<String, usize>,
72 pub total_tool_calls: usize,
74 pub token_usage: TaskTokenUsage,
76 pub recent_activities: Vec<ToolActivity>,
78 pub elapsed_ms: u64,
80 pub running: bool,
82}
83
84impl Default for AgentProgress {
85 fn default() -> Self {
86 Self {
87 tool_counts: HashMap::new(),
88 total_tool_calls: 0,
89 token_usage: TaskTokenUsage::default(),
90 recent_activities: Vec::new(),
91 elapsed_ms: 0,
92 running: true,
93 }
94 }
95}
96
97#[derive(Debug)]
102pub struct ProgressTracker {
103 start_time: Instant,
105 tool_counts: HashMap<String, usize>,
107 token_usage: TaskTokenUsage,
109 recent_activities: VecDeque<ToolActivity>,
111 max_recent: usize,
113 active: bool,
115}
116
117impl Default for ProgressTracker {
118 fn default() -> Self {
119 Self::new(30) }
121}
122
123impl ProgressTracker {
124 pub fn new(max_recent: usize) -> Self {
130 Self {
131 start_time: Instant::now(),
132 tool_counts: HashMap::new(),
133 token_usage: TaskTokenUsage::default(),
134 recent_activities: VecDeque::with_capacity(max_recent),
135 max_recent,
136 active: true,
137 }
138 }
139
140 pub fn track_tool_call(
142 &mut self,
143 tool_name: impl Into<String>,
144 args_summary: impl Into<String>,
145 success: bool,
146 ) {
147 if !self.active {
148 return;
149 }
150
151 let tool_name = tool_name.into();
152 *self.tool_counts.entry(tool_name.clone()).or_insert(0) += 1;
153
154 let activity = ToolActivity::new(&tool_name, args_summary, success);
155 self.recent_activities.push_front(activity);
156
157 while self.recent_activities.len() > self.max_recent {
159 self.recent_activities.pop_back();
160 }
161 }
162
163 pub fn track_tokens(&mut self, usage: TaskTokenUsage) {
165 if !self.active {
166 return;
167 }
168 self.token_usage.add(&usage);
169 }
170
171 pub fn progress(&self) -> AgentProgress {
173 AgentProgress {
174 tool_counts: self.tool_counts.clone(),
175 total_tool_calls: self.tool_counts.values().sum(),
176 token_usage: self.token_usage.clone(),
177 recent_activities: self.recent_activities.iter().cloned().collect(),
178 elapsed_ms: self.start_time.elapsed().as_millis() as u64,
179 running: self.active,
180 }
181 }
182
183 pub fn stop(&mut self) {
185 self.active = false;
186 }
187
188 pub fn is_active(&self) -> bool {
190 self.active
191 }
192
193 pub fn reset(&mut self) {
195 self.start_time = Instant::now();
196 self.tool_counts.clear();
197 self.token_usage = TaskTokenUsage::default();
198 self.recent_activities.clear();
199 self.active = true;
200 }
201
202 pub fn get_tool_count(&self, tool_name: &str) -> usize {
204 self.tool_counts.get(tool_name).copied().unwrap_or(0)
205 }
206
207 pub fn total_tool_calls(&self) -> usize {
209 self.tool_counts.values().sum()
210 }
211
212 pub fn elapsed(&self) -> std::time::Duration {
214 self.start_time.elapsed()
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_progress_tracker() {
224 let mut tracker = ProgressTracker::new(5);
225
226 tracker.track_tool_call("read", "file_path: test.txt", true);
227 tracker.track_tool_call("bash", "command: ls", true);
228 tracker.track_tool_call("read", "file_path: main.rs", true);
229
230 let progress = tracker.progress();
231
232 assert_eq!(progress.total_tool_calls, 3);
233 assert_eq!(tracker.get_tool_count("read"), 2);
234 assert_eq!(tracker.get_tool_count("bash"), 1);
235 assert!(progress.running);
236 }
237
238 #[test]
239 fn test_token_usage() {
240 let mut usage = TaskTokenUsage::default();
241 assert_eq!(usage.total(), 0);
242
243 usage.input_tokens = 100;
244 usage.output_tokens = 50;
245 assert_eq!(usage.total(), 150);
246
247 let other = TaskTokenUsage {
248 input_tokens: 50,
249 output_tokens: 25,
250 cache_read_tokens: 10,
251 cache_write_tokens: 5,
252 };
253 usage.add(&other);
254 assert_eq!(usage.total(), 240);
255 }
256
257 #[test]
258 fn test_progress_sliding_window() {
259 let mut tracker = ProgressTracker::new(3);
260
261 for i in 0..5 {
262 tracker.track_tool_call("tool", format!("arg_{}", i), true);
263 }
264
265 let progress = tracker.progress();
266 assert_eq!(progress.recent_activities.len(), 3);
267 assert_eq!(progress.recent_activities[0].args_summary, "arg_4");
269 assert_eq!(progress.recent_activities[2].args_summary, "arg_2");
270 }
271}