1use crate::agent::{AgentConfig, ReActLoop, ReActStep, ToolSpec};
20use crate::error::AgentRuntimeError;
21use crate::metrics::RuntimeMetrics;
22use crate::types::AgentId;
23
24#[cfg(feature = "memory")]
25use crate::memory::{EpisodicStore, WorkingMemory};
26use serde::{Deserialize, Serialize};
27use std::fmt::Write as FmtWrite;
28use std::marker::PhantomData;
29use std::sync::atomic::Ordering;
30use std::sync::Arc;
31use std::time::Instant;
32
33#[cfg(feature = "graph")]
34use crate::graph::GraphStore;
35
36#[cfg(feature = "orchestrator")]
37use crate::orchestrator::BackpressureGuard;
38
39pub struct NeedsConfig;
43pub struct HasConfig;
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct AgentSession {
51 pub session_id: String,
53 pub agent_id: AgentId,
55 pub steps: Vec<ReActStep>,
57 pub memory_hits: usize,
59 pub graph_lookups: usize,
61 pub duration_ms: u64,
63 #[serde(default)]
69 pub checkpoint_errors: Vec<String>,
70}
71
72impl AgentSession {
73 pub fn step_count(&self) -> usize {
84 self.steps.len()
85 }
86
87 pub fn is_empty(&self) -> bool {
89 self.steps.is_empty()
90 }
91
92 pub fn final_answer(&self) -> Option<String> {
97 let last = self.steps.last()?;
98 let upper = last.action.trim().to_ascii_uppercase();
99 if upper.starts_with("FINAL_ANSWER") {
100 let answer = last.action.trim()["FINAL_ANSWER".len()..].trim().to_owned();
101 Some(answer)
102 } else {
103 None
104 }
105 }
106
107 pub fn is_successful(&self) -> bool {
112 self.final_answer().is_some()
113 }
114
115 pub fn elapsed(&self) -> std::time::Duration {
117 std::time::Duration::from_millis(self.duration_ms)
118 }
119
120 pub fn tool_calls_made(&self) -> usize {
125 self.steps
126 .iter()
127 .filter(|s| {
128 !s.action.trim().to_ascii_uppercase().starts_with("FINAL_ANSWER")
131 && !s.action.trim().is_empty()
132 })
133 .count()
134 }
135
136 pub fn total_step_duration_ms(&self) -> u64 {
141 self.steps.iter().map(|s| s.step_duration_ms).sum()
142 }
143
144 pub fn average_step_duration_ms(&self) -> u64 {
148 if self.steps.is_empty() {
149 return 0;
150 }
151 self.total_step_duration_ms() / self.steps.len() as u64
152 }
153
154 pub fn slowest_step(&self) -> Option<&ReActStep> {
158 self.steps.iter().max_by_key(|s| s.step_duration_ms)
159 }
160
161 pub fn fastest_step(&self) -> Option<&ReActStep> {
165 self.steps.iter().min_by_key(|s| s.step_duration_ms)
166 }
167
168 pub fn filter_tool_call_steps(&self) -> Vec<&ReActStep> {
170 self.steps.iter().filter(|s| s.is_tool_call()).collect()
171 }
172
173 pub fn slowest_step_index(&self) -> Option<usize> {
175 self.steps
176 .iter()
177 .enumerate()
178 .max_by_key(|(_, s)| s.step_duration_ms)
179 .map(|(i, _)| i)
180 }
181
182 pub fn fastest_step_index(&self) -> Option<usize> {
184 self.steps
185 .iter()
186 .enumerate()
187 .min_by_key(|(_, s)| s.step_duration_ms)
188 .map(|(i, _)| i)
189 }
190
191 pub fn last_step(&self) -> Option<&ReActStep> {
193 self.steps.last()
194 }
195
196 pub fn first_step(&self) -> Option<&ReActStep> {
198 self.steps.first()
199 }
200
201 pub fn step_at(&self, idx: usize) -> Option<&ReActStep> {
203 self.steps.get(idx)
204 }
205
206 pub fn observation_at(&self, idx: usize) -> Option<&str> {
208 self.steps.get(idx).map(|s| s.observation.as_str())
209 }
210
211 pub fn action_at(&self, idx: usize) -> Option<&str> {
213 self.steps.get(idx).map(|s| s.action.as_str())
214 }
215
216 pub fn observations_matching(&self, pattern: &str) -> Vec<&ReActStep> {
218 let lower = pattern.to_ascii_lowercase();
219 self.steps
220 .iter()
221 .filter(|s| s.observation.to_ascii_lowercase().contains(&lower))
222 .collect()
223 }
224
225 pub fn thoughts_containing(&self, pattern: &str) -> Vec<&ReActStep> {
227 let lower = pattern.to_ascii_lowercase();
228 self.steps
229 .iter()
230 .filter(|s| s.thought.to_ascii_lowercase().contains(&lower))
231 .collect()
232 }
233
234 pub fn has_action(&self, action_name: &str) -> bool {
236 self.steps.iter().any(|s| s.action == action_name)
237 }
238
239 pub fn thought_at(&self, idx: usize) -> Option<&str> {
241 self.steps.get(idx).map(|s| s.thought.as_str())
242 }
243
244 pub fn step_count_for_action(&self, action_name: &str) -> usize {
251 self.steps.iter().filter(|s| s.action == action_name).count()
252 }
253
254 pub fn observations(&self) -> Vec<&str> {
259 self.steps.iter().map(|s| s.observation.as_str()).collect()
260 }
261
262 pub fn observation_count(&self) -> usize {
264 self.steps.iter().filter(|s| !s.observation.is_empty()).count()
265 }
266
267 pub fn last_n_observations(&self, n: usize) -> Vec<&str> {
273 let all: Vec<&str> = self
274 .steps
275 .iter()
276 .filter(|s| !s.observation.is_empty())
277 .map(|s| s.observation.as_str())
278 .collect();
279 let skip = all.len().saturating_sub(n);
280 all[skip..].to_vec()
281 }
282
283 pub fn actions_in_window(&self, n: usize) -> Vec<&str> {
287 let skip = self.steps.len().saturating_sub(n);
288 self.steps[skip..]
289 .iter()
290 .map(|s| s.action.as_str())
291 .collect()
292 }
293
294 pub fn steps_without_observation(&self) -> usize {
296 self.steps.iter().filter(|s| s.observation.is_empty()).count()
297 }
298
299 pub fn first_thought(&self) -> Option<&str> {
302 self.steps.first().map(|s| s.thought.as_str())
303 }
304
305 pub fn last_thought(&self) -> Option<&str> {
308 self.steps.last().map(|s| s.thought.as_str())
309 }
310
311 pub fn first_action(&self) -> Option<&str> {
314 self.steps.first().map(|s| s.action.as_str())
315 }
316
317 pub fn last_action(&self) -> Option<&str> {
320 self.steps.last().map(|s| s.action.as_str())
321 }
322
323 pub fn last_n_steps(&self, n: usize) -> &[crate::agent::ReActStep] {
328 let len = self.steps.len();
329 let start = len.saturating_sub(n);
330 &self.steps[start..]
331 }
332
333 pub fn first_n_steps(&self, n: usize) -> &[crate::agent::ReActStep] {
338 let end = n.min(self.steps.len());
339 &self.steps[..end]
340 }
341
342 pub fn steps_with_tool<'a>(&'a self, tool_name: &str) -> Vec<&'a crate::agent::ReActStep> {
347 self.steps
348 .iter()
349 .filter(|s| s.action.contains(tool_name) && !s.is_final_answer())
350 .collect()
351 }
352
353 pub fn total_chars(&self) -> usize {
358 self.steps
359 .iter()
360 .map(|s| s.thought.len() + s.action.len() + s.observation.len())
361 .sum()
362 }
363
364 pub fn step_durations_ms(&self) -> Vec<u64> {
368 self.steps.iter().map(|s| s.step_duration_ms).collect()
369 }
370
371 pub fn total_latency_ms(&self) -> u64 {
376 self.steps.iter().map(|s| s.step_duration_ms).sum()
377 }
378
379 pub fn avg_step_duration_ms(&self) -> f64 {
383 if self.steps.is_empty() {
384 return 0.0;
385 }
386 self.total_latency_ms() as f64 / self.steps.len() as f64
387 }
388
389 pub fn longest_step(&self) -> Option<&crate::agent::ReActStep> {
394 self.steps.iter().max_by_key(|s| s.step_duration_ms)
395 }
396
397 pub fn shortest_step(&self) -> Option<&crate::agent::ReActStep> {
402 self.steps.iter().min_by_key(|s| s.step_duration_ms)
403 }
404
405 pub fn action_sequence(&self) -> Vec<String> {
410 self.steps.iter().map(|s| s.action.clone()).collect()
411 }
412
413 pub fn unique_tools_used(&self) -> Vec<String> {
418 let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();
419 for step in &self.steps {
420 let action = step.action.trim();
421 if action.is_empty() || action.to_ascii_uppercase().starts_with("FINAL_ANSWER") {
422 continue;
423 }
424 if let Ok(v) = serde_json::from_str::<serde_json::Value>(action) {
426 if let Some(name) = v.get("tool").and_then(|n| n.as_str()) {
427 names.insert(name.to_owned());
428 continue;
429 }
430 }
431 names.insert(action.to_owned());
432 }
433 let mut sorted: Vec<String> = names.into_iter().collect();
434 sorted.sort_unstable();
435 sorted
436 }
437
438 pub fn all_thoughts(&self) -> Vec<&str> {
440 self.steps.iter().map(|s| s.thought.as_str()).collect()
441 }
442
443 pub fn all_actions(&self) -> Vec<&str> {
445 self.steps.iter().map(|s| s.action.as_str()).collect()
446 }
447
448 pub fn all_observations(&self) -> Vec<&str> {
450 self.steps.iter().map(|s| s.observation.as_str()).collect()
451 }
452
453 pub fn failed_steps(&self) -> Vec<&crate::agent::ReActStep> {
459 self.steps
460 .iter()
461 .filter(|s| {
462 let obs = s.observation.trim();
463 obs.starts_with("{\"error\"")
464 || obs.to_ascii_lowercase().contains("\"error\"")
465 })
466 .collect()
467 }
468
469 pub fn failed_tool_call_count(&self) -> usize {
473 self.steps
474 .iter()
475 .filter(|s| {
476 let obs = s.observation.trim();
477 obs.starts_with("{\"error\"")
478 || obs.to_ascii_lowercase().contains("\"error\"")
479 })
480 .count()
481 }
482
483 pub fn action_counts(&self) -> std::collections::HashMap<String, usize> {
487 let mut counts = std::collections::HashMap::new();
488 for step in &self.steps {
489 *counts.entry(step.action.clone()).or_insert(0) += 1;
490 }
491 counts
492 }
493
494 pub fn unique_actions(&self) -> Vec<String> {
496 let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
497 for step in &self.steps {
498 seen.insert(step.action.clone());
499 }
500 seen.into_iter().collect()
501 }
502
503 pub fn has_duplicate_actions(&self) -> bool {
508 let mut seen = std::collections::HashSet::new();
509 self.steps.iter().any(|s| !seen.insert(s.action.as_str()))
510 }
511
512 pub fn step_indices_with_tool(&self, tool_name: &str) -> Vec<usize> {
517 self.steps
518 .iter()
519 .enumerate()
520 .filter(|(_, s)| !s.is_final_answer() && s.action.contains(tool_name))
521 .map(|(i, _)| i)
522 .collect()
523 }
524
525 pub fn most_used_action(&self) -> Option<String> {
530 let counts = self.action_counts();
531 counts
532 .into_iter()
533 .max_by_key(|(_, count)| *count)
534 .map(|(name, _)| name)
535 }
536
537 pub fn last_observation(&self) -> Option<&str> {
542 self.steps
543 .iter()
544 .rev()
545 .find(|s| !s.observation.is_empty())
546 .map(|s| s.observation.as_str())
547 }
548
549 pub fn thought_count(&self) -> usize {
551 self.steps.iter().filter(|s| !s.thought.is_empty()).count()
552 }
553
554 pub fn observation_rate(&self) -> f64 {
558 let n = self.steps.len();
559 if n == 0 {
560 return 0.0;
561 }
562 let with_obs = self
563 .steps
564 .iter()
565 .filter(|s| !s.observation.is_empty())
566 .count();
567 with_obs as f64 / n as f64
568 }
569
570 pub fn has_graph_lookups(&self) -> bool {
573 self.graph_lookups > 0
574 }
575
576 pub fn consecutive_same_action_at_end(&self) -> usize {
583 let n = self.steps.len();
584 if n == 0 {
585 return 0;
586 }
587 let last_action = &self.steps[n - 1].action;
588 self.steps
589 .iter()
590 .rev()
591 .take_while(|s| &s.action == last_action)
592 .count()
593 .saturating_sub(1) }
595
596 pub fn action_repetition_rate(&self) -> f64 {
602 let n = self.steps.len();
603 if n < 2 {
604 return 0.0;
605 }
606 let repeats = self
607 .steps
608 .windows(2)
609 .filter(|w| w[0].action == w[1].action)
610 .count();
611 repeats as f64 / (n - 1) as f64
612 }
613
614 pub fn max_consecutive_failures(&self) -> usize {
620 let mut max_run = 0usize;
621 let mut current = 0usize;
622 for step in &self.steps {
623 let obs = step.observation.trim();
624 if obs.starts_with("{\"error\"") || obs.to_ascii_lowercase().contains("\"error\"") {
625 current += 1;
626 if current > max_run {
627 max_run = current;
628 }
629 } else {
630 current = 0;
631 }
632 }
633 max_run
634 }
635
636 pub fn avg_thought_length(&self) -> f64 {
641 let thoughts: Vec<_> = self
642 .steps
643 .iter()
644 .filter(|s| !s.thought.is_empty())
645 .collect();
646 if thoughts.is_empty() {
647 return 0.0;
648 }
649 let total: usize = thoughts.iter().map(|s| s.thought.len()).sum();
650 total as f64 / thoughts.len() as f64
651 }
652
653 pub fn graph_lookup_rate(&self) -> f64 {
658 let steps = self.steps.len();
659 if steps == 0 {
660 return 0.0;
661 }
662 self.graph_lookups as f64 / steps as f64
663 }
664
665 pub fn has_checkpoint_errors(&self) -> bool {
670 !self.checkpoint_errors.is_empty()
671 }
672
673 pub fn checkpoint_error_count(&self) -> usize {
675 self.checkpoint_errors.len()
676 }
677
678 pub fn graph_lookup_count(&self) -> usize {
680 self.graph_lookups
681 }
682
683 pub fn memory_hit_rate(&self) -> f64 {
688 let steps = self.steps.len();
689 if steps == 0 {
690 return 0.0;
691 }
692 self.memory_hits as f64 / steps as f64
693 }
694
695 pub fn total_memory_hits(&self) -> usize {
697 self.memory_hits
698 }
699
700 pub fn throughput_steps_per_sec(&self) -> f64 {
705 if self.duration_ms == 0 {
706 return 0.0;
707 }
708 self.steps.len() as f64 / (self.duration_ms as f64 / 1000.0)
709 }
710
711 pub fn duration_secs(&self) -> u64 {
713 self.duration_ms / 1000
714 }
715
716 pub fn steps_above_thought_length(&self, threshold: usize) -> usize {
718 self.steps.iter().filter(|s| s.thought.len() > threshold).count()
719 }
720
721 pub fn has_final_answer(&self) -> bool {
723 self.steps
724 .iter()
725 .any(|s| s.action.to_ascii_uppercase().starts_with("FINAL_ANSWER"))
726 }
727
728 pub fn avg_action_length(&self) -> f64 {
732 if self.steps.is_empty() {
733 return 0.0;
734 }
735 let total: usize = self.steps.iter().map(|s| s.action.len()).sum();
736 total as f64 / self.steps.len() as f64
737 }
738
739 pub fn has_tool_failures(&self) -> bool {
741 self.failed_tool_call_count() > 0
742 }
743
744 pub fn tool_call_rate(&self) -> f64 {
749 let total = self.steps.len();
750 if total == 0 {
751 return 0.0;
752 }
753 self.tool_calls_made() as f64 / total as f64
754 }
755
756 pub fn step_success_rate(&self) -> f64 {
761 let total = self.steps.len();
762 if total == 0 {
763 return 1.0;
764 }
765 1.0 - (self.failed_tool_call_count() as f64 / total as f64)
766 }
767
768 pub fn action_diversity(&self) -> f64 {
773 let total = self.steps.len();
774 if total == 0 {
775 return 0.0;
776 }
777 let unique: std::collections::HashSet<&str> =
778 self.steps.iter().map(|s| s.action.as_str()).collect();
779 unique.len() as f64 / total as f64
780 }
781
782 pub fn total_thought_length(&self) -> usize {
784 self.steps.iter().map(|s| s.thought.len()).sum()
785 }
786
787 pub fn steps_with_empty_observations(&self) -> usize {
789 self.steps.iter().filter(|s| s.observation.is_empty()).count()
790 }
791
792 pub fn observation_lengths(&self) -> Vec<usize> {
794 self.steps.iter().map(|s| s.observation.len()).collect()
795 }
796
797 pub fn avg_observation_length(&self) -> f64 {
801 let n = self.steps.len();
802 if n == 0 {
803 return 0.0;
804 }
805 let total: usize = self.steps.iter().map(|s| s.observation.len()).sum();
806 total as f64 / n as f64
807 }
808
809 pub fn min_thought_length(&self) -> usize {
812 self.steps
813 .iter()
814 .filter(|s| !s.thought.is_empty())
815 .map(|s| s.thought.len())
816 .min()
817 .unwrap_or(0)
818 }
819
820 pub fn longest_observation(&self) -> Option<&str> {
823 self.steps
824 .iter()
825 .max_by_key(|s| s.observation.len())
826 .map(|s| s.observation.as_str())
827 }
828
829 pub fn thought_lengths(&self) -> Vec<usize> {
831 self.steps.iter().map(|s| s.thought.len()).collect()
832 }
833
834 pub fn most_common_action(&self) -> Option<&str> {
838 if self.steps.is_empty() {
839 return None;
840 }
841 let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
842 for s in &self.steps {
843 *counts.entry(s.action.as_str()).or_insert(0) += 1;
844 }
845 counts.into_iter().max_by_key(|(_, c)| *c).map(|(a, _)| a)
846 }
847
848 pub fn action_lengths(&self) -> Vec<usize> {
850 self.steps.iter().map(|s| s.action.len()).collect()
851 }
852
853 pub fn step_success_count(&self) -> usize {
855 self.steps.len() - self.failed_tool_call_count()
856 }
857
858 pub fn longest_thought(&self) -> Option<&str> {
862 self.steps
863 .iter()
864 .max_by_key(|s| s.thought.len())
865 .map(|s| s.thought.as_str())
866 }
867
868 pub fn shortest_action(&self) -> Option<&str> {
872 self.steps
873 .iter()
874 .min_by_key(|s| s.action.len())
875 .map(|s| s.action.as_str())
876 }
877
878 pub fn total_thought_bytes(&self) -> usize {
880 self.steps.iter().map(|s| s.thought.len()).sum()
881 }
882
883 pub fn total_observation_bytes(&self) -> usize {
885 self.steps.iter().map(|s| s.observation.len()).sum()
886 }
887
888 pub fn first_step_action(&self) -> Option<&str> {
892 self.steps.first().map(|s| s.action.as_str())
893 }
894
895 pub fn last_step_action(&self) -> Option<&str> {
899 self.steps.last().map(|s| s.action.as_str())
900 }
901
902 pub fn count_nonempty_thoughts(&self) -> usize {
904 self.steps.iter().filter(|s| !s.thought.is_empty()).count()
905 }
906
907 pub fn observation_contains_count(&self, substring: &str) -> usize {
909 self.steps.iter().filter(|s| s.observation.contains(substring)).count()
910 }
911
912 pub fn count_steps_with_action(&self, action: &str) -> usize {
914 self.steps.iter().filter(|s| s.action == action).count()
915 }
916
917 pub fn thought_contains_count(&self, substring: &str) -> usize {
919 self.steps.iter().filter(|s| s.thought.contains(substring)).count()
920 }
921
922 pub fn failure_rate(&self) -> f64 {
927 let total = self.steps.len();
928 if total == 0 {
929 return 0.0;
930 }
931 self.failed_tool_call_count() as f64 / total as f64
932 }
933
934 pub fn unique_action_count(&self) -> usize {
936 let unique: std::collections::HashSet<&str> =
937 self.steps.iter().map(|s| s.action.as_str()).collect();
938 unique.len()
939 }
940
941 pub fn steps_in_range(&self, start: usize, end: usize) -> Vec<&ReActStep> {
946 let clamped_end = end.min(self.steps.len());
947 if start >= clamped_end {
948 return Vec::new();
949 }
950 self.steps[start..clamped_end].iter().collect()
951 }
952
953 pub fn median_step_duration_ms(&self) -> u64 {
958 if self.steps.is_empty() {
959 return 0;
960 }
961 let mut durations: Vec<u64> = self.steps.iter().map(|s| s.step_duration_ms).collect();
962 durations.sort_unstable();
963 durations[durations.len() / 2]
964 }
965
966 pub fn p95_step_duration_ms(&self) -> u64 {
972 if self.steps.is_empty() {
973 return 0;
974 }
975 let mut durations: Vec<u64> = self.steps.iter().map(|s| s.step_duration_ms).collect();
976 durations.sort_unstable();
977 let idx = ((durations.len() as f64 * 0.95).ceil() as usize)
978 .saturating_sub(1)
979 .min(durations.len() - 1);
980 durations[idx]
981 }
982
983 pub fn p99_step_duration_ms(&self) -> u64 {
989 if self.steps.is_empty() {
990 return 0;
991 }
992 let mut durations: Vec<u64> = self.steps.iter().map(|s| s.step_duration_ms).collect();
993 durations.sort_unstable();
994 let idx = ((durations.len() as f64 * 0.99).ceil() as usize)
995 .saturating_sub(1)
996 .min(durations.len() - 1);
997 durations[idx]
998 }
999
1000 pub fn step_count_above_duration_ms(&self, threshold_ms: u64) -> usize {
1006 self.steps
1007 .iter()
1008 .filter(|s| s.step_duration_ms > threshold_ms)
1009 .count()
1010 }
1011
1012 pub fn min_step_duration_ms(&self) -> u64 {
1016 self.steps.iter().map(|s| s.step_duration_ms).min().unwrap_or(0)
1017 }
1018
1019 pub fn max_step_duration_ms(&self) -> u64 {
1023 self.steps.iter().map(|s| s.step_duration_ms).max().unwrap_or(0)
1024 }
1025
1026 pub fn total_action_bytes(&self) -> usize {
1034 self.steps.iter().map(|s| s.action.len()).sum()
1035 }
1036
1037 pub fn step_duration_variance_ms(&self) -> f64 {
1041 let n = self.steps.len();
1042 if n < 2 {
1043 return 0.0;
1044 }
1045 let mean = self.average_step_duration_ms() as f64;
1046 let sum_sq: f64 = self
1047 .steps
1048 .iter()
1049 .map(|s| {
1050 let diff = s.step_duration_ms as f64 - mean;
1051 diff * diff
1052 })
1053 .sum();
1054 sum_sq / n as f64
1055 }
1056
1057 pub fn steps_with_errors(&self) -> Vec<&ReActStep> {
1064 self.steps
1065 .iter()
1066 .filter(|s| s.observation.to_ascii_lowercase().contains("error"))
1067 .collect()
1068 }
1069
1070 pub fn steps_with_long_observations(&self, threshold_bytes: usize) -> Vec<&ReActStep> {
1074 self.steps
1075 .iter()
1076 .filter(|s| s.observation.len() > threshold_bytes)
1077 .collect()
1078 }
1079
1080 pub fn observations_above_bytes(&self, min_bytes: usize) -> Vec<&ReActStep> {
1087 self.steps
1088 .iter()
1089 .filter(|s| s.observation.len() > min_bytes)
1090 .collect()
1091 }
1092
1093 pub fn total_step_chars(&self) -> usize {
1098 self.steps
1099 .iter()
1100 .map(|s| {
1101 s.thought.chars().count()
1102 + s.action.chars().count()
1103 + s.observation.chars().count()
1104 })
1105 .sum()
1106 }
1107
1108 pub fn unique_observations_count(&self) -> usize {
1113 let unique: std::collections::HashSet<&str> =
1114 self.steps.iter().map(|s| s.observation.as_str()).collect();
1115 unique.len()
1116 }
1117
1118 pub fn thought_max_bytes(&self) -> usize {
1122 self.steps.iter().map(|s| s.thought.len()).max().unwrap_or(0)
1123 }
1124
1125 pub fn observation_max_bytes(&self) -> usize {
1129 self.steps.iter().map(|s| s.observation.len()).max().unwrap_or(0)
1130 }
1131
1132 pub fn step_count_below_duration_ms(&self, threshold_ms: u64) -> usize {
1140 self.steps
1141 .iter()
1142 .filter(|s| s.step_duration_ms < threshold_ms)
1143 .count()
1144 }
1145
1146 pub fn max_action_bytes(&self) -> usize {
1150 self.steps.iter().map(|s| s.action.len()).max().unwrap_or(0)
1151 }
1152
1153 pub fn min_action_bytes(&self) -> usize {
1157 self.steps.iter().map(|s| s.action.len()).min().unwrap_or(0)
1158 }
1159
1160 pub fn proportion_tool_calls(&self) -> f64 {
1164 if self.steps.is_empty() {
1165 return 0.0;
1166 }
1167 let tool_calls = self.steps.iter().filter(|s| s.is_tool_call()).count();
1168 tool_calls as f64 / self.steps.len() as f64
1169 }
1170
1171 pub fn thought_density(&self) -> f64 {
1176 let thought_bytes: usize = self.steps.iter().map(|s| s.thought.len()).sum();
1177 let total_bytes: usize = self
1178 .steps
1179 .iter()
1180 .map(|s| s.thought.len() + s.action.len() + s.observation.len())
1181 .sum();
1182 if total_bytes == 0 {
1183 return 0.0;
1184 }
1185 thought_bytes as f64 / total_bytes as f64
1186 }
1187
1188 pub fn step_throughput_per_sec(&self) -> f64 {
1193 if self.duration_ms == 0 || self.steps.is_empty() {
1194 return 0.0;
1195 }
1196 self.steps.len() as f64 / (self.duration_ms as f64 / 1000.0)
1197 }
1198
1199 pub fn avg_action_bytes(&self) -> f64 {
1203 if self.steps.is_empty() {
1204 return 0.0;
1205 }
1206 let total: usize = self.steps.iter().map(|s| s.action.len()).sum();
1207 total as f64 / self.steps.len() as f64
1208 }
1209
1210 pub fn avg_observation_bytes(&self) -> f64 {
1214 if self.steps.is_empty() {
1215 return 0.0;
1216 }
1217 let total: usize = self.steps.iter().map(|s| s.observation.len()).sum();
1218 total as f64 / self.steps.len() as f64
1219 }
1220
1221 pub fn total_observation_count(&self) -> usize {
1225 self.steps.iter().filter(|s| !s.observation.is_empty()).count()
1226 }
1227
1228 pub fn actions_containing<'a>(&'a self, substring: &str) -> Vec<&'a ReActStep> {
1233 self.steps
1234 .iter()
1235 .filter(|s| s.action.contains(substring))
1236 .collect()
1237 }
1238
1239 pub fn avg_thought_bytes(&self) -> f64 {
1243 if self.steps.is_empty() {
1244 return 0.0;
1245 }
1246 let total: usize = self.steps.iter().map(|s| s.thought.len()).sum();
1247 total as f64 / self.steps.len() as f64
1248 }
1249
1250 pub fn steps_above_action_bytes(&self, min_bytes: usize) -> Vec<&ReActStep> {
1255 self.steps
1256 .iter()
1257 .filter(|s| s.action.len() > min_bytes)
1258 .collect()
1259 }
1260
1261 pub fn steps_between(&self, start: usize, end: usize) -> Vec<&ReActStep> {
1266 let clamped_end = end.min(self.steps.len());
1267 if start >= clamped_end {
1268 return Vec::new();
1269 }
1270 self.steps[start..clamped_end].iter().collect()
1271 }
1272
1273 pub fn step_observation_rate(&self) -> f64 {
1277 if self.steps.is_empty() {
1278 return 0.0;
1279 }
1280 let count = self.steps.iter().filter(|s| !s.observation.is_empty()).count();
1281 count as f64 / self.steps.len() as f64
1282 }
1283
1284 pub fn steps_below_thought_bytes(&self, max_bytes: usize) -> Vec<&ReActStep> {
1289 self.steps
1290 .iter()
1291 .filter(|s| s.thought.len() < max_bytes)
1292 .collect()
1293 }
1294
1295 pub fn steps_with_duplicate_thoughts(&self) -> Vec<&ReActStep> {
1302 let mut seen = std::collections::HashSet::new();
1303 self.steps
1304 .iter()
1305 .filter(|s| !seen.insert(s.thought.as_str()))
1306 .collect()
1307 }
1308
1309 pub fn max_thought_bytes(&self) -> usize {
1313 self.steps.iter().map(|s| s.thought.len()).max().unwrap_or(0)
1314 }
1315
1316 pub fn steps_by_action_prefix<'a>(&'a self, prefix: &str) -> Vec<&'a ReActStep> {
1321 self.steps
1322 .iter()
1323 .filter(|s| s.action.starts_with(prefix))
1324 .collect()
1325 }
1326
1327 pub fn action_count(&self) -> usize {
1332 self.steps.iter().filter(|s| s.is_tool_call()).count()
1333 }
1334
1335 pub fn steps_above_observation_bytes(&self, min_bytes: usize) -> Vec<&ReActStep> {
1340 self.steps
1341 .iter()
1342 .filter(|s| s.observation.len() > min_bytes)
1343 .collect()
1344 }
1345
1346 pub fn steps_matching_observation<'a>(&'a self, substr: &str) -> Vec<&'a ReActStep> {
1351 self.steps
1352 .iter()
1353 .filter(|s| s.observation.contains(substr))
1354 .collect()
1355 }
1356
1357 pub fn step_action_lengths(&self) -> Vec<usize> {
1361 self.steps.iter().map(|s| s.action.len()).collect()
1362 }
1363
1364 pub fn has_thought_starting_with(&self, prefix: &str) -> bool {
1368 self.steps.iter().any(|s| s.thought.starts_with(prefix))
1369 }
1370
1371 pub fn step_count_above_action_bytes(&self, min_bytes: usize) -> usize {
1375 self.steps.iter().filter(|s| s.action.len() > min_bytes).count()
1376 }
1377
1378 pub fn steps_with_empty_action(&self) -> Vec<&ReActStep> {
1382 self.steps.iter().filter(|s| s.action.is_empty()).collect()
1383 }
1384
1385 pub fn has_action_containing(&self, substr: &str) -> bool {
1389 self.steps.iter().any(|s| s.action.contains(substr))
1390 }
1391
1392 pub fn max_observation_chars(&self) -> usize {
1396 self.steps
1397 .iter()
1398 .map(|s| s.observation.chars().count())
1399 .max()
1400 .unwrap_or(0)
1401 }
1402
1403 pub fn step_index_of_longest_thought(&self) -> Option<usize> {
1408 self.steps
1409 .iter()
1410 .enumerate()
1411 .max_by_key(|(_, s)| s.thought.chars().count())
1412 .map(|(i, _)| i)
1413 }
1414
1415 pub fn observation_word_counts(&self) -> Vec<usize> {
1420 self.steps
1421 .iter()
1422 .map(|s| s.observation.split_whitespace().count())
1423 .collect()
1424 }
1425
1426 pub fn observation_starts_with_any(&self, prefixes: &[&str]) -> bool {
1430 self.steps
1431 .iter()
1432 .any(|s| prefixes.iter().any(|p| s.observation.starts_with(p)))
1433 }
1434
1435 pub fn has_repeated_actions(&self) -> bool {
1440 let mut seen = std::collections::HashSet::new();
1441 self.steps
1442 .iter()
1443 .filter(|s| !s.action.is_empty())
1444 .any(|s| !seen.insert(s.action.as_str()))
1445 }
1446
1447 pub fn thought_starts_with_any(&self, prefixes: &[&str]) -> bool {
1451 self.steps
1452 .iter()
1453 .any(|s| prefixes.iter().any(|p| s.thought.starts_with(p)))
1454 }
1455
1456 pub fn action_word_count(&self) -> usize {
1461 self.steps
1462 .iter()
1463 .map(|s| s.action.split_whitespace().count())
1464 .sum()
1465 }
1466
1467 pub fn steps_above_thought_chars(&self, min: usize) -> usize {
1471 self.steps
1472 .iter()
1473 .filter(|s| s.thought.chars().count() > min)
1474 .count()
1475 }
1476
1477 pub fn steps_with_non_empty_observation(&self) -> Vec<&ReActStep> {
1481 self.steps
1482 .iter()
1483 .filter(|s| !s.observation.is_empty())
1484 .collect()
1485 }
1486
1487 pub fn observations_containing(&self, substr: &str) -> Vec<&ReActStep> {
1491 self.steps
1492 .iter()
1493 .filter(|s| s.observation.contains(substr))
1494 .collect()
1495 }
1496
1497 pub fn thought_observation_ratio(&self) -> f64 {
1503 let obs: usize = self.steps.iter().map(|s| s.observation.chars().count()).sum();
1504 if obs == 0 {
1505 return 0.0;
1506 }
1507 let thoughts: usize = self.steps.iter().map(|s| s.thought.chars().count()).sum();
1508 thoughts as f64 / obs as f64
1509 }
1510
1511 pub fn steps_matching_thought(&self, substr: &str) -> Vec<&ReActStep> {
1515 self.steps
1516 .iter()
1517 .filter(|s| s.thought.contains(substr))
1518 .collect()
1519 }
1520
1521 pub fn median_observation_chars(&self) -> usize {
1526 if self.steps.is_empty() {
1527 return 0;
1528 }
1529 let mut lens: Vec<usize> = self
1530 .steps
1531 .iter()
1532 .map(|s| s.observation.chars().count())
1533 .collect();
1534 lens.sort_unstable();
1535 lens[lens.len() / 2]
1536 }
1537
1538 pub fn cumulative_thought_chars(&self) -> Vec<usize> {
1543 let mut total = 0usize;
1544 self.steps
1545 .iter()
1546 .map(|s| {
1547 total += s.thought.chars().count();
1548 total
1549 })
1550 .collect()
1551 }
1552
1553 pub fn count_steps_with_thought_containing(&self, substr: &str) -> usize {
1557 self.steps
1558 .iter()
1559 .filter(|s| s.thought.contains(substr))
1560 .count()
1561 }
1562
1563 pub fn min_observation_bytes(&self) -> usize {
1569 self.steps
1570 .iter()
1571 .map(|s| s.observation.len())
1572 .filter(|&n| n > 0)
1573 .min()
1574 .unwrap_or(0)
1575 }
1576
1577 pub fn min_thought_bytes(&self) -> usize {
1583 self.steps
1584 .iter()
1585 .map(|s| s.thought.len())
1586 .filter(|&n| n > 0)
1587 .min()
1588 .unwrap_or(0)
1589 }
1590
1591 pub fn proportion_empty_thoughts(&self) -> f64 {
1595 if self.steps.is_empty() {
1596 return 0.0;
1597 }
1598 let empty = self.steps.iter().filter(|s| s.thought.is_empty()).count();
1599 empty as f64 / self.steps.len() as f64
1600 }
1601
1602 pub fn has_failed_steps(&self) -> bool {
1607 self.steps
1608 .iter()
1609 .any(|s| s.observation.starts_with("[error]"))
1610 }
1611
1612 pub fn total_thought_chars(&self) -> usize {
1616 self.steps.iter().map(|s| s.thought.chars().count()).sum()
1617 }
1618
1619 pub fn total_action_chars(&self) -> usize {
1623 self.steps.iter().map(|s| s.action.chars().count()).sum()
1624 }
1625
1626 pub fn total_observation_chars(&self) -> usize {
1631 self.steps.iter().map(|s| s.observation.chars().count()).sum()
1632 }
1633
1634 pub fn action_byte_variance(&self) -> f64 {
1639 if self.steps.len() < 2 {
1640 return 0.0;
1641 }
1642 let lengths: Vec<f64> = self.steps.iter().map(|s| s.action.len() as f64).collect();
1643 let mean = lengths.iter().sum::<f64>() / lengths.len() as f64;
1644 lengths.iter().map(|&l| (l - mean).powi(2)).sum::<f64>() / lengths.len() as f64
1645 }
1646
1647 pub fn non_empty_action_count(&self) -> usize {
1651 self.steps.iter().filter(|s| !s.action.is_empty()).count()
1652 }
1653
1654 pub fn total_step_bytes(&self) -> usize {
1659 self.steps
1660 .iter()
1661 .map(|s| s.thought.len() + s.action.len() + s.observation.len())
1662 .sum()
1663 }
1664
1665 pub fn last_thought_bytes(&self) -> usize {
1668 self.steps.last().map_or(0, |s| s.thought.len())
1669 }
1670
1671 pub fn first_observation_bytes(&self) -> usize {
1674 self.steps.first().map_or(0, |s| s.observation.len())
1675 }
1676
1677 pub fn has_step_with_empty_observation(&self) -> bool {
1682 self.steps.iter().any(|s| s.observation.is_empty())
1683 }
1684
1685 pub fn thought_to_action_byte_ratio(&self) -> f64 {
1689 let thought_bytes: usize = self.steps.iter().map(|s| s.thought.len()).sum();
1690 let action_bytes: usize = self.steps.iter().map(|s| s.action.len()).sum();
1691 if action_bytes == 0 {
1692 return 0.0;
1693 }
1694 thought_bytes as f64 / action_bytes as f64
1695 }
1696
1697 pub fn observation_above_bytes_count(&self, min_bytes: usize) -> usize {
1702 self.steps
1703 .iter()
1704 .filter(|s| s.observation.len() > min_bytes)
1705 .count()
1706 }
1707
1708 pub fn steps_with_both_thought_and_action(&self) -> usize {
1713 self.steps
1714 .iter()
1715 .filter(|s| !s.thought.is_empty() && !s.action.is_empty())
1716 .count()
1717 }
1718
1719 pub fn steps_with_observation_prefix(&self, prefix: &str) -> usize {
1724 self.steps
1725 .iter()
1726 .filter(|s| s.observation.starts_with(prefix))
1727 .count()
1728 }
1729
1730 pub fn observation_bytes_total(&self) -> usize {
1735 self.steps.iter().map(|s| s.observation.len()).sum()
1736 }
1737
1738 pub fn first_thought_chars(&self) -> usize {
1742 self.steps.first().map_or(0, |s| s.thought.chars().count())
1743 }
1744
1745 pub fn last_observation_chars(&self) -> usize {
1749 self.steps.last().map_or(0, |s| s.observation.chars().count())
1750 }
1751
1752 pub fn observation_word_count_total(&self) -> usize {
1756 self.steps
1757 .iter()
1758 .map(|s| s.observation.split_whitespace().count())
1759 .sum()
1760 }
1761
1762 pub fn action_ends_with_count(&self, suffix: &str) -> usize {
1766 self.steps
1767 .iter()
1768 .filter(|s| s.action.ends_with(suffix))
1769 .count()
1770 }
1771
1772 pub fn avg_observation_words(&self) -> f64 {
1776 if self.steps.is_empty() {
1777 return 0.0;
1778 }
1779 let total: usize = self
1780 .steps
1781 .iter()
1782 .map(|s| s.observation.split_whitespace().count())
1783 .sum();
1784 total as f64 / self.steps.len() as f64
1785 }
1786
1787 pub fn thought_byte_variance(&self) -> f64 {
1791 if self.steps.len() < 2 {
1792 return 0.0;
1793 }
1794 let lengths: Vec<f64> = self.steps.iter().map(|s| s.thought.len() as f64).collect();
1795 let mean = lengths.iter().sum::<f64>() / lengths.len() as f64;
1796 lengths.iter().map(|&l| (l - mean).powi(2)).sum::<f64>() / lengths.len() as f64
1797 }
1798
1799 pub fn steps_above_thought_bytes(&self, min_bytes: usize) -> Vec<&ReActStep> {
1803 self.steps
1804 .iter()
1805 .filter(|s| s.thought.len() > min_bytes)
1806 .collect()
1807 }
1808
1809 pub fn total_empty_steps(&self) -> usize {
1814 self.steps
1815 .iter()
1816 .filter(|s| s.thought.is_empty() && s.action.is_empty() && s.observation.is_empty())
1817 .count()
1818 }
1819
1820 pub fn action_starts_with_count(&self, prefix: &str) -> usize {
1824 self.steps
1825 .iter()
1826 .filter(|s| s.action.starts_with(prefix))
1827 .count()
1828 }
1829
1830 pub fn longest_action(&self) -> Option<&str> {
1836 self.steps
1837 .iter()
1838 .max_by_key(|s| s.action.len())
1839 .map(|s| s.action.as_str())
1840 }
1841
1842 pub fn thought_completeness(&self) -> f64 {
1846 if self.steps.is_empty() {
1847 return 0.0;
1848 }
1849 let non_empty = self.steps.iter().filter(|s| !s.thought.is_empty()).count();
1850 non_empty as f64 / self.steps.len() as f64
1851 }
1852
1853 pub fn final_answer_step_index(&self) -> Option<usize> {
1861 self.steps.iter().position(|s| s.is_final_answer())
1862 }
1863
1864 pub fn step_duration_range_ms(&self) -> (u64, u64) {
1868 if self.steps.is_empty() {
1869 return (0, 0);
1870 }
1871 let min = self.steps.iter().map(|s| s.step_duration_ms).min().unwrap_or(0);
1872 let max = self.steps.iter().map(|s| s.step_duration_ms).max().unwrap_or(0);
1873 (min, max)
1874 }
1875
1876 pub fn count_unique_thoughts(&self) -> usize {
1881 let unique: std::collections::HashSet<&str> =
1882 self.steps.iter().map(|s| s.thought.as_str()).collect();
1883 unique.len()
1884 }
1885
1886 pub fn steps_with_empty_thoughts(&self) -> Vec<&ReActStep> {
1890 self.steps.iter().filter(|s| s.thought.is_empty()).collect()
1891 }
1892
1893 pub fn steps_with_long_thoughts(&self, threshold_bytes: usize) -> Vec<&ReActStep> {
1897 self.steps
1898 .iter()
1899 .filter(|s| s.thought.len() > threshold_bytes)
1900 .collect()
1901 }
1902
1903 pub fn action_count_containing(&self, substring: &str) -> usize {
1908 self.steps.iter().filter(|s| s.action.contains(substring)).count()
1909 }
1910
1911 pub fn total_thought_count(&self) -> usize {
1918 self.steps.iter().filter(|s| !s.thought.is_empty()).count()
1919 }
1920
1921 pub fn has_thought_containing(&self, substring: &str) -> bool {
1926 self.steps.iter().any(|s| s.thought.contains(substring))
1927 }
1928
1929 pub fn steps_with_action_length_above(&self, min_bytes: usize) -> Vec<&ReActStep> {
1934 self.steps
1935 .iter()
1936 .filter(|s| s.action.len() > min_bytes)
1937 .collect()
1938 }
1939
1940 #[cfg(feature = "persistence")]
1942 pub async fn save_checkpoint(
1943 &self,
1944 backend: &dyn crate::persistence::PersistenceBackend,
1945 ) -> Result<(), AgentRuntimeError> {
1946 let key = format!("session:{}", self.session_id);
1947 let bytes = serde_json::to_vec(self)
1948 .map_err(|e| AgentRuntimeError::Persistence(format!("serialize: {e}")))?;
1949 backend.save(&key, &bytes).await
1950 }
1951
1952 #[cfg(feature = "persistence")]
1956 pub async fn load_checkpoint(
1957 backend: &dyn crate::persistence::PersistenceBackend,
1958 session_id: &str,
1959 ) -> Result<Option<AgentSession>, AgentRuntimeError> {
1960 let key = format!("session:{session_id}");
1961 match backend.load(&key).await? {
1962 None => Ok(None),
1963 Some(bytes) => {
1964 let session = serde_json::from_slice(&bytes)
1965 .map_err(|e| AgentRuntimeError::Persistence(format!("deserialize: {e}")))?;
1966 Ok(Some(session))
1967 }
1968 }
1969 }
1970
1971 #[cfg(feature = "persistence")]
1978 #[deprecated(since = "1.1.0", note = "Use load_checkpoint_at_step instead")]
1979 pub async fn load_step_checkpoint(
1980 backend: &dyn crate::persistence::PersistenceBackend,
1981 session_id: &str,
1982 step: usize,
1983 ) -> Result<Option<AgentSession>, AgentRuntimeError> {
1984 Self::load_checkpoint_at_step(backend, session_id, step).await
1985 }
1986
1987 #[cfg(feature = "persistence")]
1992 pub async fn load_checkpoint_at_step(
1993 backend: &dyn crate::persistence::PersistenceBackend,
1994 session_id: &str,
1995 step: usize,
1996 ) -> Result<Option<AgentSession>, AgentRuntimeError> {
1997 let key = format!("session:{session_id}:step:{step}");
1998 match backend.load(&key).await? {
1999 None => Ok(None),
2000 Some(bytes) => {
2001 let session = serde_json::from_slice(&bytes)
2002 .map_err(|e| AgentRuntimeError::Persistence(format!("deserialize: {e}")))?;
2003 Ok(Some(session))
2004 }
2005 }
2006 }
2007
2008 pub fn into_steps(self) -> Vec<crate::agent::ReActStep> {
2018 self.steps
2019 }
2020
2021 pub fn iter_steps(&self) -> std::slice::Iter<'_, crate::agent::ReActStep> {
2026 self.steps.iter()
2027 }
2028
2029 pub fn has_at_least_steps(&self, n: usize) -> bool {
2034 self.steps.len() >= n
2035 }
2036
2037 pub fn all_observations_non_empty(&self) -> bool {
2041 self.steps.iter().all(|s| !s.observation.is_empty())
2042 }
2043
2044 pub fn avg_combined_step_bytes(&self) -> f64 {
2049 if self.steps.is_empty() {
2050 return 0.0;
2051 }
2052 let total: usize = self.steps.iter().map(|s| s.combined_byte_length()).sum();
2053 total as f64 / self.steps.len() as f64
2054 }
2055
2056 pub fn shortest_observation_step(&self) -> Option<&ReActStep> {
2061 self.steps.iter().min_by_key(|s| s.observation.len())
2062 }
2063
2064 pub fn unique_observation_count(&self) -> usize {
2068 self.steps
2069 .iter()
2070 .map(|s| s.observation.as_str())
2071 .collect::<std::collections::HashSet<_>>()
2072 .len()
2073 }
2074
2075 pub fn avg_thought_word_count(&self) -> f64 {
2079 if self.steps.is_empty() {
2080 return 0.0;
2081 }
2082 let total: usize = self
2083 .steps
2084 .iter()
2085 .map(|s| s.thought.split_whitespace().count())
2086 .sum();
2087 total as f64 / self.steps.len() as f64
2088 }
2089
2090 pub fn observation_contains_any(&self, terms: &[&str]) -> bool {
2095 if terms.is_empty() {
2096 return false;
2097 }
2098 self.steps
2099 .iter()
2100 .any(|s| terms.iter().any(|t| s.observation.contains(t)))
2101 }
2102
2103 pub fn step_at_index(&self, index: usize) -> Option<&ReActStep> {
2105 self.steps.get(index)
2106 }
2107
2108 pub fn thought_contains_all(&self, terms: &[&str]) -> bool {
2113 if terms.is_empty() {
2114 return false;
2115 }
2116 self.steps
2117 .iter()
2118 .any(|s| terms.iter().all(|t| s.thought.contains(t)))
2119 }
2120
2121 pub fn action_contains_any(&self, terms: &[&str]) -> bool {
2126 if terms.is_empty() {
2127 return false;
2128 }
2129 self.steps
2130 .iter()
2131 .any(|s| terms.iter().any(|t| s.action.contains(t)))
2132 }
2133
2134 pub fn max_thought_chars(&self) -> usize {
2138 self.steps
2139 .iter()
2140 .map(|s| s.thought.chars().count())
2141 .max()
2142 .unwrap_or(0)
2143 }
2144
2145 pub fn min_thought_chars(&self) -> usize {
2150 self.steps
2151 .iter()
2152 .map(|s| s.thought.chars().count())
2153 .filter(|&n| n > 0)
2154 .min()
2155 .unwrap_or(0)
2156 }
2157
2158 pub fn avg_action_chars(&self) -> f64 {
2163 if self.steps.is_empty() {
2164 return 0.0;
2165 }
2166 let total: usize = self.steps.iter().map(|s| s.action.chars().count()).sum();
2167 total as f64 / self.steps.len() as f64
2168 }
2169
2170 pub fn avg_observation_chars(&self) -> f64 {
2175 if self.steps.is_empty() {
2176 return 0.0;
2177 }
2178 let total: usize = self.steps.iter().map(|s| s.observation.chars().count()).sum();
2179 total as f64 / self.steps.len() as f64
2180 }
2181
2182 pub fn step_with_longest_action(&self) -> Option<&ReActStep> {
2187 self.steps.iter().max_by_key(|s| s.action.chars().count())
2188 }
2189
2190 pub fn action_ends_with(&self, suffix: &str) -> bool {
2192 self.steps.iter().any(|s| s.action.ends_with(suffix))
2193 }
2194
2195 pub fn thought_ends_with(&self, suffix: &str) -> bool {
2197 self.steps.iter().any(|s| s.thought.ends_with(suffix))
2198 }
2199
2200 pub fn has_step_with_both(&self, thought_term: &str, action_term: &str) -> bool {
2203 self.steps
2204 .iter()
2205 .any(|s| s.thought.contains(thought_term) && s.action.contains(action_term))
2206 }
2207
2208 pub fn step_count_with_observation_longer_than(&self, min_bytes: usize) -> usize {
2213 self.steps
2214 .iter()
2215 .filter(|s| s.observation.len() > min_bytes)
2216 .count()
2217 }
2218
2219 pub fn thought_word_counts(&self) -> Vec<usize> {
2221 self.steps
2222 .iter()
2223 .map(|s| s.thought.split_whitespace().count())
2224 .collect()
2225 }
2226
2227 pub fn steps_sorted_by_thought_len(&self) -> Vec<&ReActStep> {
2229 let mut sorted: Vec<&ReActStep> = self.steps.iter().collect();
2230 sorted.sort_by_key(|s| s.thought.len());
2231 sorted
2232 }
2233
2234 pub fn steps_with_thought_longer_than(&self, min_bytes: usize) -> Vec<&ReActStep> {
2237 self.steps
2238 .iter()
2239 .filter(|s| s.thought.len() > min_bytes)
2240 .collect()
2241 }
2242
2243 pub fn steps_with_action_containing(&self, substr: &str) -> Vec<&ReActStep> {
2245 self.steps
2246 .iter()
2247 .filter(|s| s.action.contains(substr))
2248 .collect()
2249 }
2250
2251 pub fn observation_max_chars(&self) -> usize {
2254 self.steps
2255 .iter()
2256 .map(|s| s.observation.chars().count())
2257 .max()
2258 .unwrap_or(0)
2259 }
2260
2261 pub fn observation_min_chars(&self) -> usize {
2265 self.steps
2266 .iter()
2267 .map(|s| s.observation.chars().count())
2268 .filter(|&n| n > 0)
2269 .min()
2270 .unwrap_or(0)
2271 }
2272
2273 pub fn action_word_counts(&self) -> Vec<usize> {
2275 self.steps
2276 .iter()
2277 .map(|s| s.action.split_whitespace().count())
2278 .collect()
2279 }
2280
2281 pub fn thought_avg_chars(&self) -> f64 {
2284 if self.steps.is_empty() {
2285 return 0.0;
2286 }
2287 let total: usize = self.steps.iter().map(|s| s.thought.chars().count()).sum();
2288 total as f64 / self.steps.len() as f64
2289 }
2290
2291 pub fn thought_byte_range(&self) -> (usize, usize) {
2295 if self.steps.is_empty() {
2296 return (0, 0);
2297 }
2298 let min = self.steps.iter().map(|s| s.thought.len()).min().unwrap_or(0);
2299 let max = self.steps.iter().map(|s| s.thought.len()).max().unwrap_or(0);
2300 (min, max)
2301 }
2302}
2303
2304pub struct AgentRuntimeBuilder<S = NeedsConfig> {
2321 #[cfg(feature = "memory")]
2322 memory: Option<EpisodicStore>,
2323 #[cfg(feature = "memory")]
2324 working: Option<WorkingMemory>,
2325 #[cfg(feature = "graph")]
2326 graph: Option<GraphStore>,
2327 #[cfg(feature = "orchestrator")]
2328 backpressure: Option<BackpressureGuard>,
2329 agent_config: Option<AgentConfig>,
2330 tools: Vec<Arc<ToolSpec>>,
2331 metrics: Arc<RuntimeMetrics>,
2332 #[cfg(feature = "persistence")]
2333 checkpoint_backend: Option<Arc<dyn crate::persistence::PersistenceBackend>>,
2334 token_estimator: Option<Arc<dyn TokenEstimator>>,
2335 _state: PhantomData<S>,
2336}
2337
2338trait DebugBuilderState {
2343 const NAME: &'static str;
2345 const HAS_CONFIG: bool;
2347}
2348
2349impl DebugBuilderState for NeedsConfig {
2350 const NAME: &'static str = "AgentRuntimeBuilder<NeedsConfig>";
2351 const HAS_CONFIG: bool = false;
2352}
2353
2354impl DebugBuilderState for HasConfig {
2355 const NAME: &'static str = "AgentRuntimeBuilder<HasConfig>";
2356 const HAS_CONFIG: bool = true;
2357}
2358
2359impl<S: DebugBuilderState> std::fmt::Debug for AgentRuntimeBuilder<S> {
2360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2361 let mut s = f.debug_struct(S::NAME);
2362 #[cfg(feature = "memory")]
2363 {
2364 s.field("memory", &self.memory.is_some())
2365 .field("working", &self.working.is_some());
2366 }
2367 #[cfg(feature = "graph")]
2368 s.field("graph", &self.graph.is_some());
2369 #[cfg(feature = "orchestrator")]
2370 s.field("backpressure", &self.backpressure.is_some());
2371 if S::HAS_CONFIG {
2372 s.field("agent_config", &self.agent_config.is_some());
2373 }
2374 s.field("tools", &self.tools.len()).finish()
2375 }
2376}
2377
2378impl Default for AgentRuntimeBuilder<NeedsConfig> {
2379 fn default() -> Self {
2380 Self {
2381 #[cfg(feature = "memory")]
2382 memory: None,
2383 #[cfg(feature = "memory")]
2384 working: None,
2385 #[cfg(feature = "graph")]
2386 graph: None,
2387 #[cfg(feature = "orchestrator")]
2388 backpressure: None,
2389 agent_config: None,
2390 tools: Vec::new(),
2391 metrics: RuntimeMetrics::new(),
2392 #[cfg(feature = "persistence")]
2393 checkpoint_backend: None,
2394 token_estimator: None,
2395 _state: PhantomData,
2396 }
2397 }
2398}
2399
2400impl<S> AgentRuntimeBuilder<S> {
2402 #[cfg(feature = "memory")]
2404 pub fn with_memory(mut self, store: EpisodicStore) -> Self {
2405 self.memory = Some(store);
2406 self
2407 }
2408
2409 #[cfg(feature = "memory")]
2411 pub fn with_working_memory(mut self, wm: WorkingMemory) -> Self {
2412 self.working = Some(wm);
2413 self
2414 }
2415
2416 #[cfg(feature = "graph")]
2418 pub fn with_graph(mut self, graph: GraphStore) -> Self {
2419 self.graph = Some(graph);
2420 self
2421 }
2422
2423 #[cfg(feature = "orchestrator")]
2425 pub fn with_backpressure(mut self, guard: BackpressureGuard) -> Self {
2426 self.backpressure = Some(guard);
2427 self
2428 }
2429
2430 pub fn register_tool(mut self, spec: ToolSpec) -> Self {
2432 self.tools.push(Arc::new(spec));
2433 self
2434 }
2435
2436 pub fn register_tools(mut self, specs: impl IntoIterator<Item = ToolSpec>) -> Self {
2442 for spec in specs {
2443 self.tools.push(Arc::new(spec));
2444 }
2445 self
2446 }
2447
2448 pub fn with_metrics(mut self, metrics: Arc<RuntimeMetrics>) -> Self {
2450 self.metrics = metrics;
2451 self
2452 }
2453
2454 #[cfg(feature = "persistence")]
2456 pub fn with_checkpoint_backend(
2457 mut self,
2458 backend: Arc<dyn crate::persistence::PersistenceBackend>,
2459 ) -> Self {
2460 self.checkpoint_backend = Some(backend);
2461 self
2462 }
2463
2464 pub fn with_token_estimator(mut self, estimator: Arc<dyn TokenEstimator>) -> Self {
2470 self.token_estimator = Some(estimator);
2471 self
2472 }
2473}
2474
2475impl AgentRuntimeBuilder<NeedsConfig> {
2477 pub fn new() -> Self {
2479 Self::default()
2480 }
2481
2482 pub fn with_agent_config(self, config: AgentConfig) -> AgentRuntimeBuilder<HasConfig> {
2487 AgentRuntimeBuilder {
2488 memory: self.memory,
2489 working: self.working,
2490 #[cfg(feature = "graph")]
2491 graph: self.graph,
2492 #[cfg(feature = "orchestrator")]
2493 backpressure: self.backpressure,
2494 agent_config: Some(config),
2495 tools: self.tools,
2496 metrics: self.metrics,
2497 #[cfg(feature = "persistence")]
2498 checkpoint_backend: self.checkpoint_backend,
2499 token_estimator: self.token_estimator,
2500 _state: PhantomData,
2501 }
2502 }
2503}
2504
2505impl AgentRuntimeBuilder<HasConfig> {
2507 pub fn build(self) -> AgentRuntime {
2511 #[allow(clippy::unwrap_used)]
2514 let agent_config = self.agent_config.unwrap();
2515
2516 AgentRuntime {
2517 #[cfg(feature = "memory")]
2518 memory: self.memory,
2519 #[cfg(feature = "memory")]
2520 working: self.working,
2521 #[cfg(feature = "graph")]
2522 graph: self.graph,
2523 #[cfg(feature = "orchestrator")]
2524 backpressure: self.backpressure,
2525 agent_config,
2526 tools: self.tools,
2527 metrics: self.metrics,
2528 token_estimator: self
2529 .token_estimator
2530 .unwrap_or_else(|| Arc::new(CharDivTokenEstimator)),
2531 #[cfg(feature = "persistence")]
2532 checkpoint_backend: self.checkpoint_backend,
2533 }
2534 }
2535}
2536
2537pub trait TokenEstimator: Send + Sync {
2554 fn count_tokens(&self, text: &str) -> usize;
2556}
2557
2558pub struct CharDivTokenEstimator;
2560
2561impl TokenEstimator for CharDivTokenEstimator {
2562 fn count_tokens(&self, text: &str) -> usize {
2563 (text.len() / 4).max(1)
2564 }
2565}
2566
2567pub struct AgentRuntime {
2571 #[cfg(feature = "memory")]
2572 memory: Option<EpisodicStore>,
2573 #[cfg(feature = "memory")]
2574 working: Option<WorkingMemory>,
2575 #[cfg(feature = "graph")]
2576 graph: Option<GraphStore>,
2577 #[cfg(feature = "orchestrator")]
2578 backpressure: Option<BackpressureGuard>,
2579 agent_config: AgentConfig,
2580 tools: Vec<Arc<ToolSpec>>,
2581 metrics: Arc<RuntimeMetrics>,
2582 #[cfg(feature = "persistence")]
2583 checkpoint_backend: Option<Arc<dyn crate::persistence::PersistenceBackend>>,
2584 token_estimator: Arc<dyn TokenEstimator>,
2585}
2586
2587impl std::fmt::Debug for AgentRuntime {
2588 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2589 let mut s = f.debug_struct("AgentRuntime");
2590 s.field("memory", &self.memory.is_some())
2591 .field("working", &self.working.is_some());
2592 #[cfg(feature = "graph")]
2593 s.field("graph", &self.graph.is_some());
2594 #[cfg(feature = "orchestrator")]
2595 s.field("backpressure", &self.backpressure.is_some());
2596 s.field("tools", &self.tools.len());
2597 #[cfg(feature = "persistence")]
2598 s.field("checkpoint_backend", &self.checkpoint_backend.is_some());
2599 s.finish()
2600 }
2601}
2602
2603impl AgentRuntime {
2604 pub fn builder() -> AgentRuntimeBuilder<NeedsConfig> {
2606 AgentRuntimeBuilder::new()
2607 }
2608
2609 pub fn quick(max_iterations: usize, model: impl Into<String>) -> Self {
2611 AgentRuntime::builder()
2612 .with_agent_config(AgentConfig::new(max_iterations, model))
2613 .build()
2614 }
2615
2616 pub fn metrics(&self) -> Arc<RuntimeMetrics> {
2618 Arc::clone(&self.metrics)
2619 }
2620
2621 #[tracing::instrument(skip(self, infer), fields(agent_id = %agent_id))]
2634 pub async fn run_agent<F, Fut>(
2635 &self,
2636 agent_id: AgentId,
2637 prompt: &str,
2638 infer: F,
2639 ) -> Result<AgentSession, AgentRuntimeError>
2640 where
2641 F: FnMut(String) -> Fut,
2642 Fut: std::future::Future<Output = String>,
2643 {
2644 #[cfg(feature = "orchestrator")]
2647 {
2648 let backpressure_result = if let Some(ref guard) = self.backpressure {
2649 guard.try_acquire()
2650 } else {
2651 Ok(())
2652 };
2653 if let Err(e) = backpressure_result {
2654 tracing::warn!(agent_id = %agent_id, error = %e, "backpressure shed: rejecting session");
2655 self.metrics
2656 .backpressure_shed_count
2657 .fetch_add(1, Ordering::Relaxed);
2658 return Err(e);
2659 }
2660 }
2661
2662 self.metrics.total_sessions.fetch_add(1, Ordering::Relaxed);
2663 self.metrics.active_sessions.fetch_add(1, Ordering::Relaxed);
2664
2665 tracing::info!(agent_id = %agent_id, "agent session starting");
2666 let outcome = self.run_agent_inner(agent_id.clone(), prompt, infer).await;
2667
2668 #[cfg(feature = "orchestrator")]
2670 if let Some(ref guard) = self.backpressure {
2671 let _ = guard.release();
2672 }
2673
2674 let _ = self.metrics.active_sessions.fetch_update(
2677 Ordering::Relaxed,
2678 Ordering::Relaxed,
2679 |v| Some(v.saturating_sub(1)),
2680 );
2681
2682 match &outcome {
2683 Ok(session) => {
2684 tracing::info!(
2685 agent_id = %agent_id,
2686 session_id = %session.session_id,
2687 steps = session.step_count(),
2688 duration_ms = session.duration_ms,
2689 "agent session completed"
2690 );
2691 self.metrics
2692 .total_steps
2693 .fetch_add(session.step_count() as u64, Ordering::Relaxed);
2694 }
2695 Err(e) => {
2696 tracing::error!(agent_id = %agent_id, error = %e, "agent session failed");
2697 }
2698 }
2699
2700 outcome
2701 }
2702
2703 #[tracing::instrument(skip(self, infer), fields(agent_id = %agent_id, session_id = tracing::field::Empty))]
2705 async fn run_agent_inner<F, Fut>(
2706 &self,
2707 agent_id: AgentId,
2708 prompt: &str,
2709 infer: F,
2710 ) -> Result<AgentSession, AgentRuntimeError>
2711 where
2712 F: FnMut(String) -> Fut,
2713 Fut: std::future::Future<Output = String>,
2714 {
2715 let start = Instant::now();
2716 let session_id = uuid::Uuid::new_v4().to_string();
2717
2718 let mut memory_hits = 0usize;
2719 let mut graph_lookups = 0usize;
2720
2721 #[cfg(feature = "memory")]
2723 let enriched_prompt = if let Some(ref store) = self.memory {
2724 let memories = store.recall(&agent_id, self.agent_config.max_memory_recalls)?;
2725
2726 let memories = if let Some(token_budget) = self.agent_config.max_memory_tokens {
2728 let mut used = 0usize;
2729 memories
2730 .into_iter()
2731 .filter(|m| {
2732 let tokens = self.token_estimator.count_tokens(&m.content);
2733 if used + tokens <= token_budget {
2734 used += tokens;
2735 true
2736 } else {
2737 false
2738 }
2739 })
2740 .collect::<Vec<_>>()
2741 } else {
2742 memories
2743 };
2744
2745 memory_hits = memories.len();
2746 self.metrics
2747 .memory_recall_count
2748 .fetch_add(1, Ordering::Relaxed);
2749
2750 if let Some(budget) = self.agent_config.max_memory_tokens {
2751 tracing::debug!(
2752 "memory token budget: {budget}, injecting {} items",
2753 memory_hits
2754 );
2755 } else {
2756 tracing::debug!("enriched prompt with {} memory items", memory_hits);
2757 }
2758
2759 if memories.is_empty() {
2760 prompt.to_owned()
2761 } else {
2762 let mut enriched =
2765 String::with_capacity(prompt.len() + memories.len() * 64 + 32);
2766 enriched.push_str("Relevant memories:\n");
2767 for m in &memories {
2768 let _ = writeln!(enriched, "- {}", m.content);
2769 }
2770 let _ = write!(enriched, "\nCurrent prompt: {prompt}");
2771 enriched
2772 }
2773 } else {
2774 prompt.to_owned()
2775 };
2776 #[cfg(not(feature = "memory"))]
2777 let enriched_prompt = prompt.to_owned();
2778
2779 #[cfg(feature = "memory")]
2781 let enriched_prompt = if let Some(ref wm) = self.working {
2782 let entries = wm.entries()?;
2783 if entries.is_empty() {
2784 enriched_prompt
2785 } else {
2786 let mut out = String::with_capacity(
2788 enriched_prompt.len() + entries.len() * 32 + 32,
2789 );
2790 out.push_str(&enriched_prompt);
2791 out.push_str("\n\nCurrent working state:\n");
2792 for (k, v) in &entries {
2793 let _ = writeln!(out, " {k}: {v}");
2794 }
2795 if out.ends_with('\n') {
2797 out.pop();
2798 }
2799 out
2800 }
2801 } else {
2802 enriched_prompt
2803 };
2804
2805 #[cfg(feature = "graph")]
2807 if let Some(ref graph) = self.graph {
2808 graph_lookups = graph.entity_count()?;
2809 tracing::debug!("graph has {} entities", graph_lookups);
2810 }
2811
2812 let mut react_loop = ReActLoop::new(self.agent_config.clone())
2818 .with_metrics(Arc::clone(&self.metrics));
2819
2820 #[cfg(feature = "persistence")]
2822 if let Some(ref backend) = self.checkpoint_backend {
2823 react_loop = react_loop
2824 .with_step_checkpoint(Arc::clone(backend), session_id.clone());
2825 }
2826
2827 for tool in &self.tools {
2828 let tool_arc = Arc::clone(tool);
2829 let required_fields = tool_arc.required_fields.clone();
2830 #[cfg(feature = "orchestrator")]
2831 let circuit_breaker = tool_arc.circuit_breaker.clone();
2832
2833 let mut spec = ToolSpec::new_async(
2834 tool_arc.name.clone(),
2835 tool_arc.description.clone(),
2836 move |args| {
2837 let t = Arc::clone(&tool_arc);
2838 Box::pin(async move { t.call(args).await })
2839 },
2840 )
2841 .with_required_fields(required_fields);
2842
2843 #[cfg(feature = "orchestrator")]
2844 if let Some(cb) = circuit_breaker {
2845 spec = spec.with_circuit_breaker(cb);
2846 }
2847
2848 react_loop.register_tool(spec);
2849 }
2850
2851 tracing::Span::current().record("session_id", &session_id.as_str());
2854
2855 let steps = react_loop.run(&enriched_prompt, infer).await?;
2856 let duration_ms = start.elapsed().as_millis() as u64;
2857
2858 #[cfg(feature = "persistence")]
2860 let mut ckpt_errors: Vec<String> = Vec::new();
2861
2862 #[cfg(feature = "persistence")]
2864 if let Some(ref backend) = self.checkpoint_backend {
2865 tracing::info!(session_id = %session_id, "saving session checkpoint");
2866
2867 let tmp = AgentSession {
2869 session_id: session_id.clone(),
2870 agent_id: agent_id.clone(),
2871 steps: steps.clone(),
2872 memory_hits,
2873 graph_lookups,
2874 duration_ms,
2875 checkpoint_errors: vec![],
2876 };
2877 tmp.save_checkpoint(backend.as_ref()).await?;
2878
2879 for i in 1..=steps.len() {
2881 let partial = AgentSession {
2882 session_id: session_id.clone(),
2883 agent_id: agent_id.clone(),
2884 steps: steps[..i].to_vec(),
2885 memory_hits,
2886 graph_lookups,
2887 duration_ms,
2888 checkpoint_errors: vec![],
2889 };
2890 let key = format!("session:{session_id}:step:{i}");
2891 match serde_json::to_vec(&partial) {
2892 Ok(bytes) => {
2893 if let Err(e) = backend.save(&key, &bytes).await {
2894 let msg = format!("session:{session_id} step:{i} save: {e}");
2895 tracing::warn!("{}", msg);
2896 ckpt_errors.push(msg);
2897 }
2898 }
2899 Err(e) => {
2900 let msg =
2901 format!("session:{session_id} step:{i} serialise: {e}");
2902 tracing::warn!("{}", msg);
2903 ckpt_errors.push(msg);
2904 }
2905 }
2906 }
2907 }
2908
2909 let session = AgentSession {
2910 session_id,
2911 agent_id,
2912 steps,
2913 memory_hits,
2914 graph_lookups,
2915 duration_ms,
2916 #[cfg(feature = "persistence")]
2917 checkpoint_errors: ckpt_errors,
2918 #[cfg(not(feature = "persistence"))]
2919 checkpoint_errors: vec![],
2920 };
2921
2922 Ok(session)
2923 }
2924
2925 #[cfg(feature = "memory")]
2927 pub fn memory(&self) -> Option<&EpisodicStore> {
2928 self.memory.as_ref()
2929 }
2930
2931 #[cfg(feature = "graph")]
2933 pub fn graph(&self) -> Option<&GraphStore> {
2934 self.graph.as_ref()
2935 }
2936
2937 #[cfg(feature = "memory")]
2939 pub fn working_memory(&self) -> Option<&WorkingMemory> {
2940 self.working.as_ref()
2941 }
2942
2943 #[cfg(feature = "memory")]
2945 pub fn has_memory(&self) -> bool {
2946 self.memory.is_some()
2947 }
2948
2949 #[cfg(feature = "graph")]
2951 pub fn has_graph(&self) -> bool {
2952 self.graph.is_some()
2953 }
2954
2955 #[cfg(feature = "memory")]
2957 pub fn has_working_memory(&self) -> bool {
2958 self.working.is_some()
2959 }
2960
2961 pub fn has_active_sessions(&self) -> bool {
2965 self.metrics
2966 .active_sessions
2967 .load(std::sync::atomic::Ordering::Relaxed)
2968 > 0
2969 }
2970
2971 pub fn tool_count(&self) -> usize {
2976 self.tools.len()
2977 }
2978
2979 pub fn tool_names(&self) -> Vec<&str> {
2984 let mut names: Vec<&str> = self.tools.iter().map(|t| t.name.as_str()).collect();
2985 names.sort_unstable();
2986 names
2987 }
2988
2989 pub fn registered_tool_names(&self) -> Vec<String> {
2996 let mut names: Vec<String> =
2997 self.tools.iter().map(|t| t.name.clone()).collect();
2998 names.sort_unstable();
2999 names
3000 }
3001
3002 pub fn config(&self) -> &AgentConfig {
3004 &self.agent_config
3005 }
3006
3007 pub fn model_name(&self) -> &str {
3009 &self.agent_config.model
3010 }
3011
3012 pub fn session_max_iterations(&self) -> usize {
3015 self.agent_config.max_iterations
3016 }
3017
3018 pub fn is_registered_tool(&self, name: &str) -> bool {
3021 self.tools.iter().any(|t| t.name == name)
3022 }
3023
3024 pub async fn shutdown(&self) {
3032 tracing::info!("AgentRuntime shutting down");
3033 tracing::info!(
3034 active_sessions = self.metrics.active_sessions(),
3035 total_sessions = self.metrics.total_sessions(),
3036 total_steps = self.metrics.total_steps(),
3037 total_tool_calls = self.metrics.total_tool_calls(),
3038 failed_tool_calls = self.metrics.failed_tool_calls(),
3039 "final metrics snapshot on shutdown"
3040 );
3041
3042 #[cfg(feature = "persistence")]
3043 if let Some(ref backend) = self.checkpoint_backend {
3044 let ts = chrono::Utc::now().to_rfc3339();
3045 match backend.save("runtime:shutdown", ts.as_bytes()).await {
3046 Ok(()) => tracing::debug!("shutdown sentinel saved"),
3047 Err(e) => tracing::warn!(error = %e, "failed to save shutdown sentinel"),
3048 }
3049 }
3050
3051 tracing::info!("AgentRuntime shutdown complete");
3052 }
3053
3054 #[cfg(feature = "providers")]
3064 pub async fn run_agent_with_provider(
3065 &self,
3066 agent_id: AgentId,
3067 prompt: &str,
3068 provider: std::sync::Arc<dyn crate::providers::LlmProvider>,
3069 ) -> Result<AgentSession, AgentRuntimeError> {
3070 let model = self.agent_config.model.clone();
3071 self.run_agent(agent_id, prompt, |ctx| {
3072 let provider = provider.clone();
3073 let model = model.clone();
3074 async move {
3075 provider
3076 .complete(&ctx, &model)
3077 .await
3078 .unwrap_or_else(|e| format!("FINAL ANSWER: inference error: {e}"))
3079 }
3080 })
3081 .await
3082 }
3083}
3084
3085#[cfg(test)]
3088mod tests {
3089 use super::*;
3090 use crate::graph::{Entity, GraphStore, Relationship};
3091 use crate::memory::{EpisodicStore, WorkingMemory};
3092
3093 fn simple_config() -> AgentConfig {
3094 AgentConfig::new(5, "test")
3095 }
3096
3097 async fn final_answer_infer(_ctx: String) -> String {
3098 "Thought: done\nAction: FINAL_ANSWER 42".into()
3099 }
3100
3101 #[tokio::test]
3112 async fn test_builder_with_config_compiles() {
3113 let _runtime = AgentRuntime::builder()
3114 .with_agent_config(simple_config())
3115 .build();
3116 }
3118
3119 #[tokio::test]
3120 async fn test_builder_succeeds_with_minimal_config() {
3121 let _runtime = AgentRuntime::builder()
3122 .with_agent_config(simple_config())
3123 .build();
3124 }
3125
3126 #[tokio::test]
3127 async fn test_builder_with_all_subsystems() {
3128 let _runtime = AgentRuntime::builder()
3129 .with_agent_config(simple_config())
3130 .with_memory(EpisodicStore::new())
3131 .with_graph(GraphStore::new())
3132 .with_working_memory(WorkingMemory::new(10).unwrap())
3133 .with_backpressure(BackpressureGuard::new(5).unwrap())
3134 .build();
3135 }
3136
3137 #[tokio::test]
3138 async fn test_builder_produces_runtime_with_config() {
3139 let runtime = AgentRuntime::builder()
3142 .with_agent_config(simple_config())
3143 .build();
3144 let session = runtime
3145 .run_agent(AgentId::new("agent-x"), "hello", final_answer_infer)
3146 .await
3147 .unwrap();
3148 assert!(session.step_count() >= 1);
3149 assert!(!session.session_id.is_empty());
3150 }
3151
3152 #[tokio::test]
3155 async fn test_run_agent_returns_session_with_steps() {
3156 let runtime = AgentRuntime::builder()
3157 .with_agent_config(simple_config())
3158 .build();
3159
3160 let session = runtime
3161 .run_agent(AgentId::new("agent-1"), "hello", final_answer_infer)
3162 .await
3163 .unwrap();
3164
3165 assert_eq!(session.step_count(), 1);
3166 }
3167
3168 #[tokio::test]
3169 async fn test_run_agent_session_has_agent_id() {
3170 let runtime = AgentRuntime::builder()
3171 .with_agent_config(simple_config())
3172 .build();
3173
3174 let session = runtime
3175 .run_agent(AgentId::new("agent-42"), "hello", final_answer_infer)
3176 .await
3177 .unwrap();
3178
3179 assert_eq!(session.agent_id.0, "agent-42");
3180 }
3181
3182 #[tokio::test]
3183 async fn test_run_agent_session_duration_is_set() {
3184 let runtime = AgentRuntime::builder()
3185 .with_agent_config(simple_config())
3186 .build();
3187
3188 let session = runtime
3189 .run_agent(AgentId::new("a"), "hello", final_answer_infer)
3190 .await
3191 .unwrap();
3192
3193 let _ = session.duration_ms; }
3196
3197 #[tokio::test]
3198 async fn test_run_agent_session_has_session_id() {
3199 let runtime = AgentRuntime::builder()
3200 .with_agent_config(simple_config())
3201 .build();
3202
3203 let session = runtime
3204 .run_agent(AgentId::new("a"), "hello", final_answer_infer)
3205 .await
3206 .unwrap();
3207
3208 assert!(!session.session_id.is_empty());
3210 assert_eq!(session.session_id.len(), 36); }
3212
3213 #[tokio::test]
3214 async fn test_run_agent_memory_hits_zero_without_memory() {
3215 let runtime = AgentRuntime::builder()
3216 .with_agent_config(simple_config())
3217 .build();
3218
3219 let session = runtime
3220 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3221 .await
3222 .unwrap();
3223
3224 assert_eq!(session.memory_hits, 0);
3225 }
3226
3227 #[tokio::test]
3228 async fn test_run_agent_memory_hits_counts_recalled_items() {
3229 let store = EpisodicStore::new();
3230 let agent = AgentId::new("mem-agent");
3231 store
3232 .add_episode(agent.clone(), "remembered fact", 0.8)
3233 .unwrap();
3234
3235 let runtime = AgentRuntime::builder()
3236 .with_agent_config(simple_config())
3237 .with_memory(store)
3238 .build();
3239
3240 let session = runtime
3241 .run_agent(agent, "prompt", final_answer_infer)
3242 .await
3243 .unwrap();
3244
3245 assert_eq!(session.memory_hits, 1);
3246 }
3247
3248 #[tokio::test]
3249 async fn test_run_agent_graph_lookups_counts_entities() {
3250 let graph = GraphStore::new();
3251 graph.add_entity(Entity::new("e1", "Node")).unwrap();
3252 graph.add_entity(Entity::new("e2", "Node")).unwrap();
3253
3254 let runtime = AgentRuntime::builder()
3255 .with_agent_config(simple_config())
3256 .with_graph(graph)
3257 .build();
3258
3259 let session = runtime
3260 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3261 .await
3262 .unwrap();
3263
3264 assert_eq!(session.graph_lookups, 2);
3265 }
3266
3267 #[tokio::test]
3268 async fn test_run_agent_backpressure_released_after_run() {
3269 let guard = BackpressureGuard::new(3).unwrap();
3270
3271 let runtime = AgentRuntime::builder()
3272 .with_agent_config(simple_config())
3273 .with_backpressure(guard.clone())
3274 .build();
3275
3276 runtime
3277 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3278 .await
3279 .unwrap();
3280
3281 assert_eq!(guard.depth().unwrap(), 0);
3282 }
3283
3284 #[tokio::test]
3285 async fn test_run_agent_backpressure_sheds_when_full() {
3286 let guard = BackpressureGuard::new(1).unwrap();
3287 guard.try_acquire().unwrap(); let runtime = AgentRuntime::builder()
3290 .with_agent_config(simple_config())
3291 .with_backpressure(guard)
3292 .build();
3293
3294 let result = runtime
3295 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3296 .await;
3297 assert!(matches!(
3298 result,
3299 Err(AgentRuntimeError::BackpressureShed { .. })
3300 ));
3301 }
3302
3303 #[tokio::test]
3304 async fn test_run_agent_max_iterations_error_propagated() {
3305 let cfg = AgentConfig::new(2, "model");
3306 let runtime = AgentRuntime::builder().with_agent_config(cfg).build();
3307
3308 let result = runtime
3310 .run_agent(AgentId::new("a"), "prompt", |_ctx: String| async {
3311 "Thought: looping\nAction: FINAL_ANSWER done".to_string()
3312 })
3313 .await;
3314 assert!(result.is_ok()); }
3316
3317 #[tokio::test]
3318 async fn test_agent_session_step_count_matches_steps() {
3319 let session = AgentSession {
3320 session_id: "test-session-id".into(),
3321 agent_id: AgentId::new("a"),
3322 steps: vec![
3323 ReActStep {
3324 thought: "t".into(),
3325 action: "a".into(),
3326 observation: "o".into(),
3327 step_duration_ms: 0,
3328 },
3329 ReActStep {
3330 thought: "t2".into(),
3331 action: "FINAL_ANSWER".into(),
3332 observation: "done".into(),
3333 step_duration_ms: 0,
3334 },
3335 ],
3336 memory_hits: 0,
3337 graph_lookups: 0,
3338 duration_ms: 10,
3339 checkpoint_errors: vec![],
3340 };
3341 assert_eq!(session.step_count(), 2);
3342 }
3343
3344 #[tokio::test]
3347 async fn test_runtime_memory_accessor_returns_none_when_not_configured() {
3348 let runtime = AgentRuntime::builder()
3349 .with_agent_config(simple_config())
3350 .build();
3351 assert!(runtime.memory().is_none());
3352 }
3353
3354 #[tokio::test]
3355 async fn test_runtime_memory_accessor_returns_some_when_configured() {
3356 let runtime = AgentRuntime::builder()
3357 .with_agent_config(simple_config())
3358 .with_memory(EpisodicStore::new())
3359 .build();
3360 assert!(runtime.memory().is_some());
3361 }
3362
3363 #[tokio::test]
3364 async fn test_runtime_graph_accessor_returns_none_when_not_configured() {
3365 let runtime = AgentRuntime::builder()
3366 .with_agent_config(simple_config())
3367 .build();
3368 assert!(runtime.graph().is_none());
3369 }
3370
3371 #[tokio::test]
3372 async fn test_runtime_graph_accessor_returns_some_when_configured() {
3373 let runtime = AgentRuntime::builder()
3374 .with_agent_config(simple_config())
3375 .with_graph(GraphStore::new())
3376 .build();
3377 assert!(runtime.graph().is_some());
3378 }
3379
3380 #[tokio::test]
3381 async fn test_runtime_working_memory_accessor() {
3382 let runtime = AgentRuntime::builder()
3383 .with_agent_config(simple_config())
3384 .with_working_memory(WorkingMemory::new(5).unwrap())
3385 .build();
3386 assert!(runtime.working_memory().is_some());
3387 }
3388
3389 #[tokio::test]
3390 async fn test_runtime_with_tool_registered() {
3391 let runtime = AgentRuntime::builder()
3392 .with_agent_config(simple_config())
3393 .register_tool(ToolSpec::new("calc", "math", |_| serde_json::json!(99)))
3394 .build();
3395
3396 let mut call_count = 0;
3397 let session = runtime
3398 .run_agent(AgentId::new("a"), "compute", move |_ctx: String| {
3399 call_count += 1;
3400 let count = call_count;
3401 async move {
3402 if count == 1 {
3403 "Thought: use calc\nAction: calc {}".into()
3404 } else {
3405 "Thought: done\nAction: FINAL_ANSWER result".into()
3406 }
3407 }
3408 })
3409 .await
3410 .unwrap();
3411
3412 assert!(session.step_count() >= 1);
3413 }
3414
3415 #[tokio::test]
3416 async fn test_run_agent_with_graph_relationship_lookup() {
3417 let graph = GraphStore::new();
3418 graph.add_entity(Entity::new("a", "X")).unwrap();
3419 graph.add_entity(Entity::new("b", "Y")).unwrap();
3420 graph
3421 .add_relationship(Relationship::new("a", "b", "LINKS", 1.0))
3422 .unwrap();
3423
3424 let runtime = AgentRuntime::builder()
3425 .with_agent_config(simple_config())
3426 .with_graph(graph)
3427 .build();
3428
3429 let session = runtime
3430 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3431 .await
3432 .unwrap();
3433
3434 assert_eq!(session.graph_lookups, 2); }
3436
3437 #[tokio::test]
3440 async fn test_metrics_active_sessions_decrements_after_run() {
3441 let runtime = AgentRuntime::builder()
3442 .with_agent_config(simple_config())
3443 .build();
3444
3445 runtime
3446 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3447 .await
3448 .unwrap();
3449
3450 assert_eq!(runtime.metrics().active_sessions(), 0);
3451 }
3452
3453 #[tokio::test]
3454 async fn test_metrics_total_sessions_increments() {
3455 let runtime = AgentRuntime::builder()
3456 .with_agent_config(simple_config())
3457 .build();
3458
3459 runtime
3460 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3461 .await
3462 .unwrap();
3463 runtime
3464 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3465 .await
3466 .unwrap();
3467
3468 assert_eq!(runtime.metrics().total_sessions(), 2);
3469 }
3470
3471 #[tokio::test]
3472 async fn test_metrics_backpressure_shed_increments_on_shed() {
3473 let guard = BackpressureGuard::new(1).unwrap();
3474 guard.try_acquire().unwrap(); let runtime = AgentRuntime::builder()
3477 .with_agent_config(simple_config())
3478 .with_backpressure(guard)
3479 .build();
3480
3481 let _ = runtime
3482 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
3483 .await;
3484
3485 assert_eq!(runtime.metrics().backpressure_shed_count(), 1);
3486 }
3487
3488 #[tokio::test]
3489 async fn test_metrics_memory_recall_count_increments() {
3490 let store = EpisodicStore::new();
3491 let agent = AgentId::new("a");
3492 store.add_episode(agent.clone(), "fact", 0.9).unwrap();
3493
3494 let runtime = AgentRuntime::builder()
3495 .with_agent_config(simple_config())
3496 .with_memory(store)
3497 .build();
3498
3499 runtime
3500 .run_agent(agent, "prompt", final_answer_infer)
3501 .await
3502 .unwrap();
3503
3504 assert_eq!(runtime.metrics().memory_recall_count(), 1);
3505 }
3506
3507 #[tokio::test]
3510 async fn test_agent_config_max_memory_tokens_limits_injection() {
3511 let store = EpisodicStore::new();
3512 let agent = AgentId::new("budget-agent");
3513 for i in 0..5 {
3515 let content = format!("{:0>100}", i); store.add_episode(agent.clone(), content, 0.9).unwrap();
3517 }
3518
3519 let cfg = AgentConfig::new(5, "test").with_max_memory_tokens(10);
3521 let runtime = AgentRuntime::builder()
3522 .with_agent_config(cfg)
3523 .with_memory(store)
3524 .build();
3525
3526 let session = runtime
3527 .run_agent(agent, "prompt", final_answer_infer)
3528 .await
3529 .unwrap();
3530
3531 assert!(
3532 session.memory_hits <= 1,
3533 "expected at most 1 memory hit with tight token budget, got {}",
3534 session.memory_hits
3535 );
3536 }
3537
3538 #[tokio::test]
3541 async fn test_working_memory_injected_into_prompt() {
3542 let wm = WorkingMemory::new(10).unwrap();
3543 wm.set("task", "write tests").unwrap();
3544 wm.set("status", "in progress").unwrap();
3545
3546 let runtime = AgentRuntime::builder()
3547 .with_agent_config(simple_config())
3548 .with_working_memory(wm)
3549 .build();
3550
3551 let mut captured_ctx: Option<String> = None;
3552 let captured_ref = &mut captured_ctx;
3553
3554 runtime
3555 .run_agent(AgentId::new("a"), "do stuff", |ctx: String| {
3556 *captured_ref = Some(ctx.clone());
3557 async move { "Thought: done\nAction: FINAL_ANSWER ok".to_string() }
3558 })
3559 .await
3560 .unwrap();
3561
3562 let ctx = captured_ctx.expect("infer should have been called");
3563 assert!(
3564 ctx.contains("Current working state:"),
3565 "expected working memory injection in context, got: {ctx}"
3566 );
3567 assert!(ctx.contains("task: write tests"));
3568 assert!(ctx.contains("status: in progress"));
3569 }
3570
3571 #[tokio::test]
3574 async fn test_token_budget_zero_returns_no_memories() {
3575 let store = EpisodicStore::new();
3577 let agent = AgentId::new("budget-agent");
3578 store.add_episode(agent.clone(), "short", 0.9).unwrap();
3579
3580 let mut config = AgentConfig::new(5, "test-model");
3581 config.max_memory_tokens = Some(0);
3582 config.max_memory_recalls = 10;
3583
3584 let runtime = AgentRuntime::builder()
3585 .with_memory(store)
3586 .with_agent_config(config)
3587 .build();
3588
3589 let steps = runtime
3590 .run_agent(
3591 agent,
3592 "test",
3593 |_ctx| async { "Thought: ok\nAction: FINAL_ANSWER done".to_string() },
3594 )
3595 .await
3596 .unwrap();
3597
3598 assert_eq!(steps.steps.len(), 1);
3600 }
3601
3602 #[tokio::test]
3603 async fn test_token_budget_smaller_than_smallest_item_returns_no_memories() {
3604 let store = EpisodicStore::new();
3605 let agent = AgentId::new("budget-agent2");
3606 store
3608 .add_episode(agent.clone(), "a".repeat(40), 0.9)
3609 .unwrap();
3610
3611 let mut config = AgentConfig::new(5, "test-model");
3612 config.max_memory_tokens = Some(1);
3613 config.max_memory_recalls = 10;
3614
3615 let runtime = AgentRuntime::builder()
3616 .with_memory(store)
3617 .with_agent_config(config)
3618 .build();
3619
3620 let session = runtime
3621 .run_agent(
3622 agent,
3623 "test",
3624 |_ctx| async { "Thought: ok\nAction: FINAL_ANSWER done".to_string() },
3625 )
3626 .await
3627 .unwrap();
3628
3629 assert_eq!(session.memory_hits, 0);
3630 }
3631
3632 #[tokio::test]
3635 async fn test_agent_runtime_quick_runs_agent() {
3636 let runtime = AgentRuntime::quick(5, "test-model");
3637 let agent = AgentId::new("quick-agent");
3638 let session = runtime
3639 .run_agent(agent, "hello", |_ctx| async {
3640 "Thought: done\nAction: FINAL_ANSWER ok".to_string()
3641 })
3642 .await
3643 .unwrap();
3644 assert_eq!(session.step_count(), 1);
3645 }
3646
3647 #[test]
3650 fn test_final_answer_extracts_text() {
3651 let session = AgentSession {
3652 session_id: "s".into(),
3653 agent_id: AgentId::new("a"),
3654 steps: vec![ReActStep {
3655 thought: "done".into(),
3656 action: "FINAL_ANSWER Paris".into(),
3657 observation: "".into(),
3658 step_duration_ms: 0,
3659 }],
3660 memory_hits: 0,
3661 graph_lookups: 0,
3662 duration_ms: 0,
3663 checkpoint_errors: vec![],
3664 };
3665 assert_eq!(session.final_answer(), Some("Paris".to_string()));
3666 }
3667
3668 #[test]
3669 fn test_final_answer_returns_none_without_final_step() {
3670 let session = AgentSession {
3671 session_id: "s".into(),
3672 agent_id: AgentId::new("a"),
3673 steps: vec![ReActStep {
3674 thought: "thinking".into(),
3675 action: "search {}".into(),
3676 observation: "result".into(),
3677 step_duration_ms: 0,
3678 }],
3679 memory_hits: 0,
3680 graph_lookups: 0,
3681 duration_ms: 0,
3682 checkpoint_errors: vec![],
3683 };
3684 assert_eq!(session.final_answer(), None);
3685
3686 let empty_session = AgentSession {
3687 session_id: "s2".into(),
3688 agent_id: AgentId::new("a"),
3689 steps: vec![],
3690 memory_hits: 0,
3691 graph_lookups: 0,
3692 duration_ms: 0,
3693 checkpoint_errors: vec![],
3694 };
3695 assert_eq!(empty_session.final_answer(), None);
3696 }
3697
3698 #[test]
3699 fn test_all_actions_returns_actions_in_order() {
3700 let session = AgentSession {
3701 session_id: "s".into(),
3702 agent_id: AgentId::new("a"),
3703 steps: vec![
3704 ReActStep::new("think1", "search {}", "result"),
3705 ReActStep::new("think2", "FINAL_ANSWER done", ""),
3706 ],
3707 memory_hits: 0,
3708 graph_lookups: 0,
3709 duration_ms: 10,
3710 checkpoint_errors: vec![],
3711 };
3712 assert_eq!(session.all_actions(), vec!["search {}", "FINAL_ANSWER done"]);
3713 }
3714
3715 #[test]
3716 fn test_has_checkpoint_errors_false_when_empty() {
3717 let session = AgentSession {
3718 session_id: "s".into(),
3719 agent_id: AgentId::new("a"),
3720 steps: vec![],
3721 memory_hits: 0,
3722 graph_lookups: 0,
3723 duration_ms: 0,
3724 checkpoint_errors: vec![],
3725 };
3726 assert!(!session.has_checkpoint_errors());
3727 }
3728
3729 #[test]
3730 fn test_has_checkpoint_errors_true_when_non_empty() {
3731 let session = AgentSession {
3732 session_id: "s".into(),
3733 agent_id: AgentId::new("a"),
3734 steps: vec![],
3735 memory_hits: 0,
3736 graph_lookups: 0,
3737 duration_ms: 0,
3738 checkpoint_errors: vec!["err".into()],
3739 };
3740 assert!(session.has_checkpoint_errors());
3741 }
3742
3743 #[test]
3744 fn test_memory_hit_rate_zero_with_no_steps() {
3745 let session = AgentSession {
3746 session_id: "s".into(),
3747 agent_id: AgentId::new("a"),
3748 steps: vec![],
3749 memory_hits: 5,
3750 graph_lookups: 0,
3751 duration_ms: 0,
3752 checkpoint_errors: vec![],
3753 };
3754 assert_eq!(session.memory_hit_rate(), 0.0);
3755 }
3756
3757 #[test]
3758 fn test_memory_hit_rate_correct_proportion() {
3759 let session = AgentSession {
3760 session_id: "s".into(),
3761 agent_id: AgentId::new("a"),
3762 steps: vec![
3763 ReActStep::new("t", "a", "o"),
3764 ReActStep::new("t", "a", "o"),
3765 ReActStep::new("t", "a", "o"),
3766 ReActStep::new("t", "a", "o"),
3767 ],
3768 memory_hits: 2,
3769 graph_lookups: 0,
3770 duration_ms: 0,
3771 checkpoint_errors: vec![],
3772 };
3773 assert!((session.memory_hit_rate() - 0.5).abs() < 1e-9);
3774 }
3775
3776 #[test]
3777 fn test_filter_tool_call_steps_excludes_final_answer() {
3778 let session = AgentSession {
3779 session_id: "s".into(),
3780 agent_id: AgentId::new("a"),
3781 steps: vec![
3782 ReActStep::new("t1", "search {}", "res"),
3783 ReActStep::new("t2", "FINAL_ANSWER done", ""),
3784 ],
3785 memory_hits: 0,
3786 graph_lookups: 0,
3787 duration_ms: 0,
3788 checkpoint_errors: vec![],
3789 };
3790 let tool_steps = session.filter_tool_call_steps();
3791 assert_eq!(tool_steps.len(), 1);
3792 assert_eq!(tool_steps[0].action, "search {}");
3793 }
3794
3795 #[test]
3796 fn test_slowest_step_index() {
3797 let mut s0 = ReActStep::new("t", "a", "o");
3798 s0.step_duration_ms = 5;
3799 let mut s1 = ReActStep::new("t", "a", "o");
3800 s1.step_duration_ms = 100;
3801 let mut s2 = ReActStep::new("t", "a", "o");
3802 s2.step_duration_ms = 10;
3803 let session = AgentSession {
3804 session_id: "s".into(),
3805 agent_id: AgentId::new("a"),
3806 steps: vec![s0, s1, s2],
3807 memory_hits: 0,
3808 graph_lookups: 0,
3809 duration_ms: 0,
3810 checkpoint_errors: vec![],
3811 };
3812 assert_eq!(session.slowest_step_index(), Some(1));
3813 assert_eq!(session.fastest_step_index(), Some(0));
3814 }
3815
3816 #[test]
3817 fn test_slowest_step_index_none_when_empty() {
3818 let session = AgentSession {
3819 session_id: "s".into(),
3820 agent_id: AgentId::new("a"),
3821 steps: vec![],
3822 memory_hits: 0,
3823 graph_lookups: 0,
3824 duration_ms: 0,
3825 checkpoint_errors: vec![],
3826 };
3827 assert_eq!(session.slowest_step_index(), None);
3828 assert_eq!(session.fastest_step_index(), None);
3829 }
3830
3831 #[test]
3832 fn test_last_step_returns_last() {
3833 let session = AgentSession {
3834 session_id: "s".into(),
3835 agent_id: AgentId::new("a"),
3836 steps: vec![
3837 ReActStep::new("t1", "a1", "o1"),
3838 ReActStep::new("t2", "FINAL_ANSWER done", ""),
3839 ],
3840 memory_hits: 0,
3841 graph_lookups: 0,
3842 duration_ms: 0,
3843 checkpoint_errors: vec![],
3844 };
3845 assert_eq!(session.last_step().map(|s| s.action.as_str()), Some("FINAL_ANSWER done"));
3846 }
3847
3848 #[test]
3849 fn test_last_step_none_when_empty() {
3850 let session = AgentSession {
3851 session_id: "s".into(),
3852 agent_id: AgentId::new("a"),
3853 steps: vec![],
3854 memory_hits: 0,
3855 graph_lookups: 0,
3856 duration_ms: 0,
3857 checkpoint_errors: vec![],
3858 };
3859 assert!(session.last_step().is_none());
3860 }
3861
3862 #[test]
3863 fn test_step_at_returns_correct_step() {
3864 let session = AgentSession {
3865 session_id: "s".into(),
3866 agent_id: AgentId::new("a"),
3867 steps: vec![
3868 ReActStep::new("t0", "a0", "o0"),
3869 ReActStep::new("t1", "a1", "o1"),
3870 ],
3871 memory_hits: 0,
3872 graph_lookups: 0,
3873 duration_ms: 0,
3874 checkpoint_errors: vec![],
3875 };
3876 assert_eq!(session.step_at(1).map(|s| s.thought.as_str()), Some("t1"));
3877 assert!(session.step_at(99).is_none());
3878 }
3879
3880 #[test]
3883 fn test_failed_steps_returns_steps_with_error_observation() {
3884 use crate::agent::ReActStep;
3885 let session = AgentSession {
3886 session_id: "s".into(),
3887 agent_id: AgentId::new("a"),
3888 steps: vec![
3889 ReActStep::new("t", "tool_a {}", r#"{"error":"bad input","ok":false}"#),
3890 ReActStep::new("t", "tool_b {}", r#"{"result":"ok","ok":true}"#),
3891 ],
3892 memory_hits: 0,
3893 graph_lookups: 0,
3894 duration_ms: 0,
3895 checkpoint_errors: vec![],
3896 };
3897 let failed = session.failed_steps();
3898 assert_eq!(failed.len(), 1);
3899 assert!(failed[0].observation.contains("bad input"));
3900 }
3901
3902 #[test]
3903 fn test_failed_steps_empty_when_no_errors() {
3904 use crate::agent::ReActStep;
3905 let session = AgentSession {
3906 session_id: "s".into(),
3907 agent_id: AgentId::new("a"),
3908 steps: vec![ReActStep::new("t", "FINAL_ANSWER done", "")],
3909 memory_hits: 0,
3910 graph_lookups: 0,
3911 duration_ms: 0,
3912 checkpoint_errors: vec![],
3913 };
3914 assert!(session.failed_steps().is_empty());
3915 }
3916
3917 fn make_step(thought: &str, action: &str, observation: &str) -> ReActStep {
3920 ReActStep::new(thought, action, observation)
3921 }
3922
3923 fn make_session(steps: Vec<ReActStep>, duration_ms: u64) -> AgentSession {
3924 AgentSession {
3925 session_id: "s".into(),
3926 agent_id: AgentId::new("a"),
3927 steps,
3928 memory_hits: 0,
3929 graph_lookups: 0,
3930 duration_ms,
3931 checkpoint_errors: vec![],
3932 }
3933 }
3934
3935 #[test]
3936 fn test_step_count_returns_number_of_steps() {
3937 let s = make_session(vec![ReActStep::new("t", "a", "o"), ReActStep::new("t", "a", "o")], 0);
3938 assert_eq!(s.step_count(), 2);
3939 }
3940
3941 #[test]
3942 fn test_is_empty_true_for_no_steps() {
3943 let s = make_session(vec![], 0);
3944 assert!(s.is_empty());
3945 }
3946
3947 #[test]
3948 fn test_is_empty_false_with_steps() {
3949 let s = make_session(vec![ReActStep::new("t", "a", "o")], 0);
3950 assert!(!s.is_empty());
3951 }
3952
3953 #[test]
3954 fn test_is_successful_true_with_final_answer() {
3955 let s = make_session(vec![ReActStep::new("t", "FINAL_ANSWER yes", "")], 0);
3956 assert!(s.is_successful());
3957 }
3958
3959 #[test]
3960 fn test_is_successful_false_without_final_answer() {
3961 let s = make_session(vec![ReActStep::new("t", "search {}", "result")], 0);
3962 assert!(!s.is_successful());
3963 }
3964
3965 #[test]
3966 fn test_elapsed_returns_duration_from_duration_ms() {
3967 let s = make_session(vec![], 500);
3968 assert_eq!(s.elapsed(), std::time::Duration::from_millis(500));
3969 }
3970
3971 #[test]
3972 fn test_tool_calls_made_excludes_final_answer() {
3973 let s = make_session(vec![
3974 ReActStep::new("t", "search {}", "res"),
3975 ReActStep::new("t", "lookup {}", "res"),
3976 ReActStep::new("t", "FINAL_ANSWER done", ""),
3977 ], 0);
3978 assert_eq!(s.tool_calls_made(), 2);
3979 }
3980
3981 #[test]
3982 fn test_total_step_duration_ms_sums_all_steps() {
3983 let mut s1 = ReActStep::new("t", "a", "o"); s1.step_duration_ms = 10;
3984 let mut s2 = ReActStep::new("t", "a", "o"); s2.step_duration_ms = 30;
3985 let s = make_session(vec![s1, s2], 0);
3986 assert_eq!(s.total_step_duration_ms(), 40);
3987 }
3988
3989 #[test]
3990 fn test_average_step_duration_ms() {
3991 let mut s1 = ReActStep::new("t", "a", "o"); s1.step_duration_ms = 20;
3992 let mut s2 = ReActStep::new("t", "a", "o"); s2.step_duration_ms = 40;
3993 let s = make_session(vec![s1, s2], 0);
3994 assert_eq!(s.average_step_duration_ms(), 30);
3995 }
3996
3997 #[test]
3998 fn test_all_thoughts_returns_thoughts_in_order() {
3999 let s = make_session(vec![
4000 ReActStep::new("first thought", "a1", "o1"),
4001 ReActStep::new("second thought", "a2", "o2"),
4002 ], 0);
4003 assert_eq!(s.all_thoughts(), vec!["first thought", "second thought"]);
4004 }
4005
4006 #[test]
4007 fn test_all_observations_returns_observations_in_order() {
4008 let s = make_session(vec![
4009 ReActStep::new("t1", "a1", "obs one"),
4010 ReActStep::new("t2", "a2", "obs two"),
4011 ], 0);
4012 assert_eq!(s.all_observations(), vec!["obs one", "obs two"]);
4013 }
4014
4015 #[test]
4016 fn test_observations_matching_finds_matching_steps() {
4017 let s = make_session(vec![
4018 ReActStep::new("t1", "a1", "found the answer"),
4019 ReActStep::new("t2", "a2", "nothing relevant"),
4020 ], 0);
4021 let matching = s.observations_matching("answer");
4022 assert_eq!(matching.len(), 1);
4023 assert!(matching[0].observation.contains("answer"));
4024 }
4025
4026 #[test]
4027 fn test_first_step_returns_first() {
4028 let s = make_session(vec![
4029 ReActStep::new("first", "a1", "o1"),
4030 ReActStep::new("second", "a2", "o2"),
4031 ], 0);
4032 assert_eq!(s.first_step().map(|s| s.thought.as_str()), Some("first"));
4033 }
4034
4035 #[test]
4036 fn test_first_step_none_when_empty() {
4037 let s = make_session(vec![], 0);
4038 assert!(s.first_step().is_none());
4039 }
4040
4041 #[test]
4044 fn test_graph_lookup_count_returns_field() {
4045 let session = AgentSession {
4046 session_id: "s".into(),
4047 agent_id: AgentId::new("a"),
4048 steps: vec![],
4049 memory_hits: 0,
4050 graph_lookups: 7,
4051 duration_ms: 0,
4052 checkpoint_errors: vec![],
4053 };
4054 assert_eq!(session.graph_lookup_count(), 7usize);
4055 }
4056
4057 #[test]
4060 fn test_action_counts_counts_each_action() {
4061 let session = make_session(
4062 vec![
4063 ReActStep::new("t1", "search", "r1"),
4064 ReActStep::new("t2", "search", "r2"),
4065 ReActStep::new("t3", "FINAL_ANSWER", "done"),
4066 ],
4067 0,
4068 );
4069 let counts = session.action_counts();
4070 assert_eq!(counts.get("search").copied().unwrap_or(0), 2);
4071 assert_eq!(counts.get("FINAL_ANSWER").copied().unwrap_or(0), 1);
4072 }
4073
4074 #[test]
4075 fn test_unique_actions_returns_sorted_deduped() {
4076 let session = make_session(
4077 vec![
4078 ReActStep::new("t", "b_action", "r"),
4079 ReActStep::new("t", "a_action", "r"),
4080 ReActStep::new("t", "b_action", "r"),
4081 ],
4082 0,
4083 );
4084 assert_eq!(session.unique_actions(), vec!["a_action", "b_action"]);
4085 }
4086
4087 #[test]
4088 fn test_unique_actions_empty_when_no_steps() {
4089 let session = make_session(vec![], 0);
4090 assert!(session.unique_actions().is_empty());
4091 }
4092
4093 #[test]
4096 fn test_total_latency_ms_sums_step_durations() {
4097 let mut steps = vec![
4098 ReActStep::new("t1", "a1", "o1"),
4099 ReActStep::new("t2", "a2", "o2"),
4100 ];
4101 steps[0].step_duration_ms = 100;
4102 steps[1].step_duration_ms = 250;
4103 let session = make_session(steps, 350);
4104 assert_eq!(session.total_latency_ms(), 350);
4105 }
4106
4107 #[test]
4108 fn test_total_latency_ms_zero_for_empty_session() {
4109 let session = make_session(vec![], 0);
4110 assert_eq!(session.total_latency_ms(), 0);
4111 }
4112
4113 #[test]
4114 fn test_action_sequence_returns_actions_in_order() {
4115 let session = make_session(
4116 vec![
4117 ReActStep::new("t", "search", "r"),
4118 ReActStep::new("t", "FINAL_ANSWER", "done"),
4119 ],
4120 0,
4121 );
4122 assert_eq!(session.action_sequence(), vec!["search", "FINAL_ANSWER"]);
4123 }
4124
4125 #[test]
4128 fn test_has_action_returns_true_for_present_action() {
4129 let session = make_session(
4130 vec![ReActStep::new("t", "search", "r")],
4131 0,
4132 );
4133 assert!(session.has_action("search"));
4134 }
4135
4136 #[test]
4137 fn test_has_action_returns_false_for_absent_action() {
4138 let session = make_session(
4139 vec![ReActStep::new("t", "search", "r")],
4140 0,
4141 );
4142 assert!(!session.has_action("compute"));
4143 }
4144
4145 #[test]
4146 fn test_thought_at_returns_thought_for_valid_index() {
4147 let session = make_session(
4148 vec![
4149 ReActStep::new("first thought", "a1", "r1"),
4150 ReActStep::new("second thought", "a2", "r2"),
4151 ],
4152 0,
4153 );
4154 assert_eq!(session.thought_at(0), Some("first thought"));
4155 assert_eq!(session.thought_at(1), Some("second thought"));
4156 }
4157
4158 #[test]
4159 fn test_thought_at_returns_none_for_out_of_bounds_index() {
4160 let session = make_session(vec![ReActStep::new("t", "a", "r")], 0);
4161 assert!(session.thought_at(99).is_none());
4162 }
4163
4164 #[test]
4167 fn test_step_count_for_action_counts_correctly() {
4168 let session = make_session(
4169 vec![
4170 ReActStep::new("t", "search", "r1"),
4171 ReActStep::new("t", "search", "r2"),
4172 ReActStep::new("t", "FINAL_ANSWER", "done"),
4173 ],
4174 0,
4175 );
4176 assert_eq!(session.step_count_for_action("search"), 2);
4177 assert_eq!(session.step_count_for_action("FINAL_ANSWER"), 1);
4178 assert_eq!(session.step_count_for_action("unknown"), 0);
4179 }
4180
4181 #[test]
4182 fn test_observations_returns_all_observation_strings() {
4183 let session = make_session(
4184 vec![
4185 ReActStep::new("t1", "a", "obs_one"),
4186 ReActStep::new("t2", "b", "obs_two"),
4187 ],
4188 0,
4189 );
4190 let obs = session.observations();
4191 assert_eq!(obs, vec!["obs_one", "obs_two"]);
4192 }
4193
4194 #[test]
4195 fn test_observations_empty_for_no_steps() {
4196 let session = make_session(vec![], 0);
4197 assert!(session.observations().is_empty());
4198 }
4199
4200 #[test]
4203 fn test_unique_tools_used_deduplicates_actions() {
4204 let session = make_session(
4205 vec![
4206 ReActStep::new("t", "search", "r1"),
4207 ReActStep::new("t", "lookup", "r2"),
4208 ReActStep::new("t", "search", "r3"),
4209 ],
4210 0,
4211 );
4212 let tools = session.unique_tools_used();
4213 assert_eq!(tools.len(), 2);
4214 assert!(tools.contains(&"search".to_string()));
4215 assert!(tools.contains(&"lookup".to_string()));
4216 }
4217
4218 #[test]
4219 fn test_unique_tools_used_excludes_final_answer() {
4220 let session = make_session(
4221 vec![
4222 ReActStep::new("t", "search", "r1"),
4223 ReActStep::new("t", "FINAL_ANSWER: done", "r2"),
4224 ],
4225 0,
4226 );
4227 let tools = session.unique_tools_used();
4228 assert_eq!(tools.len(), 1);
4229 assert!(tools.contains(&"search".to_string()));
4230 }
4231
4232 #[test]
4233 fn test_unique_tools_used_empty_for_no_steps() {
4234 let session = make_session(vec![], 0);
4235 assert!(session.unique_tools_used().is_empty());
4236 }
4237
4238 #[test]
4241 fn test_avg_step_duration_zero_for_empty_session() {
4242 let session = make_session(vec![], 0);
4243 assert!((session.avg_step_duration_ms() - 0.0).abs() < 1e-9);
4244 }
4245
4246 #[test]
4247 fn test_avg_step_duration_single_step() {
4248 let mut step = ReActStep::new("t", "a", "r");
4249 step.step_duration_ms = 100;
4250 let session = make_session(vec![step], 0);
4251 assert!((session.avg_step_duration_ms() - 100.0).abs() < 1e-9);
4252 }
4253
4254 #[test]
4255 fn test_avg_step_duration_multiple_steps() {
4256 let mut s1 = ReActStep::new("t1", "a", "r");
4257 s1.step_duration_ms = 100;
4258 let mut s2 = ReActStep::new("t2", "b", "r");
4259 s2.step_duration_ms = 200;
4260 let session = make_session(vec![s1, s2], 0);
4261 assert!((session.avg_step_duration_ms() - 150.0).abs() < 1e-9);
4262 }
4263
4264 #[test]
4265 fn test_longest_step_returns_step_with_max_duration() {
4266 let mut s1 = ReActStep::new("t1", "a", "r");
4267 s1.step_duration_ms = 50;
4268 let mut s2 = ReActStep::new("t2", "b", "r");
4269 s2.step_duration_ms = 200;
4270 let session = make_session(vec![s1, s2], 0);
4271 assert_eq!(session.longest_step().map(|s| s.step_duration_ms), Some(200));
4272 }
4273
4274 #[test]
4275 fn test_longest_step_returns_none_for_empty_session() {
4276 let session = make_session(vec![], 0);
4277 assert!(session.longest_step().is_none());
4278 }
4279
4280 #[test]
4281 fn test_shortest_step_returns_step_with_min_duration() {
4282 let mut s1 = ReActStep::new("t1", "a", "r");
4283 s1.step_duration_ms = 50;
4284 let mut s2 = ReActStep::new("t2", "b", "r");
4285 s2.step_duration_ms = 200;
4286 let session = make_session(vec![s1, s2], 0);
4287 assert_eq!(session.shortest_step().map(|s| s.step_duration_ms), Some(50));
4288 }
4289
4290 #[test]
4293 fn test_first_thought_returns_thought_from_first_step() {
4294 let session = make_session(
4295 vec![
4296 ReActStep::new("alpha", "a1", "r1"),
4297 ReActStep::new("beta", "a2", "r2"),
4298 ],
4299 0,
4300 );
4301 assert_eq!(session.first_thought(), Some("alpha"));
4302 }
4303
4304 #[test]
4305 fn test_last_thought_returns_thought_from_last_step() {
4306 let session = make_session(
4307 vec![
4308 ReActStep::new("alpha", "a1", "r1"),
4309 ReActStep::new("beta", "a2", "r2"),
4310 ],
4311 0,
4312 );
4313 assert_eq!(session.last_thought(), Some("beta"));
4314 }
4315
4316 #[test]
4317 fn test_first_thought_none_for_empty_session() {
4318 let session = make_session(vec![], 0);
4319 assert!(session.first_thought().is_none());
4320 }
4321
4322 #[test]
4323 fn test_last_thought_none_for_empty_session() {
4324 let session = make_session(vec![], 0);
4325 assert!(session.last_thought().is_none());
4326 }
4327
4328 #[test]
4331 fn test_first_action_returns_action_from_first_step() {
4332 let session = make_session(
4333 vec![
4334 ReActStep::new("t1", "search", "r1"),
4335 ReActStep::new("t2", "FINAL_ANSWER", "r2"),
4336 ],
4337 0,
4338 );
4339 assert_eq!(session.first_action(), Some("search"));
4340 }
4341
4342 #[test]
4343 fn test_last_action_returns_action_from_last_step() {
4344 let session = make_session(
4345 vec![
4346 ReActStep::new("t1", "search", "r1"),
4347 ReActStep::new("t2", "FINAL_ANSWER", "r2"),
4348 ],
4349 0,
4350 );
4351 assert_eq!(session.last_action(), Some("FINAL_ANSWER"));
4352 }
4353
4354 #[test]
4355 fn test_first_action_none_for_empty_session() {
4356 let session = make_session(vec![], 0);
4357 assert!(session.first_action().is_none());
4358 }
4359
4360 #[test]
4361 fn test_last_action_equals_first_action_for_single_step() {
4362 let session = make_session(vec![ReActStep::new("t", "calc", "r")], 0);
4363 assert_eq!(session.first_action(), session.last_action());
4364 }
4365
4366 #[test]
4369 fn test_checkpoint_error_count_zero_when_none() {
4370 let session = make_session(vec![], 0);
4371 assert_eq!(session.checkpoint_error_count(), 0);
4372 }
4373
4374 #[test]
4375 fn test_checkpoint_error_count_reflects_errors() {
4376 let mut session = make_session(vec![], 0);
4377 session.checkpoint_errors.push("save failed".into());
4378 session.checkpoint_errors.push("disk full".into());
4379 assert_eq!(session.checkpoint_error_count(), 2);
4380 }
4381
4382 #[test]
4385 fn test_failed_tool_call_count_zero_when_no_errors() {
4386 let step = ReActStep::new("think", "search", "results found");
4387 let session = make_session(vec![step], 0);
4388 assert_eq!(session.failed_tool_call_count(), 0);
4389 }
4390
4391 #[test]
4392 fn test_failed_tool_call_count_matches_failed_steps() {
4393 let ok_step = ReActStep::new("ok", "search", "all good");
4394 let err_step = ReActStep::new("err", "lookup", "{\"error\": \"not found\"}");
4395 let session = make_session(vec![ok_step, err_step], 0);
4396 assert_eq!(session.failed_tool_call_count(), session.failed_steps().len());
4397 assert_eq!(session.failed_tool_call_count(), 1);
4398 }
4399
4400 #[test]
4401 fn test_failed_tool_call_count_counts_all_errors() {
4402 let err1 = ReActStep::new("e1", "a", "{\"error\": \"bad\"}");
4403 let err2 = ReActStep::new("e2", "b", "some \"error\" text");
4404 let ok = ReActStep::new("ok", "c", "success");
4405 let session = make_session(vec![err1, err2, ok], 0);
4406 assert_eq!(session.failed_tool_call_count(), 2);
4407 }
4408
4409 #[test]
4412 fn test_total_memory_hits_returns_memory_hits_field() {
4413 let mut session = make_session(vec![], 0);
4414 session.memory_hits = 7;
4415 assert_eq!(session.total_memory_hits(), 7);
4416 }
4417
4418 #[test]
4419 fn test_total_memory_hits_zero_by_default() {
4420 let session = make_session(vec![], 0);
4421 assert_eq!(session.total_memory_hits(), 0);
4422 }
4423
4424 #[test]
4425 fn test_action_diversity_all_unique_is_one() {
4426 let steps = vec![
4427 ReActStep::new("t", "search", "r"),
4428 ReActStep::new("t", "calc", "r"),
4429 ReActStep::new("t", "lookup", "r"),
4430 ];
4431 let session = make_session(steps, 0);
4432 assert!((session.action_diversity() - 1.0).abs() < 1e-9);
4433 }
4434
4435 #[test]
4436 fn test_action_diversity_all_same_is_fraction() {
4437 let steps = vec![
4438 ReActStep::new("t", "search", "r"),
4439 ReActStep::new("t", "search", "r"),
4440 ReActStep::new("t", "search", "r"),
4441 ];
4442 let session = make_session(steps, 0);
4443 assert!((session.action_diversity() - 1.0 / 3.0).abs() < 1e-9);
4445 }
4446
4447 #[test]
4448 fn test_action_diversity_zero_for_empty_session() {
4449 let session = make_session(vec![], 0);
4450 assert!((session.action_diversity() - 0.0).abs() < 1e-9);
4451 }
4452
4453 #[test]
4456 fn test_last_n_steps_returns_last_n() {
4457 let steps = vec![
4458 ReActStep::new("t1", "a", "r1"),
4459 ReActStep::new("t2", "b", "r2"),
4460 ReActStep::new("t3", "c", "r3"),
4461 ];
4462 let session = make_session(steps, 0);
4463 let last2 = session.last_n_steps(2);
4464 assert_eq!(last2.len(), 2);
4465 assert_eq!(last2[0].action, "b");
4466 assert_eq!(last2[1].action, "c");
4467 }
4468
4469 #[test]
4470 fn test_last_n_steps_returns_all_when_n_exceeds_count() {
4471 let steps = vec![
4472 ReActStep::new("t1", "a", "r1"),
4473 ReActStep::new("t2", "b", "r2"),
4474 ];
4475 let session = make_session(steps, 0);
4476 assert_eq!(session.last_n_steps(10).len(), 2);
4477 }
4478
4479 #[test]
4480 fn test_last_n_steps_empty_for_no_steps() {
4481 let session = make_session(vec![], 0);
4482 assert!(session.last_n_steps(3).is_empty());
4483 }
4484
4485 #[test]
4486 fn test_last_n_steps_zero_returns_empty() {
4487 let steps = vec![ReActStep::new("t1", "a", "r1")];
4488 let session = make_session(steps, 0);
4489 assert!(session.last_n_steps(0).is_empty());
4490 }
4491
4492 #[test]
4495 fn test_observation_count_counts_non_empty() {
4496 let steps = vec![
4497 ReActStep::new("t", "a", "result"),
4498 ReActStep::new("t", "b", ""),
4499 ReActStep::new("t", "c", "data"),
4500 ];
4501 let session = make_session(steps, 0);
4502 assert_eq!(session.observation_count(), 2);
4503 }
4504
4505 #[test]
4506 fn test_observation_count_zero_when_all_empty() {
4507 let steps = vec![
4508 ReActStep::new("t", "a", ""),
4509 ReActStep::new("t", "b", ""),
4510 ];
4511 let session = make_session(steps, 0);
4512 assert_eq!(session.observation_count(), 0);
4513 }
4514
4515 #[test]
4516 fn test_steps_without_observation_counts_empty_obs() {
4517 let steps = vec![
4518 ReActStep::new("t", "a", ""),
4519 ReActStep::new("t", "b", "data"),
4520 ReActStep::new("t", "c", ""),
4521 ];
4522 let session = make_session(steps, 0);
4523 assert_eq!(session.steps_without_observation(), 2);
4524 }
4525
4526 #[test]
4527 fn test_steps_without_observation_zero_when_all_filled() {
4528 let steps = vec![
4529 ReActStep::new("t", "a", "r1"),
4530 ReActStep::new("t", "b", "r2"),
4531 ];
4532 let session = make_session(steps, 0);
4533 assert_eq!(session.steps_without_observation(), 0);
4534 }
4535
4536 #[test]
4539 fn test_throughput_steps_per_sec_correct_ratio() {
4540 let steps = vec![
4541 ReActStep::new("t", "a", "r"),
4542 ReActStep::new("t", "b", "r"),
4543 ];
4544 let session = make_session(steps, 1000);
4546 assert!((session.throughput_steps_per_sec() - 2.0).abs() < 1e-9);
4547 }
4548
4549 #[test]
4550 fn test_throughput_steps_per_sec_zero_when_no_duration() {
4551 let steps = vec![ReActStep::new("t", "a", "r")];
4552 let session = make_session(steps, 0);
4553 assert!((session.throughput_steps_per_sec() - 0.0).abs() < 1e-9);
4554 }
4555
4556 #[test]
4559 fn test_thoughts_containing_returns_matching_steps() {
4560 let session = make_session(
4561 vec![
4562 ReActStep::new("I need to search", "search", "found"),
4563 ReActStep::new("Let me calculate", "calc", "done"),
4564 ReActStep::new("search again", "search", "ok"),
4565 ],
4566 0,
4567 );
4568 let matches = session.thoughts_containing("search");
4569 assert_eq!(matches.len(), 2);
4570 }
4571
4572 #[test]
4573 fn test_thoughts_containing_is_case_insensitive() {
4574 let session = make_session(
4575 vec![ReActStep::new("SEARCH the web", "search", "r")],
4576 0,
4577 );
4578 assert_eq!(session.thoughts_containing("search").len(), 1);
4579 }
4580
4581 #[test]
4582 fn test_thoughts_containing_empty_when_no_match() {
4583 let session = make_session(vec![ReActStep::new("think about x", "a", "r")], 0);
4584 assert!(session.thoughts_containing("zebra").is_empty());
4585 }
4586
4587 #[test]
4588 fn test_step_durations_ms_returns_all_durations() {
4589 let mut steps = vec![
4590 ReActStep::new("t", "a", "r"),
4591 ReActStep::new("t", "b", "r"),
4592 ];
4593 steps[0].step_duration_ms = 100;
4594 steps[1].step_duration_ms = 200;
4595 let session = make_session(steps, 300);
4596 assert_eq!(session.step_durations_ms(), vec![100, 200]);
4597 }
4598
4599 #[test]
4600 fn test_fastest_step_index_returns_index_of_shortest_step() {
4601 let mut steps = vec![
4602 ReActStep::new("t", "a", "r"),
4603 ReActStep::new("t", "b", "r"),
4604 ReActStep::new("t", "c", "r"),
4605 ];
4606 steps[0].step_duration_ms = 300;
4607 steps[1].step_duration_ms = 50;
4608 steps[2].step_duration_ms = 200;
4609 let session = make_session(steps, 550);
4610 assert_eq!(session.fastest_step_index(), Some(1));
4611 }
4612
4613 #[test]
4614 fn test_fastest_step_index_none_for_empty_session() {
4615 let session = make_session(vec![], 0);
4616 assert!(session.fastest_step_index().is_none());
4617 }
4618
4619 #[test]
4622 fn test_most_used_action_returns_most_frequent() {
4623 let steps = vec![
4624 ReActStep::new("t", "search", "r"),
4625 ReActStep::new("t", "calc", "r"),
4626 ReActStep::new("t", "search", "r"),
4627 ];
4628 let session = make_session(steps, 0);
4629 assert_eq!(session.most_used_action().as_deref(), Some("search"));
4630 }
4631
4632 #[test]
4633 fn test_most_used_action_none_for_empty_session() {
4634 let session = make_session(vec![], 0);
4635 assert!(session.most_used_action().is_none());
4636 }
4637
4638 #[test]
4639 fn test_graph_lookup_rate_correct_ratio() {
4640 let steps = vec![
4641 ReActStep::new("t", "a", "r"),
4642 ReActStep::new("t", "b", "r"),
4643 ReActStep::new("t", "c", "r"),
4644 ReActStep::new("t", "d", "r"),
4645 ];
4646 let mut session = make_session(steps, 0);
4647 session.graph_lookups = 2;
4648 assert!((session.graph_lookup_rate() - 0.5).abs() < 1e-9);
4649 }
4650
4651 #[test]
4652 fn test_graph_lookup_rate_zero_for_empty_session() {
4653 let session = make_session(vec![], 0);
4654 assert!((session.graph_lookup_rate() - 0.0).abs() < 1e-9);
4655 }
4656
4657 #[test]
4660 fn test_has_tool_failures_false_when_no_errors() {
4661 let steps = vec![
4662 make_step("t", "action1", "ok"),
4663 make_step("t", "action2", "done"),
4664 ];
4665 let session = make_session(steps, 0);
4666 assert!(!session.has_tool_failures());
4667 }
4668
4669 #[test]
4670 fn test_has_tool_failures_true_when_error_observation() {
4671 let steps = vec![
4672 make_step("t", "action1", "{\"error\": \"timeout\"}"),
4673 ];
4674 let session = make_session(steps, 0);
4675 assert!(session.has_tool_failures());
4676 }
4677
4678 #[test]
4679 fn test_tool_call_rate_zero_for_empty_session() {
4680 let session = make_session(vec![], 0);
4681 assert!((session.tool_call_rate() - 0.0).abs() < 1e-9);
4682 }
4683
4684 #[test]
4685 fn test_tool_call_rate_correct_ratio() {
4686 let steps = vec![
4687 make_step("t", "tool_action", "ok"),
4688 make_step("t", "FINAL_ANSWER: done", ""),
4689 make_step("t", "another_tool", "ok"),
4690 ];
4691 let session = make_session(steps, 0);
4692 assert!((session.tool_call_rate() - 2.0 / 3.0).abs() < 1e-9);
4694 }
4695
4696 #[test]
4697 fn test_step_success_rate_one_for_empty_session() {
4698 let session = make_session(vec![], 0);
4699 assert!((session.step_success_rate() - 1.0).abs() < 1e-9);
4700 }
4701
4702 #[test]
4703 fn test_step_success_rate_less_than_one_when_failures() {
4704 let steps = vec![
4705 make_step("t", "act", "{\"error\": \"fail\"}"),
4706 make_step("t", "act", "success"),
4707 ];
4708 let session = make_session(steps, 0);
4709 assert!((session.step_success_rate() - 0.5).abs() < 1e-9);
4711 }
4712
4713 #[test]
4716 fn test_avg_step_duration_ms_zero_for_empty() {
4717 let session = make_session(vec![], 0);
4718 assert!((session.avg_step_duration_ms() - 0.0).abs() < 1e-9);
4719 }
4720
4721 #[test]
4722 fn test_avg_step_duration_ms_correct_mean() {
4723 let mut s1 = make_step("t", "a", "o");
4724 s1.step_duration_ms = 100;
4725 let mut s2 = make_step("t", "b", "o");
4726 s2.step_duration_ms = 200;
4727 let session = make_session(vec![s1, s2], 0);
4728 assert!((session.avg_step_duration_ms() - 150.0).abs() < 1e-9);
4729 }
4730
4731 #[test]
4732 fn test_longest_step_none_for_empty() {
4733 let session = make_session(vec![], 0);
4734 assert!(session.longest_step().is_none());
4735 }
4736
4737 #[test]
4738 fn test_longest_step_middle_has_max_duration() {
4739 let mut s1 = make_step("t", "a", "o");
4740 s1.step_duration_ms = 10;
4741 let mut s2 = make_step("t", "b", "o");
4742 s2.step_duration_ms = 500;
4743 let mut s3 = make_step("t", "c", "o");
4744 s3.step_duration_ms = 20;
4745 let session = make_session(vec![s1, s2, s3], 0);
4746 assert_eq!(session.longest_step().unwrap().action, "b");
4747 }
4748
4749 #[test]
4750 fn test_unique_tools_used_deduplicates_and_sorts() {
4751 let steps = vec![
4752 make_step("t", "search", "o"),
4753 make_step("t", "lookup", "o"),
4754 make_step("t", "search", "o"),
4755 ];
4756 let session = make_session(steps, 0);
4757 assert_eq!(session.unique_tools_used(), vec!["lookup", "search"]);
4758 }
4759
4760 #[test]
4761 fn test_all_thoughts_collects_in_order() {
4762 let steps = vec![make_step("think1", "a", "o"), make_step("think2", "b", "o")];
4763 let session = make_session(steps, 0);
4764 assert_eq!(session.all_thoughts(), vec!["think1", "think2"]);
4765 }
4766
4767 #[test]
4768 fn test_all_actions_collects_in_order() {
4769 let steps = vec![make_step("t", "act1", "o"), make_step("t", "act2", "o")];
4770 let session = make_session(steps, 0);
4771 assert_eq!(session.all_actions(), vec!["act1", "act2"]);
4772 }
4773
4774 #[test]
4775 fn test_all_observations_collects_in_order() {
4776 let steps = vec![make_step("t", "a", "obs1"), make_step("t", "b", "obs2")];
4777 let session = make_session(steps, 0);
4778 assert_eq!(session.all_observations(), vec!["obs1", "obs2"]);
4779 }
4780
4781 #[test]
4782 fn test_action_counts_returns_frequency_map() {
4783 let steps = vec![
4784 make_step("t", "search", "o"),
4785 make_step("t", "lookup", "o"),
4786 make_step("t", "search", "o"),
4787 ];
4788 let session = make_session(steps, 0);
4789 let counts = session.action_counts();
4790 assert_eq!(counts["search"], 2);
4791 assert_eq!(counts["lookup"], 1);
4792 }
4793
4794 #[test]
4795 fn test_unique_actions_three_with_repeat_yields_two() {
4796 let steps = vec![
4797 make_step("t", "beta", "o"),
4798 make_step("t", "alpha", "o"),
4799 make_step("t", "beta", "o"),
4800 ];
4801 let session = make_session(steps, 0);
4802 assert_eq!(session.unique_actions(), vec!["alpha", "beta"]);
4803 }
4804
4805 #[test]
4806 fn test_action_diversity_zero_for_empty() {
4807 let session = make_session(vec![], 0);
4808 assert!((session.action_diversity() - 0.0).abs() < 1e-9);
4809 }
4810
4811 #[test]
4812 fn test_action_diversity_one_when_all_actions_unique() {
4813 let steps = vec![
4814 make_step("t", "a", "o"),
4815 make_step("t", "b", "o"),
4816 make_step("t", "c", "o"),
4817 ];
4818 let session = make_session(steps, 0);
4819 assert!((session.action_diversity() - 1.0).abs() < 1e-9);
4820 }
4821
4822 #[test]
4823 fn test_action_diversity_fraction_when_repeated() {
4824 let steps = vec![
4825 make_step("t", "x", "o"),
4826 make_step("t", "x", "o"),
4827 ];
4828 let session = make_session(steps, 0);
4829 assert!((session.action_diversity() - 0.5).abs() < 1e-9);
4830 }
4831
4832 #[test]
4833 fn test_has_checkpoint_errors_false_for_new_session() {
4834 let session = make_session(vec![], 0);
4835 assert!(!session.has_checkpoint_errors());
4836 }
4837
4838 #[test]
4839 fn test_has_checkpoint_errors_true_when_errors_present() {
4840 let mut session = make_session(vec![], 0);
4841 session.checkpoint_errors.push("err1".to_string());
4842 assert!(session.has_checkpoint_errors());
4843 }
4844
4845 #[test]
4846 fn test_graph_lookup_count_returns_raw_value() {
4847 let mut session = make_session(vec![make_step("t", "a", "o")], 0);
4848 session.graph_lookups = 7;
4849 assert_eq!(session.graph_lookup_count(), 7);
4850 }
4851
4852 #[test]
4853 fn test_memory_hit_rate_zero_for_empty_session() {
4854 let session = make_session(vec![], 0);
4855 assert!((session.memory_hit_rate() - 0.0).abs() < 1e-9);
4856 }
4857
4858 #[test]
4859 fn test_memory_hit_rate_correct_ratio() {
4860 let steps = vec![
4861 make_step("t", "a", "o"),
4862 make_step("t", "b", "o"),
4863 make_step("t", "c", "o"),
4864 make_step("t", "d", "o"),
4865 ];
4866 let mut session = make_session(steps, 0);
4867 session.memory_hits = 2;
4868 assert!((session.memory_hit_rate() - 0.5).abs() < 1e-9);
4869 }
4870
4871 #[test]
4872 fn test_total_memory_hits_returns_raw_value() {
4873 let mut session = make_session(vec![], 0);
4874 session.memory_hits = 13;
4875 assert_eq!(session.total_memory_hits(), 13);
4876 }
4877
4878 #[cfg(feature = "memory")]
4881 #[test]
4882 fn test_has_memory_false_without_memory() {
4883 let runtime = AgentRuntime::builder()
4884 .with_agent_config(simple_config())
4885 .build();
4886 assert!(!runtime.has_memory());
4887 }
4888
4889 #[cfg(feature = "memory")]
4890 #[test]
4891 fn test_has_memory_true_with_memory() {
4892 let runtime = AgentRuntime::builder()
4893 .with_agent_config(simple_config())
4894 .with_memory(EpisodicStore::new())
4895 .build();
4896 assert!(runtime.has_memory());
4897 }
4898
4899 #[cfg(feature = "graph")]
4900 #[test]
4901 fn test_has_graph_false_without_graph() {
4902 let runtime = AgentRuntime::builder()
4903 .with_agent_config(simple_config())
4904 .build();
4905 assert!(!runtime.has_graph());
4906 }
4907
4908 #[cfg(feature = "graph")]
4909 #[test]
4910 fn test_has_graph_true_with_graph() {
4911 let runtime = AgentRuntime::builder()
4912 .with_agent_config(simple_config())
4913 .with_graph(GraphStore::new())
4914 .build();
4915 assert!(runtime.has_graph());
4916 }
4917
4918 #[cfg(feature = "memory")]
4919 #[test]
4920 fn test_has_working_memory_false_without_working_memory() {
4921 let runtime = AgentRuntime::builder()
4922 .with_agent_config(simple_config())
4923 .build();
4924 assert!(!runtime.has_working_memory());
4925 }
4926
4927 #[cfg(feature = "memory")]
4928 #[test]
4929 fn test_has_working_memory_true_with_working_memory() {
4930 let runtime = AgentRuntime::builder()
4931 .with_agent_config(simple_config())
4932 .with_working_memory(WorkingMemory::new(10).unwrap())
4933 .build();
4934 assert!(runtime.has_working_memory());
4935 }
4936
4937 #[test]
4940 fn test_last_observation_returns_most_recent_nonempty() {
4941 let steps = vec![
4942 make_step("t1", "act", "first obs"),
4943 make_step("t2", "act", ""),
4944 make_step("t3", "act", "last obs"),
4945 ];
4946 let session = make_session(steps, 0);
4947 assert_eq!(session.last_observation(), Some("last obs"));
4948 }
4949
4950 #[test]
4951 fn test_last_observation_skips_empty_steps() {
4952 let steps = vec![
4953 make_step("t1", "act", "only obs"),
4954 make_step("t2", "act", ""),
4955 ];
4956 let session = make_session(steps, 0);
4957 assert_eq!(session.last_observation(), Some("only obs"));
4958 }
4959
4960 #[test]
4961 fn test_last_observation_none_for_empty_session() {
4962 let session = make_session(vec![], 0);
4963 assert!(session.last_observation().is_none());
4964 }
4965
4966 #[test]
4967 fn test_thought_count_counts_nonempty_thoughts() {
4968 let steps = vec![
4969 make_step("think", "act", "obs"),
4970 make_step("", "act", "obs"),
4971 make_step("think again", "act", "obs"),
4972 ];
4973 let session = make_session(steps, 0);
4974 assert_eq!(session.thought_count(), 2);
4975 }
4976
4977 #[test]
4978 fn test_thought_count_zero_for_empty_session() {
4979 let session = make_session(vec![], 0);
4980 assert_eq!(session.thought_count(), 0);
4981 }
4982
4983 #[test]
4984 fn test_observation_rate_correct_fraction() {
4985 let steps = vec![
4986 make_step("t", "a", "obs"),
4987 make_step("t", "a", ""),
4988 make_step("t", "a", "obs"),
4989 make_step("t", "a", ""),
4990 ];
4991 let session = make_session(steps, 0);
4992 assert!((session.observation_rate() - 0.5).abs() < 1e-9);
4993 }
4994
4995 #[test]
4996 fn test_observation_rate_zero_for_empty_session() {
4997 let session = make_session(vec![], 0);
4998 assert!((session.observation_rate() - 0.0).abs() < 1e-9);
4999 }
5000
5001 #[test]
5004 fn test_action_repetition_rate_zero_for_empty_session() {
5005 let session = make_session(vec![], 0);
5006 assert!((session.action_repetition_rate() - 0.0).abs() < 1e-9);
5007 }
5008
5009 #[test]
5010 fn test_action_repetition_rate_zero_for_single_step() {
5011 let session = make_session(vec![make_step("t", "search", "r")], 0);
5012 assert!((session.action_repetition_rate() - 0.0).abs() < 1e-9);
5013 }
5014
5015 #[test]
5016 fn test_action_repetition_rate_one_when_all_same() {
5017 let steps = vec![
5018 make_step("t", "search", "r"),
5019 make_step("t", "search", "r"),
5020 make_step("t", "search", "r"),
5021 ];
5022 let session = make_session(steps, 0);
5023 assert!((session.action_repetition_rate() - 1.0).abs() < 1e-9);
5024 }
5025
5026 #[test]
5027 fn test_action_repetition_rate_partial_repeats() {
5028 let steps = vec![
5030 make_step("t", "search", "r"),
5031 make_step("t", "search", "r"),
5032 make_step("t", "calc", "r"),
5033 ];
5034 let session = make_session(steps, 0);
5035 assert!((session.action_repetition_rate() - 0.5).abs() < 1e-9);
5036 }
5037
5038 #[test]
5039 fn test_max_consecutive_failures_zero_for_no_errors() {
5040 let steps = vec![make_step("t", "a", "ok"), make_step("t", "b", "done")];
5041 let session = make_session(steps, 0);
5042 assert_eq!(session.max_consecutive_failures(), 0);
5043 }
5044
5045 #[test]
5046 fn test_max_consecutive_failures_counts_run() {
5047 let steps = vec![
5048 make_step("t", "a", "ok"),
5049 make_step("t", "b", r#"{"error":"x"}"#),
5050 make_step("t", "c", r#"{"error":"y"}"#),
5051 make_step("t", "d", "ok"),
5052 ];
5053 let session = make_session(steps, 0);
5054 assert_eq!(session.max_consecutive_failures(), 2);
5055 }
5056
5057 #[test]
5058 fn test_avg_thought_length_zero_for_empty_session() {
5059 let session = make_session(vec![], 0);
5060 assert!((session.avg_thought_length() - 0.0).abs() < 1e-9);
5061 }
5062
5063 #[test]
5064 fn test_avg_thought_length_excludes_empty_thoughts() {
5065 let steps = vec![
5066 make_step("hello", "a", "r"), make_step("", "b", "r"), make_step("hi", "c", "r"), ];
5070 let session = make_session(steps, 0);
5072 assert!((session.avg_thought_length() - 3.5).abs() < 1e-9);
5073 }
5074
5075 #[test]
5078 fn test_last_n_observations_empty_session() {
5079 let session = make_session(vec![], 0);
5080 assert!(session.last_n_observations(3).is_empty());
5081 }
5082
5083 #[test]
5084 fn test_last_n_observations_returns_last_n_nonempty() {
5085 let steps = vec![
5086 make_step("t", "a", "obs1"),
5087 make_step("t", "b", ""), make_step("t", "c", "obs2"),
5089 make_step("t", "d", "obs3"),
5090 ];
5091 let session = make_session(steps, 0);
5092 let last2 = session.last_n_observations(2);
5093 assert_eq!(last2, vec!["obs2", "obs3"]);
5094 }
5095
5096 #[test]
5097 fn test_last_n_observations_returns_all_when_fewer_than_n() {
5098 let steps = vec![make_step("t", "a", "only")];
5099 let session = make_session(steps, 0);
5100 assert_eq!(session.last_n_observations(5), vec!["only"]);
5101 }
5102
5103 #[test]
5104 fn test_actions_in_window_empty_session() {
5105 let session = make_session(vec![], 0);
5106 assert!(session.actions_in_window(3).is_empty());
5107 }
5108
5109 #[test]
5110 fn test_actions_in_window_returns_last_n_steps() {
5111 let steps = vec![
5112 make_step("t", "alpha", "r"),
5113 make_step("t", "beta", "r"),
5114 make_step("t", "gamma", "r"),
5115 ];
5116 let session = make_session(steps, 0);
5117 let window = session.actions_in_window(2);
5118 assert_eq!(window, vec!["beta", "gamma"]);
5119 }
5120
5121 #[test]
5122 fn test_actions_in_window_all_when_fewer_than_n() {
5123 let steps = vec![make_step("t", "solo", "r")];
5124 let session = make_session(steps, 0);
5125 assert_eq!(session.actions_in_window(10), vec!["solo"]);
5126 }
5127
5128 #[test]
5131 fn test_observation_at_returns_correct_observation() {
5132 let steps = vec![
5133 make_step("t1", "a1", "obs-zero"),
5134 make_step("t2", "a2", "obs-one"),
5135 ];
5136 let session = make_session(steps, 0);
5137 assert_eq!(session.observation_at(0), Some("obs-zero"));
5138 assert_eq!(session.observation_at(1), Some("obs-one"));
5139 }
5140
5141 #[test]
5142 fn test_observation_at_returns_none_out_of_bounds() {
5143 let session = make_session(vec![], 0);
5144 assert!(session.observation_at(0).is_none());
5145 }
5146
5147 #[test]
5148 fn test_action_at_returns_correct_action() {
5149 let steps = vec![
5150 make_step("t1", "first-action", "obs"),
5151 make_step("t2", "second-action", "obs"),
5152 ];
5153 let session = make_session(steps, 0);
5154 assert_eq!(session.action_at(0), Some("first-action"));
5155 assert_eq!(session.action_at(1), Some("second-action"));
5156 }
5157
5158 #[test]
5159 fn test_action_at_returns_none_out_of_bounds() {
5160 let session = make_session(vec![], 0);
5161 assert!(session.action_at(5).is_none());
5162 }
5163
5164 #[test]
5167 fn test_has_graph_lookups_false_when_zero() {
5168 let session = make_session(vec![], 0);
5169 assert!(!session.has_graph_lookups());
5170 }
5171
5172 #[test]
5173 fn test_has_graph_lookups_true_when_positive() {
5174 let session = AgentSession {
5175 session_id: "s".into(),
5176 agent_id: AgentId::new("a"),
5177 steps: vec![],
5178 memory_hits: 0,
5179 graph_lookups: 5,
5180 duration_ms: 0,
5181 checkpoint_errors: vec![],
5182 };
5183 assert!(session.has_graph_lookups());
5184 }
5185
5186 #[test]
5187 fn test_consecutive_same_action_at_end_empty_session() {
5188 let session = make_session(vec![], 0);
5189 assert_eq!(session.consecutive_same_action_at_end(), 0);
5190 }
5191
5192 #[test]
5193 fn test_consecutive_same_action_at_end_single_step() {
5194 let steps = vec![make_step("t", "act", "obs")];
5195 let session = make_session(steps, 0);
5196 assert_eq!(session.consecutive_same_action_at_end(), 0);
5197 }
5198
5199 #[test]
5200 fn test_consecutive_same_action_at_end_two_same_at_end() {
5201 let steps = vec![
5202 make_step("t", "other", "obs"),
5203 make_step("t", "repeat", "obs"),
5204 make_step("t", "repeat", "obs"),
5205 ];
5206 let session = make_session(steps, 0);
5207 assert_eq!(session.consecutive_same_action_at_end(), 1);
5208 }
5209
5210 #[test]
5211 fn test_consecutive_same_action_at_end_all_same() {
5212 let steps = vec![
5213 make_step("t", "same", "obs"),
5214 make_step("t", "same", "obs"),
5215 make_step("t", "same", "obs"),
5216 ];
5217 let session = make_session(steps, 0);
5218 assert_eq!(session.consecutive_same_action_at_end(), 2);
5219 }
5220
5221 #[test]
5224 fn test_failure_rate_zero_for_empty_session() {
5225 let session = make_session(vec![], 0);
5226 assert!((session.failure_rate() - 0.0).abs() < 1e-9);
5227 }
5228
5229 #[test]
5230 fn test_failure_rate_zero_when_no_failures() {
5231 let steps = vec![
5232 make_step("t", "lookup", "ok"),
5233 make_step("t", "search", "ok"),
5234 ];
5235 let session = make_session(steps, 0);
5236 assert!((session.failure_rate() - 0.0).abs() < 1e-9);
5237 }
5238
5239 #[test]
5240 fn test_unique_action_count_zero_for_empty_session() {
5241 let session = make_session(vec![], 0);
5242 assert_eq!(session.unique_action_count(), 0);
5243 }
5244
5245 #[test]
5246 fn test_unique_action_count_counts_distinct_actions() {
5247 let steps = vec![
5248 make_step("t", "search", "r"),
5249 make_step("t", "lookup", "r"),
5250 make_step("t", "search", "r"), ];
5252 let session = make_session(steps, 0);
5253 assert_eq!(session.unique_action_count(), 2);
5254 }
5255
5256 #[test]
5259 fn test_total_thought_length_zero_for_empty_session() {
5260 let session = make_session(vec![], 0);
5261 assert_eq!(session.total_thought_length(), 0);
5262 }
5263
5264 #[test]
5265 fn test_total_thought_length_sums_all_thoughts() {
5266 let steps = vec![
5267 make_step("hi", "a", "r"), make_step("hello", "b", "r"), ];
5270 let session = make_session(steps, 0);
5271 assert_eq!(session.total_thought_length(), 7);
5272 }
5273
5274 #[test]
5275 fn test_longest_observation_none_for_empty_session() {
5276 let session = make_session(vec![], 0);
5277 assert!(session.longest_observation().is_none());
5278 }
5279
5280 #[test]
5281 fn test_longest_observation_returns_longest() {
5282 let steps = vec![
5283 make_step("t", "a", "short"),
5284 make_step("t", "b", "a much longer observation"),
5285 ];
5286 let session = make_session(steps, 0);
5287 assert_eq!(session.longest_observation(), Some("a much longer observation"));
5288 }
5289
5290 #[test]
5293 fn test_steps_with_empty_observations_zero_when_all_filled() {
5294 let steps = vec![make_step("t", "a", "obs"), make_step("t", "b", "obs2")];
5295 let session = make_session(steps, 0);
5296 assert_eq!(session.steps_with_empty_observations(), 0);
5297 }
5298
5299 #[test]
5300 fn test_steps_with_empty_observations_counts_empty_ones() {
5301 let steps = vec![
5302 make_step("t", "a", ""), make_step("t", "b", "ok"),
5304 make_step("t", "c", ""), ];
5306 let session = make_session(steps, 0);
5307 assert_eq!(session.steps_with_empty_observations(), 2);
5308 }
5309
5310 #[test]
5311 fn test_min_thought_length_zero_for_empty_session() {
5312 let session = make_session(vec![], 0);
5313 assert_eq!(session.min_thought_length(), 0);
5314 }
5315
5316 #[test]
5317 fn test_min_thought_length_returns_shortest_non_empty() {
5318 let steps = vec![
5319 make_step("hi", "a", "r"), make_step("hello", "b", "r"), make_step("", "c", "r"), ];
5323 let session = make_session(steps, 0);
5324 assert_eq!(session.min_thought_length(), 2);
5325 }
5326
5327 #[test]
5330 fn test_observation_lengths_empty_for_empty_session() {
5331 let session = make_session(vec![], 0);
5332 assert!(session.observation_lengths().is_empty());
5333 }
5334
5335 #[test]
5336 fn test_observation_lengths_returns_lengths_in_order() {
5337 let steps = vec![
5338 make_step("t", "a", "hi"), make_step("t", "b", "hello"), ];
5341 let session = make_session(steps, 0);
5342 assert_eq!(session.observation_lengths(), vec![2, 5]);
5343 }
5344
5345 #[test]
5346 fn test_avg_observation_length_zero_for_empty_session() {
5347 let session = make_session(vec![], 0);
5348 assert!((session.avg_observation_length() - 0.0).abs() < 1e-9);
5349 }
5350
5351 #[test]
5352 fn test_avg_observation_length_correct_mean() {
5353 let steps = vec![
5354 make_step("t", "a", "hi"), make_step("t", "b", "hello"), ];
5357 let session = make_session(steps, 0);
5358 assert!((session.avg_observation_length() - 3.5).abs() < 1e-9);
5360 }
5361
5362 #[test]
5363 fn test_duration_secs_converts_ms_to_seconds() {
5364 let session = make_session(vec![], 7000);
5365 assert_eq!(session.duration_secs(), 7);
5366 }
5367
5368 #[test]
5369 fn test_steps_above_thought_length_counts_qualifying_steps() {
5370 let steps = vec![
5371 make_step("hi", "a", "obs"),
5372 make_step("a longer thought here", "b", "obs"),
5373 make_step("medium thought", "c", "obs"),
5374 ];
5375 let session = make_session(steps, 0);
5376 assert_eq!(session.steps_above_thought_length(5), 2);
5378 }
5379
5380 #[test]
5381 fn test_has_final_answer_true_when_step_has_final_answer_action() {
5382 let steps = vec![
5383 make_step("think", "search", "result"),
5384 make_step("done", "FINAL_ANSWER: 42", ""),
5385 ];
5386 let session = make_session(steps, 0);
5387 assert!(session.has_final_answer());
5388 }
5389
5390 #[test]
5391 fn test_has_final_answer_false_when_no_final_answer_step() {
5392 let steps = vec![make_step("think", "search", "result")];
5393 let session = make_session(steps, 0);
5394 assert!(!session.has_final_answer());
5395 }
5396
5397 #[test]
5398 fn test_avg_action_length_correct_mean() {
5399 let steps = vec![
5400 make_step("t", "ab", "o"), make_step("t", "abcd", "o"), ];
5403 let session = make_session(steps, 0);
5404 assert!((session.avg_action_length() - 3.0).abs() < 1e-9);
5405 }
5406
5407 #[test]
5408 fn test_avg_action_length_empty_returns_zero() {
5409 let session = make_session(vec![], 0);
5410 assert_eq!(session.avg_action_length(), 0.0);
5411 }
5412
5413 #[test]
5414 fn test_thought_lengths_returns_lengths_in_order() {
5415 let steps = vec![
5416 make_step("hi", "a", "o"),
5417 make_step("hello", "b", "o"),
5418 ];
5419 let session = make_session(steps, 0);
5420 assert_eq!(session.thought_lengths(), vec![2, 5]);
5421 }
5422
5423 #[test]
5424 fn test_most_common_action_returns_most_frequent() {
5425 let steps = vec![
5426 make_step("t", "search", "o"),
5427 make_step("t", "search", "o"),
5428 make_step("t", "other", "o"),
5429 ];
5430 let session = make_session(steps, 0);
5431 assert_eq!(session.most_common_action(), Some("search"));
5432 }
5433
5434 #[test]
5435 fn test_most_common_action_none_for_empty_session() {
5436 let session = make_session(vec![], 0);
5437 assert!(session.most_common_action().is_none());
5438 }
5439
5440 #[test]
5441 fn test_count_steps_with_action_counts_exact_matches() {
5442 let steps = vec![
5443 make_step("t", "search", "o"),
5444 make_step("t", "search", "o"),
5445 make_step("t", "other", "o"),
5446 ];
5447 let session = make_session(steps, 0);
5448 assert_eq!(session.count_steps_with_action("search"), 2);
5449 assert_eq!(session.count_steps_with_action("other"), 1);
5450 assert_eq!(session.count_steps_with_action("missing"), 0);
5451 }
5452
5453 #[test]
5454 fn test_thought_contains_count_counts_matching_steps() {
5455 let steps = vec![
5456 make_step("search for rust", "a", "o"),
5457 make_step("think about python", "b", "o"),
5458 make_step("rust is great", "c", "o"),
5459 ];
5460 let session = make_session(steps, 0);
5461 assert_eq!(session.thought_contains_count("rust"), 2);
5462 assert_eq!(session.thought_contains_count("python"), 1);
5463 assert_eq!(session.thought_contains_count("java"), 0);
5464 }
5465
5466 #[test]
5467 fn test_count_nonempty_thoughts_counts_steps_with_thoughts() {
5468 let steps = vec![
5469 make_step("hello", "a", "o"),
5470 make_step("", "b", "o"),
5471 make_step("world", "c", "o"),
5472 ];
5473 let session = make_session(steps, 0);
5474 assert_eq!(session.count_nonempty_thoughts(), 2);
5475 }
5476
5477 #[test]
5478 fn test_observation_contains_count_counts_matching_observations() {
5479 let steps = vec![
5480 make_step("t", "a", "result: success"),
5481 make_step("t", "b", "result: failure"),
5482 make_step("t", "c", "no match here"),
5483 ];
5484 let session = make_session(steps, 0);
5485 assert_eq!(session.observation_contains_count("result"), 2);
5486 assert_eq!(session.observation_contains_count("success"), 1);
5487 }
5488
5489 #[test]
5492 fn test_action_lengths_returns_byte_lengths_in_order() {
5493 let steps = vec![
5494 make_step("t", "ab", "o"),
5495 make_step("t", "hello", "o"),
5496 make_step("t", "", "o"),
5497 ];
5498 let session = make_session(steps, 0);
5499 assert_eq!(session.action_lengths(), vec![2, 5, 0]);
5500 }
5501
5502 #[test]
5503 fn test_action_lengths_empty_session_returns_empty_vec() {
5504 let session = make_session(vec![], 0);
5505 assert!(session.action_lengths().is_empty());
5506 }
5507
5508 #[test]
5509 fn test_step_success_count_excludes_failed_steps() {
5510 let steps = vec![
5511 make_step("t", "a", "ok"),
5512 make_step("t", "b", "{\"error\": \"timeout\"}"),
5513 make_step("t", "c", "ok"),
5514 ];
5515 let session = make_session(steps, 0);
5516 assert_eq!(session.step_success_count(), 2);
5517 }
5518
5519 #[test]
5520 fn test_step_success_count_all_success_when_no_failures() {
5521 let steps = vec![make_step("t", "a", "ok"), make_step("t", "b", "ok")];
5522 let session = make_session(steps, 0);
5523 assert_eq!(session.step_success_count(), 2);
5524 }
5525
5526 #[test]
5529 fn test_longest_thought_returns_step_with_most_bytes() {
5530 let steps = vec![
5531 make_step("hi", "a", "o"),
5532 make_step("hello world", "b", "o"),
5533 make_step("hey", "c", "o"),
5534 ];
5535 let session = make_session(steps, 0);
5536 assert_eq!(session.longest_thought(), Some("hello world"));
5537 }
5538
5539 #[test]
5540 fn test_longest_thought_returns_none_for_empty_session() {
5541 let session = make_session(vec![], 0);
5542 assert!(session.longest_thought().is_none());
5543 }
5544
5545 #[test]
5546 fn test_shortest_action_returns_step_with_fewest_bytes() {
5547 let steps = vec![
5548 make_step("t", "search", "o"),
5549 make_step("t", "go", "o"),
5550 make_step("t", "lookup", "o"),
5551 ];
5552 let session = make_session(steps, 0);
5553 assert_eq!(session.shortest_action(), Some("go"));
5554 }
5555
5556 #[test]
5557 fn test_shortest_action_returns_none_for_empty_session() {
5558 let session = make_session(vec![], 0);
5559 assert!(session.shortest_action().is_none());
5560 }
5561
5562 #[test]
5565 fn test_first_step_action_returns_action_of_first_step() {
5566 let steps = vec![
5567 make_step("t", "first", "o"),
5568 make_step("t", "second", "o"),
5569 ];
5570 let session = make_session(steps, 0);
5571 assert_eq!(session.first_step_action(), Some("first"));
5572 }
5573
5574 #[test]
5575 fn test_first_step_action_returns_none_for_empty_session() {
5576 let session = make_session(vec![], 0);
5577 assert!(session.first_step_action().is_none());
5578 }
5579
5580 #[test]
5581 fn test_last_step_action_returns_action_of_last_step() {
5582 let steps = vec![
5583 make_step("t", "first", "o"),
5584 make_step("t", "last_one", "o"),
5585 ];
5586 let session = make_session(steps, 0);
5587 assert_eq!(session.last_step_action(), Some("last_one"));
5588 }
5589
5590 #[test]
5591 fn test_last_step_action_returns_none_for_empty_session() {
5592 let session = make_session(vec![], 0);
5593 assert!(session.last_step_action().is_none());
5594 }
5595
5596 #[test]
5599 fn test_total_thought_bytes_sums_all_thought_lengths() {
5600 let steps = vec![
5601 make_step("hi", "a", "o"), make_step("hello", "b", "o"), ];
5604 let session = make_session(steps, 0);
5605 assert_eq!(session.total_thought_bytes(), 7);
5606 }
5607
5608 #[test]
5609 fn test_total_observation_bytes_sums_all_observation_lengths() {
5610 let steps = vec![
5611 make_step("t", "a", "ok"), make_step("t", "b", "done!"), ];
5614 let session = make_session(steps, 0);
5615 assert_eq!(session.total_observation_bytes(), 7);
5616 }
5617
5618 #[test]
5621 fn test_steps_in_range_returns_correct_slice() {
5622 let steps = vec![
5623 make_step("t", "a", "o"),
5624 make_step("t", "b", "o"),
5625 make_step("t", "c", "o"),
5626 ];
5627 let session = make_session(steps, 0);
5628 let slice = session.steps_in_range(1, 3);
5629 assert_eq!(slice.len(), 2);
5630 assert_eq!(slice[0].action, "b");
5631 assert_eq!(slice[1].action, "c");
5632 }
5633
5634 #[test]
5635 fn test_steps_in_range_returns_empty_for_out_of_bounds_start() {
5636 let steps = vec![make_step("t", "a", "o")];
5637 let session = make_session(steps, 0);
5638 assert!(session.steps_in_range(5, 10).is_empty());
5639 }
5640
5641 #[test]
5642 fn test_median_step_duration_ms_odd_count() {
5643 let mut steps = vec![
5644 make_step("t", "a", "o"),
5645 make_step("t", "b", "o"),
5646 make_step("t", "c", "o"),
5647 ];
5648 steps[0].step_duration_ms = 10;
5649 steps[1].step_duration_ms = 50;
5650 steps[2].step_duration_ms = 30;
5651 let session = make_session(steps, 0);
5652 assert_eq!(session.median_step_duration_ms(), 30);
5654 }
5655
5656 #[test]
5657 fn test_median_step_duration_ms_returns_zero_for_empty_session() {
5658 let session = make_session(vec![], 0);
5659 assert_eq!(session.median_step_duration_ms(), 0);
5660 }
5661
5662 #[test]
5665 fn test_into_steps_consumes_session_and_returns_owned_vec() {
5666 let steps = vec![
5667 make_step("think", "act", "obs"),
5668 make_step("think2", "act2", "obs2"),
5669 ];
5670 let session = make_session(steps, 0);
5671 let owned = session.into_steps();
5672 assert_eq!(owned.len(), 2);
5673 assert_eq!(owned[0].thought, "think");
5674 assert_eq!(owned[1].action, "act2");
5675 }
5676
5677 #[test]
5678 fn test_into_steps_returns_empty_vec_for_empty_session() {
5679 let session = make_session(vec![], 0);
5680 assert!(session.into_steps().is_empty());
5681 }
5682
5683 #[test]
5684 fn test_iter_steps_iterates_in_order() {
5685 let steps = vec![
5686 make_step("t1", "a1", "o1"),
5687 make_step("t2", "a2", "o2"),
5688 ];
5689 let session = make_session(steps, 0);
5690 let thoughts: Vec<&str> = session.iter_steps().map(|s| s.thought.as_str()).collect();
5691 assert_eq!(thoughts, vec!["t1", "t2"]);
5692 }
5693
5694 #[test]
5695 fn test_has_at_least_steps_true_when_enough_steps() {
5696 let session = make_session(vec![make_step("t", "a", "o"), make_step("t", "a", "o")], 0);
5697 assert!(session.has_at_least_steps(2));
5698 assert!(session.has_at_least_steps(1));
5699 }
5700
5701 #[test]
5702 fn test_has_at_least_steps_false_when_too_few() {
5703 let session = make_session(vec![make_step("t", "a", "o")], 0);
5704 assert!(!session.has_at_least_steps(2));
5705 }
5706
5707 #[test]
5708 fn test_has_at_least_steps_zero_always_true() {
5709 let session = make_session(vec![], 0);
5710 assert!(session.has_at_least_steps(0));
5711 }
5712
5713 #[test]
5716 fn test_p95_step_duration_ms_returns_high_percentile() {
5717 let mut steps: Vec<ReActStep> = (1u64..=20)
5719 .map(|ms| ReActStep::new("t", "a", "o").with_duration(ms))
5720 .collect();
5721 steps.reverse();
5723 let session = make_session(steps, 0);
5724 assert_eq!(session.p95_step_duration_ms(), 19);
5725 }
5726
5727 #[test]
5728 fn test_p95_step_duration_ms_returns_zero_for_empty_session() {
5729 let session = make_session(vec![], 0);
5730 assert_eq!(session.p95_step_duration_ms(), 0);
5731 }
5732
5733 #[test]
5734 fn test_p99_step_duration_ms_returns_highest_for_small_set() {
5735 let steps: Vec<ReActStep> = (1u64..=10)
5737 .map(|ms| ReActStep::new("t", "a", "o").with_duration(ms))
5738 .collect();
5739 let session = make_session(steps, 0);
5740 assert_eq!(session.p99_step_duration_ms(), 10);
5741 }
5742
5743 #[test]
5744 fn test_p99_step_duration_ms_returns_zero_for_empty_session() {
5745 let session = make_session(vec![], 0);
5746 assert_eq!(session.p99_step_duration_ms(), 0);
5747 }
5748
5749 #[test]
5750 fn test_step_count_above_duration_ms_counts_slow_steps() {
5751 let steps = vec![
5752 ReActStep::new("t", "a", "o").with_duration(10),
5753 ReActStep::new("t", "b", "o").with_duration(200),
5754 ReActStep::new("t", "c", "o").with_duration(50),
5755 ReActStep::new("t", "d", "o").with_duration(300),
5756 ];
5757 let session = make_session(steps, 0);
5758 assert_eq!(session.step_count_above_duration_ms(100), 2);
5759 assert_eq!(session.step_count_above_duration_ms(500), 0);
5760 }
5761
5762 #[test]
5763 fn test_step_count_above_duration_ms_zero_for_empty_session() {
5764 let session = make_session(vec![], 0);
5765 assert_eq!(session.step_count_above_duration_ms(0), 0);
5766 }
5767
5768 #[test]
5771 fn test_total_action_bytes_sums_action_lengths() {
5772 let steps = vec![
5773 make_step("t", "ab", "o"), make_step("t", "cde", "o"), ];
5776 let session = make_session(steps, 0);
5777 assert_eq!(session.total_action_bytes(), 5);
5778 }
5779
5780 #[test]
5781 fn test_total_action_bytes_empty_session_returns_zero() {
5782 let session = make_session(vec![], 0);
5783 assert_eq!(session.total_action_bytes(), 0);
5784 }
5785
5786 #[test]
5787 fn test_step_duration_variance_ms_computed_correctly() {
5788 let mut steps = vec![
5789 make_step("t", "a", "o"),
5790 make_step("t", "b", "o"),
5791 ];
5792 steps[0].step_duration_ms = 10;
5793 steps[1].step_duration_ms = 20;
5794 let session = make_session(steps, 0);
5795 assert!((session.step_duration_variance_ms() - 25.0).abs() < 1e-9);
5797 }
5798
5799 #[test]
5800 fn test_step_duration_variance_ms_zero_for_single_step() {
5801 let session = make_session(vec![make_step("t", "a", "o")], 0);
5802 assert_eq!(session.step_duration_variance_ms(), 0.0);
5803 }
5804
5805 #[test]
5806 fn test_steps_with_errors_returns_steps_containing_error() {
5807 let steps = vec![
5808 make_step("t", "a", "success"),
5809 make_step("t", "b", "error: timeout"),
5810 make_step("t", "c", "ok"),
5811 make_step("t", "d", "Error: not found"),
5812 ];
5813 let session = make_session(steps, 0);
5814 assert_eq!(session.steps_with_errors().len(), 2);
5815 }
5816
5817 #[test]
5818 fn test_steps_with_errors_empty_when_no_errors() {
5819 let steps = vec![make_step("t", "a", "ok"), make_step("t", "b", "done")];
5820 let session = make_session(steps, 0);
5821 assert!(session.steps_with_errors().is_empty());
5822 }
5823
5824 #[test]
5827 fn test_min_step_duration_ms_returns_minimum() {
5828 let mut steps = vec![
5829 make_step("t", "a", "o"),
5830 make_step("t", "b", "o"),
5831 make_step("t", "c", "o"),
5832 ];
5833 steps[0].step_duration_ms = 50;
5834 steps[1].step_duration_ms = 10;
5835 steps[2].step_duration_ms = 30;
5836 let session = make_session(steps, 0);
5837 assert_eq!(session.min_step_duration_ms(), 10);
5838 }
5839
5840 #[test]
5841 fn test_min_step_duration_ms_empty_returns_zero() {
5842 let session = make_session(vec![], 0);
5843 assert_eq!(session.min_step_duration_ms(), 0);
5844 }
5845
5846 #[test]
5847 fn test_max_step_duration_ms_returns_maximum() {
5848 let mut steps = vec![
5849 make_step("t", "a", "o"),
5850 make_step("t", "b", "o"),
5851 make_step("t", "c", "o"),
5852 ];
5853 steps[0].step_duration_ms = 50;
5854 steps[1].step_duration_ms = 10;
5855 steps[2].step_duration_ms = 30;
5856 let session = make_session(steps, 0);
5857 assert_eq!(session.max_step_duration_ms(), 50);
5858 }
5859
5860 #[test]
5861 fn test_max_step_duration_ms_empty_returns_zero() {
5862 let session = make_session(vec![], 0);
5863 assert_eq!(session.max_step_duration_ms(), 0);
5864 }
5865
5866 #[test]
5869 fn test_steps_with_long_observations_returns_steps_above_threshold() {
5870 let steps = vec![
5871 make_step("t", "a", "short"), make_step("t", "b", "this is a long observation"), ];
5874 let session = make_session(steps, 0);
5875 assert_eq!(session.steps_with_long_observations(10).len(), 1);
5876 assert_eq!(session.steps_with_long_observations(4).len(), 2);
5877 }
5878
5879 #[test]
5880 fn test_steps_with_long_observations_empty_for_high_threshold() {
5881 let steps = vec![make_step("t", "a", "hi")];
5882 let session = make_session(steps, 0);
5883 assert!(session.steps_with_long_observations(1000).is_empty());
5884 }
5885
5886 #[test]
5887 fn test_unique_observations_count_counts_distinct_values() {
5888 let steps = vec![
5889 make_step("t", "a", "ok"),
5890 make_step("t", "b", "ok"),
5891 make_step("t", "c", "done"),
5892 ];
5893 let session = make_session(steps, 0);
5894 assert_eq!(session.unique_observations_count(), 2);
5895 }
5896
5897 #[test]
5898 fn test_unique_observations_count_zero_for_empty_session() {
5899 let session = make_session(vec![], 0);
5900 assert_eq!(session.unique_observations_count(), 0);
5901 }
5902
5903 #[test]
5906 fn test_thought_max_bytes_returns_max_thought_length() {
5907 let steps = vec![
5908 make_step("hi", "a", "o"),
5909 make_step("hello world", "b", "o"),
5910 ];
5911 let session = make_session(steps, 0);
5912 assert_eq!(session.thought_max_bytes(), 11);
5913 }
5914
5915 #[test]
5916 fn test_thought_max_bytes_zero_for_empty_session() {
5917 let session = make_session(vec![], 0);
5918 assert_eq!(session.thought_max_bytes(), 0);
5919 }
5920
5921 #[test]
5922 fn test_observation_max_bytes_returns_max_observation_length() {
5923 let steps = vec![
5924 make_step("t", "a", "short"),
5925 make_step("t", "b", "much longer observation"),
5926 ];
5927 let session = make_session(steps, 0);
5928 assert_eq!(session.observation_max_bytes(), "much longer observation".len());
5929 }
5930
5931 #[test]
5932 fn test_step_count_below_duration_ms_counts_fast_steps() {
5933 let mut steps = vec![
5934 make_step("t", "a", "o"),
5935 make_step("t", "b", "o"),
5936 make_step("t", "c", "o"),
5937 ];
5938 steps[0].step_duration_ms = 5;
5939 steps[1].step_duration_ms = 50;
5940 steps[2].step_duration_ms = 500;
5941 let session = make_session(steps, 0);
5942 assert_eq!(session.step_count_below_duration_ms(100), 2);
5943 assert_eq!(session.step_count_below_duration_ms(6), 1);
5944 }
5945
5946 #[test]
5947 fn test_step_count_below_duration_ms_zero_for_empty() {
5948 let session = make_session(vec![], 0);
5949 assert_eq!(session.step_count_below_duration_ms(100), 0);
5950 }
5951
5952 #[test]
5955 fn test_total_observation_count_counts_non_empty_observations() {
5956 let steps = vec![
5957 make_step("t", "a", "result"),
5958 make_step("t", "b", ""),
5959 make_step("t", "c", "output"),
5960 ];
5961 let session = make_session(steps, 0);
5962 assert_eq!(session.total_observation_count(), 2);
5963 }
5964
5965 #[test]
5966 fn test_total_observation_count_zero_when_all_empty() {
5967 let steps = vec![make_step("t", "a", ""), make_step("t", "b", "")];
5968 let session = make_session(steps, 0);
5969 assert_eq!(session.total_observation_count(), 0);
5970 }
5971
5972 #[test]
5973 fn test_actions_containing_returns_matching_steps() {
5974 let steps = vec![
5975 make_step("t", "search(query)", "r"),
5976 make_step("t", "write(data)", "r"),
5977 make_step("t", "search(other)", "r"),
5978 ];
5979 let session = make_session(steps, 0);
5980 assert_eq!(session.actions_containing("search").len(), 2);
5981 }
5982
5983 #[test]
5984 fn test_actions_containing_empty_when_no_match() {
5985 let steps = vec![make_step("t", "write(x)", "r")];
5986 let session = make_session(steps, 0);
5987 assert!(session.actions_containing("read").is_empty());
5988 }
5989
5990 #[test]
5991 fn test_step_duration_range_ms_returns_min_max() {
5992 let mut steps = vec![
5993 make_step("t", "a", "o"),
5994 make_step("t", "b", "o"),
5995 make_step("t", "c", "o"),
5996 ];
5997 steps[0].step_duration_ms = 10;
5998 steps[1].step_duration_ms = 50;
5999 steps[2].step_duration_ms = 30;
6000 let session = make_session(steps, 0);
6001 assert_eq!(session.step_duration_range_ms(), (10, 50));
6002 }
6003
6004 #[test]
6005 fn test_step_duration_range_ms_zero_zero_for_empty() {
6006 let session = make_session(vec![], 0);
6007 assert_eq!(session.step_duration_range_ms(), (0, 0));
6008 }
6009
6010 #[test]
6013 fn test_count_unique_thoughts_counts_distinct_strings() {
6014 let steps = vec![
6015 make_step("alpha", "a", "o"),
6016 make_step("beta", "b", "o"),
6017 make_step("alpha", "c", "o"), ];
6019 let session = make_session(steps, 0);
6020 assert_eq!(session.count_unique_thoughts(), 2);
6021 }
6022
6023 #[test]
6024 fn test_count_unique_thoughts_zero_for_empty_session() {
6025 let session = make_session(vec![], 0);
6026 assert_eq!(session.count_unique_thoughts(), 0);
6027 }
6028
6029 #[test]
6030 fn test_steps_with_empty_thoughts_returns_matching_steps() {
6031 let steps = vec![
6032 make_step("", "a", "o"),
6033 make_step("thought", "b", "o"),
6034 make_step("", "c", "o"),
6035 ];
6036 let session = make_session(steps, 0);
6037 assert_eq!(session.steps_with_empty_thoughts().len(), 2);
6038 }
6039
6040 #[test]
6041 fn test_steps_with_empty_thoughts_returns_empty_when_all_have_thoughts() {
6042 let steps = vec![make_step("t1", "a", "o"), make_step("t2", "b", "o")];
6043 let session = make_session(steps, 0);
6044 assert!(session.steps_with_empty_thoughts().is_empty());
6045 }
6046
6047 #[test]
6050 fn test_max_action_bytes_returns_longest_action() {
6051 let steps = vec![
6052 make_step("t", "short", "o"),
6053 make_step("t", "much longer action string", "o"),
6054 ];
6055 let session = make_session(steps, 0);
6056 assert_eq!(session.max_action_bytes(), "much longer action string".len());
6057 }
6058
6059 #[test]
6060 fn test_max_action_bytes_zero_for_empty_session() {
6061 let session = make_session(vec![], 0);
6062 assert_eq!(session.max_action_bytes(), 0);
6063 }
6064
6065 #[test]
6066 fn test_min_action_bytes_returns_shortest_action() {
6067 let steps = vec![
6068 make_step("t", "ab", "o"),
6069 make_step("t", "abcde", "o"),
6070 ];
6071 let session = make_session(steps, 0);
6072 assert_eq!(session.min_action_bytes(), 2);
6073 }
6074
6075 #[test]
6076 fn test_min_action_bytes_zero_for_empty_session() {
6077 let session = make_session(vec![], 0);
6078 assert_eq!(session.min_action_bytes(), 0);
6079 }
6080
6081 #[test]
6082 fn test_step_throughput_per_sec_computes_ratio() {
6083 let steps = vec![make_step("t", "a", "o"), make_step("t", "b", "o")];
6084 let session = make_session(steps, 2000); assert!((session.step_throughput_per_sec() - 1.0).abs() < 1e-9);
6086 }
6087
6088 #[test]
6089 fn test_step_throughput_per_sec_zero_for_zero_duration() {
6090 let steps = vec![make_step("t", "a", "o")];
6091 let session = make_session(steps, 0);
6092 assert_eq!(session.step_throughput_per_sec(), 0.0);
6093 }
6094
6095 #[test]
6098 fn test_final_answer_step_index_returns_correct_index() {
6099 let steps = vec![
6100 make_step("think", "search(x)", "result"),
6101 make_step("think2", "FINAL_ANSWER: done", ""),
6102 ];
6103 let session = make_session(steps, 0);
6104 assert_eq!(session.final_answer_step_index(), Some(1));
6105 }
6106
6107 #[test]
6108 fn test_final_answer_step_index_returns_none_when_no_final_answer() {
6109 let steps = vec![make_step("t", "search(x)", "result")];
6110 let session = make_session(steps, 0);
6111 assert_eq!(session.final_answer_step_index(), None);
6112 }
6113
6114 #[test]
6115 fn test_final_answer_step_index_returns_none_for_empty_session() {
6116 let session = make_session(vec![], 0);
6117 assert_eq!(session.final_answer_step_index(), None);
6118 }
6119
6120 #[test]
6121 fn test_final_answer_step_index_returns_first_occurrence() {
6122 let steps = vec![
6123 make_step("t", "FINAL_ANSWER: first", ""),
6124 make_step("t", "FINAL_ANSWER: second", ""),
6125 ];
6126 let session = make_session(steps, 0);
6127 assert_eq!(session.final_answer_step_index(), Some(0));
6128 }
6129
6130 #[test]
6133 fn test_first_n_steps_returns_first_n() {
6134 let steps = vec![
6135 make_step("t1", "a1", "o1"),
6136 make_step("t2", "a2", "o2"),
6137 make_step("t3", "a3", "o3"),
6138 ];
6139 let session = make_session(steps, 0);
6140 assert_eq!(session.first_n_steps(2).len(), 2);
6141 assert_eq!(session.first_n_steps(2)[0].thought, "t1");
6142 }
6143
6144 #[test]
6145 fn test_first_n_steps_returns_all_when_n_exceeds_count() {
6146 let steps = vec![make_step("t", "a", "o")];
6147 let session = make_session(steps, 0);
6148 assert_eq!(session.first_n_steps(10).len(), 1);
6149 }
6150
6151 #[test]
6152 fn test_first_n_steps_empty_for_n_zero() {
6153 let steps = vec![make_step("t", "a", "o")];
6154 let session = make_session(steps, 0);
6155 assert!(session.first_n_steps(0).is_empty());
6156 }
6157
6158 #[test]
6159 fn test_steps_with_tool_returns_matching_steps() {
6160 let steps = vec![
6161 make_step("t", "search(query)", "result"),
6162 make_step("t", "write(data)", "ok"),
6163 make_step("t", "search(more)", "more"),
6164 ];
6165 let session = make_session(steps, 0);
6166 assert_eq!(session.steps_with_tool("search").len(), 2);
6167 assert_eq!(session.steps_with_tool("write").len(), 1);
6168 }
6169
6170 #[test]
6171 fn test_steps_with_tool_excludes_final_answer() {
6172 let steps = vec![
6173 make_step("t", "FINAL_ANSWER: search done", ""),
6174 ];
6175 let session = make_session(steps, 0);
6176 assert!(session.steps_with_tool("search").is_empty());
6178 }
6179
6180 #[test]
6181 fn test_total_chars_sums_all_strings() {
6182 let steps = vec![
6183 make_step("abc", "de", "f"), make_step("g", "hi", "jkl"), ];
6186 let session = make_session(steps, 0);
6187 assert_eq!(session.total_chars(), 12);
6188 }
6189
6190 #[test]
6191 fn test_total_chars_zero_for_empty_session() {
6192 let session = make_session(vec![], 0);
6193 assert_eq!(session.total_chars(), 0);
6194 }
6195
6196 #[test]
6199 fn test_avg_action_bytes_computes_mean() {
6200 let steps = vec![
6201 make_step("t", "ab", "o"), make_step("t", "abcd", "o"), ];
6204 let session = make_session(steps, 0);
6205 assert!((session.avg_action_bytes() - 3.0).abs() < 1e-9);
6206 }
6207
6208 #[test]
6209 fn test_avg_action_bytes_zero_for_empty_session() {
6210 let session = make_session(vec![], 0);
6211 assert_eq!(session.avg_action_bytes(), 0.0);
6212 }
6213
6214 #[test]
6215 fn test_avg_observation_bytes_computes_mean() {
6216 let steps = vec![
6217 make_step("t", "a", "hi"), make_step("t", "b", "world"), ];
6220 let session = make_session(steps, 0);
6221 assert!((session.avg_observation_bytes() - 3.5).abs() < 1e-9);
6222 }
6223
6224 #[test]
6225 fn test_avg_observation_bytes_zero_for_empty_session() {
6226 let session = make_session(vec![], 0);
6227 assert_eq!(session.avg_observation_bytes(), 0.0);
6228 }
6229
6230 #[test]
6233 fn test_steps_with_long_thoughts_returns_steps_exceeding_threshold() {
6234 let steps = vec![
6235 make_step("short", "a", "o"),
6236 make_step("this is a much longer thought string", "b", "o"),
6237 make_step("hi", "c", "o"),
6238 ];
6239 let session = make_session(steps, 0);
6240 assert_eq!(session.steps_with_long_thoughts(10).len(), 1);
6241 }
6242
6243 #[test]
6244 fn test_steps_with_long_thoughts_empty_when_none_exceed() {
6245 let steps = vec![make_step("hi", "a", "o")];
6246 let session = make_session(steps, 0);
6247 assert!(session.steps_with_long_thoughts(100).is_empty());
6248 }
6249
6250 #[test]
6251 fn test_action_count_containing_counts_matching_steps() {
6252 let steps = vec![
6253 make_step("t", "search(query)", "o"),
6254 make_step("t", "write(data)", "o"),
6255 make_step("t", "search(other)", "o"),
6256 ];
6257 let session = make_session(steps, 0);
6258 assert_eq!(session.action_count_containing("search"), 2);
6259 }
6260
6261 #[test]
6262 fn test_action_count_containing_zero_when_no_match() {
6263 let steps = vec![make_step("t", "write(x)", "o")];
6264 let session = make_session(steps, 0);
6265 assert_eq!(session.action_count_containing("read"), 0);
6266 }
6267
6268 #[test]
6269 fn test_total_thought_count_counts_non_empty_thoughts() {
6270 let steps = vec![
6271 make_step("thought", "a", "o"),
6272 make_step("", "b", "o"),
6273 make_step("another", "c", "o"),
6274 ];
6275 let session = make_session(steps, 0);
6276 assert_eq!(session.total_thought_count(), 2);
6277 }
6278
6279 #[test]
6280 fn test_total_thought_count_zero_for_empty_session() {
6281 let session = make_session(vec![], 0);
6282 assert_eq!(session.total_thought_count(), 0);
6283 }
6284
6285 #[test]
6288 fn test_has_thought_containing_true_when_substring_found() {
6289 let steps = vec![make_step("think about this", "act", "obs")];
6290 let session = make_session(steps, 0);
6291 assert!(session.has_thought_containing("think"));
6292 }
6293
6294 #[test]
6295 fn test_has_thought_containing_false_when_not_found() {
6296 let steps = vec![make_step("unrelated", "act", "obs")];
6297 let session = make_session(steps, 0);
6298 assert!(!session.has_thought_containing("xyz"));
6299 }
6300
6301 #[test]
6302 fn test_has_thought_containing_false_for_empty_session() {
6303 let session = make_session(vec![], 0);
6304 assert!(!session.has_thought_containing("any"));
6305 }
6306
6307 #[test]
6308 fn test_steps_with_action_length_above_returns_matching_steps() {
6309 let steps = vec![
6310 make_step("t", "hi", "o"), make_step("t", "hello world", "o"), ];
6313 let session = make_session(steps, 0);
6314 let result = session.steps_with_action_length_above(5);
6315 assert_eq!(result.len(), 1);
6316 assert_eq!(result[0].action, "hello world");
6317 }
6318
6319 #[test]
6320 fn test_steps_with_action_length_above_empty_when_none_qualify() {
6321 let steps = vec![make_step("t", "hi", "o")];
6322 let session = make_session(steps, 0);
6323 assert!(session.steps_with_action_length_above(100).is_empty());
6324 }
6325
6326 #[test]
6329 fn test_avg_thought_bytes_computes_mean() {
6330 let steps = vec![
6331 make_step("ab", "a", "o"), make_step("abcdef", "b", "o"), ];
6334 let session = make_session(steps, 0);
6335 assert!((session.avg_thought_bytes() - 4.0).abs() < 1e-9);
6336 }
6337
6338 #[test]
6339 fn test_avg_thought_bytes_zero_for_empty_session() {
6340 let session = make_session(vec![], 0);
6341 assert_eq!(session.avg_thought_bytes(), 0.0);
6342 }
6343
6344 #[test]
6345 fn test_steps_above_action_bytes_filters_correctly() {
6346 let steps = vec![
6347 make_step("t", "ab", "o"), make_step("t", "abcdefgh", "o"), make_step("t", "abc", "o"), ];
6351 let session = make_session(steps, 0);
6352 assert_eq!(session.steps_above_action_bytes(3).len(), 1);
6353 }
6354
6355 #[test]
6356 fn test_steps_above_action_bytes_empty_for_empty_session() {
6357 let session = make_session(vec![], 0);
6358 assert!(session.steps_above_action_bytes(0).is_empty());
6359 }
6360
6361 #[test]
6364 fn test_steps_between_returns_subslice() {
6365 let steps = vec![
6366 make_step("t", "a", "o"),
6367 make_step("t", "b", "o"),
6368 make_step("t", "c", "o"),
6369 make_step("t", "d", "o"),
6370 ];
6371 let session = make_session(steps, 0);
6372 let between = session.steps_between(1, 3);
6373 assert_eq!(between.len(), 2);
6374 assert_eq!(between[0].action, "b");
6375 assert_eq!(between[1].action, "c");
6376 }
6377
6378 #[test]
6379 fn test_steps_between_empty_when_start_ge_end() {
6380 let steps = vec![make_step("t", "a", "o"), make_step("t", "b", "o")];
6381 let session = make_session(steps, 0);
6382 assert!(session.steps_between(2, 1).is_empty());
6383 }
6384
6385 #[test]
6386 fn test_steps_with_duplicate_thoughts_returns_duplicates_only() {
6387 let steps = vec![
6388 make_step("alpha", "a", "o"),
6389 make_step("beta", "b", "o"),
6390 make_step("alpha", "c", "o"), ];
6392 let session = make_session(steps, 0);
6393 let dupes = session.steps_with_duplicate_thoughts();
6394 assert_eq!(dupes.len(), 1);
6395 assert_eq!(dupes[0].action, "c");
6396 }
6397
6398 #[test]
6399 fn test_steps_with_duplicate_thoughts_empty_when_all_unique() {
6400 let steps = vec![
6401 make_step("t1", "a", "o"),
6402 make_step("t2", "b", "o"),
6403 ];
6404 let session = make_session(steps, 0);
6405 assert!(session.steps_with_duplicate_thoughts().is_empty());
6406 }
6407
6408 #[test]
6411 fn test_step_observation_rate_returns_fraction_with_observations() {
6412 let steps = vec![
6413 make_step("t", "a", "obs"),
6414 make_step("t", "b", ""),
6415 make_step("t", "c", "obs2"),
6416 ];
6417 let session = make_session(steps, 0);
6418 let rate = session.step_observation_rate();
6419 assert!((rate - 2.0 / 3.0).abs() < 1e-9);
6420 }
6421
6422 #[test]
6423 fn test_step_observation_rate_zero_for_empty_session() {
6424 let session = make_session(vec![], 0);
6425 assert_eq!(session.step_observation_rate(), 0.0);
6426 }
6427
6428 #[test]
6429 fn test_steps_below_thought_bytes_filters_by_threshold() {
6430 let steps = vec![
6431 make_step("hi", "a", "o"),
6432 make_step("hello world", "b", "o"),
6433 ];
6434 let session = make_session(steps, 0);
6435 let below = session.steps_below_thought_bytes(6);
6436 assert_eq!(below.len(), 1);
6437 assert_eq!(below[0].action, "a");
6438 }
6439
6440 #[test]
6441 fn test_steps_below_thought_bytes_empty_when_all_exceed() {
6442 let steps = vec![make_step("long thought text", "a", "o")];
6443 let session = make_session(steps, 0);
6444 assert!(session.steps_below_thought_bytes(3).is_empty());
6445 }
6446
6447 #[test]
6448 fn test_agent_runtime_tool_count_reflects_registered_tools() {
6449 let rt = AgentRuntime::quick(1, "model");
6450 assert_eq!(rt.tool_count(), 0);
6451 }
6452
6453 #[test]
6456 fn test_max_thought_bytes_returns_longest_thought_length() {
6457 let steps = vec![
6458 make_step("hi", "a", "o"),
6459 make_step("hello world", "b", "o"),
6460 ];
6461 let session = make_session(steps, 0);
6462 assert_eq!(session.max_thought_bytes(), 11);
6463 }
6464
6465 #[test]
6466 fn test_max_thought_bytes_zero_for_empty_session() {
6467 let session = make_session(vec![], 0);
6468 assert_eq!(session.max_thought_bytes(), 0);
6469 }
6470
6471 #[test]
6472 fn test_steps_above_observation_bytes_filters_by_threshold() {
6473 let steps = vec![
6474 make_step("t", "a", "tiny"),
6475 make_step("t", "b", "a much longer observation"),
6476 ];
6477 let session = make_session(steps, 0);
6478 let above = session.steps_above_observation_bytes(5);
6479 assert_eq!(above.len(), 1);
6480 assert_eq!(above[0].action, "b");
6481 }
6482
6483 #[test]
6484 fn test_steps_above_observation_bytes_empty_when_all_below() {
6485 let steps = vec![make_step("t", "a", "hi")];
6486 let session = make_session(steps, 0);
6487 assert!(session.steps_above_observation_bytes(100).is_empty());
6488 }
6489
6490 #[test]
6491 fn test_agent_runtime_tool_names_empty_when_no_tools() {
6492 let rt = AgentRuntime::quick(1, "model");
6493 assert!(rt.tool_names().is_empty());
6494 }
6495
6496 #[test]
6499 fn test_steps_between_returns_correct_slice() {
6500 let steps = vec![
6501 make_step("t0", "a0", "o0"),
6502 make_step("t1", "a1", "o1"),
6503 make_step("t2", "a2", "o2"),
6504 make_step("t3", "a3", "o3"),
6505 ];
6506 let session = make_session(steps, 0);
6507 let slice = session.steps_between(1, 3);
6508 assert_eq!(slice.len(), 2);
6509 assert_eq!(slice[0].thought, "t1");
6510 assert_eq!(slice[1].thought, "t2");
6511 }
6512
6513 #[test]
6514 fn test_steps_between_returns_empty_when_start_ge_end() {
6515 let steps = vec![make_step("t", "a", "o"), make_step("t2", "a2", "o2")];
6516 let session = make_session(steps, 0);
6517 assert!(session.steps_between(2, 1).is_empty());
6518 assert!(session.steps_between(1, 1).is_empty());
6519 }
6520
6521 #[test]
6522 fn test_steps_between_clamps_to_step_count() {
6523 let steps = vec![make_step("t", "a", "o")];
6524 let session = make_session(steps, 0);
6525 let slice = session.steps_between(0, 100);
6526 assert_eq!(slice.len(), 1);
6527 }
6528
6529 #[test]
6530 fn test_has_duplicate_actions_true_when_repeated() {
6531 let steps = vec![
6532 make_step("t", "search[foo]", "o"),
6533 make_step("t", "search[foo]", "o"),
6534 ];
6535 let session = make_session(steps, 0);
6536 assert!(session.has_duplicate_actions());
6537 }
6538
6539 #[test]
6540 fn test_has_duplicate_actions_false_when_all_unique() {
6541 let steps = vec![
6542 make_step("t", "search[foo]", "o"),
6543 make_step("t", "lookup[bar]", "o"),
6544 ];
6545 let session = make_session(steps, 0);
6546 assert!(!session.has_duplicate_actions());
6547 }
6548
6549 #[test]
6550 fn test_has_duplicate_actions_false_for_empty_session() {
6551 let session = make_session(vec![], 0);
6552 assert!(!session.has_duplicate_actions());
6553 }
6554
6555 #[test]
6556 fn test_step_indices_with_tool_returns_correct_indices() {
6557 let steps = vec![
6558 make_step("t", "search[x]", "o"),
6559 make_step("t", "lookup[y]", "o"),
6560 make_step("t", "search[z]", "o"),
6561 ];
6562 let session = make_session(steps, 0);
6563 let indices = session.step_indices_with_tool("search");
6564 assert_eq!(indices, vec![0, 2]);
6565 }
6566
6567 #[test]
6568 fn test_step_indices_with_tool_empty_when_no_match() {
6569 let steps = vec![make_step("t", "lookup[x]", "o")];
6570 let session = make_session(steps, 0);
6571 assert!(session.step_indices_with_tool("search").is_empty());
6572 }
6573
6574 #[test]
6577 fn test_observations_above_bytes_returns_matching_steps() {
6578 let steps = vec![
6579 make_step("t", "a", "hi"), make_step("t", "a", "hello world"), ];
6582 let session = make_session(steps, 0);
6583 let result = session.observations_above_bytes(5);
6584 assert_eq!(result.len(), 1);
6585 assert_eq!(result[0].observation, "hello world");
6586 }
6587
6588 #[test]
6589 fn test_observations_above_bytes_empty_for_empty_session() {
6590 let session = make_session(vec![], 0);
6591 assert!(session.observations_above_bytes(0).is_empty());
6592 }
6593
6594 #[test]
6595 fn test_total_step_chars_sums_all_fields() {
6596 let steps = vec![
6597 make_step("ab", "cd", "ef"), make_step("x", "y", "z"), ];
6600 let session = make_session(steps, 0);
6601 assert_eq!(session.total_step_chars(), 9);
6602 }
6603
6604 #[test]
6605 fn test_total_step_chars_zero_for_empty_session() {
6606 let session = make_session(vec![], 0);
6607 assert_eq!(session.total_step_chars(), 0);
6608 }
6609
6610 #[test]
6613 fn test_steps_by_action_prefix_returns_matching_steps() {
6614 let steps = vec![
6615 make_step("t", "search_web", "o"),
6616 make_step("t", "search_db", "o"),
6617 make_step("t", "write_file", "o"),
6618 ];
6619 let session = make_session(steps, 0);
6620 let result = session.steps_by_action_prefix("search");
6621 assert_eq!(result.len(), 2);
6622 }
6623
6624 #[test]
6625 fn test_steps_by_action_prefix_empty_when_no_match() {
6626 let steps = vec![make_step("t", "write_file", "o")];
6627 let session = make_session(steps, 0);
6628 assert!(session.steps_by_action_prefix("search").is_empty());
6629 }
6630
6631 #[test]
6632 fn test_action_count_counts_tool_call_steps() {
6633 let steps = vec![
6634 make_step("t", "search_web", "o"),
6635 make_step("t", "FINAL_ANSWER: done", "o"),
6636 ];
6637 let session = make_session(steps, 0);
6638 assert_eq!(session.action_count(), 1);
6639 }
6640
6641 #[test]
6642 fn test_action_count_zero_for_empty_session() {
6643 let session = make_session(vec![], 0);
6644 assert_eq!(session.action_count(), 0);
6645 }
6646
6647 #[test]
6650 fn test_total_thought_bytes_sums_thought_lengths() {
6651 let steps = vec![
6652 make_step("ab", "a", "o"), make_step("abcde", "b", "o"), ];
6655 let session = make_session(steps, 0);
6656 assert_eq!(session.total_thought_bytes(), 7);
6657 }
6658
6659 #[test]
6660 fn test_total_thought_bytes_zero_for_empty_session() {
6661 let session = make_session(vec![], 0);
6662 assert_eq!(session.total_thought_bytes(), 0);
6663 }
6664
6665 #[test]
6666 fn test_total_observation_bytes_sums_observation_lengths() {
6667 let steps = vec![
6668 make_step("t", "a", "hello"), make_step("t", "b", "world"), ];
6671 let session = make_session(steps, 0);
6672 assert_eq!(session.total_observation_bytes(), 10);
6673 }
6674
6675 #[test]
6676 fn test_total_observation_bytes_zero_for_empty_session() {
6677 let session = make_session(vec![], 0);
6678 assert_eq!(session.total_observation_bytes(), 0);
6679 }
6680
6681 #[test]
6684 fn test_proportion_tool_calls_all_tool_calls() {
6685 let steps = vec![
6686 make_step("t", "search[x]", "o"),
6687 make_step("t", "lookup[y]", "o"),
6688 ];
6689 let session = make_session(steps, 0);
6690 assert!((session.proportion_tool_calls() - 1.0).abs() < 1e-9);
6691 }
6692
6693 #[test]
6694 fn test_proportion_tool_calls_zero_for_empty_session() {
6695 let session = make_session(vec![], 0);
6696 assert_eq!(session.proportion_tool_calls(), 0.0);
6697 }
6698
6699 #[test]
6700 fn test_thought_density_returns_thought_fraction_of_total_bytes() {
6701 let steps = vec![make_step("ab", "cd", "ef")]; let session = make_session(steps, 0);
6703 let density = session.thought_density();
6704 assert!((density - 1.0 / 3.0).abs() < 1e-9);
6705 }
6706
6707 #[test]
6708 fn test_thought_density_zero_for_empty_session() {
6709 let session = make_session(vec![], 0);
6710 assert_eq!(session.thought_density(), 0.0);
6711 }
6712
6713 #[test]
6716 fn test_steps_matching_observation_returns_matching_steps() {
6717 let steps = vec![
6718 make_step("t", "a", "found: result"),
6719 make_step("t", "b", "no match here"),
6720 make_step("t", "c", "found: another"),
6721 ];
6722 let session = make_session(steps, 0);
6723 let result = session.steps_matching_observation("found:");
6724 assert_eq!(result.len(), 2);
6725 }
6726
6727 #[test]
6728 fn test_steps_matching_observation_empty_when_no_match() {
6729 let steps = vec![make_step("t", "a", "nothing")];
6730 let session = make_session(steps, 0);
6731 assert!(session.steps_matching_observation("found:").is_empty());
6732 }
6733
6734 #[test]
6735 fn test_step_action_lengths_returns_lengths_in_order() {
6736 let steps = vec![
6737 make_step("t", "ab", "o"),
6738 make_step("t", "cdef", "o"),
6739 ];
6740 let session = make_session(steps, 0);
6741 assert_eq!(session.step_action_lengths(), vec![2, 4]);
6742 }
6743
6744 #[test]
6745 fn test_step_action_lengths_empty_for_empty_session() {
6746 let session = make_session(vec![], 0);
6747 assert!(session.step_action_lengths().is_empty());
6748 }
6749
6750 #[test]
6753 fn test_has_thought_starting_with_true_when_match() {
6754 let steps = vec![
6755 make_step("Plan: do something", "act", "obs"),
6756 ];
6757 let session = make_session(steps, 0);
6758 assert!(session.has_thought_starting_with("Plan:"));
6759 }
6760
6761 #[test]
6762 fn test_has_thought_starting_with_false_when_no_match() {
6763 let steps = vec![make_step("think", "act", "obs")];
6764 let session = make_session(steps, 0);
6765 assert!(!session.has_thought_starting_with("Plan:"));
6766 }
6767
6768 #[test]
6769 fn test_has_thought_starting_with_false_for_empty_session() {
6770 let session = make_session(vec![], 0);
6771 assert!(!session.has_thought_starting_with("Plan:"));
6772 }
6773
6774 #[test]
6775 fn test_step_count_above_action_bytes_counts_correctly() {
6776 let steps = vec![
6777 make_step("t", "short", "o"),
6778 make_step("t", "a_very_long_action_string", "o"),
6779 ];
6780 let session = make_session(steps, 0);
6781 assert_eq!(session.step_count_above_action_bytes(5), 1);
6782 }
6783
6784 #[test]
6785 fn test_step_count_above_action_bytes_zero_when_all_small() {
6786 let steps = vec![make_step("t", "ab", "o")];
6787 let session = make_session(steps, 0);
6788 assert_eq!(session.step_count_above_action_bytes(100), 0);
6789 }
6790
6791 #[test]
6792 fn test_runtime_config_returns_agent_config() {
6793 let rt = AgentRuntime::quick(3, "test-model");
6794 assert_eq!(rt.config().max_iterations, 3);
6795 }
6796
6797 #[test]
6800 fn test_total_thought_chars_sums_all_thoughts() {
6801 let steps = vec![
6802 make_step("ab", "x", "y"),
6803 make_step("cde", "x", "y"),
6804 ];
6805 let session = make_session(steps, 0);
6806 assert_eq!(session.total_thought_chars(), 5);
6807 }
6808
6809 #[test]
6810 fn test_total_thought_chars_zero_for_empty_session() {
6811 let session = make_session(vec![], 0);
6812 assert_eq!(session.total_thought_chars(), 0);
6813 }
6814
6815 #[test]
6816 fn test_total_action_chars_sums_all_actions() {
6817 let steps = vec![
6818 make_step("t", "hello", "o"),
6819 make_step("t", "world", "o"),
6820 ];
6821 let session = make_session(steps, 0);
6822 assert_eq!(session.total_action_chars(), 10);
6823 }
6824
6825 #[test]
6826 fn test_total_observation_chars_sums_all_observations() {
6827 let steps = vec![
6828 make_step("t", "a", "abc"),
6829 make_step("t", "a", "de"),
6830 ];
6831 let session = make_session(steps, 0);
6832 assert_eq!(session.total_observation_chars(), 5);
6833 }
6834
6835 #[test]
6836 fn test_model_name_returns_configured_model() {
6837 let rt = AgentRuntime::quick(5, "gpt-4o");
6838 assert_eq!(rt.model_name(), "gpt-4o");
6839 }
6840
6841 #[test]
6844 fn test_min_observation_bytes_returns_smallest_nonempty() {
6845 let steps = vec![
6846 make_step("t", "a", "hello"),
6847 make_step("t", "a", "hi"),
6848 make_step("t", "a", ""),
6849 ];
6850 let session = make_session(steps, 0);
6851 assert_eq!(session.min_observation_bytes(), 2);
6852 }
6853
6854 #[test]
6855 fn test_min_observation_bytes_zero_when_all_empty() {
6856 let steps = vec![make_step("t", "a", "")];
6857 let session = make_session(steps, 0);
6858 assert_eq!(session.min_observation_bytes(), 0);
6859 }
6860
6861 #[test]
6862 fn test_min_thought_bytes_returns_smallest_nonempty() {
6863 let steps = vec![
6864 make_step("abc", "a", "o"),
6865 make_step("xy", "a", "o"),
6866 make_step("", "a", "o"),
6867 ];
6868 let session = make_session(steps, 0);
6869 assert_eq!(session.min_thought_bytes(), 2);
6870 }
6871
6872 #[test]
6873 fn test_min_thought_bytes_zero_for_empty_session() {
6874 let session = make_session(vec![], 0);
6875 assert_eq!(session.min_thought_bytes(), 0);
6876 }
6877
6878 #[test]
6879 fn test_proportion_empty_thoughts_all_empty() {
6880 let steps = vec![
6881 make_step("", "a", "o"),
6882 make_step("", "a", "o"),
6883 ];
6884 let session = make_session(steps, 0);
6885 assert!((session.proportion_empty_thoughts() - 1.0).abs() < f64::EPSILON);
6886 }
6887
6888 #[test]
6889 fn test_proportion_empty_thoughts_none_empty() {
6890 let steps = vec![make_step("think", "a", "o")];
6891 let session = make_session(steps, 0);
6892 assert!((session.proportion_empty_thoughts()).abs() < f64::EPSILON);
6893 }
6894
6895 #[test]
6896 fn test_proportion_empty_thoughts_zero_for_empty_session() {
6897 let session = make_session(vec![], 0);
6898 assert!((session.proportion_empty_thoughts()).abs() < f64::EPSILON);
6899 }
6900
6901 #[test]
6902 fn test_has_failed_steps_true_when_error_observation() {
6903 let steps = vec![make_step("t", "a", "[error] something broke")];
6904 let session = make_session(steps, 0);
6905 assert!(session.has_failed_steps());
6906 }
6907
6908 #[test]
6909 fn test_has_failed_steps_false_when_no_errors() {
6910 let steps = vec![make_step("t", "a", "success")];
6911 let session = make_session(steps, 0);
6912 assert!(!session.has_failed_steps());
6913 }
6914
6915 #[test]
6916 fn test_has_failed_steps_false_for_empty_session() {
6917 let session = make_session(vec![], 0);
6918 assert!(!session.has_failed_steps());
6919 }
6920
6921 #[test]
6924 fn test_all_observations_non_empty_true_when_all_have_obs() {
6925 let steps = vec![
6926 make_step("t1", "a1", "result1"),
6927 make_step("t2", "a2", "result2"),
6928 ];
6929 let session = make_session(steps, 0);
6930 assert!(session.all_observations_non_empty());
6931 }
6932
6933 #[test]
6934 fn test_all_observations_non_empty_false_when_one_is_empty() {
6935 let steps = vec![
6936 make_step("t1", "a1", "result1"),
6937 make_step("t2", "a2", ""),
6938 ];
6939 let session = make_session(steps, 0);
6940 assert!(!session.all_observations_non_empty());
6941 }
6942
6943 #[test]
6944 fn test_all_observations_non_empty_true_for_empty_session() {
6945 let session = make_session(vec![], 0);
6946 assert!(session.all_observations_non_empty());
6947 }
6948
6949 #[test]
6950 fn test_avg_combined_step_bytes_correct() {
6951 let steps = vec![
6952 make_step("hi", "go", "ok"),
6953 make_step("hello", "world", "result"),
6954 ];
6955 let session = make_session(steps, 0);
6956 let avg = session.avg_combined_step_bytes();
6958 assert!((avg - 11.0).abs() < 1e-9);
6959 }
6960
6961 #[test]
6962 fn test_avg_combined_step_bytes_zero_for_empty_session() {
6963 let session = make_session(vec![], 0);
6964 assert_eq!(session.avg_combined_step_bytes(), 0.0);
6965 }
6966
6967 #[test]
6968 fn test_shortest_observation_step_returns_shortest() {
6969 let steps = vec![
6970 make_step("t1", "a1", "longer observation"),
6971 make_step("t2", "a2", "short"),
6972 ];
6973 let session = make_session(steps, 0);
6974 let s = session.shortest_observation_step().unwrap();
6975 assert_eq!(s.observation, "short");
6976 }
6977
6978 #[test]
6979 fn test_shortest_observation_step_none_for_empty_session() {
6980 let session = make_session(vec![], 0);
6981 assert!(session.shortest_observation_step().is_none());
6982 }
6983
6984 #[test]
6987 fn test_steps_with_empty_action_returns_matching_steps() {
6988 let steps = vec![
6989 make_step("t", "", "obs"),
6990 make_step("t", "act", "obs"),
6991 make_step("t", "", "obs"),
6992 ];
6993 let session = make_session(steps, 0);
6994 assert_eq!(session.steps_with_empty_action().len(), 2);
6995 }
6996
6997 #[test]
6998 fn test_steps_with_empty_action_empty_when_all_have_actions() {
6999 let steps = vec![make_step("t", "act", "obs")];
7000 let session = make_session(steps, 0);
7001 assert!(session.steps_with_empty_action().is_empty());
7002 }
7003
7004 #[test]
7005 fn test_observation_starts_with_any_true_when_prefix_matches() {
7006 let steps = vec![make_step("t", "a", "ERROR: something went wrong")];
7007 let session = make_session(steps, 0);
7008 assert!(session.observation_starts_with_any(&["ERROR:", "WARN:"]));
7009 }
7010
7011 #[test]
7012 fn test_observation_starts_with_any_false_when_no_match() {
7013 let steps = vec![make_step("t", "a", "success")];
7014 let session = make_session(steps, 0);
7015 assert!(!session.observation_starts_with_any(&["ERROR:", "WARN:"]));
7016 }
7017
7018 #[test]
7019 fn test_has_repeated_actions_true_when_duplicate_exists() {
7020 let steps = vec![
7021 make_step("t", "search", "obs"),
7022 make_step("t", "search", "obs"),
7023 ];
7024 let session = make_session(steps, 0);
7025 assert!(session.has_repeated_actions());
7026 }
7027
7028 #[test]
7029 fn test_has_repeated_actions_false_when_all_unique() {
7030 let steps = vec![
7031 make_step("t", "search", "obs"),
7032 make_step("t", "read", "obs"),
7033 ];
7034 let session = make_session(steps, 0);
7035 assert!(!session.has_repeated_actions());
7036 }
7037
7038 #[test]
7039 fn test_session_max_iterations_returns_config_value() {
7040 let rt = AgentRuntime::quick(7, "model");
7041 assert_eq!(rt.session_max_iterations(), 7);
7042 }
7043
7044 #[test]
7047 fn test_has_action_containing_true_when_substr_matches() {
7048 let steps = vec![make_step("t", "search[query]", "obs")];
7049 let session = make_session(steps, 0);
7050 assert!(session.has_action_containing("search"));
7051 }
7052
7053 #[test]
7054 fn test_has_action_containing_false_when_no_match() {
7055 let steps = vec![make_step("t", "read_file", "obs")];
7056 let session = make_session(steps, 0);
7057 assert!(!session.has_action_containing("write"));
7058 }
7059
7060 #[test]
7061 fn test_max_observation_chars_returns_longest_observation() {
7062 let steps = vec![
7063 make_step("t", "a", "hi"),
7064 make_step("t", "a", "hello world"),
7065 ];
7066 let session = make_session(steps, 0);
7067 assert_eq!(session.max_observation_chars(), 11);
7068 }
7069
7070 #[test]
7071 fn test_max_observation_chars_zero_for_empty_session() {
7072 let session = make_session(vec![], 0);
7073 assert_eq!(session.max_observation_chars(), 0);
7074 }
7075
7076 #[test]
7077 fn test_step_index_of_longest_thought_returns_correct_index() {
7078 let steps = vec![
7079 make_step("short", "a", "o"),
7080 make_step("a very long thought string", "a", "o"),
7081 make_step("mid", "a", "o"),
7082 ];
7083 let session = make_session(steps, 0);
7084 assert_eq!(session.step_index_of_longest_thought(), Some(1));
7085 }
7086
7087 #[test]
7088 fn test_step_index_of_longest_thought_none_for_empty_session() {
7089 let session = make_session(vec![], 0);
7090 assert_eq!(session.step_index_of_longest_thought(), None);
7091 }
7092
7093 #[test]
7094 fn test_observation_word_counts_returns_word_counts_in_order() {
7095 let steps = vec![
7096 make_step("t", "a", "one two three"),
7097 make_step("t", "a", "single"),
7098 ];
7099 let session = make_session(steps, 0);
7100 assert_eq!(session.observation_word_counts(), vec![3, 1]);
7101 }
7102
7103 #[test]
7106 fn test_action_byte_variance_zero_for_equal_lengths() {
7107 let steps = vec![
7108 make_step("t", "abc", "o"),
7109 make_step("t", "def", "o"),
7110 ];
7111 let session = make_session(steps, 0);
7112 assert!((session.action_byte_variance()).abs() < f64::EPSILON);
7113 }
7114
7115 #[test]
7116 fn test_action_byte_variance_nonzero_for_different_lengths() {
7117 let steps = vec![
7118 make_step("t", "a", "o"),
7119 make_step("t", "abcde", "o"),
7120 ];
7121 let session = make_session(steps, 0);
7122 assert!(session.action_byte_variance() > 0.0);
7123 }
7124
7125 #[test]
7126 fn test_action_byte_variance_zero_for_single_step() {
7127 let steps = vec![make_step("t", "hello", "o")];
7128 let session = make_session(steps, 0);
7129 assert!((session.action_byte_variance()).abs() < f64::EPSILON);
7130 }
7131
7132 #[test]
7133 fn test_thought_byte_variance_nonzero_for_different_lengths() {
7134 let steps = vec![
7135 make_step("a", "act", "o"),
7136 make_step("abcde", "act", "o"),
7137 ];
7138 let session = make_session(steps, 0);
7139 assert!(session.thought_byte_variance() > 0.0);
7140 }
7141
7142 #[test]
7143 fn test_thought_byte_variance_zero_for_empty_session() {
7144 let session = make_session(vec![], 0);
7145 assert!((session.thought_byte_variance()).abs() < f64::EPSILON);
7146 }
7147
7148 #[test]
7149 fn test_steps_above_thought_bytes_filters_correctly() {
7150 let steps = vec![
7151 make_step("hi", "a", "o"),
7152 make_step("hello world", "a", "o"),
7153 make_step("x", "a", "o"),
7154 ];
7155 let session = make_session(steps, 0);
7156 let above = session.steps_above_thought_bytes(4);
7157 assert_eq!(above.len(), 1);
7158 assert_eq!(above[0].thought, "hello world");
7159 }
7160
7161 #[test]
7162 fn test_steps_above_thought_bytes_empty_when_none_qualify() {
7163 let steps = vec![make_step("hi", "a", "o")];
7164 let session = make_session(steps, 0);
7165 assert!(session.steps_above_thought_bytes(100).is_empty());
7166 }
7167
7168 #[test]
7171 fn test_unique_observation_count_counts_distinct_observations() {
7172 let steps = vec![
7173 make_step("t1", "a1", "result"),
7174 make_step("t2", "a2", "result"),
7175 make_step("t3", "a3", "other"),
7176 ];
7177 let session = make_session(steps, 0);
7178 assert_eq!(session.unique_observation_count(), 2);
7179 }
7180
7181 #[test]
7182 fn test_unique_observation_count_zero_for_empty_session() {
7183 let session = make_session(vec![], 0);
7184 assert_eq!(session.unique_observation_count(), 0);
7185 }
7186
7187 #[test]
7188 fn test_avg_thought_word_count_computes_correctly() {
7189 let steps = vec![
7190 make_step("one word", "a", "o"), make_step("three word count", "a", "o"), ];
7193 let session = make_session(steps, 0);
7194 let avg = session.avg_thought_word_count();
7195 assert!((avg - 2.5).abs() < 1e-9);
7196 }
7197
7198 #[test]
7199 fn test_avg_thought_word_count_zero_for_empty_session() {
7200 let session = make_session(vec![], 0);
7201 assert_eq!(session.avg_thought_word_count(), 0.0);
7202 }
7203
7204 #[test]
7207 fn test_thought_starts_with_any_true_when_prefix_matches() {
7208 let steps = vec![make_step("Plan: do it", "act", "obs")];
7209 let session = make_session(steps, 0);
7210 assert!(session.thought_starts_with_any(&["Plan:", "Think:"]));
7211 }
7212
7213 #[test]
7214 fn test_thought_starts_with_any_false_when_no_match() {
7215 let steps = vec![make_step("just thinking", "act", "obs")];
7216 let session = make_session(steps, 0);
7217 assert!(!session.thought_starts_with_any(&["Plan:", "Think:"]));
7218 }
7219
7220 #[test]
7221 fn test_action_word_count_sums_words_across_steps() {
7222 let steps = vec![
7223 make_step("t", "search for answer", "obs"),
7224 make_step("t", "write result", "obs"),
7225 ];
7226 let session = make_session(steps, 0);
7227 assert_eq!(session.action_word_count(), 5);
7228 }
7229
7230 #[test]
7231 fn test_action_word_count_zero_for_empty_session() {
7232 let session = make_session(vec![], 0);
7233 assert_eq!(session.action_word_count(), 0);
7234 }
7235
7236 #[test]
7237 fn test_steps_above_thought_chars_counts_correctly() {
7238 let steps = vec![
7239 make_step("short", "a", "o"),
7240 make_step("a very long thought here", "a", "o"),
7241 ];
7242 let session = make_session(steps, 0);
7243 assert_eq!(session.steps_above_thought_chars(5), 1);
7244 }
7245
7246 #[test]
7247 fn test_steps_above_thought_chars_zero_for_empty_session() {
7248 let session = make_session(vec![], 0);
7249 assert_eq!(session.steps_above_thought_chars(0), 0);
7250 }
7251
7252 #[test]
7255 fn test_total_empty_steps_counts_fully_empty_steps() {
7256 let steps = vec![
7257 make_step("", "", ""),
7258 make_step("t", "a", "o"),
7259 make_step("", "", ""),
7260 ];
7261 let session = make_session(steps, 0);
7262 assert_eq!(session.total_empty_steps(), 2);
7263 }
7264
7265 #[test]
7266 fn test_total_empty_steps_zero_when_no_empty_steps() {
7267 let steps = vec![make_step("t", "a", "o")];
7268 let session = make_session(steps, 0);
7269 assert_eq!(session.total_empty_steps(), 0);
7270 }
7271
7272 #[test]
7273 fn test_action_starts_with_count_correct() {
7274 let steps = vec![
7275 make_step("t", "search:foo", "o"),
7276 make_step("t", "search:bar", "o"),
7277 make_step("t", "write:baz", "o"),
7278 ];
7279 let session = make_session(steps, 0);
7280 assert_eq!(session.action_starts_with_count("search"), 2);
7281 }
7282
7283 #[test]
7284 fn test_action_starts_with_count_zero_for_no_match() {
7285 let steps = vec![make_step("t", "write:x", "o")];
7286 let session = make_session(steps, 0);
7287 assert_eq!(session.action_starts_with_count("read"), 0);
7288 }
7289
7290 #[test]
7291 fn test_longest_action_returns_longest() {
7292 let steps = vec![
7293 make_step("t", "short", "o"),
7294 make_step("t", "much_longer_action", "o"),
7295 make_step("t", "mid", "o"),
7296 ];
7297 let session = make_session(steps, 0);
7298 assert_eq!(session.longest_action(), Some("much_longer_action"));
7299 }
7300
7301 #[test]
7302 fn test_longest_action_none_for_empty_session() {
7303 let session = make_session(vec![], 0);
7304 assert_eq!(session.longest_action(), None);
7305 }
7306
7307 #[test]
7308 fn test_thought_completeness_all_non_empty() {
7309 let steps = vec![
7310 make_step("think", "a", "o"),
7311 make_step("also thinking", "a", "o"),
7312 ];
7313 let session = make_session(steps, 0);
7314 assert!((session.thought_completeness() - 1.0).abs() < f64::EPSILON);
7315 }
7316
7317 #[test]
7318 fn test_thought_completeness_half_empty() {
7319 let steps = vec![
7320 make_step("think", "a", "o"),
7321 make_step("", "a", "o"),
7322 ];
7323 let session = make_session(steps, 0);
7324 assert!((session.thought_completeness() - 0.5).abs() < f64::EPSILON);
7325 }
7326
7327 #[test]
7328 fn test_thought_completeness_zero_for_empty_session() {
7329 let session = make_session(vec![], 0);
7330 assert!((session.thought_completeness()).abs() < f64::EPSILON);
7331 }
7332
7333 #[test]
7336 fn test_steps_with_non_empty_observation_returns_matching_steps() {
7337 let steps = vec![
7338 make_step("t", "a", "result"),
7339 make_step("t", "a", ""),
7340 make_step("t", "a", "more"),
7341 ];
7342 let session = make_session(steps, 0);
7343 assert_eq!(session.steps_with_non_empty_observation().len(), 2);
7344 }
7345
7346 #[test]
7347 fn test_steps_with_non_empty_observation_empty_for_all_empty() {
7348 let steps = vec![make_step("t", "a", "")];
7349 let session = make_session(steps, 0);
7350 assert!(session.steps_with_non_empty_observation().is_empty());
7351 }
7352
7353 #[test]
7354 fn test_observations_containing_returns_matching_steps() {
7355 let steps = vec![
7356 make_step("t", "a", "found the answer"),
7357 make_step("t", "a", "no match"),
7358 ];
7359 let session = make_session(steps, 0);
7360 assert_eq!(session.observations_containing("found").len(), 1);
7361 }
7362
7363 #[test]
7364 fn test_thought_observation_ratio_returns_correct_ratio() {
7365 let steps = vec![make_step("ab", "x", "abcd")];
7366 let session = make_session(steps, 0);
7367 assert_eq!(session.thought_observation_ratio(), 0.5);
7368 }
7369
7370 #[test]
7371 fn test_thought_observation_ratio_zero_for_empty_session() {
7372 let session = make_session(vec![], 0);
7373 assert_eq!(session.thought_observation_ratio(), 0.0);
7374 }
7375
7376 #[test]
7379 fn test_non_empty_action_count_correct() {
7380 let steps = vec![
7381 make_step("t", "search", "o"),
7382 make_step("t", "", "o"),
7383 make_step("t", "write", "o"),
7384 ];
7385 let session = make_session(steps, 0);
7386 assert_eq!(session.non_empty_action_count(), 2);
7387 }
7388
7389 #[test]
7390 fn test_non_empty_action_count_zero_for_all_empty() {
7391 let steps = vec![make_step("t", "", "o"), make_step("t", "", "o")];
7392 let session = make_session(steps, 0);
7393 assert_eq!(session.non_empty_action_count(), 0);
7394 }
7395
7396 #[test]
7397 fn test_total_step_bytes_sums_all_fields() {
7398 let steps = vec![make_step("abc", "de", "f")];
7399 let session = make_session(steps, 0);
7400 assert_eq!(session.total_step_bytes(), 6); }
7402
7403 #[test]
7404 fn test_total_step_bytes_zero_for_empty_session() {
7405 let session = make_session(vec![], 0);
7406 assert_eq!(session.total_step_bytes(), 0);
7407 }
7408
7409 #[test]
7410 fn test_last_thought_bytes_returns_last_step_thought() {
7411 let steps = vec![
7412 make_step("short", "a", "o"),
7413 make_step("much longer thought", "a", "o"),
7414 ];
7415 let session = make_session(steps, 0);
7416 assert_eq!(session.last_thought_bytes(), "much longer thought".len());
7417 }
7418
7419 #[test]
7420 fn test_last_thought_bytes_zero_for_empty_session() {
7421 let session = make_session(vec![], 0);
7422 assert_eq!(session.last_thought_bytes(), 0);
7423 }
7424
7425 #[test]
7426 fn test_first_observation_bytes_returns_first_step_observation() {
7427 let steps = vec![
7428 make_step("t", "a", "first obs"),
7429 make_step("t", "a", "second obs is longer"),
7430 ];
7431 let session = make_session(steps, 0);
7432 assert_eq!(session.first_observation_bytes(), "first obs".len());
7433 }
7434
7435 #[test]
7436 fn test_first_observation_bytes_zero_for_empty_session() {
7437 let session = make_session(vec![], 0);
7438 assert_eq!(session.first_observation_bytes(), 0);
7439 }
7440
7441 #[test]
7444 fn test_has_step_with_empty_observation_true() {
7445 let steps = vec![make_step("t", "a", "")];
7446 let session = make_session(steps, 0);
7447 assert!(session.has_step_with_empty_observation());
7448 }
7449
7450 #[test]
7451 fn test_has_step_with_empty_observation_false_when_all_nonempty() {
7452 let steps = vec![make_step("t", "a", "obs")];
7453 let session = make_session(steps, 0);
7454 assert!(!session.has_step_with_empty_observation());
7455 }
7456
7457 #[test]
7458 fn test_has_step_with_empty_observation_false_for_empty_session() {
7459 let session = make_session(vec![], 0);
7460 assert!(!session.has_step_with_empty_observation());
7461 }
7462
7463 #[test]
7464 fn test_thought_to_action_byte_ratio_correct() {
7465 let steps = vec![make_step("hello", "hi", "o")];
7467 let session = make_session(steps, 0);
7468 assert!((session.thought_to_action_byte_ratio() - 2.5).abs() < 1e-9);
7469 }
7470
7471 #[test]
7472 fn test_thought_to_action_byte_ratio_zero_when_no_action_bytes() {
7473 let steps = vec![make_step("thought", "", "o")];
7474 let session = make_session(steps, 0);
7475 assert_eq!(session.thought_to_action_byte_ratio(), 0.0);
7476 }
7477
7478 #[test]
7479 fn test_thought_to_action_byte_ratio_zero_for_empty_session() {
7480 let session = make_session(vec![], 0);
7481 assert_eq!(session.thought_to_action_byte_ratio(), 0.0);
7482 }
7483
7484 #[test]
7487 fn test_observation_above_bytes_count_correct() {
7488 let steps = vec![
7489 make_step("t", "a", "short"),
7490 make_step("t", "a", "this is quite long"),
7491 make_step("t", "a", "x"),
7492 ];
7493 let session = make_session(steps, 0);
7494 assert_eq!(session.observation_above_bytes_count(5), 1);
7495 }
7496
7497 #[test]
7498 fn test_observation_above_bytes_count_zero_for_empty_session() {
7499 let session = make_session(vec![], 0);
7500 assert_eq!(session.observation_above_bytes_count(0), 0);
7501 }
7502
7503 #[test]
7504 fn test_steps_with_both_thought_and_action_correct() {
7505 let steps = vec![
7506 make_step("think", "act", "obs"),
7507 make_step("", "act", "obs"),
7508 make_step("think", "", "obs"),
7509 ];
7510 let session = make_session(steps, 0);
7511 assert_eq!(session.steps_with_both_thought_and_action(), 1);
7512 }
7513
7514 #[test]
7515 fn test_steps_with_both_thought_and_action_zero_for_empty_session() {
7516 let session = make_session(vec![], 0);
7517 assert_eq!(session.steps_with_both_thought_and_action(), 0);
7518 }
7519
7520 #[test]
7523 fn test_steps_with_observation_prefix_correct() {
7524 let steps = vec![
7525 make_step("t", "a", "[error] bad"),
7526 make_step("t", "a", "ok"),
7527 make_step("t", "a", "[error] also bad"),
7528 ];
7529 let session = make_session(steps, 0);
7530 assert_eq!(session.steps_with_observation_prefix("[error]"), 2);
7531 }
7532
7533 #[test]
7534 fn test_steps_with_observation_prefix_zero_when_none_match() {
7535 let steps = vec![make_step("t", "a", "ok")];
7536 let session = make_session(steps, 0);
7537 assert_eq!(session.steps_with_observation_prefix("[error]"), 0);
7538 }
7539
7540 #[test]
7541 fn test_observation_bytes_total_sums_all_observations() {
7542 let steps = vec![
7543 make_step("t", "a", "abc"), make_step("t", "a", "de"), ];
7546 let session = make_session(steps, 0);
7547 assert_eq!(session.observation_bytes_total(), 5);
7548 }
7549
7550 #[test]
7551 fn test_observation_bytes_total_zero_for_empty_session() {
7552 let session = make_session(vec![], 0);
7553 assert_eq!(session.observation_bytes_total(), 0);
7554 }
7555
7556 #[test]
7557 fn test_first_thought_chars_returns_first_step_count() {
7558 let steps = vec![make_step("héllo", "a", "o"), make_step("ignored", "a", "o")];
7559 let session = make_session(steps, 0);
7560 assert_eq!(session.first_thought_chars(), 5);
7562 }
7563
7564 #[test]
7565 fn test_first_thought_chars_zero_for_empty_session() {
7566 let session = make_session(vec![], 0);
7567 assert_eq!(session.first_thought_chars(), 0);
7568 }
7569
7570 #[test]
7573 fn test_last_observation_chars_returns_last_step() {
7574 let steps = vec![
7575 make_step("t", "a", "first"),
7576 make_step("t", "a", "last one"),
7577 ];
7578 let session = make_session(steps, 0);
7579 assert_eq!(session.last_observation_chars(), "last one".chars().count());
7580 }
7581
7582 #[test]
7583 fn test_last_observation_chars_zero_for_empty_session() {
7584 let session = make_session(vec![], 0);
7585 assert_eq!(session.last_observation_chars(), 0);
7586 }
7587
7588 #[test]
7589 fn test_observation_word_count_total_sums_all_words() {
7590 let steps = vec![
7591 make_step("t", "a", "one two"), make_step("t", "a", "three"), ];
7594 let session = make_session(steps, 0);
7595 assert_eq!(session.observation_word_count_total(), 3);
7596 }
7597
7598 #[test]
7599 fn test_observation_word_count_total_zero_for_empty_session() {
7600 let session = make_session(vec![], 0);
7601 assert_eq!(session.observation_word_count_total(), 0);
7602 }
7603
7604 #[test]
7607 fn test_action_ends_with_count_correct() {
7608 let steps = vec![
7609 make_step("t", "search.", "o"),
7610 make_step("t", "browse.", "o"),
7611 make_step("t", "calculate", "o"),
7612 ];
7613 let session = make_session(steps, 0);
7614 assert_eq!(session.action_ends_with_count("."), 2);
7615 }
7616
7617 #[test]
7618 fn test_action_ends_with_count_zero_when_none_match() {
7619 let steps = vec![make_step("t", "nope", "o")];
7620 let session = make_session(steps, 0);
7621 assert_eq!(session.action_ends_with_count("."), 0);
7622 }
7623
7624 #[test]
7625 fn test_avg_observation_words_correct() {
7626 let steps = vec![
7627 make_step("t", "a", "one two"), make_step("t", "a", "three four five"), ];
7630 let session = make_session(steps, 0);
7631 assert!((session.avg_observation_words() - 2.5).abs() < 1e-9);
7632 }
7633
7634 #[test]
7635 fn test_avg_observation_words_zero_for_empty_session() {
7636 let session = make_session(vec![], 0);
7637 assert_eq!(session.avg_observation_words(), 0.0);
7638 }
7639
7640 #[test]
7643 fn test_steps_matching_thought_returns_correct_steps() {
7644 let steps = vec![
7645 make_step("I need to search", "a", "o"),
7646 make_step("I found the answer", "a", "o"),
7647 make_step("no match here", "a", "o"),
7648 ];
7649 let session = make_session(steps, 0);
7650 assert_eq!(session.steps_matching_thought("I").len(), 2);
7651 }
7652
7653 #[test]
7654 fn test_median_observation_chars_returns_middle_value() {
7655 let steps = vec![
7656 make_step("t", "a", "ab"),
7657 make_step("t", "a", "abcde"),
7658 make_step("t", "a", "abc"),
7659 ];
7660 let session = make_session(steps, 0);
7661 assert_eq!(session.median_observation_chars(), 3);
7663 }
7664
7665 #[test]
7666 fn test_median_observation_chars_zero_for_empty_session() {
7667 let session = make_session(vec![], 0);
7668 assert_eq!(session.median_observation_chars(), 0);
7669 }
7670
7671 #[test]
7672 fn test_cumulative_thought_chars_accumulates_correctly() {
7673 let steps = vec![
7674 make_step("ab", "a", "o"),
7675 make_step("cde", "a", "o"),
7676 ];
7677 let session = make_session(steps, 0);
7678 assert_eq!(session.cumulative_thought_chars(), vec![2, 5]);
7679 }
7680
7681 #[test]
7682 fn test_count_steps_with_thought_containing_counts_matches() {
7683 let steps = vec![
7684 make_step("call function foo", "a", "o"),
7685 make_step("call function bar", "a", "o"),
7686 make_step("nothing", "a", "o"),
7687 ];
7688 let session = make_session(steps, 0);
7689 assert_eq!(session.count_steps_with_thought_containing("function"), 2);
7690 }
7691
7692 #[test]
7695 fn test_observation_contains_any_true_when_term_present() {
7696 let steps = vec![
7697 make_step("t", "a", "result: success"),
7698 make_step("t", "a", "result: failure"),
7699 ];
7700 let session = make_session(steps, 0);
7701 assert!(session.observation_contains_any(&["success", "error"]));
7702 }
7703
7704 #[test]
7705 fn test_observation_contains_any_false_when_no_match() {
7706 let steps = vec![make_step("t", "a", "nothing here")];
7707 let session = make_session(steps, 0);
7708 assert!(!session.observation_contains_any(&["success", "error"]));
7709 }
7710
7711 #[test]
7712 fn test_observation_contains_any_false_for_empty_session() {
7713 let session = make_session(vec![], 0);
7714 assert!(!session.observation_contains_any(&["anything"]));
7715 }
7716
7717 #[test]
7718 fn test_observation_contains_any_false_for_empty_terms() {
7719 let steps = vec![make_step("t", "a", "something")];
7720 let session = make_session(steps, 0);
7721 assert!(!session.observation_contains_any(&[]));
7722 }
7723
7724 #[test]
7727 fn test_step_at_index_returns_correct_step() {
7728 let steps = vec![
7729 make_step("first", "a1", "o1"),
7730 make_step("second", "a2", "o2"),
7731 ];
7732 let session = make_session(steps, 0);
7733 assert_eq!(session.step_at_index(1).map(|s| s.thought.as_str()), Some("second"));
7734 }
7735
7736 #[test]
7737 fn test_step_at_index_returns_none_out_of_bounds() {
7738 let session = make_session(vec![], 0);
7739 assert!(session.step_at_index(0).is_none());
7740 }
7741
7742 #[test]
7743 fn test_thought_contains_all_true_when_all_present_in_one_step() {
7744 let steps = vec![
7745 make_step("alpha beta gamma", "a", "o"),
7746 make_step("alpha only", "a", "o"),
7747 ];
7748 let session = make_session(steps, 0);
7749 assert!(session.thought_contains_all(&["alpha", "beta"]));
7750 }
7751
7752 #[test]
7753 fn test_thought_contains_all_false_when_no_single_step_has_all() {
7754 let steps = vec![
7755 make_step("alpha", "a", "o"),
7756 make_step("beta", "a", "o"),
7757 ];
7758 let session = make_session(steps, 0);
7759 assert!(!session.thought_contains_all(&["alpha", "beta"]));
7760 }
7761
7762 #[test]
7763 fn test_action_contains_any_true_when_present() {
7764 let steps = vec![
7765 make_step("t", "search(query)", "o"),
7766 make_step("t", "read(file)", "o"),
7767 ];
7768 let session = make_session(steps, 0);
7769 assert!(session.action_contains_any(&["search", "write"]));
7770 }
7771
7772 #[test]
7773 fn test_action_contains_any_false_when_not_present() {
7774 let steps = vec![make_step("t", "read(file)", "o")];
7775 let session = make_session(steps, 0);
7776 assert!(!session.action_contains_any(&["search", "write"]));
7777 }
7778
7779 #[test]
7780 fn test_max_thought_chars_returns_longest() {
7781 let steps = vec![
7782 make_step("hi", "a", "o"),
7783 make_step("hello world", "a", "o"),
7784 make_step("hey", "a", "o"),
7785 ];
7786 let session = make_session(steps, 0);
7787 assert_eq!(session.max_thought_chars(), 11);
7788 }
7789
7790 #[test]
7791 fn test_max_thought_chars_zero_for_empty_session() {
7792 let session = make_session(vec![], 0);
7793 assert_eq!(session.max_thought_chars(), 0);
7794 }
7795
7796 #[test]
7797 fn test_min_thought_chars_returns_shortest_non_empty() {
7798 let steps = vec![
7799 make_step("", "a", "o"),
7800 make_step("ab", "a", "o"),
7801 make_step("abcd", "a", "o"),
7802 ];
7803 let session = make_session(steps, 0);
7804 assert_eq!(session.min_thought_chars(), 2);
7805 }
7806
7807 #[test]
7808 fn test_min_thought_chars_zero_when_all_empty() {
7809 let steps = vec![make_step("", "a", "o")];
7810 let session = make_session(steps, 0);
7811 assert_eq!(session.min_thought_chars(), 0);
7812 }
7813
7814 #[test]
7815 fn test_is_registered_tool_true_for_registered_tool() {
7816 let spec = crate::agent::ToolSpec::new("calculator", "Does math", |_| {
7817 serde_json::json!("ok")
7818 });
7819 let rt = AgentRuntime::builder()
7820 .with_agent_config(AgentConfig::new(3, "m"))
7821 .register_tool(spec)
7822 .build();
7823 assert!(rt.is_registered_tool("calculator"));
7824 }
7825
7826 #[test]
7827 fn test_is_registered_tool_false_for_unknown_tool() {
7828 let rt = AgentRuntime::quick(3, "m");
7829 assert!(!rt.is_registered_tool("nonexistent"));
7830 }
7831
7832 #[test]
7835 fn test_registered_tool_names_returns_owned_sorted_names() {
7836 let rt = AgentRuntime::builder()
7837 .with_agent_config(AgentConfig::new(3, "test-model"))
7838 .register_tool(crate::agent::ToolSpec::new("beta", "b", |_| {
7839 serde_json::json!("ok")
7840 }))
7841 .register_tool(crate::agent::ToolSpec::new("alpha", "a", |_| {
7842 serde_json::json!("ok")
7843 }))
7844 .build();
7845 let names = rt.registered_tool_names();
7846 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
7847 }
7848
7849 #[test]
7850 fn test_registered_tool_names_empty_when_no_tools() {
7851 let rt = AgentRuntime::quick(3, "test-model");
7852 assert!(rt.registered_tool_names().is_empty());
7853 }
7854
7855 #[test]
7858 fn test_avg_action_chars_correct() {
7859 let steps = vec![
7860 make_step("t", "ab", "o"),
7861 make_step("t", "abcd", "o"),
7862 ];
7863 let session = make_session(steps, 0);
7864 assert!((session.avg_action_chars() - 3.0).abs() < 1e-9);
7865 }
7866
7867 #[test]
7868 fn test_avg_action_chars_zero_for_empty_session() {
7869 let session = make_session(vec![], 0);
7870 assert_eq!(session.avg_action_chars(), 0.0);
7871 }
7872
7873 #[test]
7874 fn test_avg_observation_chars_correct() {
7875 let steps = vec![
7876 make_step("t", "a", "hello"),
7877 make_step("t", "a", "hi"),
7878 ];
7879 let session = make_session(steps, 0);
7880 assert!((session.avg_observation_chars() - 3.5).abs() < 1e-9);
7882 }
7883
7884 #[test]
7885 fn test_step_with_longest_action_returns_correct_step() {
7886 let steps = vec![
7887 make_step("t", "short", "o"),
7888 make_step("t", "much longer action string", "o"),
7889 make_step("t", "medium act", "o"),
7890 ];
7891 let session = make_session(steps, 0);
7892 assert_eq!(
7893 session.step_with_longest_action().map(|s| s.action.as_str()),
7894 Some("much longer action string")
7895 );
7896 }
7897
7898 #[test]
7899 fn test_step_with_longest_action_none_for_empty_session() {
7900 let session = make_session(vec![], 0);
7901 assert!(session.step_with_longest_action().is_none());
7902 }
7903
7904 #[test]
7907 fn test_step_count_with_observation_longer_than_counts_correctly() {
7908 let steps = vec![
7909 make_step("t", "a", "short"), make_step("t", "a", "a longer string"), make_step("t", "a", "x"), ];
7913 let session = make_session(steps, 0);
7914 assert_eq!(session.step_count_with_observation_longer_than(5), 1);
7915 }
7916
7917 #[test]
7918 fn test_step_count_with_observation_longer_than_zero_for_empty_session() {
7919 let session = make_session(vec![], 0);
7920 assert_eq!(session.step_count_with_observation_longer_than(0), 0);
7921 }
7922
7923 #[test]
7926 fn test_action_ends_with_true_when_present() {
7927 let steps = vec![make_step("t", "search(query)", "o")];
7928 let session = make_session(steps, 0);
7929 assert!(session.action_ends_with(")"));
7930 }
7931
7932 #[test]
7933 fn test_action_ends_with_false_when_absent() {
7934 let steps = vec![make_step("t", "search(query)", "o")];
7935 let session = make_session(steps, 0);
7936 assert!(!session.action_ends_with("!"));
7937 }
7938
7939 #[test]
7940 fn test_thought_ends_with_true_when_present() {
7941 let steps = vec![make_step("I should search.", "a", "o")];
7942 let session = make_session(steps, 0);
7943 assert!(session.thought_ends_with("."));
7944 }
7945
7946 #[test]
7947 fn test_thought_ends_with_false_for_empty_session() {
7948 let session = make_session(vec![], 0);
7949 assert!(!session.thought_ends_with("x"));
7950 }
7951
7952 #[test]
7953 fn test_has_step_with_both_true_when_step_matches_both() {
7954 let steps = vec![
7955 make_step("need to search", "search(foo)", "o"),
7956 make_step("done", "noop", "o"),
7957 ];
7958 let session = make_session(steps, 0);
7959 assert!(session.has_step_with_both("search", "foo"));
7960 }
7961
7962 #[test]
7963 fn test_has_step_with_both_false_when_no_step_matches_both() {
7964 let steps = vec![
7965 make_step("need to search", "noop", "o"),
7966 make_step("done", "search(foo)", "o"),
7967 ];
7968 let session = make_session(steps, 0);
7969 assert!(!session.has_step_with_both("search", "foo"));
7970 }
7971
7972 #[test]
7975 fn test_thought_word_counts_returns_per_step_counts() {
7976 let steps = vec![
7977 make_step("one two three", "a", "o"),
7978 make_step("hello", "a", "o"),
7979 ];
7980 let session = make_session(steps, 0);
7981 assert_eq!(session.thought_word_counts(), vec![3, 1]);
7982 }
7983
7984 #[test]
7985 fn test_thought_word_counts_empty_for_empty_session() {
7986 let session = make_session(vec![], 0);
7987 assert!(session.thought_word_counts().is_empty());
7988 }
7989
7990 #[test]
7991 fn test_steps_sorted_by_thought_len_ascending_order() {
7992 let steps = vec![
7993 make_step("longest thought here", "a", "o"),
7994 make_step("hi", "a", "o"),
7995 make_step("medium thought", "a", "o"),
7996 ];
7997 let session = make_session(steps, 0);
7998 let sorted = session.steps_sorted_by_thought_len();
7999 assert!(sorted[0].thought.len() <= sorted[1].thought.len());
8000 assert!(sorted[1].thought.len() <= sorted[2].thought.len());
8001 }
8002
8003 #[test]
8004 fn test_steps_with_thought_longer_than_filters_correctly() {
8005 let steps = vec![
8006 make_step("short", "a", "o"),
8007 make_step("this is a longer thought", "a", "o"),
8008 ];
8009 let session = make_session(steps, 0);
8010 assert_eq!(session.steps_with_thought_longer_than(5).len(), 1);
8011 }
8012
8013 #[test]
8016 fn test_steps_with_action_containing_returns_matching_steps() {
8017 let steps = vec![
8018 make_step("t", "search(foo)", "o"),
8019 make_step("t", "read(file)", "o"),
8020 make_step("t", "search(bar)", "o"),
8021 ];
8022 let session = make_session(steps, 0);
8023 assert_eq!(session.steps_with_action_containing("search").len(), 2);
8024 }
8025
8026 #[test]
8027 fn test_steps_with_action_containing_empty_when_no_match() {
8028 let steps = vec![make_step("t", "read(file)", "o")];
8029 let session = make_session(steps, 0);
8030 assert!(session.steps_with_action_containing("search").is_empty());
8031 }
8032
8033 #[test]
8034 fn test_observation_max_chars_returns_longest() {
8035 let steps = vec![
8036 make_step("t", "a", "hi"),
8037 make_step("t", "a", "hello world"),
8038 ];
8039 let session = make_session(steps, 0);
8040 assert_eq!(session.observation_max_chars(), 11);
8041 }
8042
8043 #[test]
8044 fn test_observation_min_chars_skips_empty_observations() {
8045 let steps = vec![
8046 make_step("t", "a", ""),
8047 make_step("t", "a", "abcd"),
8048 make_step("t", "a", "ab"),
8049 ];
8050 let session = make_session(steps, 0);
8051 assert_eq!(session.observation_min_chars(), 2);
8052 }
8053
8054 #[test]
8055 fn test_observation_min_chars_zero_when_all_empty() {
8056 let steps = vec![make_step("t", "a", "")];
8057 let session = make_session(steps, 0);
8058 assert_eq!(session.observation_min_chars(), 0);
8059 }
8060
8061 #[test]
8064 fn test_action_word_counts_returns_per_step_counts() {
8065 let steps = vec![
8066 make_step("t1", "one two three", "o1"),
8067 make_step("t2", "hello", "o2"),
8068 make_step("t3", "a b", "o3"),
8069 ];
8070 let session = make_session(steps, 0);
8071 assert_eq!(session.action_word_counts(), vec![3, 1, 2]);
8072 }
8073
8074 #[test]
8075 fn test_action_word_counts_empty_for_no_steps() {
8076 let session = make_session(vec![], 0);
8077 assert!(session.action_word_counts().is_empty());
8078 }
8079
8080 #[test]
8081 fn test_thought_avg_chars_returns_average() {
8082 let steps = vec![
8083 make_step("ab", "a", "o"), make_step("abcd", "a", "o"), ];
8086 let session = make_session(steps, 0);
8087 assert_eq!(session.thought_avg_chars(), 3.0);
8088 }
8089
8090 #[test]
8091 fn test_thought_avg_chars_zero_for_empty() {
8092 let session = make_session(vec![], 0);
8093 assert_eq!(session.thought_avg_chars(), 0.0);
8094 }
8095
8096 #[test]
8097 fn test_thought_byte_range_returns_min_max() {
8098 let steps = vec![
8099 make_step("hi", "a", "o"), make_step("hello", "a", "o"), make_step("hey", "a", "o"), ];
8103 let session = make_session(steps, 0);
8104 assert_eq!(session.thought_byte_range(), (2, 5));
8105 }
8106
8107 #[test]
8108 fn test_thought_byte_range_zero_zero_for_empty() {
8109 let session = make_session(vec![], 0);
8110 assert_eq!(session.thought_byte_range(), (0, 0));
8111 }
8112}