use std::collections::{HashMap, HashSet};
use std::time::Duration;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{AgentPid, GoalId, TaskDecompositionError, TaskDecompositionResult};
pub type TaskId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Task {
pub task_id: TaskId,
pub description: String,
pub complexity: TaskComplexity,
pub required_capabilities: Vec<String>,
pub knowledge_context: TaskKnowledgeContext,
pub constraints: Vec<TaskConstraint>,
pub dependencies: Vec<TaskId>,
pub estimated_effort: Duration,
pub priority: u32,
pub status: TaskStatus,
pub metadata: TaskMetadata,
pub parent_goal: Option<GoalId>,
pub assigned_agents: Vec<AgentPid>,
pub subtasks: Vec<TaskId>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TaskComplexity {
Simple,
Moderate,
Complex,
VeryComplex,
}
impl TaskComplexity {
pub fn score(&self) -> u32 {
match self {
TaskComplexity::Simple => 1,
TaskComplexity::Moderate => 2,
TaskComplexity::Complex => 3,
TaskComplexity::VeryComplex => 4,
}
}
pub fn requires_decomposition(&self) -> bool {
matches!(self, TaskComplexity::Complex | TaskComplexity::VeryComplex)
}
pub fn recommended_depth(&self) -> u32 {
match self {
TaskComplexity::Simple => 0,
TaskComplexity::Moderate => 1,
TaskComplexity::Complex => 2,
TaskComplexity::VeryComplex => 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct TaskKnowledgeContext {
pub domains: Vec<String>,
pub concepts: Vec<String>,
pub relationships: Vec<String>,
pub keywords: Vec<String>,
pub input_types: Vec<String>,
pub output_types: Vec<String>,
pub similarity_thresholds: HashMap<String, f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskConstraint {
pub constraint_type: TaskConstraintType,
pub description: String,
pub parameters: HashMap<String, serde_json::Value>,
pub is_hard: bool,
pub priority: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskConstraintType {
Temporal,
Resource,
Quality,
Security,
Performance,
Dependency,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskStatus {
Pending,
Ready,
InProgress,
Paused,
Completed,
Failed(String),
Cancelled(String),
Blocked(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskMetadata {
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: String,
pub version: u32,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub progress: f64,
pub success_criteria: Vec<SuccessCriterion>,
pub tags: Vec<String>,
pub custom_fields: HashMap<String, serde_json::Value>,
}
impl Default for TaskMetadata {
fn default() -> Self {
Self {
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: "system".to_string(),
version: 1,
started_at: None,
completed_at: None,
progress: 0.0,
success_criteria: Vec::new(),
tags: Vec::new(),
custom_fields: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SuccessCriterion {
pub description: String,
pub metric: String,
pub target_value: f64,
pub current_value: f64,
pub is_met: bool,
pub weight: f64,
}
impl Task {
pub fn new(
task_id: TaskId,
description: String,
complexity: TaskComplexity,
priority: u32,
) -> Self {
Self {
task_id,
description,
complexity,
required_capabilities: Vec::new(),
knowledge_context: TaskKnowledgeContext::default(),
constraints: Vec::new(),
dependencies: Vec::new(),
estimated_effort: Duration::from_secs(3600), priority,
status: TaskStatus::Pending,
metadata: TaskMetadata::default(),
parent_goal: None,
assigned_agents: Vec::new(),
subtasks: Vec::new(),
}
}
pub fn add_constraint(&mut self, constraint: TaskConstraint) -> TaskDecompositionResult<()> {
self.validate_constraint(&constraint)?;
self.constraints.push(constraint);
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
Ok(())
}
pub fn add_dependency(&mut self, dependency_task_id: TaskId) -> TaskDecompositionResult<()> {
if dependency_task_id == self.task_id {
return Err(TaskDecompositionError::DependencyCycle(format!(
"Task {} cannot depend on itself",
self.task_id
)));
}
if !self.dependencies.contains(&dependency_task_id) {
self.dependencies.push(dependency_task_id);
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
}
Ok(())
}
pub fn assign_agent(&mut self, agent_id: AgentPid) -> TaskDecompositionResult<()> {
if !self.assigned_agents.contains(&agent_id) {
self.assigned_agents.push(agent_id);
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
}
Ok(())
}
pub fn unassign_agent(&mut self, agent_id: &AgentPid) -> TaskDecompositionResult<()> {
self.assigned_agents.retain(|id| id != agent_id);
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
Ok(())
}
pub fn update_status(&mut self, status: TaskStatus) -> TaskDecompositionResult<()> {
let old_status = self.status.clone();
self.status = status;
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
match (&old_status, &self.status) {
(TaskStatus::Pending | TaskStatus::Ready, TaskStatus::InProgress) => {
self.metadata.started_at = Some(Utc::now());
}
(_, TaskStatus::Completed) => {
self.metadata.completed_at = Some(Utc::now());
self.metadata.progress = 1.0;
}
(_, TaskStatus::Failed(_)) | (_, TaskStatus::Cancelled(_)) => {
self.metadata.completed_at = Some(Utc::now());
}
_ => {}
}
Ok(())
}
pub fn update_progress(&mut self, progress: f64) -> TaskDecompositionResult<()> {
if !(0.0..=1.0).contains(&progress) {
return Err(TaskDecompositionError::InvalidTaskSpec(
self.task_id.clone(),
"Progress must be between 0.0 and 1.0".to_string(),
));
}
self.metadata.progress = progress;
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
if progress >= 1.0 && !matches!(self.status, TaskStatus::Completed) {
self.update_status(TaskStatus::Completed)?;
}
Ok(())
}
pub fn add_subtask(&mut self, subtask_id: TaskId) -> TaskDecompositionResult<()> {
if !self.subtasks.contains(&subtask_id) {
self.subtasks.push(subtask_id);
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
}
Ok(())
}
pub fn can_start(&self, completed_tasks: &HashSet<TaskId>) -> bool {
self.dependencies
.iter()
.all(|dep| completed_tasks.contains(dep))
}
pub fn is_ready(&self) -> bool {
matches!(self.status, TaskStatus::Ready)
}
pub fn is_in_progress(&self) -> bool {
matches!(self.status, TaskStatus::InProgress)
}
pub fn is_completed(&self) -> bool {
matches!(self.status, TaskStatus::Completed)
}
pub fn has_failed(&self) -> bool {
matches!(self.status, TaskStatus::Failed(_))
}
pub fn is_blocked(&self) -> bool {
matches!(self.status, TaskStatus::Blocked(_))
}
pub fn get_duration(&self) -> Option<chrono::Duration> {
if let (Some(started), Some(completed)) =
(self.metadata.started_at, self.metadata.completed_at)
{
Some(completed - started)
} else {
None
}
}
pub fn calculate_success_score(&self) -> f64 {
if self.metadata.success_criteria.is_empty() {
return if self.is_completed() { 1.0 } else { 0.0 };
}
let total_weight: f64 = self
.metadata
.success_criteria
.iter()
.map(|c| c.weight)
.sum();
if total_weight == 0.0 {
return 0.0;
}
let weighted_score: f64 = self
.metadata
.success_criteria
.iter()
.map(|criterion| {
let score = if criterion.is_met {
1.0
} else {
(criterion.current_value / criterion.target_value).clamp(0.0, 1.0)
};
score * criterion.weight
})
.sum();
weighted_score / total_weight
}
pub fn validate(&self) -> TaskDecompositionResult<()> {
if self.task_id.is_empty() {
return Err(TaskDecompositionError::InvalidTaskSpec(
self.task_id.clone(),
"Task ID cannot be empty".to_string(),
));
}
if self.description.is_empty() {
return Err(TaskDecompositionError::InvalidTaskSpec(
self.task_id.clone(),
"Task description cannot be empty".to_string(),
));
}
if !(0.0..=1.0).contains(&self.metadata.progress) {
return Err(TaskDecompositionError::InvalidTaskSpec(
self.task_id.clone(),
"Progress must be between 0.0 and 1.0".to_string(),
));
}
for constraint in &self.constraints {
self.validate_constraint(constraint)?;
}
let total_weight: f64 = self
.metadata
.success_criteria
.iter()
.map(|c| c.weight)
.sum();
if !self.metadata.success_criteria.is_empty() && (total_weight - 1.0).abs() > 0.01 {
return Err(TaskDecompositionError::InvalidTaskSpec(
self.task_id.clone(),
"Success criteria weights must sum to 1.0".to_string(),
));
}
Ok(())
}
fn validate_constraint(&self, constraint: &TaskConstraint) -> TaskDecompositionResult<()> {
if constraint.description.is_empty() {
return Err(TaskDecompositionError::InvalidTaskSpec(
self.task_id.clone(),
"Constraint description cannot be empty".to_string(),
));
}
#[allow(clippy::collapsible_match)]
match &constraint.constraint_type {
TaskConstraintType::Temporal
if !constraint.parameters.contains_key("deadline")
&& !constraint.parameters.contains_key("duration") =>
{
return Err(TaskDecompositionError::InvalidTaskSpec(
self.task_id.clone(),
"Temporal constraints must have deadline or duration parameter".to_string(),
));
}
TaskConstraintType::Resource
if !constraint.parameters.contains_key("resource_type") =>
{
return Err(TaskDecompositionError::InvalidTaskSpec(
self.task_id.clone(),
"Resource constraints must specify resource_type".to_string(),
));
}
_ => {}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_creation() {
let task = Task::new(
"test_task".to_string(),
"Test task description".to_string(),
TaskComplexity::Simple,
1,
);
assert_eq!(task.task_id, "test_task");
assert_eq!(task.description, "Test task description");
assert_eq!(task.complexity, TaskComplexity::Simple);
assert_eq!(task.priority, 1);
assert_eq!(task.status, TaskStatus::Pending);
assert!(task.dependencies.is_empty());
assert!(task.assigned_agents.is_empty());
assert!(task.subtasks.is_empty());
}
#[test]
fn test_task_complexity_scoring() {
assert_eq!(TaskComplexity::Simple.score(), 1);
assert_eq!(TaskComplexity::Moderate.score(), 2);
assert_eq!(TaskComplexity::Complex.score(), 3);
assert_eq!(TaskComplexity::VeryComplex.score(), 4);
}
#[test]
fn test_task_complexity_decomposition_requirements() {
assert!(!TaskComplexity::Simple.requires_decomposition());
assert!(!TaskComplexity::Moderate.requires_decomposition());
assert!(TaskComplexity::Complex.requires_decomposition());
assert!(TaskComplexity::VeryComplex.requires_decomposition());
}
#[test]
fn test_task_dependency_management() {
let mut task = Task::new(
"test_task".to_string(),
"Test task".to_string(),
TaskComplexity::Simple,
1,
);
assert!(task.add_dependency("dep_task".to_string()).is_ok());
assert_eq!(task.dependencies.len(), 1);
assert!(task.dependencies.contains(&"dep_task".to_string()));
assert!(task.add_dependency("test_task".to_string()).is_err());
assert!(task.add_dependency("dep_task".to_string()).is_ok());
assert_eq!(task.dependencies.len(), 1);
}
#[test]
fn test_task_agent_assignment() {
let mut task = Task::new(
"test_task".to_string(),
"Test task".to_string(),
TaskComplexity::Simple,
1,
);
let agent_id: AgentPid = "test_agent".to_string();
assert!(task.assign_agent(agent_id.clone()).is_ok());
assert_eq!(task.assigned_agents.len(), 1);
assert!(task.assigned_agents.contains(&agent_id));
assert!(task.assign_agent(agent_id.clone()).is_ok());
assert_eq!(task.assigned_agents.len(), 1);
assert!(task.unassign_agent(&agent_id).is_ok());
assert!(task.assigned_agents.is_empty());
}
#[test]
fn test_task_status_updates() {
let mut task = Task::new(
"test_task".to_string(),
"Test task".to_string(),
TaskComplexity::Simple,
1,
);
assert!(task.update_status(TaskStatus::InProgress).is_ok());
assert!(task.is_in_progress());
assert!(task.metadata.started_at.is_some());
assert!(task.update_status(TaskStatus::Completed).is_ok());
assert!(task.is_completed());
assert!(task.metadata.completed_at.is_some());
assert_eq!(task.metadata.progress, 1.0);
}
#[test]
fn test_task_progress_updates() {
let mut task = Task::new(
"test_task".to_string(),
"Test task".to_string(),
TaskComplexity::Simple,
1,
);
assert!(task.update_progress(0.5).is_ok());
assert_eq!(task.metadata.progress, 0.5);
assert!(task.update_progress(1.5).is_err());
assert!(task.update_progress(-0.1).is_err());
assert!(task.update_progress(1.0).is_ok());
assert!(task.is_completed());
}
#[test]
fn test_task_readiness_check() {
let mut task = Task::new(
"test_task".to_string(),
"Test task".to_string(),
TaskComplexity::Simple,
1,
);
task.add_dependency("dep1".to_string()).unwrap();
task.add_dependency("dep2".to_string()).unwrap();
let mut completed_tasks = HashSet::new();
assert!(!task.can_start(&completed_tasks));
completed_tasks.insert("dep1".to_string());
assert!(!task.can_start(&completed_tasks));
completed_tasks.insert("dep2".to_string());
assert!(task.can_start(&completed_tasks));
}
#[test]
fn test_task_validation() {
let mut task = Task::new(
"test_task".to_string(),
"Test task".to_string(),
TaskComplexity::Simple,
1,
);
assert!(task.validate().is_ok());
task.task_id = "".to_string();
assert!(task.validate().is_err());
task.task_id = "test_task".to_string();
task.description = "".to_string();
assert!(task.validate().is_err());
task.description = "Test task".to_string();
task.metadata.progress = 1.5;
assert!(task.validate().is_err());
}
#[test]
fn test_success_score_calculation() {
let mut task = Task::new(
"test_task".to_string(),
"Test task".to_string(),
TaskComplexity::Simple,
1,
);
task.update_status(TaskStatus::Completed).unwrap();
assert_eq!(task.calculate_success_score(), 1.0);
task.metadata.success_criteria = vec![
SuccessCriterion {
description: "Quality metric".to_string(),
metric: "quality".to_string(),
target_value: 100.0,
current_value: 80.0,
is_met: false,
weight: 0.6,
},
SuccessCriterion {
description: "Performance metric".to_string(),
metric: "performance".to_string(),
target_value: 50.0,
current_value: 50.0,
is_met: true,
weight: 0.4,
},
];
let score = task.calculate_success_score();
assert!((score - 0.88).abs() < 0.01);
}
}