1use std::collections::VecDeque;
13use std::sync::Arc;
14use std::sync::atomic::{AtomicU32, Ordering};
15use std::time::{Duration, Instant};
16
17use gemini_genai_rs::session::{SessionEvent, SessionWriter};
18
19use super::BoxFuture;
20use crate::state::State;
21
22pub trait PatternDetector: Send + Sync {
30 fn check(&self, state: &State, event: Option<&SessionEvent>, now: Instant) -> bool;
36
37 fn reset(&self);
39
40 fn needs_timer(&self) -> bool {
45 false
46 }
47}
48
49pub struct TemporalPattern {
53 pub name: String,
55 pub detector: Box<dyn PatternDetector>,
57 pub action: super::SessionHook,
60 pub cooldown: Option<Duration>,
62 last_triggered: parking_lot::Mutex<Option<Instant>>,
64}
65
66impl TemporalPattern {
67 pub fn new(
69 name: impl Into<String>,
70 detector: Box<dyn PatternDetector>,
71 action: super::SessionHook,
72 cooldown: Option<Duration>,
73 ) -> Self {
74 Self {
75 name: name.into(),
76 detector,
77 action,
78 cooldown,
79 last_triggered: parking_lot::Mutex::new(None),
80 }
81 }
82
83 fn try_fire(
85 &self,
86 state: &State,
87 event: Option<&SessionEvent>,
88 writer: &Arc<dyn SessionWriter>,
89 now: Instant,
90 ) -> Option<BoxFuture<()>> {
91 if !pattern_fires(
92 self.detector.as_ref(),
93 state,
94 event,
95 now,
96 self.cooldown,
97 &mut self.last_triggered.lock(),
98 ) {
99 return None;
100 }
101
102 let s = state.clone();
103 let w = writer.clone();
104 Some((self.action)(s, w))
105 }
106}
107
108pub(crate) fn pattern_fires(
109 detector: &dyn PatternDetector,
110 state: &State,
111 event: Option<&SessionEvent>,
112 now: Instant,
113 cooldown: Option<Duration>,
114 last: &mut Option<Instant>,
115) -> bool {
116 if !detector.check(state, event, now) {
117 return false;
118 }
119 if let (Some(cooldown), Some(previous)) = (cooldown, *last)
120 && now.saturating_duration_since(previous) < cooldown
121 {
122 return false;
123 }
124 *last = Some(now);
125 true
126}
127
128pub struct TemporalRegistry {
132 patterns: Vec<TemporalPattern>,
133}
134
135impl Default for TemporalRegistry {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl TemporalRegistry {
142 pub fn new() -> Self {
144 Self {
145 patterns: Vec::new(),
146 }
147 }
148
149 pub fn is_empty(&self) -> bool {
151 self.patterns.is_empty()
152 }
153
154 pub fn register(&mut self, pattern: TemporalPattern) {
156 self.patterns.push(pattern);
157 }
158
159 pub fn check_all(
164 &self,
165 state: &State,
166 event: Option<&SessionEvent>,
167 writer: &Arc<dyn SessionWriter>,
168 ) -> Vec<BoxFuture<()>> {
169 let now = state.clock().now();
170 self.patterns
171 .iter()
172 .filter_map(|p| p.try_fire(state, event, writer, now))
173 .collect()
174 }
175
176 pub fn needs_timer(&self) -> bool {
179 self.patterns.iter().any(|p| p.detector.needs_timer())
180 }
181}
182
183pub struct SustainedDetector {
196 condition: Arc<dyn Fn(&State) -> bool + Send + Sync>,
197 duration: Duration,
198 became_true_at: parking_lot::Mutex<Option<Instant>>,
199}
200
201impl SustainedDetector {
202 pub fn new(condition: Arc<dyn Fn(&State) -> bool + Send + Sync>, duration: Duration) -> Self {
207 Self {
208 condition,
209 duration,
210 became_true_at: parking_lot::Mutex::new(None),
211 }
212 }
213}
214
215impl PatternDetector for SustainedDetector {
216 fn check(&self, state: &State, _event: Option<&SessionEvent>, now: Instant) -> bool {
217 if (self.condition)(state) {
218 let mut guard = self.became_true_at.lock();
219 match *guard {
220 None => {
221 *guard = Some(now);
222 false
223 }
224 Some(t) => now.duration_since(t) >= self.duration,
225 }
226 } else {
227 *self.became_true_at.lock() = None;
228 false
229 }
230 }
231
232 fn reset(&self) {
233 *self.became_true_at.lock() = None;
234 }
235
236 fn needs_timer(&self) -> bool {
237 true
238 }
239}
240
241pub struct RateDetector {
251 filter: Arc<dyn Fn(&SessionEvent) -> bool + Send + Sync>,
252 count: u32,
253 window: Duration,
254 timestamps: parking_lot::Mutex<VecDeque<Instant>>,
255}
256
257impl RateDetector {
258 pub fn new(
264 filter: Arc<dyn Fn(&SessionEvent) -> bool + Send + Sync>,
265 count: u32,
266 window: Duration,
267 ) -> Self {
268 Self {
269 filter,
270 count,
271 window,
272 timestamps: parking_lot::Mutex::new(VecDeque::new()),
273 }
274 }
275}
276
277impl PatternDetector for RateDetector {
278 fn check(&self, _state: &State, event: Option<&SessionEvent>, now: Instant) -> bool {
279 let mut ts = self.timestamps.lock();
280
281 if let Some(evt) = event
283 && (self.filter)(evt)
284 {
285 ts.push_back(now);
286 }
287
288 while let Some(&front) = ts.front() {
290 if now.duration_since(front) > self.window {
291 ts.pop_front();
292 } else {
293 break;
294 }
295 }
296
297 ts.len() as u32 >= self.count
298 }
299
300 fn reset(&self) {
301 self.timestamps.lock().clear();
302 }
303
304 }
306
307pub struct TurnCountDetector {
315 condition: Arc<dyn Fn(&State) -> bool + Send + Sync>,
316 required: u32,
317 consecutive: AtomicU32,
318}
319
320impl TurnCountDetector {
321 pub fn new(condition: Arc<dyn Fn(&State) -> bool + Send + Sync>, required: u32) -> Self {
326 Self {
327 condition,
328 required,
329 consecutive: AtomicU32::new(0),
330 }
331 }
332}
333
334impl PatternDetector for TurnCountDetector {
335 fn check(&self, state: &State, _event: Option<&SessionEvent>, _now: Instant) -> bool {
336 if (self.condition)(state) {
337 let prev = self.consecutive.fetch_add(1, Ordering::SeqCst);
338 prev + 1 >= self.required
339 } else {
340 self.consecutive.store(0, Ordering::SeqCst);
341 false
342 }
343 }
344
345 fn reset(&self) {
346 self.consecutive.store(0, Ordering::SeqCst);
347 }
348}
349
350pub struct ConsecutiveFailureDetector {
358 tool_name: String,
359 threshold: u32,
360 consecutive: AtomicU32,
361}
362
363impl ConsecutiveFailureDetector {
364 pub fn new(tool_name: impl Into<String>, threshold: u32) -> Self {
369 Self {
370 tool_name: tool_name.into(),
371 threshold,
372 consecutive: AtomicU32::new(0),
373 }
374 }
375}
376
377impl PatternDetector for ConsecutiveFailureDetector {
378 fn check(&self, state: &State, _event: Option<&SessionEvent>, _now: Instant) -> bool {
379 let key = format!("bg:{}_failed", self.tool_name);
380 let failed: bool = state.get(&key).unwrap_or(false);
381
382 if failed {
383 let prev = self.consecutive.fetch_add(1, Ordering::SeqCst);
384 prev + 1 >= self.threshold
385 } else {
386 self.consecutive.store(0, Ordering::SeqCst);
387 false
388 }
389 }
390
391 fn reset(&self) {
392 self.consecutive.store(0, Ordering::SeqCst);
393 }
394}
395
396#[cfg(test)]
399mod tests {
400 use super::*;
401 use std::sync::atomic::{AtomicU32, Ordering};
402
403 struct MockWriter;
405
406 #[async_trait::async_trait]
407 impl SessionWriter for MockWriter {
408 async fn send_audio(
409 &self,
410 _: bytes::Bytes,
411 ) -> Result<(), gemini_genai_rs::session::SessionError> {
412 Ok(())
413 }
414 async fn send_text(&self, _: String) -> Result<(), gemini_genai_rs::session::SessionError> {
415 Ok(())
416 }
417 async fn send_tool_response(
418 &self,
419 _: Vec<gemini_genai_rs::protocol::FunctionResponse>,
420 ) -> Result<(), gemini_genai_rs::session::SessionError> {
421 Ok(())
422 }
423 async fn send_client_content(
424 &self,
425 _: Vec<gemini_genai_rs::protocol::Content>,
426 _: bool,
427 ) -> Result<(), gemini_genai_rs::session::SessionError> {
428 Ok(())
429 }
430 async fn send_video(
431 &self,
432 _: bytes::Bytes,
433 ) -> Result<(), gemini_genai_rs::session::SessionError> {
434 Ok(())
435 }
436 async fn update_instruction(
437 &self,
438 _: String,
439 ) -> Result<(), gemini_genai_rs::session::SessionError> {
440 Ok(())
441 }
442 async fn signal_activity_start(
443 &self,
444 ) -> Result<(), gemini_genai_rs::session::SessionError> {
445 Ok(())
446 }
447 async fn signal_activity_end(&self) -> Result<(), gemini_genai_rs::session::SessionError> {
448 Ok(())
449 }
450 async fn disconnect(&self) -> Result<(), gemini_genai_rs::session::SessionError> {
451 Ok(())
452 }
453 }
454
455 fn mock_writer() -> Arc<dyn SessionWriter> {
456 Arc::new(MockWriter)
457 }
458
459 fn counting_action(counter: Arc<AtomicU32>) -> crate::live::SessionHook {
461 Arc::new(move |_state, _writer| {
462 let c = counter.clone();
463 Box::pin(async move {
464 c.fetch_add(1, Ordering::SeqCst);
465 })
466 })
467 }
468
469 #[test]
472 fn sustained_fires_after_duration() {
473 let state = State::new();
474 let _ = state.set("hot", true);
475
476 let detector = SustainedDetector::new(
477 Arc::new(|s: &State| s.get::<bool>("hot").unwrap_or(false)),
478 Duration::from_secs(5),
479 );
480
481 let t0 = Instant::now();
482
483 assert!(!detector.check(&state, None, t0));
485
486 assert!(!detector.check(&state, None, t0 + Duration::from_secs(3)));
488
489 assert!(detector.check(&state, None, t0 + Duration::from_secs(5)));
491
492 assert!(detector.check(&state, None, t0 + Duration::from_secs(6)));
494 }
495
496 #[test]
499 fn sustained_resets_on_false() {
500 let state = State::new();
501 let _ = state.set("hot", true);
502
503 let detector = SustainedDetector::new(
504 Arc::new(|s: &State| s.get::<bool>("hot").unwrap_or(false)),
505 Duration::from_secs(5),
506 );
507
508 let t0 = Instant::now();
509
510 assert!(!detector.check(&state, None, t0));
512
513 let _ = state.set("hot", false);
515 assert!(!detector.check(&state, None, t0 + Duration::from_secs(2)));
516
517 let _ = state.set("hot", true);
519 assert!(!detector.check(&state, None, t0 + Duration::from_secs(3)));
520
521 assert!(!detector.check(&state, None, t0 + Duration::from_secs(7)));
523
524 assert!(detector.check(&state, None, t0 + Duration::from_secs(8)));
526 }
527
528 #[test]
531 fn sustained_reset_clears_state() {
532 let state = State::new();
533 let _ = state.set("hot", true);
534
535 let detector = SustainedDetector::new(
536 Arc::new(|s: &State| s.get::<bool>("hot").unwrap_or(false)),
537 Duration::from_secs(5),
538 );
539
540 let t0 = Instant::now();
541
542 assert!(!detector.check(&state, None, t0));
544
545 detector.reset();
547
548 assert!(!detector.check(&state, None, t0 + Duration::from_secs(4)));
550 assert!(detector.check(&state, None, t0 + Duration::from_secs(9)));
551 }
552
553 #[test]
556 fn rate_fires_when_count_reached() {
557 let state = State::new();
558 let detector = RateDetector::new(
559 Arc::new(|evt: &SessionEvent| matches!(evt, SessionEvent::TurnComplete)),
560 3,
561 Duration::from_secs(10),
562 );
563
564 let t0 = Instant::now();
565 let event = SessionEvent::TurnComplete;
566
567 assert!(!detector.check(&state, Some(&event), t0));
568 assert!(!detector.check(&state, Some(&event), t0 + Duration::from_secs(1)));
569 assert!(detector.check(&state, Some(&event), t0 + Duration::from_secs(2)));
571 }
572
573 #[test]
576 fn rate_does_not_fire_when_events_outside_window() {
577 let state = State::new();
578 let detector = RateDetector::new(
579 Arc::new(|evt: &SessionEvent| matches!(evt, SessionEvent::TurnComplete)),
580 3,
581 Duration::from_secs(5),
582 );
583
584 let t0 = Instant::now();
585 let event = SessionEvent::TurnComplete;
586
587 assert!(!detector.check(&state, Some(&event), t0));
589 assert!(!detector.check(&state, Some(&event), t0 + Duration::from_secs(1)));
590
591 assert!(!detector.check(&state, Some(&event), t0 + Duration::from_secs(10)));
593 }
594
595 #[test]
598 fn rate_filter_rejects_events() {
599 let state = State::new();
600 let detector = RateDetector::new(
601 Arc::new(|evt: &SessionEvent| matches!(evt, SessionEvent::TurnComplete)),
602 2,
603 Duration::from_secs(10),
604 );
605
606 let t0 = Instant::now();
607
608 let text_event = SessionEvent::TextDelta("hello".to_string());
610 assert!(!detector.check(&state, Some(&text_event), t0));
611 assert!(!detector.check(&state, Some(&text_event), t0 + Duration::from_secs(1)));
612 assert!(!detector.check(&state, Some(&text_event), t0 + Duration::from_secs(2)));
613
614 assert!(!detector.check(&state, None, t0 + Duration::from_secs(3)));
616 }
617
618 #[test]
621 fn turn_count_fires_after_n_consecutive() {
622 let state = State::new();
623 let _ = state.set("confused", true);
624
625 let detector = TurnCountDetector::new(
626 Arc::new(|s: &State| s.get::<bool>("confused").unwrap_or(false)),
627 3,
628 );
629
630 let t0 = Instant::now();
631
632 assert!(!detector.check(&state, None, t0));
633 assert!(!detector.check(&state, None, t0));
634 assert!(detector.check(&state, None, t0));
636 }
637
638 #[test]
641 fn turn_count_resets_on_false() {
642 let state = State::new();
643 let _ = state.set("confused", true);
644
645 let detector = TurnCountDetector::new(
646 Arc::new(|s: &State| s.get::<bool>("confused").unwrap_or(false)),
647 3,
648 );
649
650 let t0 = Instant::now();
651
652 assert!(!detector.check(&state, None, t0));
653 assert!(!detector.check(&state, None, t0));
654
655 let _ = state.set("confused", false);
657 assert!(!detector.check(&state, None, t0));
658
659 let _ = state.set("confused", true);
661 assert!(!detector.check(&state, None, t0));
662 assert!(!detector.check(&state, None, t0));
663 assert!(detector.check(&state, None, t0));
664 }
665
666 #[test]
669 fn consecutive_failure_fires_after_threshold() {
670 let state = State::new();
671 let _ = state.set("bg:search_failed", true);
672
673 let detector = ConsecutiveFailureDetector::new("search", 3);
674
675 let t0 = Instant::now();
676
677 assert!(!detector.check(&state, None, t0));
678 assert!(!detector.check(&state, None, t0));
679 assert!(detector.check(&state, None, t0));
681 }
682
683 #[test]
686 fn consecutive_failure_resets_on_success() {
687 let state = State::new();
688 let _ = state.set("bg:search_failed", true);
689
690 let detector = ConsecutiveFailureDetector::new("search", 3);
691
692 let t0 = Instant::now();
693
694 assert!(!detector.check(&state, None, t0));
695 assert!(!detector.check(&state, None, t0));
696
697 let _ = state.set("bg:search_failed", false);
699 assert!(!detector.check(&state, None, t0));
700
701 let _ = state.set("bg:search_failed", true);
703 assert!(!detector.check(&state, None, t0));
704 assert!(!detector.check(&state, None, t0));
705 assert!(detector.check(&state, None, t0));
706 }
707
708 #[tokio::test]
711 async fn pattern_cooldown_prevents_rapid_refiring() {
712 let counter = Arc::new(AtomicU32::new(0));
713 let state = State::new();
714 let _ = state.set("active", true);
715 let writer = mock_writer();
716
717 let pattern = TemporalPattern::new(
718 "test-cooldown",
719 Box::new(SustainedDetector::new(
720 Arc::new(|s: &State| s.get::<bool>("active").unwrap_or(false)),
721 Duration::from_secs(0), )),
723 counting_action(counter.clone()),
724 Some(Duration::from_secs(10)), );
726
727 let t0 = Instant::now();
728
729 assert!(pattern.try_fire(&state, None, &writer, t0).is_none());
732
733 let fut = pattern.try_fire(&state, None, &writer, t0 + Duration::from_millis(1));
735 assert!(fut.is_some());
736 fut.unwrap().await;
737 assert_eq!(counter.load(Ordering::SeqCst), 1);
738
739 assert!(
741 pattern
742 .try_fire(&state, None, &writer, t0 + Duration::from_millis(2))
743 .is_none()
744 );
745
746 let fut = pattern.try_fire(&state, None, &writer, t0 + Duration::from_secs(11));
748 assert!(fut.is_some());
749 fut.unwrap().await;
750 assert_eq!(counter.load(Ordering::SeqCst), 2);
751 }
752
753 #[tokio::test]
756 async fn registry_check_all_returns_actions() {
757 let counter = Arc::new(AtomicU32::new(0));
758 let state = State::new();
759 let _ = state.set("confused", true);
760 let writer = mock_writer();
761
762 let mut registry = TemporalRegistry::new();
763
764 registry.register(TemporalPattern::new(
766 "confusion",
767 Box::new(TurnCountDetector::new(
768 Arc::new(|s: &State| s.get::<bool>("confused").unwrap_or(false)),
769 1,
770 )),
771 counting_action(counter.clone()),
772 None,
773 ));
774
775 let actions = registry.check_all(&state, None, &writer);
776 assert_eq!(actions.len(), 1);
777
778 for fut in actions {
779 fut.await;
780 }
781 assert_eq!(counter.load(Ordering::SeqCst), 1);
782 }
783
784 #[tokio::test]
787 async fn registry_reads_time_from_the_state_clock() {
788 let counter = Arc::new(AtomicU32::new(0));
789 let clock = Arc::new(crate::clock::ManualClock::new());
790 let state = State::new().with_clock(clock.clone());
791 let writer = mock_writer();
792
793 let mut registry = TemporalRegistry::new();
794 registry.register(TemporalPattern::new(
795 "sustained",
796 Box::new(SustainedDetector::new(
797 Arc::new(|_: &State| true),
798 Duration::from_secs(5),
799 )),
800 counting_action(counter.clone()),
801 None,
802 ));
803
804 assert!(registry.check_all(&state, None, &writer).is_empty());
805 clock.advance(Duration::from_secs(4));
806 assert!(registry.check_all(&state, None, &writer).is_empty());
807 clock.advance(Duration::from_secs(1));
808 let fired = registry.check_all(&state, None, &writer);
809 assert_eq!(fired.len(), 1, "fires once five clock seconds have passed");
810 for fut in fired {
811 fut.await;
812 }
813 assert_eq!(counter.load(Ordering::SeqCst), 1);
814 }
815
816 #[test]
819 fn needs_timer_true_with_sustained_detector() {
820 let counter = Arc::new(AtomicU32::new(0));
821 let mut registry = TemporalRegistry::new();
822
823 registry.register(TemporalPattern::new(
824 "sustained",
825 Box::new(SustainedDetector::new(
826 Arc::new(|_: &State| true),
827 Duration::from_secs(5),
828 )),
829 counting_action(counter),
830 None,
831 ));
832
833 assert!(registry.needs_timer());
834 }
835
836 #[test]
839 fn needs_timer_false_without_sustained_detector() {
840 let counter = Arc::new(AtomicU32::new(0));
841 let mut registry = TemporalRegistry::new();
842
843 registry.register(TemporalPattern::new(
844 "turn-count",
845 Box::new(TurnCountDetector::new(Arc::new(|_: &State| true), 3)),
846 counting_action(counter.clone()),
847 None,
848 ));
849
850 registry.register(TemporalPattern::new(
851 "rate",
852 Box::new(RateDetector::new(
853 Arc::new(|_: &SessionEvent| true),
854 5,
855 Duration::from_secs(10),
856 )),
857 counting_action(counter),
858 None,
859 ));
860
861 assert!(!registry.needs_timer());
862 }
863
864 #[test]
867 fn default_creates_empty_registry() {
868 let registry = TemporalRegistry::default();
869 assert!(!registry.needs_timer());
870 }
871
872 #[test]
875 fn rate_reset_clears_timestamps() {
876 let state = State::new();
877 let detector = RateDetector::new(
878 Arc::new(|evt: &SessionEvent| matches!(evt, SessionEvent::TurnComplete)),
879 2,
880 Duration::from_secs(10),
881 );
882
883 let t0 = Instant::now();
884 let event = SessionEvent::TurnComplete;
885
886 assert!(!detector.check(&state, Some(&event), t0));
887 detector.reset();
888 assert!(!detector.check(&state, Some(&event), t0 + Duration::from_secs(1)));
890 assert!(detector.check(&state, Some(&event), t0 + Duration::from_secs(2)));
892 }
893
894 #[test]
897 fn turn_count_reset_clears_counter() {
898 let state = State::new();
899 let _ = state.set("confused", true);
900
901 let detector = TurnCountDetector::new(
902 Arc::new(|s: &State| s.get::<bool>("confused").unwrap_or(false)),
903 3,
904 );
905
906 let t0 = Instant::now();
907
908 assert!(!detector.check(&state, None, t0));
909 assert!(!detector.check(&state, None, t0));
910 detector.reset();
913
914 assert!(!detector.check(&state, None, t0));
916 assert!(!detector.check(&state, None, t0));
917 assert!(detector.check(&state, None, t0));
918 }
919
920 #[test]
923 fn consecutive_failure_reset_clears_counter() {
924 let state = State::new();
925 let _ = state.set("bg:search_failed", true);
926
927 let detector = ConsecutiveFailureDetector::new("search", 3);
928 let t0 = Instant::now();
929
930 assert!(!detector.check(&state, None, t0));
931 assert!(!detector.check(&state, None, t0));
932
933 detector.reset();
934
935 assert!(!detector.check(&state, None, t0));
936 assert!(!detector.check(&state, None, t0));
937 assert!(detector.check(&state, None, t0));
938 }
939
940 #[test]
943 fn sustained_detector_needs_timer() {
944 let detector = SustainedDetector::new(Arc::new(|_: &State| true), Duration::from_secs(5));
945 assert!(detector.needs_timer());
946 }
947
948 #[test]
951 fn rate_detector_does_not_need_timer() {
952 let detector = RateDetector::new(
953 Arc::new(|_: &SessionEvent| true),
954 5,
955 Duration::from_secs(10),
956 );
957 assert!(!detector.needs_timer());
958 }
959
960 #[test]
963 fn turn_count_detector_does_not_need_timer() {
964 let detector = TurnCountDetector::new(Arc::new(|_: &State| true), 3);
965 assert!(!detector.needs_timer());
966 }
967
968 #[tokio::test]
971 async fn pattern_without_cooldown_fires_every_time() {
972 let counter = Arc::new(AtomicU32::new(0));
973 let state = State::new();
974 let _ = state.set("active", true);
975 let writer = mock_writer();
976
977 let pattern = TemporalPattern::new(
978 "no-cooldown",
979 Box::new(TurnCountDetector::new(
980 Arc::new(|s: &State| s.get::<bool>("active").unwrap_or(false)),
981 1,
982 )),
983 counting_action(counter.clone()),
984 None, );
986
987 let t0 = Instant::now();
988
989 for i in 0..5u32 {
990 let fut = pattern.try_fire(&state, None, &writer, t0 + Duration::from_millis(i as u64));
991 assert!(fut.is_some(), "should fire on iteration {i}");
992 fut.unwrap().await;
993 }
994
995 assert_eq!(counter.load(Ordering::SeqCst), 5);
996 }
997}