//! Growth Tracker - Tracks agent growth metrics and evolution //! //! This module provides the `GrowthTracker` which monitors and records //! the evolution of an agent's capabilities and knowledge over time. use crate::types::{GrowthStats, MemoryType}; use crate::viking_adapter::VikingAdapter; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use zclaw_types::{AgentId, Result}; /// Growth Tracker - tracks agent growth metrics pub struct GrowthTracker { /// OpenViking adapter for storage viking: Arc, } impl GrowthTracker { /// Create a new growth tracker pub fn new(viking: Arc) -> Self { Self { viking } } /// Get current growth statistics for an agent pub async fn get_stats(&self, agent_id: &AgentId) -> Result { // Query all memories for the agent let memories = self.viking.find_by_prefix(&format!("agent://{}", agent_id)).await?; let mut stats = GrowthStats::default(); stats.total_memories = memories.len(); for memory in &memories { match memory.memory_type { MemoryType::Preference => stats.preference_count += 1, MemoryType::Knowledge => stats.knowledge_count += 1, MemoryType::Experience => stats.experience_count += 1, MemoryType::Session => stats.sessions_processed += 1, } } // Get last learning time from metadata let meta: Option = self.viking .get_metadata(&format!("agent://{}", agent_id)) .await?; if let Some(meta) = meta { stats.last_learning_time = meta.last_learning_time; } Ok(stats) } /// Record a learning event pub async fn record_learning( &self, agent_id: &AgentId, session_id: &str, memories_extracted: usize, ) -> Result<()> { let event = LearningEvent { agent_id: agent_id.to_string(), session_id: session_id.to_string(), memories_extracted, timestamp: Utc::now(), }; // Store learning event self.viking .store_metadata( &format!("agent://{}/events/{}", agent_id, session_id), &event, ) .await?; // Update last learning time self.viking .store_metadata( &format!("agent://{}", agent_id), &AgentMetadata { last_learning_time: Some(Utc::now()), total_learning_events: None, // Will be computed }, ) .await?; tracing::info!( "[GrowthTracker] Recorded learning event: agent={}, session={}, memories={}", agent_id, session_id, memories_extracted ); Ok(()) } /// Get growth timeline for an agent pub async fn get_timeline(&self, agent_id: &AgentId) -> Result> { let memories = self .viking .find_by_prefix(&format!("agent://{}/events/", agent_id)) .await?; // Parse events from stored memory content let mut timeline = Vec::new(); for memory in memories { if let Ok(event) = serde_json::from_str::(&memory.content) { timeline.push(event); } } // Sort by timestamp descending timeline.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); Ok(timeline) } /// Calculate growth velocity (memories per day) pub async fn get_growth_velocity(&self, agent_id: &AgentId) -> Result { let timeline = self.get_timeline(agent_id).await?; if timeline.is_empty() { return Ok(0.0); } // Get first and last event let first = timeline.iter().min_by_key(|e| e.timestamp); let last = timeline.iter().max_by_key(|e| e.timestamp); match (first, last) { (Some(first), Some(last)) => { let days = (last.timestamp - first.timestamp).num_days().max(1) as f64; let total_memories: usize = timeline.iter().map(|e| e.memories_extracted).sum(); Ok(total_memories as f64 / days) } _ => Ok(0.0), } } /// Get memory distribution by category pub async fn get_memory_distribution( &self, agent_id: &AgentId, ) -> Result> { let memories = self.viking.find_by_prefix(&format!("agent://{}", agent_id)).await?; let mut distribution = HashMap::new(); for memory in memories { *distribution.entry(memory.memory_type.to_string()).or_insert(0) += 1; } Ok(distribution) } } /// Learning event record #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LearningEvent { /// Agent ID pub agent_id: String, /// Session ID where learning occurred pub session_id: String, /// Number of memories extracted pub memories_extracted: usize, /// Event timestamp pub timestamp: DateTime, } /// Agent metadata stored in OpenViking #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentMetadata { /// Last learning time pub last_learning_time: Option>, /// Total learning events (computed) pub total_learning_events: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_learning_event_serialization() { let event = LearningEvent { agent_id: "test-agent".to_string(), session_id: "test-session".to_string(), memories_extracted: 5, timestamp: Utc::now(), }; let json = serde_json::to_string(&event).unwrap(); let parsed: LearningEvent = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.agent_id, event.agent_id); assert_eq!(parsed.memories_extracted, event.memories_extracted); } #[test] fn test_agent_metadata_serialization() { let meta = AgentMetadata { last_learning_time: Some(Utc::now()), total_learning_events: Some(10), }; let json = serde_json::to_string(&meta).unwrap(); let parsed: AgentMetadata = serde_json::from_str(&json).unwrap(); assert!(parsed.last_learning_time.is_some()); assert_eq!(parsed.total_learning_events, Some(10)); } }