Full Cargo workspace with 11 crates: - gyandoot-core: Shared types, config, error types - gyandoot-agent: Agent loop, tool execution, context compression - gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama) - gyandoot-memory: MemPalace persistent memory system - gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI) - gyandoot-security: Credential vault, sanitizer, threat detector - gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics - gyandoot-subagents: Sub-agent spawning and orchestration - gyandoot-skills: Skills discovery, loading, preprocessing, bundles - gyandoot-profile: Multi-agent profile manager - gyandoot-gateway: Gateway sessions, routing, access control Plus: CLI entry point with clap, cross-platform installer script, KANBAN.md, .gnpl/AGENTS.md Workspace compiles clean: cargo check passes with 0 errors.
152 lines
4.6 KiB
Rust
152 lines
4.6 KiB
Rust
//! Diagnostics — task tracking, bottlenecks, and performance metrics.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use gyandoot_core::{KanbanResult, Task, TaskStatus};
|
|
use uuid::Uuid;
|
|
|
|
/// A diagnostic snapshot of the Kanban system.
|
|
#[derive(Debug, Clone)]
|
|
pub struct KanbanDiagnostics {
|
|
pub total_tasks: usize,
|
|
pub backlog_count: usize,
|
|
pub in_progress_count: usize,
|
|
pub done_count: usize,
|
|
pub blocked_count: usize,
|
|
pub average_completion_time_ms: Option<f64>,
|
|
pub bottlenecks: Vec<String>,
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
/// Tracks task lifecycle and produces diagnostic information.
|
|
pub struct Diagnostics {
|
|
task_timings: HashMap<Uuid, TaskTiming>,
|
|
bottlenecks: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct TaskTiming {
|
|
created_at: DateTime<Utc>,
|
|
started_at: Option<DateTime<Utc>>,
|
|
completed_at: Option<DateTime<Utc>>,
|
|
status_changes: Vec<(DateTime<Utc>, TaskStatus)>,
|
|
}
|
|
|
|
impl Diagnostics {
|
|
/// Create a new diagnostics tracker.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
task_timings: HashMap::new(),
|
|
bottlenecks: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Record a task creation.
|
|
pub fn record_creation(&mut self, task_id: Uuid) {
|
|
self.task_timings.insert(
|
|
task_id,
|
|
TaskTiming {
|
|
created_at: Utc::now(),
|
|
started_at: None,
|
|
completed_at: None,
|
|
status_changes: vec![(Utc::now(), TaskStatus::Backlog)],
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Record a task status change.
|
|
pub fn record_status_change(&mut self, task_id: &Uuid, new_status: TaskStatus) {
|
|
if let Some(timing) = self.task_timings.get_mut(task_id) {
|
|
timing.status_changes.push((Utc::now(), new_status.clone()));
|
|
|
|
if new_status == TaskStatus::InProgress && timing.started_at.is_none() {
|
|
timing.started_at = Some(Utc::now());
|
|
}
|
|
|
|
if new_status == TaskStatus::Done {
|
|
timing.completed_at = Some(Utc::now());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate a diagnostics snapshot for a set of tasks.
|
|
pub fn analyze(&self, tasks: &[Task]) -> KanbanResult<KanbanDiagnostics> {
|
|
let total = tasks.len();
|
|
let backlog = tasks.iter().filter(|t| t.status == TaskStatus::Backlog).count();
|
|
let in_progress = tasks.iter().filter(|t| t.status == TaskStatus::InProgress).count();
|
|
let done = tasks.iter().filter(|t| t.status == TaskStatus::Done).count();
|
|
let blocked = tasks.iter().filter(|t| t.status == TaskStatus::Blocked).count();
|
|
|
|
// Calculate average completion time
|
|
let completion_times: Vec<f64> = self
|
|
.task_timings
|
|
.values()
|
|
.filter_map(|t| {
|
|
let start = t.started_at?;
|
|
let end = t.completed_at?;
|
|
Some((end - start).num_milliseconds() as f64)
|
|
})
|
|
.collect();
|
|
|
|
let avg_completion = if completion_times.is_empty() {
|
|
None
|
|
} else {
|
|
Some(completion_times.iter().sum::<f64>() / completion_times.len() as f64)
|
|
};
|
|
|
|
Ok(KanbanDiagnostics {
|
|
total_tasks: total,
|
|
backlog_count: backlog,
|
|
in_progress_count: in_progress,
|
|
done_count: done,
|
|
blocked_count: blocked,
|
|
average_completion_time_ms: avg_completion,
|
|
bottlenecks: self.bottlenecks.clone(),
|
|
timestamp: Utc::now(),
|
|
})
|
|
}
|
|
|
|
/// Add a bottleneck warning.
|
|
pub fn report_bottleneck(&mut self, description: String) {
|
|
self.bottlenecks.push(description);
|
|
}
|
|
}
|
|
|
|
impl Default for Diagnostics {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_diagnostics_tracking() {
|
|
let mut diagnostics = Diagnostics::new();
|
|
let task_id = Uuid::new_v4();
|
|
diagnostics.record_creation(task_id);
|
|
diagnostics.record_status_change(&task_id, TaskStatus::InProgress);
|
|
diagnostics.record_status_change(&task_id, TaskStatus::Done);
|
|
|
|
let tasks = vec![Task {
|
|
id: task_id,
|
|
title: "Test".to_string(),
|
|
description: String::new(),
|
|
priority: gyandoot_core::Priority::Low,
|
|
status: TaskStatus::Done,
|
|
assigned_agent: None,
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
subtasks: Vec::new(),
|
|
dependencies: Vec::new(),
|
|
tags: Vec::new(),
|
|
}];
|
|
|
|
let report = diagnostics.analyze(&tasks).unwrap();
|
|
assert_eq!(report.total_tasks, 1);
|
|
assert_eq!(report.done_count, 1);
|
|
}
|
|
} |