1use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet, VecDeque};
7
8use crate::ids::{DecisionId, PlanId, StepId};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Plan {
13 pub id: PlanId,
15 pub name: String,
17 pub description: Option<String>,
19 pub steps: Vec<PlanStep>,
21 pub decision_points: Vec<DecisionPoint>,
23 pub metadata: HashMap<String, serde_json::Value>,
25}
26
27impl Plan {
28 #[must_use]
30 pub fn new(id: PlanId, name: impl Into<String>) -> Self {
31 Self {
32 id,
33 name: name.into(),
34 description: None,
35 steps: Vec::new(),
36 decision_points: Vec::new(),
37 metadata: HashMap::new(),
38 }
39 }
40
41 #[must_use]
43 pub fn get_step(&self, step_id: &StepId) -> Option<&PlanStep> {
44 self.steps.iter().find(|s| &s.id == step_id)
45 }
46
47 #[must_use]
49 pub fn get_decision_point(&self, decision_id: &DecisionId) -> Option<&DecisionPoint> {
50 self.decision_points.iter().find(|d| &d.id == decision_id)
51 }
52
53 #[must_use]
59 pub fn get_ready_steps(&self, completed: &HashSet<StepId>) -> Vec<&PlanStep> {
60 self.steps
61 .iter()
62 .filter(|step| {
63 !completed.contains(&step.id)
65 && step.dependencies.iter().all(|dep| completed.contains(dep))
67 })
68 .collect()
69 }
70
71 #[must_use]
75 pub fn topological_order(&self) -> Option<Vec<StepId>> {
76 let mut in_degree: HashMap<&StepId, usize> = HashMap::new();
77 let mut dependents: HashMap<&StepId, Vec<&StepId>> = HashMap::new();
78
79 for step in &self.steps {
81 in_degree.entry(&step.id).or_insert(0);
82 for dep in &step.dependencies {
83 *in_degree.entry(&step.id).or_insert(0) += 1;
84 dependents.entry(dep).or_default().push(&step.id);
85 }
86 }
87
88 let mut queue: VecDeque<&StepId> = in_degree
90 .iter()
91 .filter(|(_, °ree)| degree == 0)
92 .map(|(&id, _)| id)
93 .collect();
94
95 let mut result = Vec::new();
96
97 while let Some(step_id) = queue.pop_front() {
98 result.push(step_id.clone());
99
100 if let Some(deps) = dependents.get(step_id) {
101 for dep in deps {
102 if let Some(degree) = in_degree.get_mut(dep) {
103 *degree -= 1;
104 if *degree == 0 {
105 queue.push_back(dep);
106 }
107 }
108 }
109 }
110 }
111
112 if result.len() == self.steps.len() {
114 Some(result)
115 } else {
116 None
117 }
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct PlanStep {
124 pub id: StepId,
126 pub name: String,
128 pub activity: ActivitySpec,
130 pub inputs: HashMap<String, serde_json::Value>,
132 pub outputs: Vec<String>,
134 pub dependencies: Vec<StepId>,
136}
137
138impl PlanStep {
139 #[must_use]
141 pub fn new(id: StepId, name: impl Into<String>, activity: ActivitySpec) -> Self {
142 Self {
143 id,
144 name: name.into(),
145 activity,
146 inputs: HashMap::new(),
147 outputs: Vec::new(),
148 dependencies: Vec::new(),
149 }
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(tag = "type")]
156pub enum ActivitySpec {
157 Atomic {
159 activity_type: String,
161 retry_policy: Option<RetryPolicy>,
163 },
164 Composite {
166 sub_activities: Vec<ActivitySpec>,
168 },
169 ClaudeDecision {
171 context_template: String,
173 allowed_actions: Vec<String>,
175 },
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct RetryPolicy {
181 pub max_attempts: u32,
183 pub initial_interval_ms: u64,
185 pub max_interval_ms: u64,
187 pub backoff_coefficient: f64,
189}
190
191impl Default for RetryPolicy {
192 fn default() -> Self {
193 Self {
194 max_attempts: 3,
195 initial_interval_ms: 1000,
196 max_interval_ms: 60000,
197 backoff_coefficient: 2.0,
198 }
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct DecisionPoint {
205 pub id: DecisionId,
207 pub name: String,
209 pub after_step: Option<StepId>,
211 pub context_template: String,
213 pub constraints: Vec<String>,
215}
216
217impl DecisionPoint {
218 #[must_use]
220 pub fn new(id: DecisionId, name: impl Into<String>) -> Self {
221 Self {
222 id,
223 name: name.into(),
224 after_step: None,
225 context_template: String::new(),
226 constraints: Vec::new(),
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::ids::StepId;
235
236 #[test]
237 fn test_plan_creation() {
238 let plan = Plan::new(PlanId::generate(), "test-plan");
239 assert_eq!(plan.name, "test-plan");
240 assert!(plan.steps.is_empty());
241 }
242
243 #[test]
244 fn test_plan_step_creation() {
245 let step = PlanStep::new(
246 StepId::generate(),
247 "test-step",
248 ActivitySpec::Atomic {
249 activity_type: "test-activity".to_string(),
250 retry_policy: None,
251 },
252 );
253 assert_eq!(step.name, "test-step");
254 }
255
256 #[test]
257 fn test_retry_policy_default() {
258 let policy = RetryPolicy::default();
259 assert_eq!(policy.max_attempts, 3);
260 assert_eq!(policy.backoff_coefficient, 2.0);
261 }
262
263 #[test]
264 fn test_get_ready_steps_no_dependencies() {
265 let mut plan = Plan::new(PlanId::generate(), "test-plan");
266 let step1 = PlanStep::new(
267 StepId::new("step1").unwrap(),
268 "Step 1",
269 ActivitySpec::Atomic {
270 activity_type: "activity1".to_string(),
271 retry_policy: None,
272 },
273 );
274 let step2 = PlanStep::new(
275 StepId::new("step2").unwrap(),
276 "Step 2",
277 ActivitySpec::Atomic {
278 activity_type: "activity2".to_string(),
279 retry_policy: None,
280 },
281 );
282 plan.steps.push(step1);
283 plan.steps.push(step2);
284
285 let completed = HashSet::new();
286 let ready = plan.get_ready_steps(&completed);
287 assert_eq!(ready.len(), 2);
288 }
289
290 #[test]
291 fn test_get_ready_steps_with_dependencies() {
292 let mut plan = Plan::new(PlanId::generate(), "test-plan");
293 let step1 = PlanStep::new(
294 StepId::new("step1").unwrap(),
295 "Step 1",
296 ActivitySpec::Atomic {
297 activity_type: "activity1".to_string(),
298 retry_policy: None,
299 },
300 );
301 let mut step2 = PlanStep::new(
302 StepId::new("step2").unwrap(),
303 "Step 2",
304 ActivitySpec::Atomic {
305 activity_type: "activity2".to_string(),
306 retry_policy: None,
307 },
308 );
309 step2.dependencies.push(StepId::new("step1").unwrap());
310 plan.steps.push(step1);
311 plan.steps.push(step2);
312
313 let completed = HashSet::new();
315 let ready = plan.get_ready_steps(&completed);
316 assert_eq!(ready.len(), 1);
317 assert_eq!(ready[0].id.as_str(), "step1");
318
319 let mut completed = HashSet::new();
321 completed.insert(StepId::new("step1").unwrap());
322 let ready = plan.get_ready_steps(&completed);
323 assert_eq!(ready.len(), 1);
324 assert_eq!(ready[0].id.as_str(), "step2");
325 }
326
327 #[test]
328 fn test_topological_order_simple() {
329 let mut plan = Plan::new(PlanId::generate(), "test-plan");
330 let step1 = PlanStep::new(
331 StepId::new("step1").unwrap(),
332 "Step 1",
333 ActivitySpec::Atomic {
334 activity_type: "activity1".to_string(),
335 retry_policy: None,
336 },
337 );
338 let mut step2 = PlanStep::new(
339 StepId::new("step2").unwrap(),
340 "Step 2",
341 ActivitySpec::Atomic {
342 activity_type: "activity2".to_string(),
343 retry_policy: None,
344 },
345 );
346 step2.dependencies.push(StepId::new("step1").unwrap());
347 let mut step3 = PlanStep::new(
348 StepId::new("step3").unwrap(),
349 "Step 3",
350 ActivitySpec::Atomic {
351 activity_type: "activity3".to_string(),
352 retry_policy: None,
353 },
354 );
355 step3.dependencies.push(StepId::new("step2").unwrap());
356
357 plan.steps.push(step1);
358 plan.steps.push(step2);
359 plan.steps.push(step3);
360
361 let order = plan.topological_order().unwrap();
362 assert_eq!(order.len(), 3);
363
364 let step1_pos = order.iter().position(|id| id.as_str() == "step1").unwrap();
366 let step2_pos = order.iter().position(|id| id.as_str() == "step2").unwrap();
367 let step3_pos = order.iter().position(|id| id.as_str() == "step3").unwrap();
368
369 assert!(step1_pos < step2_pos);
370 assert!(step2_pos < step3_pos);
371 }
372
373 #[test]
374 fn test_topological_order_with_cycle() {
375 let mut plan = Plan::new(PlanId::generate(), "test-plan");
376 let mut step1 = PlanStep::new(
377 StepId::new("step1").unwrap(),
378 "Step 1",
379 ActivitySpec::Atomic {
380 activity_type: "activity1".to_string(),
381 retry_policy: None,
382 },
383 );
384 step1.dependencies.push(StepId::new("step2").unwrap());
385
386 let mut step2 = PlanStep::new(
387 StepId::new("step2").unwrap(),
388 "Step 2",
389 ActivitySpec::Atomic {
390 activity_type: "activity2".to_string(),
391 retry_policy: None,
392 },
393 );
394 step2.dependencies.push(StepId::new("step1").unwrap());
395
396 plan.steps.push(step1);
397 plan.steps.push(step2);
398
399 assert!(plan.topological_order().is_none());
401 }
402}