#![allow(clippy::collapsible_match)]
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{AgentPid, GoalAlignmentError, GoalAlignmentResult};
pub type GoalId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Goal {
pub goal_id: GoalId,
pub level: GoalLevel,
pub description: String,
pub priority: u32,
pub constraints: Vec<GoalConstraint>,
pub dependencies: Vec<GoalId>,
pub knowledge_context: GoalKnowledgeContext,
pub assigned_roles: Vec<String>,
pub assigned_agents: Vec<AgentPid>,
pub status: GoalStatus,
pub metadata: GoalMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum GoalLevel {
Global,
HighLevel,
Local,
}
impl GoalLevel {
pub fn numeric_level(&self) -> u32 {
match self {
GoalLevel::Global => 0,
GoalLevel::HighLevel => 1,
GoalLevel::Local => 2,
}
}
pub fn can_contain(&self, other: &GoalLevel) -> bool {
self.numeric_level() < other.numeric_level()
}
pub fn parent_level(&self) -> Option<GoalLevel> {
match self {
GoalLevel::Global => None,
GoalLevel::HighLevel => Some(GoalLevel::Global),
GoalLevel::Local => Some(GoalLevel::HighLevel),
}
}
pub fn child_levels(&self) -> Vec<GoalLevel> {
match self {
GoalLevel::Global => vec![GoalLevel::HighLevel],
GoalLevel::HighLevel => vec![GoalLevel::Local],
GoalLevel::Local => vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GoalConstraint {
pub constraint_type: ConstraintType,
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 ConstraintType {
Temporal,
Resource,
Dependency,
Quality,
Security,
BusinessRule,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct GoalKnowledgeContext {
pub domains: Vec<String>,
pub concepts: Vec<String>,
pub relationships: Vec<String>,
pub keywords: Vec<String>,
pub similarity_thresholds: HashMap<String, f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum GoalStatus {
Pending,
Active,
Paused,
Completed,
Failed(String),
Blocked(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GoalMetadata {
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: String,
pub version: u32,
pub expected_duration: Option<Duration>,
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 GoalMetadata {
fn default() -> Self {
Self {
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: "system".to_string(),
version: 1,
expected_duration: None,
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 Goal {
pub fn new(goal_id: GoalId, level: GoalLevel, description: String, priority: u32) -> Self {
Self {
goal_id,
level,
description,
priority,
constraints: Vec::new(),
dependencies: Vec::new(),
knowledge_context: GoalKnowledgeContext::default(),
assigned_roles: Vec::new(),
assigned_agents: Vec::new(),
status: GoalStatus::Pending,
metadata: GoalMetadata::default(),
}
}
pub fn add_constraint(&mut self, constraint: GoalConstraint) -> GoalAlignmentResult<()> {
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_goal_id: GoalId) -> GoalAlignmentResult<()> {
if dependency_goal_id == self.goal_id {
return Err(GoalAlignmentError::DependencyCycle(format!(
"Goal {} cannot depend on itself",
self.goal_id
)));
}
if !self.dependencies.contains(&dependency_goal_id) {
self.dependencies.push(dependency_goal_id);
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
}
Ok(())
}
pub fn assign_agent(&mut self, agent_id: AgentPid) -> GoalAlignmentResult<()> {
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) -> GoalAlignmentResult<()> {
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: GoalStatus) -> GoalAlignmentResult<()> {
let old_status = self.status.clone();
self.status = status;
self.metadata.updated_at = Utc::now();
self.metadata.version += 1;
match (&old_status, &self.status) {
(GoalStatus::Pending, GoalStatus::Active) => {
self.metadata.started_at = Some(Utc::now());
}
(_, GoalStatus::Completed) | (_, GoalStatus::Failed(_)) => {
self.metadata.completed_at = Some(Utc::now());
self.metadata.progress = if matches!(self.status, GoalStatus::Completed) {
1.0
} else {
self.metadata.progress
};
}
_ => {}
}
Ok(())
}
pub fn update_progress(&mut self, progress: f64) -> GoalAlignmentResult<()> {
if !(0.0..=1.0).contains(&progress) {
return Err(GoalAlignmentError::InvalidGoalSpec(
self.goal_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, GoalStatus::Completed) {
self.update_status(GoalStatus::Completed)?;
}
Ok(())
}
pub fn can_start(&self, completed_goals: &HashSet<GoalId>) -> bool {
self.dependencies
.iter()
.all(|dep| completed_goals.contains(dep))
}
pub fn is_blocked(&self) -> bool {
matches!(self.status, GoalStatus::Blocked(_))
}
pub fn is_active(&self) -> bool {
matches!(self.status, GoalStatus::Active)
}
pub fn is_completed(&self) -> bool {
matches!(self.status, GoalStatus::Completed)
}
pub fn has_failed(&self) -> bool {
matches!(self.status, GoalStatus::Failed(_))
}
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) -> GoalAlignmentResult<()> {
if self.goal_id.is_empty() {
return Err(GoalAlignmentError::InvalidGoalSpec(
self.goal_id.clone(),
"Goal ID cannot be empty".to_string(),
));
}
if self.description.is_empty() {
return Err(GoalAlignmentError::InvalidGoalSpec(
self.goal_id.clone(),
"Goal description cannot be empty".to_string(),
));
}
if !(0.0..=1.0).contains(&self.metadata.progress) {
return Err(GoalAlignmentError::InvalidGoalSpec(
self.goal_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(GoalAlignmentError::InvalidGoalSpec(
self.goal_id.clone(),
"Success criteria weights must sum to 1.0".to_string(),
));
}
Ok(())
}
fn validate_constraint(&self, constraint: &GoalConstraint) -> GoalAlignmentResult<()> {
if constraint.description.is_empty() {
return Err(GoalAlignmentError::InvalidGoalSpec(
self.goal_id.clone(),
"Constraint description cannot be empty".to_string(),
));
}
#[allow(clippy::collapsible_match)]
match &constraint.constraint_type {
ConstraintType::Temporal => {
if !constraint.parameters.contains_key("deadline")
&& !constraint.parameters.contains_key("duration")
{
return Err(GoalAlignmentError::InvalidGoalSpec(
self.goal_id.clone(),
"Temporal constraint must specify deadline or duration".to_string(),
));
}
}
ConstraintType::Resource => {
if !constraint.parameters.contains_key("resource_type") {
return Err(GoalAlignmentError::InvalidGoalSpec(
self.goal_id.clone(),
"Resource constraint must specify resource_type".to_string(),
));
}
}
_ => {
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalHierarchy {
pub goals: HashMap<GoalId, Goal>,
pub parent_child: HashMap<GoalId, Vec<GoalId>>,
pub child_parent: HashMap<GoalId, GoalId>,
pub dependencies: HashMap<GoalId, Vec<GoalId>>,
}
impl GoalHierarchy {
pub fn new() -> Self {
Self {
goals: HashMap::new(),
parent_child: HashMap::new(),
child_parent: HashMap::new(),
dependencies: HashMap::new(),
}
}
pub fn add_goal(&mut self, goal: Goal) -> GoalAlignmentResult<()> {
if self.goals.contains_key(&goal.goal_id) {
return Err(GoalAlignmentError::GoalAlreadyExists(goal.goal_id.clone()));
}
goal.validate()?;
if !goal.dependencies.is_empty() {
self.dependencies
.insert(goal.goal_id.clone(), goal.dependencies.clone());
}
self.goals.insert(goal.goal_id.clone(), goal);
Ok(())
}
pub fn remove_goal(&mut self, goal_id: &GoalId) -> GoalAlignmentResult<()> {
if !self.goals.contains_key(goal_id) {
return Err(GoalAlignmentError::GoalNotFound(goal_id.clone()));
}
if let Some(parent_id) = self.child_parent.remove(goal_id)
&& let Some(children) = self.parent_child.get_mut(&parent_id)
{
children.retain(|id| id != goal_id);
}
if let Some(children) = self.parent_child.remove(goal_id) {
for child_id in children {
self.child_parent.remove(&child_id);
}
}
self.dependencies.remove(goal_id);
for deps in self.dependencies.values_mut() {
deps.retain(|id| id != goal_id);
}
self.goals.remove(goal_id);
Ok(())
}
pub fn set_parent_child(
&mut self,
parent_id: GoalId,
child_id: GoalId,
) -> GoalAlignmentResult<()> {
if !self.goals.contains_key(&parent_id) {
return Err(GoalAlignmentError::GoalNotFound(parent_id));
}
if !self.goals.contains_key(&child_id) {
return Err(GoalAlignmentError::GoalNotFound(child_id));
}
let parent_level = &self.goals[&parent_id].level;
let child_level = &self.goals[&child_id].level;
if !parent_level.can_contain(child_level) {
return Err(GoalAlignmentError::HierarchyValidationFailed(format!(
"Goal level {:?} cannot contain {:?}",
parent_level, child_level
)));
}
self.parent_child
.entry(parent_id.clone())
.or_default()
.push(child_id.clone());
self.child_parent.insert(child_id, parent_id);
Ok(())
}
pub fn get_children(&self, goal_id: &GoalId) -> Vec<&Goal> {
if let Some(child_ids) = self.parent_child.get(goal_id) {
child_ids
.iter()
.filter_map(|id| self.goals.get(id))
.collect()
} else {
Vec::new()
}
}
pub fn get_parent(&self, goal_id: &GoalId) -> Option<&Goal> {
self.child_parent
.get(goal_id)
.and_then(|parent_id| self.goals.get(parent_id))
}
pub fn get_goals_by_level(&self, level: &GoalLevel) -> Vec<&Goal> {
self.goals
.values()
.filter(|goal| &goal.level == level)
.collect()
}
pub fn has_dependency_cycle(&self) -> Option<Vec<GoalId>> {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
for goal_id in self.goals.keys() {
if !visited.contains(goal_id)
&& let Some(cycle) = self.dfs_cycle_check(goal_id, &mut visited, &mut rec_stack)
{
return Some(cycle);
}
}
None
}
fn dfs_cycle_check(
&self,
goal_id: &GoalId,
visited: &mut HashSet<GoalId>,
rec_stack: &mut HashSet<GoalId>,
) -> Option<Vec<GoalId>> {
visited.insert(goal_id.clone());
rec_stack.insert(goal_id.clone());
if let Some(dependencies) = self.dependencies.get(goal_id) {
for dep_id in dependencies {
if !visited.contains(dep_id) {
if let Some(cycle) = self.dfs_cycle_check(dep_id, visited, rec_stack) {
return Some(cycle);
}
} else if rec_stack.contains(dep_id) {
return Some(vec![goal_id.clone(), dep_id.clone()]);
}
}
}
rec_stack.remove(goal_id);
None
}
pub fn get_startable_goals(&self) -> Vec<&Goal> {
let completed_goals: HashSet<GoalId> = self
.goals
.values()
.filter(|goal| goal.is_completed())
.map(|goal| goal.goal_id.clone())
.collect();
self.goals
.values()
.filter(|goal| {
matches!(goal.status, GoalStatus::Pending) && goal.can_start(&completed_goals)
})
.collect()
}
}
impl Default for GoalHierarchy {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_goal_creation() {
let goal = Goal::new(
"test_goal".to_string(),
GoalLevel::Local,
"Test goal description".to_string(),
1,
);
assert_eq!(goal.goal_id, "test_goal");
assert_eq!(goal.level, GoalLevel::Local);
assert_eq!(goal.priority, 1);
assert_eq!(goal.status, GoalStatus::Pending);
assert_eq!(goal.metadata.progress, 0.0);
}
#[test]
fn test_goal_level_hierarchy() {
assert!(GoalLevel::Global.can_contain(&GoalLevel::HighLevel));
assert!(GoalLevel::HighLevel.can_contain(&GoalLevel::Local));
assert!(!GoalLevel::Local.can_contain(&GoalLevel::Global));
assert_eq!(GoalLevel::Global.numeric_level(), 0);
assert_eq!(GoalLevel::HighLevel.numeric_level(), 1);
assert_eq!(GoalLevel::Local.numeric_level(), 2);
}
#[test]
fn test_goal_constraints() {
let mut goal = Goal::new(
"test_goal".to_string(),
GoalLevel::Local,
"Test goal".to_string(),
1,
);
let constraint = GoalConstraint {
constraint_type: ConstraintType::Temporal,
description: "Must complete within 1 hour".to_string(),
parameters: {
let mut params = HashMap::new();
params.insert("duration".to_string(), serde_json::json!("1h"));
params
},
is_hard: true,
priority: 1,
};
goal.add_constraint(constraint).unwrap();
assert_eq!(goal.constraints.len(), 1);
assert_eq!(goal.metadata.version, 2); }
#[test]
fn test_goal_dependencies() {
let mut goal = Goal::new(
"test_goal".to_string(),
GoalLevel::Local,
"Test goal".to_string(),
1,
);
goal.add_dependency("dependency_goal".to_string()).unwrap();
assert_eq!(goal.dependencies.len(), 1);
let result = goal.add_dependency("test_goal".to_string());
assert!(result.is_err());
}
#[test]
fn test_goal_progress() {
let mut goal = Goal::new(
"test_goal".to_string(),
GoalLevel::Local,
"Test goal".to_string(),
1,
);
goal.update_progress(0.5).unwrap();
assert_eq!(goal.metadata.progress, 0.5);
goal.update_progress(1.0).unwrap();
assert_eq!(goal.metadata.progress, 1.0);
assert!(goal.is_completed());
let result = goal.update_progress(1.5);
assert!(result.is_err());
}
#[test]
fn test_goal_hierarchy() {
let mut hierarchy = GoalHierarchy::new();
let global_goal = Goal::new(
"global_goal".to_string(),
GoalLevel::Global,
"Global objective".to_string(),
1,
);
let local_goal = Goal::new(
"local_goal".to_string(),
GoalLevel::Local,
"Local objective".to_string(),
1,
);
hierarchy.add_goal(global_goal).unwrap();
hierarchy.add_goal(local_goal).unwrap();
hierarchy
.set_parent_child("global_goal".to_string(), "local_goal".to_string())
.unwrap();
let children = hierarchy.get_children(&"global_goal".to_string());
assert_eq!(children.len(), 1);
assert_eq!(children[0].goal_id, "local_goal");
let parent = hierarchy.get_parent(&"local_goal".to_string());
assert!(parent.is_some());
assert_eq!(parent.unwrap().goal_id, "global_goal");
}
#[test]
fn test_dependency_cycle_detection() {
let mut hierarchy = GoalHierarchy::new();
let mut goal1 = Goal::new(
"goal1".to_string(),
GoalLevel::Local,
"Goal 1".to_string(),
1,
);
let mut goal2 = Goal::new(
"goal2".to_string(),
GoalLevel::Local,
"Goal 2".to_string(),
1,
);
goal1.add_dependency("goal2".to_string()).unwrap();
goal2.add_dependency("goal1".to_string()).unwrap();
hierarchy.add_goal(goal1).unwrap();
hierarchy.add_goal(goal2).unwrap();
let cycle = hierarchy.has_dependency_cycle();
assert!(cycle.is_some());
}
}