1use std::future::Future;
70use std::pin::Pin;
71use std::sync::Arc;
72
73use tokio_util::sync::CancellationToken;
74
75use crate::error::EngineError;
76use crate::traits::context::ContextBuilder;
77use crate::traits::processor::OutputProcessor;
78use crate::traits::thinker::Thinker;
79use crate::types::signal::Signal;
80
81trait ErasedThinker<Ctx, Out>: Send + Sync {
91 fn think_erased<'a>(
93 &'a self,
94 ctx: &'a Ctx,
95 ) -> Pin<Box<dyn Future<Output = Result<Out, EngineError>> + Send + 'a>>;
96}
97
98trait ErasedContextBuilder<Ctx>: Send + Sync {
103 fn build_erased<'a>(
105 &'a self,
106 ctx: Ctx,
107 ) -> Pin<Box<dyn Future<Output = Result<Ctx, EngineError>> + Send + 'a>>;
108}
109
110trait ErasedOutputProcessor<Ctx, Out>: Send + Sync {
115 fn process_erased<'a>(
117 &'a self,
118 output: &'a Out,
119 ctx: &'a mut Ctx,
120 ) -> Pin<Box<dyn Future<Output = Result<Signal, EngineError>> + Send + 'a>>;
121}
122
123impl<T, Ctx, Out> ErasedThinker<Ctx, Out> for T
126where
127 T: Thinker<Context = Ctx, Output = Out> + 'static,
128 Ctx: Send + Sync,
129 Out: Send,
130{
131 fn think_erased<'a>(
132 &'a self,
133 ctx: &'a Ctx,
134 ) -> Pin<Box<dyn Future<Output = Result<Out, EngineError>> + Send + 'a>> {
135 Box::pin(T::think(self, ctx))
136 }
137}
138
139impl<T, Ctx> ErasedContextBuilder<Ctx> for T
140where
141 T: ContextBuilder<Context = Ctx> + 'static,
142 Ctx: Send,
143{
144 fn build_erased<'a>(
145 &'a self,
146 ctx: Ctx,
147 ) -> Pin<Box<dyn Future<Output = Result<Ctx, EngineError>> + Send + 'a>> {
148 Box::pin(T::build(self, ctx))
149 }
150}
151
152impl<T, Ctx, Out> ErasedOutputProcessor<Ctx, Out> for T
153where
154 T: OutputProcessor<Context = Ctx, Output = Out> + 'static,
155 Ctx: Send,
156 Out: Sync,
157{
158 fn process_erased<'a>(
159 &'a self,
160 output: &'a Out,
161 ctx: &'a mut Ctx,
162 ) -> Pin<Box<dyn Future<Output = Result<Signal, EngineError>> + Send + 'a>> {
163 Box::pin(T::process(self, output, ctx))
164 }
165}
166
167pub struct DynThinker<Ctx, Out>(Arc<dyn ErasedThinker<Ctx, Out>>);
175
176pub struct DynContextBuilder<Ctx>(Arc<dyn ErasedContextBuilder<Ctx>>);
180
181pub struct DynOutputProcessor<Ctx, Out>(Arc<dyn ErasedOutputProcessor<Ctx, Out>>);
185
186impl<Ctx, Out> DynThinker<Ctx, Out> {
189 pub fn new<T>(thinker: T) -> Self
193 where
194 T: Thinker<Context = Ctx, Output = Out> + 'static,
195 Ctx: Send + Sync,
196 Out: Send,
197 {
198 DynThinker(Arc::new(thinker))
199 }
200}
201
202impl<Ctx> DynContextBuilder<Ctx> {
203 pub fn new<T>(builder: T) -> Self
207 where
208 T: ContextBuilder<Context = Ctx> + 'static,
209 Ctx: Send,
210 {
211 DynContextBuilder(Arc::new(builder))
212 }
213}
214
215impl<Ctx, Out> DynOutputProcessor<Ctx, Out> {
216 pub fn new<T>(processor: T) -> Self
220 where
221 T: OutputProcessor<Context = Ctx, Output = Out> + 'static,
222 Ctx: Send,
223 Out: Sync,
224 {
225 DynOutputProcessor(Arc::new(processor))
226 }
227}
228
229impl<Ctx, Out> Thinker for DynThinker<Ctx, Out>
232where
233 Ctx: Sync,
234{
235 type Context = Ctx;
236 type Output = Out;
237
238 async fn think(&self, ctx: &Self::Context) -> Result<Self::Output, EngineError> {
239 self.0.think_erased(ctx).await
240 }
241}
242
243impl<Ctx> ContextBuilder for DynContextBuilder<Ctx>
244where
245 Ctx: Send,
246{
247 type Context = Ctx;
248
249 async fn build(&self, ctx: Self::Context) -> Result<Self::Context, EngineError> {
250 self.0.build_erased(ctx).await
251 }
252}
253
254impl<Ctx, Out> OutputProcessor for DynOutputProcessor<Ctx, Out>
255where
256 Ctx: Send,
257 Out: Sync,
258{
259 type Context = Ctx;
260 type Output = Out;
261
262 async fn process(
263 &self,
264 output: &Self::Output,
265 ctx: &mut Self::Context,
266 ) -> Result<Signal, EngineError> {
267 self.0.process_erased(output, ctx).await
268 }
269}
270
271#[derive(Debug, Clone)]
281pub struct TurnResult<Out> {
282 pub output: Out,
284 pub stopped: bool,
286}
287
288pub struct CoreEngine<Ctx, Out> {
298 context_builders: Vec<DynContextBuilder<Ctx>>,
299 thinker: DynThinker<Ctx, Out>,
300 output_processors: Vec<DynOutputProcessor<Ctx, Out>>,
301 cancel: CancellationToken,
302}
303
304impl<Ctx, Out> CoreEngine<Ctx, Out>
305where
306 Ctx: Clone + Send + Sync,
307 Out: Send + Sync,
308{
309 pub async fn run(&self, initial: Ctx) -> Result<Ctx, EngineError> {
340 let (ctx, _last) = self.run_with_output(initial).await?;
341 Ok(ctx)
342 }
343
344 pub async fn run_with_output(&self, initial: Ctx) -> Result<(Ctx, Option<Out>), EngineError> {
357 let mut ctx = initial;
358 let mut last_output: Option<Out> = None;
359
360 loop {
361 if self.cancel.is_cancelled() {
362 return Err(EngineError::Cancelled);
363 }
364
365 for builder in &self.context_builders {
366 ctx = builder.build(ctx).await?;
367 }
368
369 let output = self.thinker.think(&ctx).await?;
370
371 if self.output_processors.is_empty() {
372 last_output = Some(output);
373 break;
374 }
375
376 let mut stop = false;
377 for processor in &self.output_processors {
378 match processor.process(&output, &mut ctx).await? {
379 Signal::Stop => {
380 stop = true;
381 break;
382 }
383 Signal::Continue => {}
384 }
385 }
386
387 last_output = Some(output);
388
389 if stop {
390 break;
391 }
392 }
393
394 Ok((ctx, last_output))
395 }
396
397 pub async fn run_once(&self, ctx: &mut Ctx) -> Result<TurnResult<Out>, EngineError> {
416 if self.cancel.is_cancelled() {
417 return Err(EngineError::Cancelled);
418 }
419
420 for builder in &self.context_builders {
421 let next = builder.build(ctx.clone()).await?;
422 *ctx = next;
423 }
424
425 let output = self.thinker.think(ctx).await?;
426
427 let mut stopped = self.output_processors.is_empty();
429 for processor in &self.output_processors {
430 match processor.process(&output, ctx).await? {
431 Signal::Stop => {
432 stopped = true;
433 break;
434 }
435 Signal::Continue => {}
436 }
437 }
438
439 Ok(TurnResult { output, stopped })
440 }
441
442 pub fn cancel_handle(&self) -> CancellationToken {
448 self.cancel.clone()
449 }
450}
451
452pub struct EngineBuilder<Ctx, Out> {
480 thinker: Option<DynThinker<Ctx, Out>>,
481 context_builders: Vec<DynContextBuilder<Ctx>>,
482 output_processors: Vec<DynOutputProcessor<Ctx, Out>>,
483 cancel: CancellationToken,
484}
485
486impl<Ctx, Out> Default for EngineBuilder<Ctx, Out> {
487 fn default() -> Self {
488 Self::new()
489 }
490}
491
492impl<Ctx, Out> EngineBuilder<Ctx, Out> {
493 pub fn new() -> Self {
498 EngineBuilder {
499 thinker: None,
500 context_builders: Vec::new(),
501 output_processors: Vec::new(),
502 cancel: CancellationToken::new(),
503 }
504 }
505}
506
507impl<Ctx, Out> EngineBuilder<Ctx, Out>
508where
509 Ctx: Send + Sync,
510 Out: Send + Sync,
511{
512 pub fn thinker(mut self, thinker: impl Thinker<Context = Ctx, Output = Out> + 'static) -> Self {
517 self.thinker = Some(DynThinker::new(thinker));
518 self
519 }
520
521 pub fn context(mut self, builder: impl ContextBuilder<Context = Ctx> + 'static) -> Self {
527 self.context_builders.push(DynContextBuilder::new(builder));
528 self
529 }
530
531 pub fn processor(
536 mut self,
537 processor: impl OutputProcessor<Context = Ctx, Output = Out> + 'static,
538 ) -> Self {
539 self.output_processors.push(DynOutputProcessor::new(processor));
540 self
541 }
542
543 pub fn cancel(mut self, token: CancellationToken) -> Self {
548 self.cancel = token;
549 self
550 }
551
552 pub fn build(self) -> Result<CoreEngine<Ctx, Out>, EngineError> {
559 let thinker = self
560 .thinker
561 .ok_or_else(|| EngineError::Config("thinker is required".into()))?;
562 Ok(CoreEngine {
563 context_builders: self.context_builders,
564 thinker,
565 output_processors: self.output_processors,
566 cancel: self.cancel,
567 })
568 }
569}
570
571#[cfg(test)]
576mod tests {
577 use super::*;
578 use std::sync::Arc as StdArc;
579 use std::sync::atomic::{AtomicUsize, Ordering};
580
581 struct EchoThinker;
585 impl Thinker for EchoThinker {
586 type Context = String;
587 type Output = String;
588 async fn think(&self, ctx: &Self::Context) -> Result<Self::Output, EngineError> {
589 Ok(ctx.clone())
590 }
591 }
592
593 struct FixedThinker {
595 output: String,
596 }
597 impl Thinker for FixedThinker {
598 type Context = String;
599 type Output = String;
600 async fn think(&self, _ctx: &Self::Context) -> Result<Self::Output, EngineError> {
601 Ok(self.output.clone())
602 }
603 }
604
605 struct AppendSuffix {
607 suffix: String,
608 }
609 impl ContextBuilder for AppendSuffix {
610 type Context = String;
611 async fn build(&self, mut ctx: Self::Context) -> Result<Self::Context, EngineError> {
612 ctx.push_str(&self.suffix);
613 Ok(ctx)
614 }
615 }
616
617 struct CountingBuilder {
619 count: StdArc<AtomicUsize>,
620 }
621 impl CountingBuilder {
622 fn new(count: StdArc<AtomicUsize>) -> Self {
623 CountingBuilder { count }
624 }
625 }
626 impl ContextBuilder for CountingBuilder {
627 type Context = String;
628 async fn build(&self, ctx: Self::Context) -> Result<Self::Context, EngineError> {
629 self.count.fetch_add(1, Ordering::SeqCst);
630 Ok(ctx)
631 }
632 }
633
634 struct ContinueProcessor;
636 impl OutputProcessor for ContinueProcessor {
637 type Context = String;
638 type Output = String;
639 async fn process(
640 &self,
641 _output: &Self::Output,
642 _ctx: &mut Self::Context,
643 ) -> Result<Signal, EngineError> {
644 Ok(Signal::Continue)
645 }
646 }
647
648 struct StopProcessor;
650 impl OutputProcessor for StopProcessor {
651 type Context = String;
652 type Output = String;
653 async fn process(
654 &self,
655 _output: &Self::Output,
656 _ctx: &mut Self::Context,
657 ) -> Result<Signal, EngineError> {
658 Ok(Signal::Stop)
659 }
660 }
661
662 struct AppendOutput;
664 impl OutputProcessor for AppendOutput {
665 type Context = String;
666 type Output = String;
667 async fn process(
668 &self,
669 output: &Self::Output,
670 ctx: &mut Self::Context,
671 ) -> Result<Signal, EngineError> {
672 ctx.push_str(output);
673 Ok(Signal::Continue)
674 }
675 }
676
677 struct StopOnMatch {
679 keyword: &'static str,
680 }
681 impl OutputProcessor for StopOnMatch {
682 type Context = String;
683 type Output = String;
684 async fn process(
685 &self,
686 output: &Self::Output,
687 _ctx: &mut Self::Context,
688 ) -> Result<Signal, EngineError> {
689 if output.contains(self.keyword) { Ok(Signal::Stop) } else { Ok(Signal::Continue) }
690 }
691 }
692
693 struct CountingEchoThinker {
695 count: StdArc<AtomicUsize>,
696 }
697 impl Thinker for CountingEchoThinker {
698 type Context = String;
699 type Output = String;
700 async fn think(&self, ctx: &Self::Context) -> Result<Self::Output, EngineError> {
701 self.count.fetch_add(1, Ordering::SeqCst);
702 Ok(ctx.clone())
703 }
704 }
705
706 struct RecordingBuilder {
708 id: &'static str,
709 count: StdArc<AtomicUsize>,
710 }
711 impl ContextBuilder for RecordingBuilder {
712 type Context = String;
713 async fn build(&self, mut ctx: Self::Context) -> Result<Self::Context, EngineError> {
714 self.count.fetch_add(1, Ordering::SeqCst);
715 ctx.push_str(&format!("|{}", self.id));
716 Ok(ctx)
717 }
718 }
719
720 struct CountingSignalProcessor {
722 count: StdArc<AtomicUsize>,
723 signal: Signal,
724 }
725 impl OutputProcessor for CountingSignalProcessor {
726 type Context = String;
727 type Output = String;
728 async fn process(
729 &self,
730 _output: &Self::Output,
731 _ctx: &mut Self::Context,
732 ) -> Result<Signal, EngineError> {
733 self.count.fetch_add(1, Ordering::SeqCst);
734 Ok(self.signal)
735 }
736 }
737
738 #[test]
741 fn test_builder_missing_thinker_returns_err() {
742 let result: Result<CoreEngine<String, String>, _> = EngineBuilder::new().build();
743 assert!(result.is_err(), "build without thinker should fail");
744 }
745
746 #[test]
747 fn test_builder_with_thinker_returns_ok() {
748 let result = EngineBuilder::new().thinker(EchoThinker).build();
749 assert!(result.is_ok());
750 }
751
752 #[tokio::test]
753 async fn test_minimal_engine_single_iteration() {
754 let engine: CoreEngine<String, String> =
757 EngineBuilder::new().thinker(EchoThinker).processor(StopProcessor).build().unwrap();
758
759 let result = engine.run("hello".into()).await;
760 assert!(result.is_ok());
761 }
762
763 #[tokio::test]
764 async fn test_context_builder_chain() {
765 let count = StdArc::new(AtomicUsize::new(0));
766
767 let engine: CoreEngine<String, String> = EngineBuilder::new()
768 .context(AppendSuffix { suffix: " world".into() })
769 .context(CountingBuilder::new(StdArc::clone(&count)))
770 .context(AppendSuffix { suffix: "!".into() })
771 .thinker(EchoThinker)
772 .processor(StopProcessor)
773 .build()
774 .unwrap();
775
776 let result = engine.run("hello".into()).await;
777 assert!(result.is_ok());
778
779 assert_eq!(count.load(Ordering::SeqCst), 1);
781 }
782
783 #[tokio::test]
784 async fn test_context_builder_modifies_context() {
785 let engine: CoreEngine<String, String> = EngineBuilder::new()
791 .context(AppendSuffix { suffix: " world".into() })
792 .thinker(EchoThinker)
793 .processor(StopProcessor)
794 .build()
795 .unwrap();
796
797 let result = engine.run("hello".into()).await;
798 assert!(result.is_ok());
799 }
800
801 #[tokio::test]
802 async fn test_processor_continues_loop() {
803 let iteration_count = StdArc::new(AtomicUsize::new(0));
808 let count_clone = StdArc::clone(&iteration_count);
809
810 struct StopAfterN {
811 count: StdArc<AtomicUsize>,
812 limit: usize,
813 }
814 impl OutputProcessor for StopAfterN {
815 type Context = String;
816 type Output = String;
817 async fn process(
818 &self,
819 _output: &Self::Output,
820 _ctx: &mut Self::Context,
821 ) -> Result<Signal, EngineError> {
822 let current = self.count.fetch_add(1, Ordering::SeqCst) + 1;
823 if current >= self.limit { Ok(Signal::Stop) } else { Ok(Signal::Continue) }
824 }
825 }
826
827 let engine: CoreEngine<String, String> = EngineBuilder::new()
828 .thinker(EchoThinker)
829 .processor(StopAfterN { count: StdArc::clone(&count_clone), limit: 3 })
830 .build()
831 .unwrap();
832
833 let result = engine.run("hello".into()).await;
834 assert!(result.is_ok());
835
836 assert_eq!(count_clone.load(Ordering::SeqCst), 3);
838 }
839
840 #[tokio::test]
841 async fn test_stop_signal_breaks_loop() {
842 let engine: CoreEngine<String, String> =
843 EngineBuilder::new().thinker(EchoThinker).processor(StopProcessor).build().unwrap();
844
845 let result = engine.run("test".into()).await;
846 assert!(result.is_ok());
847 }
848
849 #[tokio::test]
850 async fn test_cancellation_returns_cancelled_error() {
851 let token = CancellationToken::new();
852 token.cancel(); let engine: CoreEngine<String, String> =
855 EngineBuilder::new().thinker(EchoThinker).cancel(token).build().unwrap();
856
857 let result = engine.run("test".into()).await;
858
859 assert!(result.is_err());
860 match result.unwrap_err() {
861 EngineError::Cancelled => {} other => panic!("expected Cancelled, got {other:?}"),
863 }
864 }
865
866 #[tokio::test]
867 async fn test_cancel_handle() {
868 let engine: CoreEngine<String, String> =
869 EngineBuilder::new().thinker(EchoThinker).processor(ContinueProcessor).build().unwrap();
870
871 let handle = engine.cancel_handle();
872 assert!(!handle.is_cancelled());
873
874 handle.cancel();
875 assert!(handle.is_cancelled());
876
877 let result = engine.run("test".into()).await;
878 assert!(matches!(result.unwrap_err(), EngineError::Cancelled));
879 }
880
881 #[tokio::test]
882 async fn test_processor_chain_order() {
883 let engine: CoreEngine<String, String> = EngineBuilder::new()
887 .thinker(EchoThinker)
888 .processor(ContinueProcessor)
889 .processor(StopProcessor)
890 .build()
891 .unwrap();
892
893 let result = engine.run("test".into()).await;
894 assert!(result.is_ok());
895 }
896
897 #[tokio::test]
898 async fn test_processor_modifies_context() {
899 let count = StdArc::new(AtomicUsize::new(0));
909 let count2 = StdArc::clone(&count);
910
911 struct StopAfterOne {
912 count: StdArc<AtomicUsize>,
913 }
914 impl OutputProcessor for StopAfterOne {
915 type Context = String;
916 type Output = String;
917 async fn process(
918 &self,
919 _output: &Self::Output,
920 _ctx: &mut Self::Context,
921 ) -> Result<Signal, EngineError> {
922 let n = self.count.fetch_add(1, Ordering::SeqCst) + 1;
923 if n >= 2 { Ok(Signal::Stop) } else { Ok(Signal::Continue) }
924 }
925 }
926
927 let engine: CoreEngine<String, String> = EngineBuilder::new()
928 .thinker(EchoThinker)
929 .processor(AppendOutput)
930 .processor(StopAfterOne { count: StdArc::clone(&count2) })
931 .build()
932 .unwrap();
933
934 let result = engine.run("hello".into()).await;
938 assert!(result.is_ok());
939 assert_eq!(count2.load(Ordering::SeqCst), 2);
940 assert_eq!(result.unwrap(), "hellohellohellohello");
942 }
943
944 #[tokio::test]
945 async fn test_multiple_context_builders() {
946 let engine: CoreEngine<String, String> = EngineBuilder::new()
947 .context(AppendSuffix { suffix: " world".into() })
948 .context(AppendSuffix { suffix: "!".into() })
949 .thinker(FixedThinker { output: "done".into() })
950 .processor(StopOnMatch { keyword: "done" })
951 .build()
952 .unwrap();
953
954 let result = engine.run("hello".into()).await;
955 assert!(result.is_ok());
956 }
957
958 #[tokio::test]
959 async fn test_dyn_context_builder_clone() {
960 let builder = DynContextBuilder::<String>::new(AppendSuffix { suffix: " test".into() });
962 let ctx: String = builder.build("hello".into()).await.unwrap();
963 assert_eq!(ctx, "hello test");
964 }
965
966 #[tokio::test]
967 async fn test_dyn_thinker_clone() {
968 let thinker = DynThinker::<String, String>::new(EchoThinker);
969 let result = thinker.think(&"input".to_string()).await.unwrap();
970 assert_eq!(result, "input");
971 }
972
973 #[tokio::test]
974 async fn test_dyn_output_processor_clone() {
975 let processor = DynOutputProcessor::<String, String>::new(StopProcessor);
976 let mut ctx = String::from("test");
977 let result = processor.process(&"output".into(), &mut ctx).await.unwrap();
978 assert_eq!(result, Signal::Stop);
979 }
980
981 #[tokio::test]
984 async fn test_complete_loop_with_mock() {
985 let cb_count = StdArc::new(AtomicUsize::new(0));
988 let t_count = StdArc::new(AtomicUsize::new(0));
989 let op_count = StdArc::new(AtomicUsize::new(0));
990
991 let engine: CoreEngine<String, String> = EngineBuilder::new()
992 .context(CountingBuilder::new(StdArc::clone(&cb_count)))
993 .thinker(CountingEchoThinker { count: StdArc::clone(&t_count) })
994 .processor(CountingSignalProcessor {
995 count: StdArc::clone(&op_count),
996 signal: Signal::Stop,
997 })
998 .build()
999 .unwrap();
1000
1001 let result = engine.run("hello".into()).await;
1002 assert!(result.is_ok());
1003
1004 assert_eq!(cb_count.load(Ordering::SeqCst), 1);
1005 assert_eq!(t_count.load(Ordering::SeqCst), 1);
1006 assert_eq!(op_count.load(Ordering::SeqCst), 1);
1007 }
1008
1009 #[tokio::test]
1010 async fn test_three_context_builders_executed() {
1011 let count_a = StdArc::new(AtomicUsize::new(0));
1013 let count_b = StdArc::new(AtomicUsize::new(0));
1014 let count_c = StdArc::new(AtomicUsize::new(0));
1015
1016 let engine: CoreEngine<String, String> = EngineBuilder::new()
1017 .context(RecordingBuilder { id: "A", count: StdArc::clone(&count_a) })
1018 .context(RecordingBuilder { id: "B", count: StdArc::clone(&count_b) })
1019 .context(RecordingBuilder { id: "C", count: StdArc::clone(&count_c) })
1020 .thinker(FixedThinker { output: "done".into() })
1021 .processor(StopOnMatch { keyword: "done" })
1022 .build()
1023 .unwrap();
1024
1025 let result = engine.run("init".into()).await;
1026 assert!(result.is_ok());
1027
1028 assert_eq!(count_a.load(Ordering::SeqCst), 1);
1029 assert_eq!(count_b.load(Ordering::SeqCst), 1);
1030 assert_eq!(count_c.load(Ordering::SeqCst), 1);
1031 }
1032
1033 #[tokio::test]
1034 async fn test_output_processor_stop_signal() {
1035 let t_count = StdArc::new(AtomicUsize::new(0));
1037 let op_count = StdArc::new(AtomicUsize::new(0));
1038
1039 let engine: CoreEngine<String, String> = EngineBuilder::new()
1040 .thinker(CountingEchoThinker { count: StdArc::clone(&t_count) })
1041 .processor(CountingSignalProcessor {
1042 count: StdArc::clone(&op_count),
1043 signal: Signal::Stop,
1044 })
1045 .build()
1046 .unwrap();
1047
1048 let result = engine.run("test".into()).await;
1049 assert!(result.is_ok());
1050
1051 assert_eq!(t_count.load(Ordering::SeqCst), 1);
1052 assert_eq!(op_count.load(Ordering::SeqCst), 1);
1053 }
1054
1055 #[tokio::test]
1056 async fn test_continue_signal_continues() {
1057 let continue_count = StdArc::new(AtomicUsize::new(0));
1060 let stop_count = StdArc::new(AtomicUsize::new(0));
1061
1062 let engine: CoreEngine<String, String> = EngineBuilder::new()
1063 .thinker(EchoThinker)
1064 .processor(CountingSignalProcessor {
1065 count: StdArc::clone(&continue_count),
1066 signal: Signal::Continue,
1067 })
1068 .processor(CountingSignalProcessor {
1069 count: StdArc::clone(&stop_count),
1070 signal: Signal::Stop,
1071 })
1072 .build()
1073 .unwrap();
1074
1075 let result = engine.run("test".into()).await;
1076 assert!(result.is_ok());
1077
1078 assert_eq!(continue_count.load(Ordering::SeqCst), 1);
1080 assert_eq!(stop_count.load(Ordering::SeqCst), 1);
1081 }
1082
1083 #[tokio::test]
1084 async fn test_empty_context_and_no_processors() {
1085 let token = CancellationToken::new();
1089 token.cancel();
1090
1091 let engine: CoreEngine<String, String> =
1092 EngineBuilder::new().thinker(EchoThinker).cancel(token).build().unwrap();
1093
1094 let result = engine.run("data".into()).await;
1095 assert!(
1096 matches!(result, Err(EngineError::Cancelled)),
1097 "expected Cancelled, got {result:?}"
1098 );
1099 }
1100
1101 #[tokio::test]
1102 async fn test_run_returns_final_context() {
1103 let engine: CoreEngine<String, String> = EngineBuilder::new()
1104 .context(AppendSuffix { suffix: " world".into() })
1105 .thinker(EchoThinker)
1106 .processor(AppendOutput)
1107 .processor(StopProcessor)
1108 .build()
1109 .unwrap();
1110
1111 let final_ctx = engine.run("hi".into()).await.unwrap();
1113 assert_eq!(final_ctx, "hi worldhi world");
1114 }
1115
1116 #[tokio::test]
1117 async fn test_no_processors_stops_after_one_turn() {
1118 let t_count = StdArc::new(AtomicUsize::new(0));
1119 let engine: CoreEngine<String, String> = EngineBuilder::new()
1120 .thinker(CountingEchoThinker { count: StdArc::clone(&t_count) })
1121 .build()
1122 .unwrap();
1123
1124 let final_ctx = engine.run("once".into()).await.unwrap();
1125 assert_eq!(final_ctx, "once");
1126 assert_eq!(t_count.load(Ordering::SeqCst), 1);
1127 }
1128
1129 #[tokio::test]
1130 async fn test_iterator_counting() {
1131 let iter_count = StdArc::new(AtomicUsize::new(0));
1133 let count_clone = StdArc::clone(&iter_count);
1134
1135 struct StopAfterN {
1136 count: StdArc<AtomicUsize>,
1137 limit: usize,
1138 }
1139 impl OutputProcessor for StopAfterN {
1140 type Context = String;
1141 type Output = String;
1142 async fn process(
1143 &self,
1144 _output: &Self::Output,
1145 _ctx: &mut Self::Context,
1146 ) -> Result<Signal, EngineError> {
1147 let current = self.count.fetch_add(1, Ordering::SeqCst) + 1;
1148 if current >= self.limit { Ok(Signal::Stop) } else { Ok(Signal::Continue) }
1149 }
1150 }
1151
1152 let engine: CoreEngine<String, String> = EngineBuilder::new()
1153 .thinker(EchoThinker)
1154 .processor(StopAfterN { count: StdArc::clone(&count_clone), limit: 5 })
1155 .build()
1156 .unwrap();
1157
1158 let result = engine.run("start".into()).await;
1159 assert!(result.is_ok());
1160 assert_eq!(count_clone.load(Ordering::SeqCst), 5);
1161 }
1162}