Skip to main content

agent_cadence_progress/
lib.rs

1//! # agent-cadence-progress
2//!
3//! Musical cadence as task completion signal. Musical cadences signal resolution
4//! and finality — agents can signal task completion using the same patterns.
5
6use std::collections::HashMap;
7
8/// Types of musical cadence mapped to task completion states.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum CadenceType {
11    /// Perfect authentic cadence (V → I) — fully complete, no ambiguity
12    PerfectAuthentic,
13    /// Plagal cadence (IV → I) — complete but softer, "amen" ending
14    Plagal,
15    /// Deceptive cadence (V → vi) — looks done but isn't, surprise twist
16    Deceptive,
17    /// Half cadence (→ V) — paused mid-way, expects continuation
18    Half,
19    /// Phrygian half cadence (↓v → V) — tense pause, dramatic interruption
20    Phrygian,
21}
22
23impl CadenceType {
24    /// Human-readable description of the cadence.
25    pub fn description(&self) -> &str {
26        match self {
27            Self::PerfectAuthentic => "Full resolution — task definitively complete",
28            Self::Plagal => "Gentle resolution — task complete with soft landing",
29            Self::Deceptive => "False resolution — task appears done but isn't",
30            Self::Half => "Suspension — task paused, awaiting continuation",
31            Self::Phrygian => "Dramatic pause — task interrupted with tension",
32        }
33    }
34
35    /// Is this cadence a final resolution?
36    pub fn is_resolved(&self) -> bool {
37        matches!(self, Self::PerfectAuthentic | Self::Plagal)
38    }
39
40    /// Resolution strength from 0.0 (no resolution) to 1.0 (complete).
41    pub fn resolution_strength(&self) -> f64 {
42        match self {
43            Self::PerfectAuthentic => 1.0,
44            Self::Plagal => 0.85,
45            Self::Deceptive => 0.3,
46            Self::Half => 0.1,
47            Self::Phrygian => 0.05,
48        }
49    }
50}
51
52/// Task progress mapped from cadence patterns.
53#[derive(Debug, Clone)]
54pub struct TaskProgress {
55    task_id: String,
56    progress: f64, // 0.0..1.0
57    cadence: Option<CadenceType>,
58}
59
60impl TaskProgress {
61    pub fn new(task_id: impl Into<String>) -> Self {
62        Self {
63            task_id: task_id.into(),
64            progress: 0.0,
65            cadence: None,
66        }
67    }
68
69    pub fn with_progress(task_id: impl Into<String>, progress: f64) -> Self {
70        Self {
71            task_id: task_id.into(),
72            progress: progress.clamp(0.0, 1.0),
73            cadence: None,
74        }
75    }
76
77    /// Map a progress value to the most likely cadence type.
78    pub fn detect_cadence(&mut self) -> CadenceType {
79        let cadence = if self.progress >= 0.98 {
80            CadenceType::PerfectAuthentic
81        } else if self.progress >= 0.90 {
82            CadenceType::Plagal
83        } else if self.progress >= 0.70 {
84            // Ambiguous zone — could be deceptive
85            if self.progress >= 0.80 {
86                CadenceType::Deceptive
87            } else {
88                CadenceType::Half
89            }
90        } else if self.progress >= 0.40 {
91            CadenceType::Half
92        } else {
93            CadenceType::Phrygian
94        };
95        self.cadence = Some(cadence);
96        cadence
97    }
98
99    pub fn set_progress(&mut self, p: f64) {
100        self.progress = p.clamp(0.0, 1.0);
101    }
102
103    pub fn progress(&self) -> f64 {
104        self.progress
105    }
106
107    pub fn cadence(&self) -> Option<CadenceType> {
108        self.cadence
109    }
110
111    pub fn task_id(&self) -> &str {
112        &self.task_id
113    }
114
115    pub fn is_complete(&self) -> bool {
116        self.cadence.map_or(false, |c| c.is_resolved())
117    }
118}
119
120/// Signal when a group of agents reaches a cadence point.
121#[derive(Debug, Clone)]
122pub struct CompletionSignal {
123    /// The cadence type reached
124    pub cadence: CadenceType,
125    /// Agents that contributed to the signal
126    pub agents: Vec<String>,
127    /// Overall completion percentage
128    pub overall_completion: f64,
129    /// Timestamp or step number
130    pub step: u64,
131}
132
133impl CompletionSignal {
134    /// Detect completion signals from a group of task progresses.
135    pub fn detect_from_group(tasks: &[TaskProgress], step: u64) -> Option<Self> {
136        if tasks.is_empty() {
137            return None;
138        }
139
140        let total: f64 = tasks.iter().map(|t| t.progress()).sum();
141        let avg = total / tasks.len() as f64;
142
143        // Determine group cadence based on average progress
144        let cadence = if avg >= 0.98 {
145            CadenceType::PerfectAuthentic
146        } else if avg >= 0.90 {
147            CadenceType::Plagal
148        } else if avg >= 0.75 {
149            CadenceType::Deceptive
150        } else if avg >= 0.40 {
151            CadenceType::Half
152        } else {
153            CadenceType::Phrygian
154        };
155
156        // Only emit signal for meaningful cadences
157        if matches!(cadence, CadenceType::Phrygian) && avg < 0.1 {
158            return None;
159        }
160
161        let agents: Vec<String> = tasks.iter().map(|t| t.task_id().to_string()).collect();
162
163        Some(Self {
164            cadence,
165            agents,
166            overall_completion: avg,
167            step,
168        })
169    }
170
171    /// Is this a final completion signal?
172    pub fn is_final(&self) -> bool {
173        self.cadence.is_resolved()
174    }
175}
176
177/// Detects when tasks look done but aren't (deceptive cadence).
178#[derive(Debug, Clone)]
179pub struct DeceptiveResolution {
180    /// Tasks that appeared complete but regressed
181    regressions: Vec<RegressionEvent>,
182    /// Threshold for detecting a regression
183    regression_threshold: f64,
184}
185
186#[derive(Debug, Clone)]
187struct RegressionEvent {
188    task_id: String,
189    apparent_progress: f64,
190    actual_progress: f64,
191    step: u64,
192}
193
194impl DeceptiveResolution {
195    pub fn new(regression_threshold: f64) -> Self {
196        Self {
197            regressions: Vec::new(),
198            regression_threshold,
199        }
200    }
201
202    /// Track progress and detect if a task regresses (deceptive resolution).
203    pub fn track(&mut self, task_id: impl Into<String>, previous: f64, current: f64, step: u64) -> bool {
204        let regression = previous - current;
205        if regression > self.regression_threshold {
206            self.regressions.push(RegressionEvent {
207                task_id: task_id.into(),
208                apparent_progress: previous,
209                actual_progress: current,
210                step,
211            });
212            true
213        } else {
214            false
215        }
216    }
217
218    /// Number of regressions detected.
219    pub fn regression_count(&self) -> usize {
220        self.regressions.len()
221    }
222
223    /// Get all tasks that had deceptive resolutions.
224    pub fn deceptive_tasks(&self) -> Vec<&str> {
225        self.regressions.iter().map(|r| r.task_id.as_str()).collect()
226    }
227
228    /// Check if a task had a deceptive resolution.
229    pub fn is_deceptive(&self, task_id: &str) -> bool {
230        self.regressions.iter().any(|r| r.task_id == task_id)
231    }
232}
233
234/// Chord progressions mapped to task progressions.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236pub enum ChordFunction {
237    Tonic,      // I — home base, stable
238    Supertonic, // ii — pre-dominant, moving away
239    Mediant,    // iii — ambiguous
240    Subdominant,// IV — pre-dominant, building tension
241    Dominant,   // V — tension, expecting resolution
242    Submediant, // vi — relative minor, deceptive resolution
243    Leading,    // vii° — diminished, highly unstable
244}
245
246impl ChordFunction {
247    /// Stability of the chord function (0.0 = unstable, 1.0 = stable).
248    pub fn stability(&self) -> f64 {
249        match self {
250            Self::Tonic => 1.0,
251            Self::Submediant => 0.7,
252            Self::Subdominant => 0.5,
253            Self::Mediant => 0.4,
254            Self::Supertonic => 0.3,
255            Self::Dominant => 0.15,
256            Self::Leading => 0.05,
257        }
258    }
259
260    /// Tension level (inverse of stability).
261    pub fn tension(&self) -> f64 {
262        1.0 - self.stability()
263    }
264}
265
266/// Tracker that models task progression as a chord progression.
267#[derive(Debug, Clone)]
268pub struct ProgressionTracker {
269    /// The progression of chord functions for each step
270    progression: Vec<ChordFunction>,
271    /// Current position in the progression
272    position: usize,
273}
274
275impl ProgressionTracker {
276    pub fn new() -> Self {
277        Self {
278            progression: Vec::new(),
279            position: 0,
280        }
281    }
282
283    /// Create a standard I-IV-V-I progression (common task flow).
284    pub fn standard() -> Self {
285        Self {
286            progression: vec![
287                ChordFunction::Tonic,      // Start: task begins
288                ChordFunction::Supertonic,  // Setup: gathering resources
289                ChordFunction::Subdominant, // Build: working
290                ChordFunction::Dominant,    // Climax: approaching completion
291                ChordFunction::Tonic,       // Resolve: task complete
292            ],
293            position: 0,
294        }
295    }
296
297    /// Create a deceptive progression (I-IV-V-vi).
298    pub fn deceptive() -> Self {
299        Self {
300            progression: vec![
301                ChordFunction::Tonic,
302                ChordFunction::Subdominant,
303                ChordFunction::Dominant,
304                ChordFunction::Submediant, // Deceptive!
305                ChordFunction::Subdominant, // Try again
306                ChordFunction::Dominant,
307                ChordFunction::Tonic,       // Finally resolve
308            ],
309            position: 0,
310        }
311    }
312
313    /// Advance to the next step in the progression.
314    pub fn advance(&mut self) -> Option<ChordFunction> {
315        if self.position < self.progression.len() {
316            let chord = self.progression[self.position];
317            self.position += 1;
318            Some(chord)
319        } else {
320            None
321        }
322    }
323
324    /// Get the current chord function.
325    pub fn current(&self) -> Option<&ChordFunction> {
326        self.progression.get(self.position.saturating_sub(1))
327    }
328
329    /// Detect the cadence type from the last two chords.
330    pub fn detect_cadence(&self) -> Option<CadenceType> {
331        if self.position < 2 {
332            return None;
333        }
334        let prev = self.progression[self.position - 2];
335        let curr = self.progression[self.position - 1];
336        Some(match (prev, curr) {
337            (ChordFunction::Dominant, ChordFunction::Tonic) => CadenceType::PerfectAuthentic,
338            (ChordFunction::Subdominant, ChordFunction::Tonic) => CadenceType::Plagal,
339            (ChordFunction::Dominant, ChordFunction::Submediant) => CadenceType::Deceptive,
340            (ChordFunction::Supertonic, ChordFunction::Dominant) => CadenceType::Phrygian,
341            (_, ChordFunction::Dominant) => CadenceType::Half,
342            _ => CadenceType::Half, // Default to half for unclear endings
343        })
344    }
345
346    /// Current tension level of the progression.
347    pub fn tension(&self) -> f64 {
348        self.current().map(|c| c.tension()).unwrap_or(0.0)
349    }
350
351    /// Progress through the progression as a ratio.
352    pub fn progress_ratio(&self) -> f64 {
353        if self.progression.is_empty() {
354            0.0
355        } else {
356            self.position as f64 / self.progression.len() as f64
357        }
358    }
359
360    pub fn is_complete(&self) -> bool {
361        self.position >= self.progression.len()
362    }
363
364    pub fn progression(&self) -> &[ChordFunction] {
365        &self.progression
366    }
367}
368
369/// A multi-agent cadence coordinator.
370#[derive(Debug, Clone)]
371pub struct CadenceCoordinator {
372    tasks: HashMap<String, TaskProgress>,
373    tracker: ProgressionTracker,
374}
375
376impl CadenceCoordinator {
377    pub fn new(tracker: ProgressionTracker) -> Self {
378        Self {
379            tasks: HashMap::new(),
380            tracker,
381        }
382    }
383
384    pub fn add_task(&mut self, task: TaskProgress) {
385        self.tasks.insert(task.task_id().to_string(), task);
386    }
387
388    pub fn update_task(&mut self, task_id: &str, progress: f64) {
389        if let Some(task) = self.tasks.get_mut(task_id) {
390            task.set_progress(progress);
391        }
392    }
393
394    /// Check for group cadence at the current step.
395    pub fn check_cadence(&mut self, step: u64) -> Option<CompletionSignal> {
396        let tasks: Vec<TaskProgress> = self.tasks.values().cloned().collect();
397        CompletionSignal::detect_from_group(&tasks, step)
398    }
399
400    pub fn task_count(&self) -> usize {
401        self.tasks.len()
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn test_cadence_type_properties() {
411        assert!(CadenceType::PerfectAuthentic.is_resolved());
412        assert!(CadenceType::Plagal.is_resolved());
413        assert!(!CadenceType::Deceptive.is_resolved());
414        assert!(!CadenceType::Half.is_resolved());
415        assert!(!CadenceType::Phrygian.is_resolved());
416
417        assert!(CadenceType::PerfectAuthentic.resolution_strength() > CadenceType::Plagal.resolution_strength());
418        assert!(CadenceType::Plagal.resolution_strength() > CadenceType::Deceptive.resolution_strength());
419    }
420
421    #[test]
422    fn test_cadence_descriptions() {
423        for ct in [
424            CadenceType::PerfectAuthentic,
425            CadenceType::Plagal,
426            CadenceType::Deceptive,
427            CadenceType::Half,
428            CadenceType::Phrygian,
429        ] {
430            assert!(!ct.description().is_empty());
431        }
432    }
433
434    #[test]
435    fn test_task_progress_detect_cadence() {
436        let mut tp = TaskProgress::with_progress("t1", 0.99);
437        assert_eq!(tp.detect_cadence(), CadenceType::PerfectAuthentic);
438
439        let mut tp = TaskProgress::with_progress("t2", 0.92);
440        assert_eq!(tp.detect_cadence(), CadenceType::Plagal);
441
442        let mut tp = TaskProgress::with_progress("t3", 0.83);
443        assert_eq!(tp.detect_cadence(), CadenceType::Deceptive);
444
445        let mut tp = TaskProgress::with_progress("t4", 0.50);
446        assert_eq!(tp.detect_cadence(), CadenceType::Half);
447
448        let mut tp = TaskProgress::with_progress("t5", 0.10);
449        assert_eq!(tp.detect_cadence(), CadenceType::Phrygian);
450    }
451
452    #[test]
453    fn test_task_progress_completion() {
454        let mut tp = TaskProgress::new("t1");
455        assert!(!tp.is_complete());
456        tp.set_progress(1.0);
457        tp.detect_cadence();
458        assert!(tp.is_complete());
459    }
460
461    #[test]
462    fn test_task_progress_clamp() {
463        let tp = TaskProgress::with_progress("t1", 1.5);
464        assert!((tp.progress() - 1.0).abs() < 1e-9);
465        let tp = TaskProgress::with_progress("t2", -0.5);
466        assert!((tp.progress()).abs() < 1e-9);
467    }
468
469    #[test]
470    fn test_completion_signal_from_group() {
471        let tasks = vec![
472            TaskProgress::with_progress("a", 0.99),
473            TaskProgress::with_progress("b", 0.98),
474            TaskProgress::with_progress("c", 1.0),
475        ];
476        let signal = CompletionSignal::detect_from_group(&tasks, 1).unwrap();
477        assert!(signal.is_final());
478        assert_eq!(signal.agents.len(), 3);
479        assert!(signal.overall_completion > 0.95);
480    }
481
482    #[test]
483    fn test_completion_signal_deceptive() {
484        let tasks = vec![
485            TaskProgress::with_progress("a", 0.80),
486            TaskProgress::with_progress("b", 0.75),
487        ];
488        let signal = CompletionSignal::detect_from_group(&tasks, 1).unwrap();
489        assert_eq!(signal.cadence, CadenceType::Deceptive);
490    }
491
492    #[test]
493    fn test_completion_signal_empty() {
494        let tasks: Vec<TaskProgress> = vec![];
495        assert!(CompletionSignal::detect_from_group(&tasks, 1).is_none());
496    }
497
498    #[test]
499    fn test_deceptive_resolution_detection() {
500        let mut dr = DeceptiveResolution::new(0.05);
501        assert!(!dr.track("t1", 0.5, 0.6, 1)); // Progress, not regression
502        assert!(dr.track("t1", 0.6, 0.3, 2));  // Big regression!
503        assert_eq!(dr.regression_count(), 1);
504        assert!(dr.is_deceptive("t1"));
505        assert!(!dr.is_deceptive("t2"));
506    }
507
508    #[test]
509    fn test_deceptive_resolution_small_regression() {
510        let mut dr = DeceptiveResolution::new(0.2);
511        assert!(!dr.track("t1", 0.8, 0.7, 1)); // Small regression below threshold
512        assert_eq!(dr.regression_count(), 0);
513    }
514
515    #[test]
516    fn test_deceptive_tasks_list() {
517        let mut dr = DeceptiveResolution::new(0.05);
518        dr.track("t1", 0.9, 0.5, 1);
519        dr.track("t2", 0.8, 0.4, 2);
520        let tasks = dr.deceptive_tasks();
521        assert_eq!(tasks.len(), 2);
522    }
523
524    #[test]
525    fn test_chord_function_stability() {
526        assert!((ChordFunction::Tonic.stability() - 1.0).abs() < 1e-9);
527        assert!((ChordFunction::Leading.stability() - 0.05).abs() < 1e-9);
528        assert!((ChordFunction::Dominant.tension() - 0.85).abs() < 1e-9);
529    }
530
531    #[test]
532    fn test_progression_tracker_standard() {
533        let mut tracker = ProgressionTracker::standard();
534        assert_eq!(tracker.progression().len(), 5);
535
536        let c1 = tracker.advance();
537        assert_eq!(c1, Some(ChordFunction::Tonic));
538        assert!(!tracker.is_complete());
539
540        tracker.advance(); // Supertonic
541        tracker.advance(); // Subdominant
542        tracker.advance(); // Dominant
543        let c5 = tracker.advance(); // Tonic
544        assert_eq!(c5, Some(ChordFunction::Tonic));
545        assert!(tracker.is_complete());
546        assert!(tracker.advance().is_none());
547    }
548
549    #[test]
550    fn test_progression_tracker_cadence_detection() {
551        let mut tracker = ProgressionTracker::standard();
552        tracker.advance(); // I
553        tracker.advance(); // ii
554        tracker.advance(); // IV
555        tracker.advance(); // V
556        assert_eq!(tracker.detect_cadence(), Some(CadenceType::Half)); // IV->V = half
557        tracker.advance(); // I
558        assert_eq!(tracker.detect_cadence(), Some(CadenceType::PerfectAuthentic));
559    }
560
561    #[test]
562    fn test_progression_tracker_deceptive() {
563        let mut tracker = ProgressionTracker::deceptive();
564        tracker.advance(); // I
565        tracker.advance(); // IV
566        tracker.advance(); // V
567        tracker.advance(); // vi — deceptive!
568        assert_eq!(tracker.detect_cadence(), Some(CadenceType::Deceptive));
569    }
570
571    #[test]
572    fn test_progression_tracker_tension() {
573        let mut tracker = ProgressionTracker::standard();
574        tracker.advance(); // Tonic
575        assert!((tracker.tension() - 0.0).abs() < 1e-9);
576        tracker.advance(); // Supertonic
577        assert!(tracker.tension() > 0.0);
578    }
579
580    #[test]
581    fn test_progression_tracker_progress_ratio() {
582        let mut tracker = ProgressionTracker::new();
583        assert!((tracker.progress_ratio()).abs() < 1e-9);
584        tracker.progression.push(ChordFunction::Tonic);
585        tracker.progression.push(ChordFunction::Dominant);
586        tracker.advance();
587        assert!((tracker.progress_ratio() - 0.5).abs() < 1e-9);
588    }
589
590    #[test]
591    fn test_cadence_coordinator() {
592        let tracker = ProgressionTracker::standard();
593        let mut coord = CadenceCoordinator::new(tracker);
594        coord.add_task(TaskProgress::with_progress("a", 0.0));
595        coord.add_task(TaskProgress::with_progress("b", 0.0));
596        assert_eq!(coord.task_count(), 2);
597
598        coord.update_task("a", 1.0);
599        coord.update_task("b", 1.0);
600        let signal = coord.check_cadence(1).unwrap();
601        assert!(signal.is_final());
602    }
603}