Skip to main content

ccboard_core/models/
invocations.rs

1//! Invocation statistics for agents, commands, and skills
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Statistics about agent/command/skill invocations across all sessions
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct InvocationStats {
10    /// Agent invocations (subagent_type -> count)
11    /// Example: "technical-writer" -> 5
12    pub agents: HashMap<String, usize>,
13
14    /// Command invocations (/command -> count)
15    /// Example: "/commit" -> 12
16    pub commands: HashMap<String, usize>,
17
18    /// Skill invocations (skill name -> count)
19    /// Example: "pdf-generator" -> 3
20    pub skills: HashMap<String, usize>,
21
22    /// Per-agent token consumption (subagent_type -> total tokens)
23    /// Populated when Task tool calls have associated token data
24    #[serde(default)]
25    pub agent_token_stats: HashMap<String, u64>,
26
27    /// When stats were last computed
28    pub last_computed: DateTime<Utc>,
29
30    /// Number of sessions analyzed
31    pub sessions_analyzed: usize,
32}
33
34impl InvocationStats {
35    /// Create new empty stats
36    pub fn new() -> Self {
37        Self {
38            agents: HashMap::new(),
39            commands: HashMap::new(),
40            skills: HashMap::new(),
41            agent_token_stats: HashMap::new(),
42            last_computed: Utc::now(),
43            sessions_analyzed: 0,
44        }
45    }
46
47    /// Get total number of invocations across all types
48    pub fn total_invocations(&self) -> usize {
49        self.agents.values().sum::<usize>()
50            + self.commands.values().sum::<usize>()
51            + self.skills.values().sum::<usize>()
52    }
53
54    /// Merge another InvocationStats into this one
55    pub fn merge(&mut self, other: &InvocationStats) {
56        for (name, count) in &other.agents {
57            *self.agents.entry(name.clone()).or_insert(0) += count;
58        }
59        for (name, count) in &other.commands {
60            *self.commands.entry(name.clone()).or_insert(0) += count;
61        }
62        for (name, count) in &other.skills {
63            *self.skills.entry(name.clone()).or_insert(0) += count;
64        }
65        for (name, tokens) in &other.agent_token_stats {
66            *self.agent_token_stats.entry(name.clone()).or_insert(0) += tokens;
67        }
68        self.sessions_analyzed += other.sessions_analyzed;
69        // Keep the most recent timestamp
70        if other.last_computed > self.last_computed {
71            self.last_computed = other.last_computed;
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_total_invocations() {
82        let mut stats = InvocationStats::new();
83        stats.agents.insert("technical-writer".to_string(), 5);
84        stats.commands.insert("/commit".to_string(), 12);
85        stats.skills.insert("pdf-generator".to_string(), 3);
86
87        assert_eq!(stats.total_invocations(), 20);
88    }
89
90    #[test]
91    fn test_merge() {
92        let mut stats1 = InvocationStats::new();
93        stats1.agents.insert("debugger".to_string(), 3);
94        stats1.commands.insert("/commit".to_string(), 5);
95        stats1.sessions_analyzed = 10;
96
97        let mut stats2 = InvocationStats::new();
98        stats2.agents.insert("debugger".to_string(), 2);
99        stats2.agents.insert("code-reviewer".to_string(), 1);
100        stats2.commands.insert("/commit".to_string(), 7);
101        stats2.sessions_analyzed = 5;
102
103        stats1.merge(&stats2);
104
105        assert_eq!(stats1.agents.get("debugger"), Some(&5));
106        assert_eq!(stats1.agents.get("code-reviewer"), Some(&1));
107        assert_eq!(stats1.commands.get("/commit"), Some(&12));
108        assert_eq!(stats1.sessions_analyzed, 15);
109    }
110}