use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AgentType {
Analyzer,
Coder,
Tester,
Reviewer,
Integrator,
}
impl std::fmt::Display for AgentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AgentType::Analyzer => write!(f, "analyzer"),
AgentType::Coder => write!(f, "coder"),
AgentType::Tester => write!(f, "tester"),
AgentType::Reviewer => write!(f, "reviewer"),
AgentType::Integrator => write!(f, "integrator"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum AgentStatus {
#[default]
Idle,
Busy,
Offline,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Agent {
#[serde(rename = "_key")]
pub id: String,
#[serde(default = "default_name")]
pub name: String,
#[serde(default = "default_agent_type")]
pub agent_type: AgentType,
#[serde(default)]
pub status: AgentStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub config: Option<Value>,
#[serde(default = "Utc::now")]
pub registered_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_heartbeat: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_task_id: Option<String>,
#[serde(default)]
pub tasks_completed: u64,
#[serde(default)]
pub tasks_failed: u64,
}
fn default_name() -> String {
"Unnamed Agent".to_string()
}
fn default_agent_type() -> AgentType {
AgentType::Analyzer
}
impl Agent {
pub fn new(name: String, agent_type: AgentType, capabilities: Vec<String>) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
name,
agent_type,
status: AgentStatus::Idle,
url: None,
capabilities,
config: None,
registered_at: Utc::now(),
last_heartbeat: Some(Utc::now()),
current_task_id: None,
tasks_completed: 0,
tasks_failed: 0,
}
}
pub fn new_with_url(
name: String,
agent_type: AgentType,
capabilities: Vec<String>,
url: Option<String>,
) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
name,
agent_type,
status: AgentStatus::Idle,
url,
capabilities,
config: None,
registered_at: Utc::now(),
last_heartbeat: Some(Utc::now()),
current_task_id: None,
tasks_completed: 0,
tasks_failed: 0,
}
}
pub fn heartbeat(&mut self) {
self.last_heartbeat = Some(Utc::now());
}
pub fn is_healthy(&self, timeout_seconds: i64) -> bool {
if let Some(last) = self.last_heartbeat {
let elapsed = Utc::now().signed_duration_since(last);
elapsed.num_seconds() < timeout_seconds
} else {
false
}
}
pub fn start_task(&mut self, task_id: String) {
self.status = AgentStatus::Busy;
self.current_task_id = Some(task_id);
self.heartbeat();
}
pub fn complete_task(&mut self, success: bool) {
self.status = AgentStatus::Idle;
self.current_task_id = None;
if success {
self.tasks_completed += 1;
} else {
self.tasks_failed += 1;
}
self.heartbeat();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisResult {
pub affected_files: Vec<String>,
pub risk_score: f64,
pub requires_review: bool,
pub risk_reason: Option<String>,
pub suggested_approach: Option<String>,
pub complexity: u8,
#[serde(default)]
pub dependencies: Vec<String>,
#[serde(default)]
pub related_patterns: Vec<String>,
}
impl AnalysisResult {
pub fn safe(affected_files: Vec<String>) -> Self {
Self {
affected_files,
risk_score: 0.2,
requires_review: false,
risk_reason: None,
suggested_approach: None,
complexity: 3,
dependencies: Vec::new(),
related_patterns: Vec::new(),
}
}
pub fn high_risk(affected_files: Vec<String>, reason: String) -> Self {
Self {
affected_files,
risk_score: 0.8,
requires_review: true,
risk_reason: Some(reason),
suggested_approach: None,
complexity: 7,
dependencies: Vec::new(),
related_patterns: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeGenerationResult {
pub files: Vec<GeneratedFile>,
pub summary: String,
pub test_coverage_estimate: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedFile {
pub path: String,
pub content: String,
pub is_new: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_content: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub passed: bool,
pub stages: Vec<ValidationStageResult>,
pub error_count: usize,
pub warning_count: usize,
}
impl ValidationResult {
pub fn new() -> Self {
Self {
passed: true,
stages: Vec::new(),
error_count: 0,
warning_count: 0,
}
}
pub fn add_stage(&mut self, stage: ValidationStageResult) {
if !stage.passed {
self.passed = false;
}
self.error_count += stage.errors.len();
self.warning_count += stage.warnings.len();
self.stages.push(stage);
}
}
impl Default for ValidationResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationStageResult {
pub stage: ValidationStage,
pub passed: bool,
#[serde(default)]
pub errors: Vec<ValidationMessage>,
#[serde(default)]
pub warnings: Vec<ValidationMessage>,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ValidationStage {
Syntax,
Linting,
TypeCheck,
UnitTests,
Schema,
Security,
}
impl std::fmt::Display for ValidationStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationStage::Syntax => write!(f, "syntax"),
ValidationStage::Linting => write!(f, "linting"),
ValidationStage::TypeCheck => write!(f, "type_check"),
ValidationStage::UnitTests => write!(f, "unit_tests"),
ValidationStage::Schema => write!(f, "schema"),
ValidationStage::Security => write!(f, "security"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationMessage {
pub file: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
pub message: String,
pub code: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ListAgentsResponse {
pub agents: Vec<Agent>,
pub total: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_creation() {
let agent = Agent::new(
"test-analyzer".to_string(),
AgentType::Analyzer,
vec!["rust".to_string(), "typescript".to_string()],
);
assert_eq!(agent.name, "test-analyzer");
assert_eq!(agent.agent_type, AgentType::Analyzer);
assert_eq!(agent.status, AgentStatus::Idle);
assert_eq!(agent.capabilities.len(), 2);
}
#[test]
fn test_agent_task_lifecycle() {
let mut agent = Agent::new("test-coder".to_string(), AgentType::Coder, vec![]);
assert_eq!(agent.status, AgentStatus::Idle);
assert!(agent.current_task_id.is_none());
agent.start_task("task-123".to_string());
assert_eq!(agent.status, AgentStatus::Busy);
assert_eq!(agent.current_task_id, Some("task-123".to_string()));
agent.complete_task(true);
assert_eq!(agent.status, AgentStatus::Idle);
assert!(agent.current_task_id.is_none());
assert_eq!(agent.tasks_completed, 1);
assert_eq!(agent.tasks_failed, 0);
agent.start_task("task-456".to_string());
agent.complete_task(false);
assert_eq!(agent.tasks_completed, 1);
assert_eq!(agent.tasks_failed, 1);
}
#[test]
fn test_analysis_result() {
let safe = AnalysisResult::safe(vec!["src/utils.rs".to_string()]);
assert!(!safe.requires_review);
assert!(safe.risk_score < 0.5);
let risky = AnalysisResult::high_risk(
vec!["src/storage/engine.rs".to_string()],
"Modifies core storage engine".to_string(),
);
assert!(risky.requires_review);
assert!(risky.risk_score > 0.7);
}
#[test]
fn test_validation_result() {
let mut result = ValidationResult::new();
assert!(result.passed);
result.add_stage(ValidationStageResult {
stage: ValidationStage::Syntax,
passed: true,
errors: vec![],
warnings: vec![],
duration_ms: 100,
});
assert!(result.passed);
result.add_stage(ValidationStageResult {
stage: ValidationStage::Linting,
passed: false,
errors: vec![ValidationMessage {
file: Some("src/main.rs".to_string()),
line: Some(10),
column: Some(5),
message: "unused variable".to_string(),
code: Some("W001".to_string()),
}],
warnings: vec![],
duration_ms: 200,
});
assert!(!result.passed);
assert_eq!(result.error_count, 1);
}
}