use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
use crate::ids::{DecisionId, PlanId, StepId};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Plan {
pub id: PlanId,
pub name: String,
pub description: Option<String>,
pub steps: Vec<PlanStep>,
pub decision_points: Vec<DecisionPoint>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl Plan {
#[must_use]
pub fn new(id: PlanId, name: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
description: None,
steps: Vec::new(),
decision_points: Vec::new(),
metadata: HashMap::new(),
}
}
#[must_use]
pub fn get_step(&self, step_id: &StepId) -> Option<&PlanStep> {
self.steps.iter().find(|s| &s.id == step_id)
}
#[must_use]
pub fn get_decision_point(&self, decision_id: &DecisionId) -> Option<&DecisionPoint> {
self.decision_points.iter().find(|d| &d.id == decision_id)
}
#[must_use]
pub fn get_ready_steps(&self, completed: &HashSet<StepId>) -> Vec<&PlanStep> {
self.steps
.iter()
.filter(|step| {
!completed.contains(&step.id)
&& step.dependencies.iter().all(|dep| completed.contains(dep))
})
.collect()
}
#[must_use]
pub fn topological_order(&self) -> Option<Vec<StepId>> {
let mut in_degree: HashMap<&StepId, usize> = HashMap::new();
let mut dependents: HashMap<&StepId, Vec<&StepId>> = HashMap::new();
for step in &self.steps {
in_degree.entry(&step.id).or_insert(0);
for dep in &step.dependencies {
*in_degree.entry(&step.id).or_insert(0) += 1;
dependents.entry(dep).or_default().push(&step.id);
}
}
let mut queue: VecDeque<&StepId> = in_degree
.iter()
.filter(|(_, °ree)| degree == 0)
.map(|(&id, _)| id)
.collect();
let mut result = Vec::new();
while let Some(step_id) = queue.pop_front() {
result.push(step_id.clone());
if let Some(deps) = dependents.get(step_id) {
for dep in deps {
if let Some(degree) = in_degree.get_mut(dep) {
*degree -= 1;
if *degree == 0 {
queue.push_back(dep);
}
}
}
}
}
if result.len() == self.steps.len() {
Some(result)
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanStep {
pub id: StepId,
pub name: String,
pub activity: ActivitySpec,
pub inputs: HashMap<String, serde_json::Value>,
pub outputs: Vec<String>,
pub dependencies: Vec<StepId>,
}
impl PlanStep {
#[must_use]
pub fn new(id: StepId, name: impl Into<String>, activity: ActivitySpec) -> Self {
Self {
id,
name: name.into(),
activity,
inputs: HashMap::new(),
outputs: Vec::new(),
dependencies: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ActivitySpec {
Atomic {
activity_type: String,
retry_policy: Option<RetryPolicy>,
},
Composite {
sub_activities: Vec<ActivitySpec>,
},
ClaudeDecision {
context_template: String,
allowed_actions: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_interval_ms: u64,
pub max_interval_ms: u64,
pub backoff_coefficient: f64,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_interval_ms: 1000,
max_interval_ms: 60000,
backoff_coefficient: 2.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionPoint {
pub id: DecisionId,
pub name: String,
pub after_step: Option<StepId>,
pub context_template: String,
pub constraints: Vec<String>,
}
impl DecisionPoint {
#[must_use]
pub fn new(id: DecisionId, name: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
after_step: None,
context_template: String::new(),
constraints: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::StepId;
#[test]
fn test_plan_creation() {
let plan = Plan::new(PlanId::generate(), "test-plan");
assert_eq!(plan.name, "test-plan");
assert!(plan.steps.is_empty());
}
#[test]
fn test_plan_step_creation() {
let step = PlanStep::new(
StepId::generate(),
"test-step",
ActivitySpec::Atomic {
activity_type: "test-activity".to_string(),
retry_policy: None,
},
);
assert_eq!(step.name, "test-step");
}
#[test]
fn test_retry_policy_default() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_attempts, 3);
assert_eq!(policy.backoff_coefficient, 2.0);
}
#[test]
fn test_get_ready_steps_no_dependencies() {
let mut plan = Plan::new(PlanId::generate(), "test-plan");
let step1 = PlanStep::new(
StepId::new("step1").unwrap(),
"Step 1",
ActivitySpec::Atomic {
activity_type: "activity1".to_string(),
retry_policy: None,
},
);
let step2 = PlanStep::new(
StepId::new("step2").unwrap(),
"Step 2",
ActivitySpec::Atomic {
activity_type: "activity2".to_string(),
retry_policy: None,
},
);
plan.steps.push(step1);
plan.steps.push(step2);
let completed = HashSet::new();
let ready = plan.get_ready_steps(&completed);
assert_eq!(ready.len(), 2);
}
#[test]
fn test_get_ready_steps_with_dependencies() {
let mut plan = Plan::new(PlanId::generate(), "test-plan");
let step1 = PlanStep::new(
StepId::new("step1").unwrap(),
"Step 1",
ActivitySpec::Atomic {
activity_type: "activity1".to_string(),
retry_policy: None,
},
);
let mut step2 = PlanStep::new(
StepId::new("step2").unwrap(),
"Step 2",
ActivitySpec::Atomic {
activity_type: "activity2".to_string(),
retry_policy: None,
},
);
step2.dependencies.push(StepId::new("step1").unwrap());
plan.steps.push(step1);
plan.steps.push(step2);
let completed = HashSet::new();
let ready = plan.get_ready_steps(&completed);
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].id.as_str(), "step1");
let mut completed = HashSet::new();
completed.insert(StepId::new("step1").unwrap());
let ready = plan.get_ready_steps(&completed);
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].id.as_str(), "step2");
}
#[test]
fn test_topological_order_simple() {
let mut plan = Plan::new(PlanId::generate(), "test-plan");
let step1 = PlanStep::new(
StepId::new("step1").unwrap(),
"Step 1",
ActivitySpec::Atomic {
activity_type: "activity1".to_string(),
retry_policy: None,
},
);
let mut step2 = PlanStep::new(
StepId::new("step2").unwrap(),
"Step 2",
ActivitySpec::Atomic {
activity_type: "activity2".to_string(),
retry_policy: None,
},
);
step2.dependencies.push(StepId::new("step1").unwrap());
let mut step3 = PlanStep::new(
StepId::new("step3").unwrap(),
"Step 3",
ActivitySpec::Atomic {
activity_type: "activity3".to_string(),
retry_policy: None,
},
);
step3.dependencies.push(StepId::new("step2").unwrap());
plan.steps.push(step1);
plan.steps.push(step2);
plan.steps.push(step3);
let order = plan.topological_order().unwrap();
assert_eq!(order.len(), 3);
let step1_pos = order.iter().position(|id| id.as_str() == "step1").unwrap();
let step2_pos = order.iter().position(|id| id.as_str() == "step2").unwrap();
let step3_pos = order.iter().position(|id| id.as_str() == "step3").unwrap();
assert!(step1_pos < step2_pos);
assert!(step2_pos < step3_pos);
}
#[test]
fn test_topological_order_with_cycle() {
let mut plan = Plan::new(PlanId::generate(), "test-plan");
let mut step1 = PlanStep::new(
StepId::new("step1").unwrap(),
"Step 1",
ActivitySpec::Atomic {
activity_type: "activity1".to_string(),
retry_policy: None,
},
);
step1.dependencies.push(StepId::new("step2").unwrap());
let mut step2 = PlanStep::new(
StepId::new("step2").unwrap(),
"Step 2",
ActivitySpec::Atomic {
activity_type: "activity2".to_string(),
retry_policy: None,
},
);
step2.dependencies.push(StepId::new("step1").unwrap());
plan.steps.push(step1);
plan.steps.push(step2);
assert!(plan.topological_order().is_none());
}
}