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 step_durations_ms(&self) -> Vec<u64> {
337 self.steps.iter().map(|s| s.step_duration_ms).collect()
338 }
339
340 pub fn total_latency_ms(&self) -> u64 {
345 self.steps.iter().map(|s| s.step_duration_ms).sum()
346 }
347
348 pub fn avg_step_duration_ms(&self) -> f64 {
352 if self.steps.is_empty() {
353 return 0.0;
354 }
355 self.total_latency_ms() as f64 / self.steps.len() as f64
356 }
357
358 pub fn longest_step(&self) -> Option<&crate::agent::ReActStep> {
363 self.steps.iter().max_by_key(|s| s.step_duration_ms)
364 }
365
366 pub fn shortest_step(&self) -> Option<&crate::agent::ReActStep> {
371 self.steps.iter().min_by_key(|s| s.step_duration_ms)
372 }
373
374 pub fn action_sequence(&self) -> Vec<String> {
379 self.steps.iter().map(|s| s.action.clone()).collect()
380 }
381
382 pub fn unique_tools_used(&self) -> Vec<String> {
387 let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();
388 for step in &self.steps {
389 let action = step.action.trim();
390 if action.is_empty() || action.to_ascii_uppercase().starts_with("FINAL_ANSWER") {
391 continue;
392 }
393 if let Ok(v) = serde_json::from_str::<serde_json::Value>(action) {
395 if let Some(name) = v.get("tool").and_then(|n| n.as_str()) {
396 names.insert(name.to_owned());
397 continue;
398 }
399 }
400 names.insert(action.to_owned());
401 }
402 let mut sorted: Vec<String> = names.into_iter().collect();
403 sorted.sort_unstable();
404 sorted
405 }
406
407 pub fn all_thoughts(&self) -> Vec<&str> {
409 self.steps.iter().map(|s| s.thought.as_str()).collect()
410 }
411
412 pub fn all_actions(&self) -> Vec<&str> {
414 self.steps.iter().map(|s| s.action.as_str()).collect()
415 }
416
417 pub fn all_observations(&self) -> Vec<&str> {
419 self.steps.iter().map(|s| s.observation.as_str()).collect()
420 }
421
422 pub fn failed_steps(&self) -> Vec<&crate::agent::ReActStep> {
428 self.steps
429 .iter()
430 .filter(|s| {
431 let obs = s.observation.trim();
432 obs.starts_with("{\"error\"")
433 || obs.to_ascii_lowercase().contains("\"error\"")
434 })
435 .collect()
436 }
437
438 pub fn failed_tool_call_count(&self) -> usize {
442 self.steps
443 .iter()
444 .filter(|s| {
445 let obs = s.observation.trim();
446 obs.starts_with("{\"error\"")
447 || obs.to_ascii_lowercase().contains("\"error\"")
448 })
449 .count()
450 }
451
452 pub fn action_counts(&self) -> std::collections::HashMap<String, usize> {
456 let mut counts = std::collections::HashMap::new();
457 for step in &self.steps {
458 *counts.entry(step.action.clone()).or_insert(0) += 1;
459 }
460 counts
461 }
462
463 pub fn unique_actions(&self) -> Vec<String> {
465 let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
466 for step in &self.steps {
467 seen.insert(step.action.clone());
468 }
469 seen.into_iter().collect()
470 }
471
472 pub fn most_used_action(&self) -> Option<String> {
477 let counts = self.action_counts();
478 counts
479 .into_iter()
480 .max_by_key(|(_, count)| *count)
481 .map(|(name, _)| name)
482 }
483
484 pub fn last_observation(&self) -> Option<&str> {
489 self.steps
490 .iter()
491 .rev()
492 .find(|s| !s.observation.is_empty())
493 .map(|s| s.observation.as_str())
494 }
495
496 pub fn thought_count(&self) -> usize {
498 self.steps.iter().filter(|s| !s.thought.is_empty()).count()
499 }
500
501 pub fn observation_rate(&self) -> f64 {
505 let n = self.steps.len();
506 if n == 0 {
507 return 0.0;
508 }
509 let with_obs = self
510 .steps
511 .iter()
512 .filter(|s| !s.observation.is_empty())
513 .count();
514 with_obs as f64 / n as f64
515 }
516
517 pub fn has_graph_lookups(&self) -> bool {
520 self.graph_lookups > 0
521 }
522
523 pub fn consecutive_same_action_at_end(&self) -> usize {
530 let n = self.steps.len();
531 if n == 0 {
532 return 0;
533 }
534 let last_action = &self.steps[n - 1].action;
535 self.steps
536 .iter()
537 .rev()
538 .take_while(|s| &s.action == last_action)
539 .count()
540 .saturating_sub(1) }
542
543 pub fn action_repetition_rate(&self) -> f64 {
549 let n = self.steps.len();
550 if n < 2 {
551 return 0.0;
552 }
553 let repeats = self
554 .steps
555 .windows(2)
556 .filter(|w| w[0].action == w[1].action)
557 .count();
558 repeats as f64 / (n - 1) as f64
559 }
560
561 pub fn max_consecutive_failures(&self) -> usize {
567 let mut max_run = 0usize;
568 let mut current = 0usize;
569 for step in &self.steps {
570 let obs = step.observation.trim();
571 if obs.starts_with("{\"error\"") || obs.to_ascii_lowercase().contains("\"error\"") {
572 current += 1;
573 if current > max_run {
574 max_run = current;
575 }
576 } else {
577 current = 0;
578 }
579 }
580 max_run
581 }
582
583 pub fn avg_thought_length(&self) -> f64 {
588 let thoughts: Vec<_> = self
589 .steps
590 .iter()
591 .filter(|s| !s.thought.is_empty())
592 .collect();
593 if thoughts.is_empty() {
594 return 0.0;
595 }
596 let total: usize = thoughts.iter().map(|s| s.thought.len()).sum();
597 total as f64 / thoughts.len() as f64
598 }
599
600 pub fn graph_lookup_rate(&self) -> f64 {
605 let steps = self.steps.len();
606 if steps == 0 {
607 return 0.0;
608 }
609 self.graph_lookups as f64 / steps as f64
610 }
611
612 pub fn has_checkpoint_errors(&self) -> bool {
617 !self.checkpoint_errors.is_empty()
618 }
619
620 pub fn checkpoint_error_count(&self) -> usize {
622 self.checkpoint_errors.len()
623 }
624
625 pub fn graph_lookup_count(&self) -> usize {
627 self.graph_lookups
628 }
629
630 pub fn memory_hit_rate(&self) -> f64 {
635 let steps = self.steps.len();
636 if steps == 0 {
637 return 0.0;
638 }
639 self.memory_hits as f64 / steps as f64
640 }
641
642 pub fn total_memory_hits(&self) -> usize {
644 self.memory_hits
645 }
646
647 pub fn throughput_steps_per_sec(&self) -> f64 {
652 if self.duration_ms == 0 {
653 return 0.0;
654 }
655 self.steps.len() as f64 / (self.duration_ms as f64 / 1000.0)
656 }
657
658 pub fn duration_secs(&self) -> u64 {
660 self.duration_ms / 1000
661 }
662
663 pub fn steps_above_thought_length(&self, threshold: usize) -> usize {
665 self.steps.iter().filter(|s| s.thought.len() > threshold).count()
666 }
667
668 pub fn has_final_answer(&self) -> bool {
670 self.steps
671 .iter()
672 .any(|s| s.action.to_ascii_uppercase().starts_with("FINAL_ANSWER"))
673 }
674
675 pub fn avg_action_length(&self) -> f64 {
679 if self.steps.is_empty() {
680 return 0.0;
681 }
682 let total: usize = self.steps.iter().map(|s| s.action.len()).sum();
683 total as f64 / self.steps.len() as f64
684 }
685
686 pub fn has_tool_failures(&self) -> bool {
688 self.failed_tool_call_count() > 0
689 }
690
691 pub fn tool_call_rate(&self) -> f64 {
696 let total = self.steps.len();
697 if total == 0 {
698 return 0.0;
699 }
700 self.tool_calls_made() as f64 / total as f64
701 }
702
703 pub fn step_success_rate(&self) -> f64 {
708 let total = self.steps.len();
709 if total == 0 {
710 return 1.0;
711 }
712 1.0 - (self.failed_tool_call_count() as f64 / total as f64)
713 }
714
715 pub fn action_diversity(&self) -> f64 {
720 let total = self.steps.len();
721 if total == 0 {
722 return 0.0;
723 }
724 let unique: std::collections::HashSet<&str> =
725 self.steps.iter().map(|s| s.action.as_str()).collect();
726 unique.len() as f64 / total as f64
727 }
728
729 pub fn total_thought_length(&self) -> usize {
731 self.steps.iter().map(|s| s.thought.len()).sum()
732 }
733
734 pub fn steps_with_empty_observations(&self) -> usize {
736 self.steps.iter().filter(|s| s.observation.is_empty()).count()
737 }
738
739 pub fn observation_lengths(&self) -> Vec<usize> {
741 self.steps.iter().map(|s| s.observation.len()).collect()
742 }
743
744 pub fn avg_observation_length(&self) -> f64 {
748 let n = self.steps.len();
749 if n == 0 {
750 return 0.0;
751 }
752 let total: usize = self.steps.iter().map(|s| s.observation.len()).sum();
753 total as f64 / n as f64
754 }
755
756 pub fn min_thought_length(&self) -> usize {
759 self.steps
760 .iter()
761 .filter(|s| !s.thought.is_empty())
762 .map(|s| s.thought.len())
763 .min()
764 .unwrap_or(0)
765 }
766
767 pub fn longest_observation(&self) -> Option<&str> {
770 self.steps
771 .iter()
772 .max_by_key(|s| s.observation.len())
773 .map(|s| s.observation.as_str())
774 }
775
776 pub fn thought_lengths(&self) -> Vec<usize> {
778 self.steps.iter().map(|s| s.thought.len()).collect()
779 }
780
781 pub fn most_common_action(&self) -> Option<&str> {
785 if self.steps.is_empty() {
786 return None;
787 }
788 let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
789 for s in &self.steps {
790 *counts.entry(s.action.as_str()).or_insert(0) += 1;
791 }
792 counts.into_iter().max_by_key(|(_, c)| *c).map(|(a, _)| a)
793 }
794
795 pub fn action_lengths(&self) -> Vec<usize> {
797 self.steps.iter().map(|s| s.action.len()).collect()
798 }
799
800 pub fn step_success_count(&self) -> usize {
802 self.steps.len() - self.failed_tool_call_count()
803 }
804
805 pub fn longest_thought(&self) -> Option<&str> {
809 self.steps
810 .iter()
811 .max_by_key(|s| s.thought.len())
812 .map(|s| s.thought.as_str())
813 }
814
815 pub fn shortest_action(&self) -> Option<&str> {
819 self.steps
820 .iter()
821 .min_by_key(|s| s.action.len())
822 .map(|s| s.action.as_str())
823 }
824
825 pub fn total_thought_bytes(&self) -> usize {
827 self.steps.iter().map(|s| s.thought.len()).sum()
828 }
829
830 pub fn total_observation_bytes(&self) -> usize {
832 self.steps.iter().map(|s| s.observation.len()).sum()
833 }
834
835 pub fn first_step_action(&self) -> Option<&str> {
839 self.steps.first().map(|s| s.action.as_str())
840 }
841
842 pub fn last_step_action(&self) -> Option<&str> {
846 self.steps.last().map(|s| s.action.as_str())
847 }
848
849 pub fn count_nonempty_thoughts(&self) -> usize {
851 self.steps.iter().filter(|s| !s.thought.is_empty()).count()
852 }
853
854 pub fn observation_contains_count(&self, substring: &str) -> usize {
856 self.steps.iter().filter(|s| s.observation.contains(substring)).count()
857 }
858
859 pub fn count_steps_with_action(&self, action: &str) -> usize {
861 self.steps.iter().filter(|s| s.action == action).count()
862 }
863
864 pub fn thought_contains_count(&self, substring: &str) -> usize {
866 self.steps.iter().filter(|s| s.thought.contains(substring)).count()
867 }
868
869 pub fn failure_rate(&self) -> f64 {
874 let total = self.steps.len();
875 if total == 0 {
876 return 0.0;
877 }
878 self.failed_tool_call_count() as f64 / total as f64
879 }
880
881 pub fn unique_action_count(&self) -> usize {
883 let unique: std::collections::HashSet<&str> =
884 self.steps.iter().map(|s| s.action.as_str()).collect();
885 unique.len()
886 }
887
888 #[cfg(feature = "persistence")]
890 pub async fn save_checkpoint(
891 &self,
892 backend: &dyn crate::persistence::PersistenceBackend,
893 ) -> Result<(), AgentRuntimeError> {
894 let key = format!("session:{}", self.session_id);
895 let bytes = serde_json::to_vec(self)
896 .map_err(|e| AgentRuntimeError::Persistence(format!("serialize: {e}")))?;
897 backend.save(&key, &bytes).await
898 }
899
900 #[cfg(feature = "persistence")]
904 pub async fn load_checkpoint(
905 backend: &dyn crate::persistence::PersistenceBackend,
906 session_id: &str,
907 ) -> Result<Option<AgentSession>, AgentRuntimeError> {
908 let key = format!("session:{session_id}");
909 match backend.load(&key).await? {
910 None => Ok(None),
911 Some(bytes) => {
912 let session = serde_json::from_slice(&bytes)
913 .map_err(|e| AgentRuntimeError::Persistence(format!("deserialize: {e}")))?;
914 Ok(Some(session))
915 }
916 }
917 }
918
919 #[cfg(feature = "persistence")]
926 #[deprecated(since = "1.1.0", note = "Use load_checkpoint_at_step instead")]
927 pub async fn load_step_checkpoint(
928 backend: &dyn crate::persistence::PersistenceBackend,
929 session_id: &str,
930 step: usize,
931 ) -> Result<Option<AgentSession>, AgentRuntimeError> {
932 Self::load_checkpoint_at_step(backend, session_id, step).await
933 }
934
935 #[cfg(feature = "persistence")]
940 pub async fn load_checkpoint_at_step(
941 backend: &dyn crate::persistence::PersistenceBackend,
942 session_id: &str,
943 step: usize,
944 ) -> Result<Option<AgentSession>, AgentRuntimeError> {
945 let key = format!("session:{session_id}:step:{step}");
946 match backend.load(&key).await? {
947 None => Ok(None),
948 Some(bytes) => {
949 let session = serde_json::from_slice(&bytes)
950 .map_err(|e| AgentRuntimeError::Persistence(format!("deserialize: {e}")))?;
951 Ok(Some(session))
952 }
953 }
954 }
955}
956
957pub struct AgentRuntimeBuilder<S = NeedsConfig> {
974 #[cfg(feature = "memory")]
975 memory: Option<EpisodicStore>,
976 #[cfg(feature = "memory")]
977 working: Option<WorkingMemory>,
978 #[cfg(feature = "graph")]
979 graph: Option<GraphStore>,
980 #[cfg(feature = "orchestrator")]
981 backpressure: Option<BackpressureGuard>,
982 agent_config: Option<AgentConfig>,
983 tools: Vec<Arc<ToolSpec>>,
984 metrics: Arc<RuntimeMetrics>,
985 #[cfg(feature = "persistence")]
986 checkpoint_backend: Option<Arc<dyn crate::persistence::PersistenceBackend>>,
987 token_estimator: Option<Arc<dyn TokenEstimator>>,
988 _state: PhantomData<S>,
989}
990
991trait DebugBuilderState {
996 const NAME: &'static str;
998 const HAS_CONFIG: bool;
1000}
1001
1002impl DebugBuilderState for NeedsConfig {
1003 const NAME: &'static str = "AgentRuntimeBuilder<NeedsConfig>";
1004 const HAS_CONFIG: bool = false;
1005}
1006
1007impl DebugBuilderState for HasConfig {
1008 const NAME: &'static str = "AgentRuntimeBuilder<HasConfig>";
1009 const HAS_CONFIG: bool = true;
1010}
1011
1012impl<S: DebugBuilderState> std::fmt::Debug for AgentRuntimeBuilder<S> {
1013 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1014 let mut s = f.debug_struct(S::NAME);
1015 #[cfg(feature = "memory")]
1016 {
1017 s.field("memory", &self.memory.is_some())
1018 .field("working", &self.working.is_some());
1019 }
1020 #[cfg(feature = "graph")]
1021 s.field("graph", &self.graph.is_some());
1022 #[cfg(feature = "orchestrator")]
1023 s.field("backpressure", &self.backpressure.is_some());
1024 if S::HAS_CONFIG {
1025 s.field("agent_config", &self.agent_config.is_some());
1026 }
1027 s.field("tools", &self.tools.len()).finish()
1028 }
1029}
1030
1031impl Default for AgentRuntimeBuilder<NeedsConfig> {
1032 fn default() -> Self {
1033 Self {
1034 #[cfg(feature = "memory")]
1035 memory: None,
1036 #[cfg(feature = "memory")]
1037 working: None,
1038 #[cfg(feature = "graph")]
1039 graph: None,
1040 #[cfg(feature = "orchestrator")]
1041 backpressure: None,
1042 agent_config: None,
1043 tools: Vec::new(),
1044 metrics: RuntimeMetrics::new(),
1045 #[cfg(feature = "persistence")]
1046 checkpoint_backend: None,
1047 token_estimator: None,
1048 _state: PhantomData,
1049 }
1050 }
1051}
1052
1053impl<S> AgentRuntimeBuilder<S> {
1055 #[cfg(feature = "memory")]
1057 pub fn with_memory(mut self, store: EpisodicStore) -> Self {
1058 self.memory = Some(store);
1059 self
1060 }
1061
1062 #[cfg(feature = "memory")]
1064 pub fn with_working_memory(mut self, wm: WorkingMemory) -> Self {
1065 self.working = Some(wm);
1066 self
1067 }
1068
1069 #[cfg(feature = "graph")]
1071 pub fn with_graph(mut self, graph: GraphStore) -> Self {
1072 self.graph = Some(graph);
1073 self
1074 }
1075
1076 #[cfg(feature = "orchestrator")]
1078 pub fn with_backpressure(mut self, guard: BackpressureGuard) -> Self {
1079 self.backpressure = Some(guard);
1080 self
1081 }
1082
1083 pub fn register_tool(mut self, spec: ToolSpec) -> Self {
1085 self.tools.push(Arc::new(spec));
1086 self
1087 }
1088
1089 pub fn register_tools(mut self, specs: impl IntoIterator<Item = ToolSpec>) -> Self {
1095 for spec in specs {
1096 self.tools.push(Arc::new(spec));
1097 }
1098 self
1099 }
1100
1101 pub fn with_metrics(mut self, metrics: Arc<RuntimeMetrics>) -> Self {
1103 self.metrics = metrics;
1104 self
1105 }
1106
1107 #[cfg(feature = "persistence")]
1109 pub fn with_checkpoint_backend(
1110 mut self,
1111 backend: Arc<dyn crate::persistence::PersistenceBackend>,
1112 ) -> Self {
1113 self.checkpoint_backend = Some(backend);
1114 self
1115 }
1116
1117 pub fn with_token_estimator(mut self, estimator: Arc<dyn TokenEstimator>) -> Self {
1123 self.token_estimator = Some(estimator);
1124 self
1125 }
1126}
1127
1128impl AgentRuntimeBuilder<NeedsConfig> {
1130 pub fn new() -> Self {
1132 Self::default()
1133 }
1134
1135 pub fn with_agent_config(self, config: AgentConfig) -> AgentRuntimeBuilder<HasConfig> {
1140 AgentRuntimeBuilder {
1141 memory: self.memory,
1142 working: self.working,
1143 #[cfg(feature = "graph")]
1144 graph: self.graph,
1145 #[cfg(feature = "orchestrator")]
1146 backpressure: self.backpressure,
1147 agent_config: Some(config),
1148 tools: self.tools,
1149 metrics: self.metrics,
1150 #[cfg(feature = "persistence")]
1151 checkpoint_backend: self.checkpoint_backend,
1152 token_estimator: self.token_estimator,
1153 _state: PhantomData,
1154 }
1155 }
1156}
1157
1158impl AgentRuntimeBuilder<HasConfig> {
1160 pub fn build(self) -> AgentRuntime {
1164 #[allow(clippy::unwrap_used)]
1167 let agent_config = self.agent_config.unwrap();
1168
1169 AgentRuntime {
1170 #[cfg(feature = "memory")]
1171 memory: self.memory,
1172 #[cfg(feature = "memory")]
1173 working: self.working,
1174 #[cfg(feature = "graph")]
1175 graph: self.graph,
1176 #[cfg(feature = "orchestrator")]
1177 backpressure: self.backpressure,
1178 agent_config,
1179 tools: self.tools,
1180 metrics: self.metrics,
1181 token_estimator: self
1182 .token_estimator
1183 .unwrap_or_else(|| Arc::new(CharDivTokenEstimator)),
1184 #[cfg(feature = "persistence")]
1185 checkpoint_backend: self.checkpoint_backend,
1186 }
1187 }
1188}
1189
1190pub trait TokenEstimator: Send + Sync {
1207 fn count_tokens(&self, text: &str) -> usize;
1209}
1210
1211pub struct CharDivTokenEstimator;
1213
1214impl TokenEstimator for CharDivTokenEstimator {
1215 fn count_tokens(&self, text: &str) -> usize {
1216 (text.len() / 4).max(1)
1217 }
1218}
1219
1220pub struct AgentRuntime {
1224 #[cfg(feature = "memory")]
1225 memory: Option<EpisodicStore>,
1226 #[cfg(feature = "memory")]
1227 working: Option<WorkingMemory>,
1228 #[cfg(feature = "graph")]
1229 graph: Option<GraphStore>,
1230 #[cfg(feature = "orchestrator")]
1231 backpressure: Option<BackpressureGuard>,
1232 agent_config: AgentConfig,
1233 tools: Vec<Arc<ToolSpec>>,
1234 metrics: Arc<RuntimeMetrics>,
1235 #[cfg(feature = "persistence")]
1236 checkpoint_backend: Option<Arc<dyn crate::persistence::PersistenceBackend>>,
1237 token_estimator: Arc<dyn TokenEstimator>,
1238}
1239
1240impl std::fmt::Debug for AgentRuntime {
1241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1242 let mut s = f.debug_struct("AgentRuntime");
1243 s.field("memory", &self.memory.is_some())
1244 .field("working", &self.working.is_some());
1245 #[cfg(feature = "graph")]
1246 s.field("graph", &self.graph.is_some());
1247 #[cfg(feature = "orchestrator")]
1248 s.field("backpressure", &self.backpressure.is_some());
1249 s.field("tools", &self.tools.len());
1250 #[cfg(feature = "persistence")]
1251 s.field("checkpoint_backend", &self.checkpoint_backend.is_some());
1252 s.finish()
1253 }
1254}
1255
1256impl AgentRuntime {
1257 pub fn builder() -> AgentRuntimeBuilder<NeedsConfig> {
1259 AgentRuntimeBuilder::new()
1260 }
1261
1262 pub fn quick(max_iterations: usize, model: impl Into<String>) -> Self {
1264 AgentRuntime::builder()
1265 .with_agent_config(AgentConfig::new(max_iterations, model))
1266 .build()
1267 }
1268
1269 pub fn metrics(&self) -> Arc<RuntimeMetrics> {
1271 Arc::clone(&self.metrics)
1272 }
1273
1274 #[tracing::instrument(skip(self, infer), fields(agent_id = %agent_id))]
1287 pub async fn run_agent<F, Fut>(
1288 &self,
1289 agent_id: AgentId,
1290 prompt: &str,
1291 infer: F,
1292 ) -> Result<AgentSession, AgentRuntimeError>
1293 where
1294 F: FnMut(String) -> Fut,
1295 Fut: std::future::Future<Output = String>,
1296 {
1297 #[cfg(feature = "orchestrator")]
1300 {
1301 let backpressure_result = if let Some(ref guard) = self.backpressure {
1302 guard.try_acquire()
1303 } else {
1304 Ok(())
1305 };
1306 if let Err(e) = backpressure_result {
1307 tracing::warn!(agent_id = %agent_id, error = %e, "backpressure shed: rejecting session");
1308 self.metrics
1309 .backpressure_shed_count
1310 .fetch_add(1, Ordering::Relaxed);
1311 return Err(e);
1312 }
1313 }
1314
1315 self.metrics.total_sessions.fetch_add(1, Ordering::Relaxed);
1316 self.metrics.active_sessions.fetch_add(1, Ordering::Relaxed);
1317
1318 tracing::info!(agent_id = %agent_id, "agent session starting");
1319 let outcome = self.run_agent_inner(agent_id.clone(), prompt, infer).await;
1320
1321 #[cfg(feature = "orchestrator")]
1323 if let Some(ref guard) = self.backpressure {
1324 let _ = guard.release();
1325 }
1326
1327 let _ = self.metrics.active_sessions.fetch_update(
1330 Ordering::Relaxed,
1331 Ordering::Relaxed,
1332 |v| Some(v.saturating_sub(1)),
1333 );
1334
1335 match &outcome {
1336 Ok(session) => {
1337 tracing::info!(
1338 agent_id = %agent_id,
1339 session_id = %session.session_id,
1340 steps = session.step_count(),
1341 duration_ms = session.duration_ms,
1342 "agent session completed"
1343 );
1344 self.metrics
1345 .total_steps
1346 .fetch_add(session.step_count() as u64, Ordering::Relaxed);
1347 }
1348 Err(e) => {
1349 tracing::error!(agent_id = %agent_id, error = %e, "agent session failed");
1350 }
1351 }
1352
1353 outcome
1354 }
1355
1356 #[tracing::instrument(skip(self, infer), fields(agent_id = %agent_id, session_id = tracing::field::Empty))]
1358 async fn run_agent_inner<F, Fut>(
1359 &self,
1360 agent_id: AgentId,
1361 prompt: &str,
1362 infer: F,
1363 ) -> Result<AgentSession, AgentRuntimeError>
1364 where
1365 F: FnMut(String) -> Fut,
1366 Fut: std::future::Future<Output = String>,
1367 {
1368 let start = Instant::now();
1369 let session_id = uuid::Uuid::new_v4().to_string();
1370
1371 let mut memory_hits = 0usize;
1372 let mut graph_lookups = 0usize;
1373
1374 #[cfg(feature = "memory")]
1376 let enriched_prompt = if let Some(ref store) = self.memory {
1377 let memories = store.recall(&agent_id, self.agent_config.max_memory_recalls)?;
1378
1379 let memories = if let Some(token_budget) = self.agent_config.max_memory_tokens {
1381 let mut used = 0usize;
1382 memories
1383 .into_iter()
1384 .filter(|m| {
1385 let tokens = self.token_estimator.count_tokens(&m.content);
1386 if used + tokens <= token_budget {
1387 used += tokens;
1388 true
1389 } else {
1390 false
1391 }
1392 })
1393 .collect::<Vec<_>>()
1394 } else {
1395 memories
1396 };
1397
1398 memory_hits = memories.len();
1399 self.metrics
1400 .memory_recall_count
1401 .fetch_add(1, Ordering::Relaxed);
1402
1403 if let Some(budget) = self.agent_config.max_memory_tokens {
1404 tracing::debug!(
1405 "memory token budget: {budget}, injecting {} items",
1406 memory_hits
1407 );
1408 } else {
1409 tracing::debug!("enriched prompt with {} memory items", memory_hits);
1410 }
1411
1412 if memories.is_empty() {
1413 prompt.to_owned()
1414 } else {
1415 let mut enriched =
1418 String::with_capacity(prompt.len() + memories.len() * 64 + 32);
1419 enriched.push_str("Relevant memories:\n");
1420 for m in &memories {
1421 let _ = writeln!(enriched, "- {}", m.content);
1422 }
1423 let _ = write!(enriched, "\nCurrent prompt: {prompt}");
1424 enriched
1425 }
1426 } else {
1427 prompt.to_owned()
1428 };
1429 #[cfg(not(feature = "memory"))]
1430 let enriched_prompt = prompt.to_owned();
1431
1432 #[cfg(feature = "memory")]
1434 let enriched_prompt = if let Some(ref wm) = self.working {
1435 let entries = wm.entries()?;
1436 if entries.is_empty() {
1437 enriched_prompt
1438 } else {
1439 let mut out = String::with_capacity(
1441 enriched_prompt.len() + entries.len() * 32 + 32,
1442 );
1443 out.push_str(&enriched_prompt);
1444 out.push_str("\n\nCurrent working state:\n");
1445 for (k, v) in &entries {
1446 let _ = writeln!(out, " {k}: {v}");
1447 }
1448 if out.ends_with('\n') {
1450 out.pop();
1451 }
1452 out
1453 }
1454 } else {
1455 enriched_prompt
1456 };
1457
1458 #[cfg(feature = "graph")]
1460 if let Some(ref graph) = self.graph {
1461 graph_lookups = graph.entity_count()?;
1462 tracing::debug!("graph has {} entities", graph_lookups);
1463 }
1464
1465 let mut react_loop = ReActLoop::new(self.agent_config.clone())
1471 .with_metrics(Arc::clone(&self.metrics));
1472
1473 #[cfg(feature = "persistence")]
1475 if let Some(ref backend) = self.checkpoint_backend {
1476 react_loop = react_loop
1477 .with_step_checkpoint(Arc::clone(backend), session_id.clone());
1478 }
1479
1480 for tool in &self.tools {
1481 let tool_arc = Arc::clone(tool);
1482 let required_fields = tool_arc.required_fields.clone();
1483 #[cfg(feature = "orchestrator")]
1484 let circuit_breaker = tool_arc.circuit_breaker.clone();
1485
1486 let mut spec = ToolSpec::new_async(
1487 tool_arc.name.clone(),
1488 tool_arc.description.clone(),
1489 move |args| {
1490 let t = Arc::clone(&tool_arc);
1491 Box::pin(async move { t.call(args).await })
1492 },
1493 )
1494 .with_required_fields(required_fields);
1495
1496 #[cfg(feature = "orchestrator")]
1497 if let Some(cb) = circuit_breaker {
1498 spec = spec.with_circuit_breaker(cb);
1499 }
1500
1501 react_loop.register_tool(spec);
1502 }
1503
1504 tracing::Span::current().record("session_id", &session_id.as_str());
1507
1508 let steps = react_loop.run(&enriched_prompt, infer).await?;
1509 let duration_ms = start.elapsed().as_millis() as u64;
1510
1511 #[cfg(feature = "persistence")]
1513 let mut ckpt_errors: Vec<String> = Vec::new();
1514
1515 #[cfg(feature = "persistence")]
1517 if let Some(ref backend) = self.checkpoint_backend {
1518 tracing::info!(session_id = %session_id, "saving session checkpoint");
1519
1520 let tmp = AgentSession {
1522 session_id: session_id.clone(),
1523 agent_id: agent_id.clone(),
1524 steps: steps.clone(),
1525 memory_hits,
1526 graph_lookups,
1527 duration_ms,
1528 checkpoint_errors: vec![],
1529 };
1530 tmp.save_checkpoint(backend.as_ref()).await?;
1531
1532 for i in 1..=steps.len() {
1534 let partial = AgentSession {
1535 session_id: session_id.clone(),
1536 agent_id: agent_id.clone(),
1537 steps: steps[..i].to_vec(),
1538 memory_hits,
1539 graph_lookups,
1540 duration_ms,
1541 checkpoint_errors: vec![],
1542 };
1543 let key = format!("session:{session_id}:step:{i}");
1544 match serde_json::to_vec(&partial) {
1545 Ok(bytes) => {
1546 if let Err(e) = backend.save(&key, &bytes).await {
1547 let msg = format!("session:{session_id} step:{i} save: {e}");
1548 tracing::warn!("{}", msg);
1549 ckpt_errors.push(msg);
1550 }
1551 }
1552 Err(e) => {
1553 let msg =
1554 format!("session:{session_id} step:{i} serialise: {e}");
1555 tracing::warn!("{}", msg);
1556 ckpt_errors.push(msg);
1557 }
1558 }
1559 }
1560 }
1561
1562 let session = AgentSession {
1563 session_id,
1564 agent_id,
1565 steps,
1566 memory_hits,
1567 graph_lookups,
1568 duration_ms,
1569 #[cfg(feature = "persistence")]
1570 checkpoint_errors: ckpt_errors,
1571 #[cfg(not(feature = "persistence"))]
1572 checkpoint_errors: vec![],
1573 };
1574
1575 Ok(session)
1576 }
1577
1578 #[cfg(feature = "memory")]
1580 pub fn memory(&self) -> Option<&EpisodicStore> {
1581 self.memory.as_ref()
1582 }
1583
1584 #[cfg(feature = "graph")]
1586 pub fn graph(&self) -> Option<&GraphStore> {
1587 self.graph.as_ref()
1588 }
1589
1590 #[cfg(feature = "memory")]
1592 pub fn working_memory(&self) -> Option<&WorkingMemory> {
1593 self.working.as_ref()
1594 }
1595
1596 #[cfg(feature = "memory")]
1598 pub fn has_memory(&self) -> bool {
1599 self.memory.is_some()
1600 }
1601
1602 #[cfg(feature = "graph")]
1604 pub fn has_graph(&self) -> bool {
1605 self.graph.is_some()
1606 }
1607
1608 #[cfg(feature = "memory")]
1610 pub fn has_working_memory(&self) -> bool {
1611 self.working.is_some()
1612 }
1613
1614 pub async fn shutdown(&self) {
1622 tracing::info!("AgentRuntime shutting down");
1623 tracing::info!(
1624 active_sessions = self.metrics.active_sessions(),
1625 total_sessions = self.metrics.total_sessions(),
1626 total_steps = self.metrics.total_steps(),
1627 total_tool_calls = self.metrics.total_tool_calls(),
1628 failed_tool_calls = self.metrics.failed_tool_calls(),
1629 "final metrics snapshot on shutdown"
1630 );
1631
1632 #[cfg(feature = "persistence")]
1633 if let Some(ref backend) = self.checkpoint_backend {
1634 let ts = chrono::Utc::now().to_rfc3339();
1635 match backend.save("runtime:shutdown", ts.as_bytes()).await {
1636 Ok(()) => tracing::debug!("shutdown sentinel saved"),
1637 Err(e) => tracing::warn!(error = %e, "failed to save shutdown sentinel"),
1638 }
1639 }
1640
1641 tracing::info!("AgentRuntime shutdown complete");
1642 }
1643
1644 #[cfg(feature = "providers")]
1654 pub async fn run_agent_with_provider(
1655 &self,
1656 agent_id: AgentId,
1657 prompt: &str,
1658 provider: std::sync::Arc<dyn crate::providers::LlmProvider>,
1659 ) -> Result<AgentSession, AgentRuntimeError> {
1660 let model = self.agent_config.model.clone();
1661 self.run_agent(agent_id, prompt, |ctx| {
1662 let provider = provider.clone();
1663 let model = model.clone();
1664 async move {
1665 provider
1666 .complete(&ctx, &model)
1667 .await
1668 .unwrap_or_else(|e| format!("FINAL ANSWER: inference error: {e}"))
1669 }
1670 })
1671 .await
1672 }
1673}
1674
1675#[cfg(test)]
1678mod tests {
1679 use super::*;
1680 use crate::graph::{Entity, GraphStore, Relationship};
1681 use crate::memory::{EpisodicStore, WorkingMemory};
1682
1683 fn simple_config() -> AgentConfig {
1684 AgentConfig::new(5, "test")
1685 }
1686
1687 async fn final_answer_infer(_ctx: String) -> String {
1688 "Thought: done\nAction: FINAL_ANSWER 42".into()
1689 }
1690
1691 #[tokio::test]
1702 async fn test_builder_with_config_compiles() {
1703 let _runtime = AgentRuntime::builder()
1704 .with_agent_config(simple_config())
1705 .build();
1706 }
1708
1709 #[tokio::test]
1710 async fn test_builder_succeeds_with_minimal_config() {
1711 let _runtime = AgentRuntime::builder()
1712 .with_agent_config(simple_config())
1713 .build();
1714 }
1715
1716 #[tokio::test]
1717 async fn test_builder_with_all_subsystems() {
1718 let _runtime = AgentRuntime::builder()
1719 .with_agent_config(simple_config())
1720 .with_memory(EpisodicStore::new())
1721 .with_graph(GraphStore::new())
1722 .with_working_memory(WorkingMemory::new(10).unwrap())
1723 .with_backpressure(BackpressureGuard::new(5).unwrap())
1724 .build();
1725 }
1726
1727 #[tokio::test]
1728 async fn test_builder_produces_runtime_with_config() {
1729 let runtime = AgentRuntime::builder()
1732 .with_agent_config(simple_config())
1733 .build();
1734 let session = runtime
1735 .run_agent(AgentId::new("agent-x"), "hello", final_answer_infer)
1736 .await
1737 .unwrap();
1738 assert!(session.step_count() >= 1);
1739 assert!(!session.session_id.is_empty());
1740 }
1741
1742 #[tokio::test]
1745 async fn test_run_agent_returns_session_with_steps() {
1746 let runtime = AgentRuntime::builder()
1747 .with_agent_config(simple_config())
1748 .build();
1749
1750 let session = runtime
1751 .run_agent(AgentId::new("agent-1"), "hello", final_answer_infer)
1752 .await
1753 .unwrap();
1754
1755 assert_eq!(session.step_count(), 1);
1756 }
1757
1758 #[tokio::test]
1759 async fn test_run_agent_session_has_agent_id() {
1760 let runtime = AgentRuntime::builder()
1761 .with_agent_config(simple_config())
1762 .build();
1763
1764 let session = runtime
1765 .run_agent(AgentId::new("agent-42"), "hello", final_answer_infer)
1766 .await
1767 .unwrap();
1768
1769 assert_eq!(session.agent_id.0, "agent-42");
1770 }
1771
1772 #[tokio::test]
1773 async fn test_run_agent_session_duration_is_set() {
1774 let runtime = AgentRuntime::builder()
1775 .with_agent_config(simple_config())
1776 .build();
1777
1778 let session = runtime
1779 .run_agent(AgentId::new("a"), "hello", final_answer_infer)
1780 .await
1781 .unwrap();
1782
1783 let _ = session.duration_ms; }
1786
1787 #[tokio::test]
1788 async fn test_run_agent_session_has_session_id() {
1789 let runtime = AgentRuntime::builder()
1790 .with_agent_config(simple_config())
1791 .build();
1792
1793 let session = runtime
1794 .run_agent(AgentId::new("a"), "hello", final_answer_infer)
1795 .await
1796 .unwrap();
1797
1798 assert!(!session.session_id.is_empty());
1800 assert_eq!(session.session_id.len(), 36); }
1802
1803 #[tokio::test]
1804 async fn test_run_agent_memory_hits_zero_without_memory() {
1805 let runtime = AgentRuntime::builder()
1806 .with_agent_config(simple_config())
1807 .build();
1808
1809 let session = runtime
1810 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
1811 .await
1812 .unwrap();
1813
1814 assert_eq!(session.memory_hits, 0);
1815 }
1816
1817 #[tokio::test]
1818 async fn test_run_agent_memory_hits_counts_recalled_items() {
1819 let store = EpisodicStore::new();
1820 let agent = AgentId::new("mem-agent");
1821 store
1822 .add_episode(agent.clone(), "remembered fact", 0.8)
1823 .unwrap();
1824
1825 let runtime = AgentRuntime::builder()
1826 .with_agent_config(simple_config())
1827 .with_memory(store)
1828 .build();
1829
1830 let session = runtime
1831 .run_agent(agent, "prompt", final_answer_infer)
1832 .await
1833 .unwrap();
1834
1835 assert_eq!(session.memory_hits, 1);
1836 }
1837
1838 #[tokio::test]
1839 async fn test_run_agent_graph_lookups_counts_entities() {
1840 let graph = GraphStore::new();
1841 graph.add_entity(Entity::new("e1", "Node")).unwrap();
1842 graph.add_entity(Entity::new("e2", "Node")).unwrap();
1843
1844 let runtime = AgentRuntime::builder()
1845 .with_agent_config(simple_config())
1846 .with_graph(graph)
1847 .build();
1848
1849 let session = runtime
1850 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
1851 .await
1852 .unwrap();
1853
1854 assert_eq!(session.graph_lookups, 2);
1855 }
1856
1857 #[tokio::test]
1858 async fn test_run_agent_backpressure_released_after_run() {
1859 let guard = BackpressureGuard::new(3).unwrap();
1860
1861 let runtime = AgentRuntime::builder()
1862 .with_agent_config(simple_config())
1863 .with_backpressure(guard.clone())
1864 .build();
1865
1866 runtime
1867 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
1868 .await
1869 .unwrap();
1870
1871 assert_eq!(guard.depth().unwrap(), 0);
1872 }
1873
1874 #[tokio::test]
1875 async fn test_run_agent_backpressure_sheds_when_full() {
1876 let guard = BackpressureGuard::new(1).unwrap();
1877 guard.try_acquire().unwrap(); let runtime = AgentRuntime::builder()
1880 .with_agent_config(simple_config())
1881 .with_backpressure(guard)
1882 .build();
1883
1884 let result = runtime
1885 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
1886 .await;
1887 assert!(matches!(
1888 result,
1889 Err(AgentRuntimeError::BackpressureShed { .. })
1890 ));
1891 }
1892
1893 #[tokio::test]
1894 async fn test_run_agent_max_iterations_error_propagated() {
1895 let cfg = AgentConfig::new(2, "model");
1896 let runtime = AgentRuntime::builder().with_agent_config(cfg).build();
1897
1898 let result = runtime
1900 .run_agent(AgentId::new("a"), "prompt", |_ctx: String| async {
1901 "Thought: looping\nAction: FINAL_ANSWER done".to_string()
1902 })
1903 .await;
1904 assert!(result.is_ok()); }
1906
1907 #[tokio::test]
1908 async fn test_agent_session_step_count_matches_steps() {
1909 let session = AgentSession {
1910 session_id: "test-session-id".into(),
1911 agent_id: AgentId::new("a"),
1912 steps: vec![
1913 ReActStep {
1914 thought: "t".into(),
1915 action: "a".into(),
1916 observation: "o".into(),
1917 step_duration_ms: 0,
1918 },
1919 ReActStep {
1920 thought: "t2".into(),
1921 action: "FINAL_ANSWER".into(),
1922 observation: "done".into(),
1923 step_duration_ms: 0,
1924 },
1925 ],
1926 memory_hits: 0,
1927 graph_lookups: 0,
1928 duration_ms: 10,
1929 checkpoint_errors: vec![],
1930 };
1931 assert_eq!(session.step_count(), 2);
1932 }
1933
1934 #[tokio::test]
1937 async fn test_runtime_memory_accessor_returns_none_when_not_configured() {
1938 let runtime = AgentRuntime::builder()
1939 .with_agent_config(simple_config())
1940 .build();
1941 assert!(runtime.memory().is_none());
1942 }
1943
1944 #[tokio::test]
1945 async fn test_runtime_memory_accessor_returns_some_when_configured() {
1946 let runtime = AgentRuntime::builder()
1947 .with_agent_config(simple_config())
1948 .with_memory(EpisodicStore::new())
1949 .build();
1950 assert!(runtime.memory().is_some());
1951 }
1952
1953 #[tokio::test]
1954 async fn test_runtime_graph_accessor_returns_none_when_not_configured() {
1955 let runtime = AgentRuntime::builder()
1956 .with_agent_config(simple_config())
1957 .build();
1958 assert!(runtime.graph().is_none());
1959 }
1960
1961 #[tokio::test]
1962 async fn test_runtime_graph_accessor_returns_some_when_configured() {
1963 let runtime = AgentRuntime::builder()
1964 .with_agent_config(simple_config())
1965 .with_graph(GraphStore::new())
1966 .build();
1967 assert!(runtime.graph().is_some());
1968 }
1969
1970 #[tokio::test]
1971 async fn test_runtime_working_memory_accessor() {
1972 let runtime = AgentRuntime::builder()
1973 .with_agent_config(simple_config())
1974 .with_working_memory(WorkingMemory::new(5).unwrap())
1975 .build();
1976 assert!(runtime.working_memory().is_some());
1977 }
1978
1979 #[tokio::test]
1980 async fn test_runtime_with_tool_registered() {
1981 let runtime = AgentRuntime::builder()
1982 .with_agent_config(simple_config())
1983 .register_tool(ToolSpec::new("calc", "math", |_| serde_json::json!(99)))
1984 .build();
1985
1986 let mut call_count = 0;
1987 let session = runtime
1988 .run_agent(AgentId::new("a"), "compute", move |_ctx: String| {
1989 call_count += 1;
1990 let count = call_count;
1991 async move {
1992 if count == 1 {
1993 "Thought: use calc\nAction: calc {}".into()
1994 } else {
1995 "Thought: done\nAction: FINAL_ANSWER result".into()
1996 }
1997 }
1998 })
1999 .await
2000 .unwrap();
2001
2002 assert!(session.step_count() >= 1);
2003 }
2004
2005 #[tokio::test]
2006 async fn test_run_agent_with_graph_relationship_lookup() {
2007 let graph = GraphStore::new();
2008 graph.add_entity(Entity::new("a", "X")).unwrap();
2009 graph.add_entity(Entity::new("b", "Y")).unwrap();
2010 graph
2011 .add_relationship(Relationship::new("a", "b", "LINKS", 1.0))
2012 .unwrap();
2013
2014 let runtime = AgentRuntime::builder()
2015 .with_agent_config(simple_config())
2016 .with_graph(graph)
2017 .build();
2018
2019 let session = runtime
2020 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
2021 .await
2022 .unwrap();
2023
2024 assert_eq!(session.graph_lookups, 2); }
2026
2027 #[tokio::test]
2030 async fn test_metrics_active_sessions_decrements_after_run() {
2031 let runtime = AgentRuntime::builder()
2032 .with_agent_config(simple_config())
2033 .build();
2034
2035 runtime
2036 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
2037 .await
2038 .unwrap();
2039
2040 assert_eq!(runtime.metrics().active_sessions(), 0);
2041 }
2042
2043 #[tokio::test]
2044 async fn test_metrics_total_sessions_increments() {
2045 let runtime = AgentRuntime::builder()
2046 .with_agent_config(simple_config())
2047 .build();
2048
2049 runtime
2050 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
2051 .await
2052 .unwrap();
2053 runtime
2054 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
2055 .await
2056 .unwrap();
2057
2058 assert_eq!(runtime.metrics().total_sessions(), 2);
2059 }
2060
2061 #[tokio::test]
2062 async fn test_metrics_backpressure_shed_increments_on_shed() {
2063 let guard = BackpressureGuard::new(1).unwrap();
2064 guard.try_acquire().unwrap(); let runtime = AgentRuntime::builder()
2067 .with_agent_config(simple_config())
2068 .with_backpressure(guard)
2069 .build();
2070
2071 let _ = runtime
2072 .run_agent(AgentId::new("a"), "prompt", final_answer_infer)
2073 .await;
2074
2075 assert_eq!(runtime.metrics().backpressure_shed_count(), 1);
2076 }
2077
2078 #[tokio::test]
2079 async fn test_metrics_memory_recall_count_increments() {
2080 let store = EpisodicStore::new();
2081 let agent = AgentId::new("a");
2082 store.add_episode(agent.clone(), "fact", 0.9).unwrap();
2083
2084 let runtime = AgentRuntime::builder()
2085 .with_agent_config(simple_config())
2086 .with_memory(store)
2087 .build();
2088
2089 runtime
2090 .run_agent(agent, "prompt", final_answer_infer)
2091 .await
2092 .unwrap();
2093
2094 assert_eq!(runtime.metrics().memory_recall_count(), 1);
2095 }
2096
2097 #[tokio::test]
2100 async fn test_agent_config_max_memory_tokens_limits_injection() {
2101 let store = EpisodicStore::new();
2102 let agent = AgentId::new("budget-agent");
2103 for i in 0..5 {
2105 let content = format!("{:0>100}", i); store.add_episode(agent.clone(), content, 0.9).unwrap();
2107 }
2108
2109 let cfg = AgentConfig::new(5, "test").with_max_memory_tokens(10);
2111 let runtime = AgentRuntime::builder()
2112 .with_agent_config(cfg)
2113 .with_memory(store)
2114 .build();
2115
2116 let session = runtime
2117 .run_agent(agent, "prompt", final_answer_infer)
2118 .await
2119 .unwrap();
2120
2121 assert!(
2122 session.memory_hits <= 1,
2123 "expected at most 1 memory hit with tight token budget, got {}",
2124 session.memory_hits
2125 );
2126 }
2127
2128 #[tokio::test]
2131 async fn test_working_memory_injected_into_prompt() {
2132 let wm = WorkingMemory::new(10).unwrap();
2133 wm.set("task", "write tests").unwrap();
2134 wm.set("status", "in progress").unwrap();
2135
2136 let runtime = AgentRuntime::builder()
2137 .with_agent_config(simple_config())
2138 .with_working_memory(wm)
2139 .build();
2140
2141 let mut captured_ctx: Option<String> = None;
2142 let captured_ref = &mut captured_ctx;
2143
2144 runtime
2145 .run_agent(AgentId::new("a"), "do stuff", |ctx: String| {
2146 *captured_ref = Some(ctx.clone());
2147 async move { "Thought: done\nAction: FINAL_ANSWER ok".to_string() }
2148 })
2149 .await
2150 .unwrap();
2151
2152 let ctx = captured_ctx.expect("infer should have been called");
2153 assert!(
2154 ctx.contains("Current working state:"),
2155 "expected working memory injection in context, got: {ctx}"
2156 );
2157 assert!(ctx.contains("task: write tests"));
2158 assert!(ctx.contains("status: in progress"));
2159 }
2160
2161 #[tokio::test]
2164 async fn test_token_budget_zero_returns_no_memories() {
2165 let store = EpisodicStore::new();
2167 let agent = AgentId::new("budget-agent");
2168 store.add_episode(agent.clone(), "short", 0.9).unwrap();
2169
2170 let mut config = AgentConfig::new(5, "test-model");
2171 config.max_memory_tokens = Some(0);
2172 config.max_memory_recalls = 10;
2173
2174 let runtime = AgentRuntime::builder()
2175 .with_memory(store)
2176 .with_agent_config(config)
2177 .build();
2178
2179 let steps = runtime
2180 .run_agent(
2181 agent,
2182 "test",
2183 |_ctx| async { "Thought: ok\nAction: FINAL_ANSWER done".to_string() },
2184 )
2185 .await
2186 .unwrap();
2187
2188 assert_eq!(steps.steps.len(), 1);
2190 }
2191
2192 #[tokio::test]
2193 async fn test_token_budget_smaller_than_smallest_item_returns_no_memories() {
2194 let store = EpisodicStore::new();
2195 let agent = AgentId::new("budget-agent2");
2196 store
2198 .add_episode(agent.clone(), "a".repeat(40), 0.9)
2199 .unwrap();
2200
2201 let mut config = AgentConfig::new(5, "test-model");
2202 config.max_memory_tokens = Some(1);
2203 config.max_memory_recalls = 10;
2204
2205 let runtime = AgentRuntime::builder()
2206 .with_memory(store)
2207 .with_agent_config(config)
2208 .build();
2209
2210 let session = runtime
2211 .run_agent(
2212 agent,
2213 "test",
2214 |_ctx| async { "Thought: ok\nAction: FINAL_ANSWER done".to_string() },
2215 )
2216 .await
2217 .unwrap();
2218
2219 assert_eq!(session.memory_hits, 0);
2220 }
2221
2222 #[tokio::test]
2225 async fn test_agent_runtime_quick_runs_agent() {
2226 let runtime = AgentRuntime::quick(5, "test-model");
2227 let agent = AgentId::new("quick-agent");
2228 let session = runtime
2229 .run_agent(agent, "hello", |_ctx| async {
2230 "Thought: done\nAction: FINAL_ANSWER ok".to_string()
2231 })
2232 .await
2233 .unwrap();
2234 assert_eq!(session.step_count(), 1);
2235 }
2236
2237 #[test]
2240 fn test_final_answer_extracts_text() {
2241 let session = AgentSession {
2242 session_id: "s".into(),
2243 agent_id: AgentId::new("a"),
2244 steps: vec![ReActStep {
2245 thought: "done".into(),
2246 action: "FINAL_ANSWER Paris".into(),
2247 observation: "".into(),
2248 step_duration_ms: 0,
2249 }],
2250 memory_hits: 0,
2251 graph_lookups: 0,
2252 duration_ms: 0,
2253 checkpoint_errors: vec![],
2254 };
2255 assert_eq!(session.final_answer(), Some("Paris".to_string()));
2256 }
2257
2258 #[test]
2259 fn test_final_answer_returns_none_without_final_step() {
2260 let session = AgentSession {
2261 session_id: "s".into(),
2262 agent_id: AgentId::new("a"),
2263 steps: vec![ReActStep {
2264 thought: "thinking".into(),
2265 action: "search {}".into(),
2266 observation: "result".into(),
2267 step_duration_ms: 0,
2268 }],
2269 memory_hits: 0,
2270 graph_lookups: 0,
2271 duration_ms: 0,
2272 checkpoint_errors: vec![],
2273 };
2274 assert_eq!(session.final_answer(), None);
2275
2276 let empty_session = AgentSession {
2277 session_id: "s2".into(),
2278 agent_id: AgentId::new("a"),
2279 steps: vec![],
2280 memory_hits: 0,
2281 graph_lookups: 0,
2282 duration_ms: 0,
2283 checkpoint_errors: vec![],
2284 };
2285 assert_eq!(empty_session.final_answer(), None);
2286 }
2287
2288 #[test]
2289 fn test_all_actions_returns_actions_in_order() {
2290 let session = AgentSession {
2291 session_id: "s".into(),
2292 agent_id: AgentId::new("a"),
2293 steps: vec![
2294 ReActStep::new("think1", "search {}", "result"),
2295 ReActStep::new("think2", "FINAL_ANSWER done", ""),
2296 ],
2297 memory_hits: 0,
2298 graph_lookups: 0,
2299 duration_ms: 10,
2300 checkpoint_errors: vec![],
2301 };
2302 assert_eq!(session.all_actions(), vec!["search {}", "FINAL_ANSWER done"]);
2303 }
2304
2305 #[test]
2306 fn test_has_checkpoint_errors_false_when_empty() {
2307 let session = AgentSession {
2308 session_id: "s".into(),
2309 agent_id: AgentId::new("a"),
2310 steps: vec![],
2311 memory_hits: 0,
2312 graph_lookups: 0,
2313 duration_ms: 0,
2314 checkpoint_errors: vec![],
2315 };
2316 assert!(!session.has_checkpoint_errors());
2317 }
2318
2319 #[test]
2320 fn test_has_checkpoint_errors_true_when_non_empty() {
2321 let session = AgentSession {
2322 session_id: "s".into(),
2323 agent_id: AgentId::new("a"),
2324 steps: vec![],
2325 memory_hits: 0,
2326 graph_lookups: 0,
2327 duration_ms: 0,
2328 checkpoint_errors: vec!["err".into()],
2329 };
2330 assert!(session.has_checkpoint_errors());
2331 }
2332
2333 #[test]
2334 fn test_memory_hit_rate_zero_with_no_steps() {
2335 let session = AgentSession {
2336 session_id: "s".into(),
2337 agent_id: AgentId::new("a"),
2338 steps: vec![],
2339 memory_hits: 5,
2340 graph_lookups: 0,
2341 duration_ms: 0,
2342 checkpoint_errors: vec![],
2343 };
2344 assert_eq!(session.memory_hit_rate(), 0.0);
2345 }
2346
2347 #[test]
2348 fn test_memory_hit_rate_correct_proportion() {
2349 let session = AgentSession {
2350 session_id: "s".into(),
2351 agent_id: AgentId::new("a"),
2352 steps: vec![
2353 ReActStep::new("t", "a", "o"),
2354 ReActStep::new("t", "a", "o"),
2355 ReActStep::new("t", "a", "o"),
2356 ReActStep::new("t", "a", "o"),
2357 ],
2358 memory_hits: 2,
2359 graph_lookups: 0,
2360 duration_ms: 0,
2361 checkpoint_errors: vec![],
2362 };
2363 assert!((session.memory_hit_rate() - 0.5).abs() < 1e-9);
2364 }
2365
2366 #[test]
2367 fn test_filter_tool_call_steps_excludes_final_answer() {
2368 let session = AgentSession {
2369 session_id: "s".into(),
2370 agent_id: AgentId::new("a"),
2371 steps: vec![
2372 ReActStep::new("t1", "search {}", "res"),
2373 ReActStep::new("t2", "FINAL_ANSWER done", ""),
2374 ],
2375 memory_hits: 0,
2376 graph_lookups: 0,
2377 duration_ms: 0,
2378 checkpoint_errors: vec![],
2379 };
2380 let tool_steps = session.filter_tool_call_steps();
2381 assert_eq!(tool_steps.len(), 1);
2382 assert_eq!(tool_steps[0].action, "search {}");
2383 }
2384
2385 #[test]
2386 fn test_slowest_step_index() {
2387 let mut s0 = ReActStep::new("t", "a", "o");
2388 s0.step_duration_ms = 5;
2389 let mut s1 = ReActStep::new("t", "a", "o");
2390 s1.step_duration_ms = 100;
2391 let mut s2 = ReActStep::new("t", "a", "o");
2392 s2.step_duration_ms = 10;
2393 let session = AgentSession {
2394 session_id: "s".into(),
2395 agent_id: AgentId::new("a"),
2396 steps: vec![s0, s1, s2],
2397 memory_hits: 0,
2398 graph_lookups: 0,
2399 duration_ms: 0,
2400 checkpoint_errors: vec![],
2401 };
2402 assert_eq!(session.slowest_step_index(), Some(1));
2403 assert_eq!(session.fastest_step_index(), Some(0));
2404 }
2405
2406 #[test]
2407 fn test_slowest_step_index_none_when_empty() {
2408 let session = AgentSession {
2409 session_id: "s".into(),
2410 agent_id: AgentId::new("a"),
2411 steps: vec![],
2412 memory_hits: 0,
2413 graph_lookups: 0,
2414 duration_ms: 0,
2415 checkpoint_errors: vec![],
2416 };
2417 assert_eq!(session.slowest_step_index(), None);
2418 assert_eq!(session.fastest_step_index(), None);
2419 }
2420
2421 #[test]
2422 fn test_last_step_returns_last() {
2423 let session = AgentSession {
2424 session_id: "s".into(),
2425 agent_id: AgentId::new("a"),
2426 steps: vec![
2427 ReActStep::new("t1", "a1", "o1"),
2428 ReActStep::new("t2", "FINAL_ANSWER done", ""),
2429 ],
2430 memory_hits: 0,
2431 graph_lookups: 0,
2432 duration_ms: 0,
2433 checkpoint_errors: vec![],
2434 };
2435 assert_eq!(session.last_step().map(|s| s.action.as_str()), Some("FINAL_ANSWER done"));
2436 }
2437
2438 #[test]
2439 fn test_last_step_none_when_empty() {
2440 let session = AgentSession {
2441 session_id: "s".into(),
2442 agent_id: AgentId::new("a"),
2443 steps: vec![],
2444 memory_hits: 0,
2445 graph_lookups: 0,
2446 duration_ms: 0,
2447 checkpoint_errors: vec![],
2448 };
2449 assert!(session.last_step().is_none());
2450 }
2451
2452 #[test]
2453 fn test_step_at_returns_correct_step() {
2454 let session = AgentSession {
2455 session_id: "s".into(),
2456 agent_id: AgentId::new("a"),
2457 steps: vec![
2458 ReActStep::new("t0", "a0", "o0"),
2459 ReActStep::new("t1", "a1", "o1"),
2460 ],
2461 memory_hits: 0,
2462 graph_lookups: 0,
2463 duration_ms: 0,
2464 checkpoint_errors: vec![],
2465 };
2466 assert_eq!(session.step_at(1).map(|s| s.thought.as_str()), Some("t1"));
2467 assert!(session.step_at(99).is_none());
2468 }
2469
2470 #[test]
2473 fn test_failed_steps_returns_steps_with_error_observation() {
2474 use crate::agent::ReActStep;
2475 let session = AgentSession {
2476 session_id: "s".into(),
2477 agent_id: AgentId::new("a"),
2478 steps: vec![
2479 ReActStep::new("t", "tool_a {}", r#"{"error":"bad input","ok":false}"#),
2480 ReActStep::new("t", "tool_b {}", r#"{"result":"ok","ok":true}"#),
2481 ],
2482 memory_hits: 0,
2483 graph_lookups: 0,
2484 duration_ms: 0,
2485 checkpoint_errors: vec![],
2486 };
2487 let failed = session.failed_steps();
2488 assert_eq!(failed.len(), 1);
2489 assert!(failed[0].observation.contains("bad input"));
2490 }
2491
2492 #[test]
2493 fn test_failed_steps_empty_when_no_errors() {
2494 use crate::agent::ReActStep;
2495 let session = AgentSession {
2496 session_id: "s".into(),
2497 agent_id: AgentId::new("a"),
2498 steps: vec![ReActStep::new("t", "FINAL_ANSWER done", "")],
2499 memory_hits: 0,
2500 graph_lookups: 0,
2501 duration_ms: 0,
2502 checkpoint_errors: vec![],
2503 };
2504 assert!(session.failed_steps().is_empty());
2505 }
2506
2507 fn make_step(thought: &str, action: &str, observation: &str) -> ReActStep {
2510 ReActStep::new(thought, action, observation)
2511 }
2512
2513 fn make_session(steps: Vec<ReActStep>, duration_ms: u64) -> AgentSession {
2514 AgentSession {
2515 session_id: "s".into(),
2516 agent_id: AgentId::new("a"),
2517 steps,
2518 memory_hits: 0,
2519 graph_lookups: 0,
2520 duration_ms,
2521 checkpoint_errors: vec![],
2522 }
2523 }
2524
2525 #[test]
2526 fn test_step_count_returns_number_of_steps() {
2527 let s = make_session(vec![ReActStep::new("t", "a", "o"), ReActStep::new("t", "a", "o")], 0);
2528 assert_eq!(s.step_count(), 2);
2529 }
2530
2531 #[test]
2532 fn test_is_empty_true_for_no_steps() {
2533 let s = make_session(vec![], 0);
2534 assert!(s.is_empty());
2535 }
2536
2537 #[test]
2538 fn test_is_empty_false_with_steps() {
2539 let s = make_session(vec![ReActStep::new("t", "a", "o")], 0);
2540 assert!(!s.is_empty());
2541 }
2542
2543 #[test]
2544 fn test_is_successful_true_with_final_answer() {
2545 let s = make_session(vec![ReActStep::new("t", "FINAL_ANSWER yes", "")], 0);
2546 assert!(s.is_successful());
2547 }
2548
2549 #[test]
2550 fn test_is_successful_false_without_final_answer() {
2551 let s = make_session(vec![ReActStep::new("t", "search {}", "result")], 0);
2552 assert!(!s.is_successful());
2553 }
2554
2555 #[test]
2556 fn test_elapsed_returns_duration_from_duration_ms() {
2557 let s = make_session(vec![], 500);
2558 assert_eq!(s.elapsed(), std::time::Duration::from_millis(500));
2559 }
2560
2561 #[test]
2562 fn test_tool_calls_made_excludes_final_answer() {
2563 let s = make_session(vec![
2564 ReActStep::new("t", "search {}", "res"),
2565 ReActStep::new("t", "lookup {}", "res"),
2566 ReActStep::new("t", "FINAL_ANSWER done", ""),
2567 ], 0);
2568 assert_eq!(s.tool_calls_made(), 2);
2569 }
2570
2571 #[test]
2572 fn test_total_step_duration_ms_sums_all_steps() {
2573 let mut s1 = ReActStep::new("t", "a", "o"); s1.step_duration_ms = 10;
2574 let mut s2 = ReActStep::new("t", "a", "o"); s2.step_duration_ms = 30;
2575 let s = make_session(vec![s1, s2], 0);
2576 assert_eq!(s.total_step_duration_ms(), 40);
2577 }
2578
2579 #[test]
2580 fn test_average_step_duration_ms() {
2581 let mut s1 = ReActStep::new("t", "a", "o"); s1.step_duration_ms = 20;
2582 let mut s2 = ReActStep::new("t", "a", "o"); s2.step_duration_ms = 40;
2583 let s = make_session(vec![s1, s2], 0);
2584 assert_eq!(s.average_step_duration_ms(), 30);
2585 }
2586
2587 #[test]
2588 fn test_all_thoughts_returns_thoughts_in_order() {
2589 let s = make_session(vec![
2590 ReActStep::new("first thought", "a1", "o1"),
2591 ReActStep::new("second thought", "a2", "o2"),
2592 ], 0);
2593 assert_eq!(s.all_thoughts(), vec!["first thought", "second thought"]);
2594 }
2595
2596 #[test]
2597 fn test_all_observations_returns_observations_in_order() {
2598 let s = make_session(vec![
2599 ReActStep::new("t1", "a1", "obs one"),
2600 ReActStep::new("t2", "a2", "obs two"),
2601 ], 0);
2602 assert_eq!(s.all_observations(), vec!["obs one", "obs two"]);
2603 }
2604
2605 #[test]
2606 fn test_observations_matching_finds_matching_steps() {
2607 let s = make_session(vec![
2608 ReActStep::new("t1", "a1", "found the answer"),
2609 ReActStep::new("t2", "a2", "nothing relevant"),
2610 ], 0);
2611 let matching = s.observations_matching("answer");
2612 assert_eq!(matching.len(), 1);
2613 assert!(matching[0].observation.contains("answer"));
2614 }
2615
2616 #[test]
2617 fn test_first_step_returns_first() {
2618 let s = make_session(vec![
2619 ReActStep::new("first", "a1", "o1"),
2620 ReActStep::new("second", "a2", "o2"),
2621 ], 0);
2622 assert_eq!(s.first_step().map(|s| s.thought.as_str()), Some("first"));
2623 }
2624
2625 #[test]
2626 fn test_first_step_none_when_empty() {
2627 let s = make_session(vec![], 0);
2628 assert!(s.first_step().is_none());
2629 }
2630
2631 #[test]
2634 fn test_graph_lookup_count_returns_field() {
2635 let session = AgentSession {
2636 session_id: "s".into(),
2637 agent_id: AgentId::new("a"),
2638 steps: vec![],
2639 memory_hits: 0,
2640 graph_lookups: 7,
2641 duration_ms: 0,
2642 checkpoint_errors: vec![],
2643 };
2644 assert_eq!(session.graph_lookup_count(), 7usize);
2645 }
2646
2647 #[test]
2650 fn test_action_counts_counts_each_action() {
2651 let session = make_session(
2652 vec![
2653 ReActStep::new("t1", "search", "r1"),
2654 ReActStep::new("t2", "search", "r2"),
2655 ReActStep::new("t3", "FINAL_ANSWER", "done"),
2656 ],
2657 0,
2658 );
2659 let counts = session.action_counts();
2660 assert_eq!(counts.get("search").copied().unwrap_or(0), 2);
2661 assert_eq!(counts.get("FINAL_ANSWER").copied().unwrap_or(0), 1);
2662 }
2663
2664 #[test]
2665 fn test_unique_actions_returns_sorted_deduped() {
2666 let session = make_session(
2667 vec![
2668 ReActStep::new("t", "b_action", "r"),
2669 ReActStep::new("t", "a_action", "r"),
2670 ReActStep::new("t", "b_action", "r"),
2671 ],
2672 0,
2673 );
2674 assert_eq!(session.unique_actions(), vec!["a_action", "b_action"]);
2675 }
2676
2677 #[test]
2678 fn test_unique_actions_empty_when_no_steps() {
2679 let session = make_session(vec![], 0);
2680 assert!(session.unique_actions().is_empty());
2681 }
2682
2683 #[test]
2686 fn test_total_latency_ms_sums_step_durations() {
2687 let mut steps = vec![
2688 ReActStep::new("t1", "a1", "o1"),
2689 ReActStep::new("t2", "a2", "o2"),
2690 ];
2691 steps[0].step_duration_ms = 100;
2692 steps[1].step_duration_ms = 250;
2693 let session = make_session(steps, 350);
2694 assert_eq!(session.total_latency_ms(), 350);
2695 }
2696
2697 #[test]
2698 fn test_total_latency_ms_zero_for_empty_session() {
2699 let session = make_session(vec![], 0);
2700 assert_eq!(session.total_latency_ms(), 0);
2701 }
2702
2703 #[test]
2704 fn test_action_sequence_returns_actions_in_order() {
2705 let session = make_session(
2706 vec![
2707 ReActStep::new("t", "search", "r"),
2708 ReActStep::new("t", "FINAL_ANSWER", "done"),
2709 ],
2710 0,
2711 );
2712 assert_eq!(session.action_sequence(), vec!["search", "FINAL_ANSWER"]);
2713 }
2714
2715 #[test]
2718 fn test_has_action_returns_true_for_present_action() {
2719 let session = make_session(
2720 vec![ReActStep::new("t", "search", "r")],
2721 0,
2722 );
2723 assert!(session.has_action("search"));
2724 }
2725
2726 #[test]
2727 fn test_has_action_returns_false_for_absent_action() {
2728 let session = make_session(
2729 vec![ReActStep::new("t", "search", "r")],
2730 0,
2731 );
2732 assert!(!session.has_action("compute"));
2733 }
2734
2735 #[test]
2736 fn test_thought_at_returns_thought_for_valid_index() {
2737 let session = make_session(
2738 vec![
2739 ReActStep::new("first thought", "a1", "r1"),
2740 ReActStep::new("second thought", "a2", "r2"),
2741 ],
2742 0,
2743 );
2744 assert_eq!(session.thought_at(0), Some("first thought"));
2745 assert_eq!(session.thought_at(1), Some("second thought"));
2746 }
2747
2748 #[test]
2749 fn test_thought_at_returns_none_for_out_of_bounds_index() {
2750 let session = make_session(vec![ReActStep::new("t", "a", "r")], 0);
2751 assert!(session.thought_at(99).is_none());
2752 }
2753
2754 #[test]
2757 fn test_step_count_for_action_counts_correctly() {
2758 let session = make_session(
2759 vec![
2760 ReActStep::new("t", "search", "r1"),
2761 ReActStep::new("t", "search", "r2"),
2762 ReActStep::new("t", "FINAL_ANSWER", "done"),
2763 ],
2764 0,
2765 );
2766 assert_eq!(session.step_count_for_action("search"), 2);
2767 assert_eq!(session.step_count_for_action("FINAL_ANSWER"), 1);
2768 assert_eq!(session.step_count_for_action("unknown"), 0);
2769 }
2770
2771 #[test]
2772 fn test_observations_returns_all_observation_strings() {
2773 let session = make_session(
2774 vec![
2775 ReActStep::new("t1", "a", "obs_one"),
2776 ReActStep::new("t2", "b", "obs_two"),
2777 ],
2778 0,
2779 );
2780 let obs = session.observations();
2781 assert_eq!(obs, vec!["obs_one", "obs_two"]);
2782 }
2783
2784 #[test]
2785 fn test_observations_empty_for_no_steps() {
2786 let session = make_session(vec![], 0);
2787 assert!(session.observations().is_empty());
2788 }
2789
2790 #[test]
2793 fn test_unique_tools_used_deduplicates_actions() {
2794 let session = make_session(
2795 vec![
2796 ReActStep::new("t", "search", "r1"),
2797 ReActStep::new("t", "lookup", "r2"),
2798 ReActStep::new("t", "search", "r3"),
2799 ],
2800 0,
2801 );
2802 let tools = session.unique_tools_used();
2803 assert_eq!(tools.len(), 2);
2804 assert!(tools.contains(&"search".to_string()));
2805 assert!(tools.contains(&"lookup".to_string()));
2806 }
2807
2808 #[test]
2809 fn test_unique_tools_used_excludes_final_answer() {
2810 let session = make_session(
2811 vec![
2812 ReActStep::new("t", "search", "r1"),
2813 ReActStep::new("t", "FINAL_ANSWER: done", "r2"),
2814 ],
2815 0,
2816 );
2817 let tools = session.unique_tools_used();
2818 assert_eq!(tools.len(), 1);
2819 assert!(tools.contains(&"search".to_string()));
2820 }
2821
2822 #[test]
2823 fn test_unique_tools_used_empty_for_no_steps() {
2824 let session = make_session(vec![], 0);
2825 assert!(session.unique_tools_used().is_empty());
2826 }
2827
2828 #[test]
2831 fn test_avg_step_duration_zero_for_empty_session() {
2832 let session = make_session(vec![], 0);
2833 assert!((session.avg_step_duration_ms() - 0.0).abs() < 1e-9);
2834 }
2835
2836 #[test]
2837 fn test_avg_step_duration_single_step() {
2838 let mut step = ReActStep::new("t", "a", "r");
2839 step.step_duration_ms = 100;
2840 let session = make_session(vec![step], 0);
2841 assert!((session.avg_step_duration_ms() - 100.0).abs() < 1e-9);
2842 }
2843
2844 #[test]
2845 fn test_avg_step_duration_multiple_steps() {
2846 let mut s1 = ReActStep::new("t1", "a", "r");
2847 s1.step_duration_ms = 100;
2848 let mut s2 = ReActStep::new("t2", "b", "r");
2849 s2.step_duration_ms = 200;
2850 let session = make_session(vec![s1, s2], 0);
2851 assert!((session.avg_step_duration_ms() - 150.0).abs() < 1e-9);
2852 }
2853
2854 #[test]
2855 fn test_longest_step_returns_step_with_max_duration() {
2856 let mut s1 = ReActStep::new("t1", "a", "r");
2857 s1.step_duration_ms = 50;
2858 let mut s2 = ReActStep::new("t2", "b", "r");
2859 s2.step_duration_ms = 200;
2860 let session = make_session(vec![s1, s2], 0);
2861 assert_eq!(session.longest_step().map(|s| s.step_duration_ms), Some(200));
2862 }
2863
2864 #[test]
2865 fn test_longest_step_returns_none_for_empty_session() {
2866 let session = make_session(vec![], 0);
2867 assert!(session.longest_step().is_none());
2868 }
2869
2870 #[test]
2871 fn test_shortest_step_returns_step_with_min_duration() {
2872 let mut s1 = ReActStep::new("t1", "a", "r");
2873 s1.step_duration_ms = 50;
2874 let mut s2 = ReActStep::new("t2", "b", "r");
2875 s2.step_duration_ms = 200;
2876 let session = make_session(vec![s1, s2], 0);
2877 assert_eq!(session.shortest_step().map(|s| s.step_duration_ms), Some(50));
2878 }
2879
2880 #[test]
2883 fn test_first_thought_returns_thought_from_first_step() {
2884 let session = make_session(
2885 vec![
2886 ReActStep::new("alpha", "a1", "r1"),
2887 ReActStep::new("beta", "a2", "r2"),
2888 ],
2889 0,
2890 );
2891 assert_eq!(session.first_thought(), Some("alpha"));
2892 }
2893
2894 #[test]
2895 fn test_last_thought_returns_thought_from_last_step() {
2896 let session = make_session(
2897 vec![
2898 ReActStep::new("alpha", "a1", "r1"),
2899 ReActStep::new("beta", "a2", "r2"),
2900 ],
2901 0,
2902 );
2903 assert_eq!(session.last_thought(), Some("beta"));
2904 }
2905
2906 #[test]
2907 fn test_first_thought_none_for_empty_session() {
2908 let session = make_session(vec![], 0);
2909 assert!(session.first_thought().is_none());
2910 }
2911
2912 #[test]
2913 fn test_last_thought_none_for_empty_session() {
2914 let session = make_session(vec![], 0);
2915 assert!(session.last_thought().is_none());
2916 }
2917
2918 #[test]
2921 fn test_first_action_returns_action_from_first_step() {
2922 let session = make_session(
2923 vec![
2924 ReActStep::new("t1", "search", "r1"),
2925 ReActStep::new("t2", "FINAL_ANSWER", "r2"),
2926 ],
2927 0,
2928 );
2929 assert_eq!(session.first_action(), Some("search"));
2930 }
2931
2932 #[test]
2933 fn test_last_action_returns_action_from_last_step() {
2934 let session = make_session(
2935 vec![
2936 ReActStep::new("t1", "search", "r1"),
2937 ReActStep::new("t2", "FINAL_ANSWER", "r2"),
2938 ],
2939 0,
2940 );
2941 assert_eq!(session.last_action(), Some("FINAL_ANSWER"));
2942 }
2943
2944 #[test]
2945 fn test_first_action_none_for_empty_session() {
2946 let session = make_session(vec![], 0);
2947 assert!(session.first_action().is_none());
2948 }
2949
2950 #[test]
2951 fn test_last_action_equals_first_action_for_single_step() {
2952 let session = make_session(vec![ReActStep::new("t", "calc", "r")], 0);
2953 assert_eq!(session.first_action(), session.last_action());
2954 }
2955
2956 #[test]
2959 fn test_checkpoint_error_count_zero_when_none() {
2960 let session = make_session(vec![], 0);
2961 assert_eq!(session.checkpoint_error_count(), 0);
2962 }
2963
2964 #[test]
2965 fn test_checkpoint_error_count_reflects_errors() {
2966 let mut session = make_session(vec![], 0);
2967 session.checkpoint_errors.push("save failed".into());
2968 session.checkpoint_errors.push("disk full".into());
2969 assert_eq!(session.checkpoint_error_count(), 2);
2970 }
2971
2972 #[test]
2975 fn test_failed_tool_call_count_zero_when_no_errors() {
2976 let step = ReActStep::new("think", "search", "results found");
2977 let session = make_session(vec![step], 0);
2978 assert_eq!(session.failed_tool_call_count(), 0);
2979 }
2980
2981 #[test]
2982 fn test_failed_tool_call_count_matches_failed_steps() {
2983 let ok_step = ReActStep::new("ok", "search", "all good");
2984 let err_step = ReActStep::new("err", "lookup", "{\"error\": \"not found\"}");
2985 let session = make_session(vec![ok_step, err_step], 0);
2986 assert_eq!(session.failed_tool_call_count(), session.failed_steps().len());
2987 assert_eq!(session.failed_tool_call_count(), 1);
2988 }
2989
2990 #[test]
2991 fn test_failed_tool_call_count_counts_all_errors() {
2992 let err1 = ReActStep::new("e1", "a", "{\"error\": \"bad\"}");
2993 let err2 = ReActStep::new("e2", "b", "some \"error\" text");
2994 let ok = ReActStep::new("ok", "c", "success");
2995 let session = make_session(vec![err1, err2, ok], 0);
2996 assert_eq!(session.failed_tool_call_count(), 2);
2997 }
2998
2999 #[test]
3002 fn test_total_memory_hits_returns_memory_hits_field() {
3003 let mut session = make_session(vec![], 0);
3004 session.memory_hits = 7;
3005 assert_eq!(session.total_memory_hits(), 7);
3006 }
3007
3008 #[test]
3009 fn test_total_memory_hits_zero_by_default() {
3010 let session = make_session(vec![], 0);
3011 assert_eq!(session.total_memory_hits(), 0);
3012 }
3013
3014 #[test]
3015 fn test_action_diversity_all_unique_is_one() {
3016 let steps = vec![
3017 ReActStep::new("t", "search", "r"),
3018 ReActStep::new("t", "calc", "r"),
3019 ReActStep::new("t", "lookup", "r"),
3020 ];
3021 let session = make_session(steps, 0);
3022 assert!((session.action_diversity() - 1.0).abs() < 1e-9);
3023 }
3024
3025 #[test]
3026 fn test_action_diversity_all_same_is_fraction() {
3027 let steps = vec![
3028 ReActStep::new("t", "search", "r"),
3029 ReActStep::new("t", "search", "r"),
3030 ReActStep::new("t", "search", "r"),
3031 ];
3032 let session = make_session(steps, 0);
3033 assert!((session.action_diversity() - 1.0 / 3.0).abs() < 1e-9);
3035 }
3036
3037 #[test]
3038 fn test_action_diversity_zero_for_empty_session() {
3039 let session = make_session(vec![], 0);
3040 assert!((session.action_diversity() - 0.0).abs() < 1e-9);
3041 }
3042
3043 #[test]
3046 fn test_last_n_steps_returns_last_n() {
3047 let steps = vec![
3048 ReActStep::new("t1", "a", "r1"),
3049 ReActStep::new("t2", "b", "r2"),
3050 ReActStep::new("t3", "c", "r3"),
3051 ];
3052 let session = make_session(steps, 0);
3053 let last2 = session.last_n_steps(2);
3054 assert_eq!(last2.len(), 2);
3055 assert_eq!(last2[0].action, "b");
3056 assert_eq!(last2[1].action, "c");
3057 }
3058
3059 #[test]
3060 fn test_last_n_steps_returns_all_when_n_exceeds_count() {
3061 let steps = vec![
3062 ReActStep::new("t1", "a", "r1"),
3063 ReActStep::new("t2", "b", "r2"),
3064 ];
3065 let session = make_session(steps, 0);
3066 assert_eq!(session.last_n_steps(10).len(), 2);
3067 }
3068
3069 #[test]
3070 fn test_last_n_steps_empty_for_no_steps() {
3071 let session = make_session(vec![], 0);
3072 assert!(session.last_n_steps(3).is_empty());
3073 }
3074
3075 #[test]
3076 fn test_last_n_steps_zero_returns_empty() {
3077 let steps = vec![ReActStep::new("t1", "a", "r1")];
3078 let session = make_session(steps, 0);
3079 assert!(session.last_n_steps(0).is_empty());
3080 }
3081
3082 #[test]
3085 fn test_observation_count_counts_non_empty() {
3086 let steps = vec![
3087 ReActStep::new("t", "a", "result"),
3088 ReActStep::new("t", "b", ""),
3089 ReActStep::new("t", "c", "data"),
3090 ];
3091 let session = make_session(steps, 0);
3092 assert_eq!(session.observation_count(), 2);
3093 }
3094
3095 #[test]
3096 fn test_observation_count_zero_when_all_empty() {
3097 let steps = vec![
3098 ReActStep::new("t", "a", ""),
3099 ReActStep::new("t", "b", ""),
3100 ];
3101 let session = make_session(steps, 0);
3102 assert_eq!(session.observation_count(), 0);
3103 }
3104
3105 #[test]
3106 fn test_steps_without_observation_counts_empty_obs() {
3107 let steps = vec![
3108 ReActStep::new("t", "a", ""),
3109 ReActStep::new("t", "b", "data"),
3110 ReActStep::new("t", "c", ""),
3111 ];
3112 let session = make_session(steps, 0);
3113 assert_eq!(session.steps_without_observation(), 2);
3114 }
3115
3116 #[test]
3117 fn test_steps_without_observation_zero_when_all_filled() {
3118 let steps = vec![
3119 ReActStep::new("t", "a", "r1"),
3120 ReActStep::new("t", "b", "r2"),
3121 ];
3122 let session = make_session(steps, 0);
3123 assert_eq!(session.steps_without_observation(), 0);
3124 }
3125
3126 #[test]
3129 fn test_throughput_steps_per_sec_correct_ratio() {
3130 let steps = vec![
3131 ReActStep::new("t", "a", "r"),
3132 ReActStep::new("t", "b", "r"),
3133 ];
3134 let session = make_session(steps, 1000);
3136 assert!((session.throughput_steps_per_sec() - 2.0).abs() < 1e-9);
3137 }
3138
3139 #[test]
3140 fn test_throughput_steps_per_sec_zero_when_no_duration() {
3141 let steps = vec![ReActStep::new("t", "a", "r")];
3142 let session = make_session(steps, 0);
3143 assert!((session.throughput_steps_per_sec() - 0.0).abs() < 1e-9);
3144 }
3145
3146 #[test]
3149 fn test_thoughts_containing_returns_matching_steps() {
3150 let session = make_session(
3151 vec![
3152 ReActStep::new("I need to search", "search", "found"),
3153 ReActStep::new("Let me calculate", "calc", "done"),
3154 ReActStep::new("search again", "search", "ok"),
3155 ],
3156 0,
3157 );
3158 let matches = session.thoughts_containing("search");
3159 assert_eq!(matches.len(), 2);
3160 }
3161
3162 #[test]
3163 fn test_thoughts_containing_is_case_insensitive() {
3164 let session = make_session(
3165 vec![ReActStep::new("SEARCH the web", "search", "r")],
3166 0,
3167 );
3168 assert_eq!(session.thoughts_containing("search").len(), 1);
3169 }
3170
3171 #[test]
3172 fn test_thoughts_containing_empty_when_no_match() {
3173 let session = make_session(vec![ReActStep::new("think about x", "a", "r")], 0);
3174 assert!(session.thoughts_containing("zebra").is_empty());
3175 }
3176
3177 #[test]
3178 fn test_step_durations_ms_returns_all_durations() {
3179 let mut steps = vec![
3180 ReActStep::new("t", "a", "r"),
3181 ReActStep::new("t", "b", "r"),
3182 ];
3183 steps[0].step_duration_ms = 100;
3184 steps[1].step_duration_ms = 200;
3185 let session = make_session(steps, 300);
3186 assert_eq!(session.step_durations_ms(), vec![100, 200]);
3187 }
3188
3189 #[test]
3190 fn test_fastest_step_index_returns_index_of_shortest_step() {
3191 let mut steps = vec![
3192 ReActStep::new("t", "a", "r"),
3193 ReActStep::new("t", "b", "r"),
3194 ReActStep::new("t", "c", "r"),
3195 ];
3196 steps[0].step_duration_ms = 300;
3197 steps[1].step_duration_ms = 50;
3198 steps[2].step_duration_ms = 200;
3199 let session = make_session(steps, 550);
3200 assert_eq!(session.fastest_step_index(), Some(1));
3201 }
3202
3203 #[test]
3204 fn test_fastest_step_index_none_for_empty_session() {
3205 let session = make_session(vec![], 0);
3206 assert!(session.fastest_step_index().is_none());
3207 }
3208
3209 #[test]
3212 fn test_most_used_action_returns_most_frequent() {
3213 let steps = vec![
3214 ReActStep::new("t", "search", "r"),
3215 ReActStep::new("t", "calc", "r"),
3216 ReActStep::new("t", "search", "r"),
3217 ];
3218 let session = make_session(steps, 0);
3219 assert_eq!(session.most_used_action().as_deref(), Some("search"));
3220 }
3221
3222 #[test]
3223 fn test_most_used_action_none_for_empty_session() {
3224 let session = make_session(vec![], 0);
3225 assert!(session.most_used_action().is_none());
3226 }
3227
3228 #[test]
3229 fn test_graph_lookup_rate_correct_ratio() {
3230 let steps = vec![
3231 ReActStep::new("t", "a", "r"),
3232 ReActStep::new("t", "b", "r"),
3233 ReActStep::new("t", "c", "r"),
3234 ReActStep::new("t", "d", "r"),
3235 ];
3236 let mut session = make_session(steps, 0);
3237 session.graph_lookups = 2;
3238 assert!((session.graph_lookup_rate() - 0.5).abs() < 1e-9);
3239 }
3240
3241 #[test]
3242 fn test_graph_lookup_rate_zero_for_empty_session() {
3243 let session = make_session(vec![], 0);
3244 assert!((session.graph_lookup_rate() - 0.0).abs() < 1e-9);
3245 }
3246
3247 #[test]
3250 fn test_has_tool_failures_false_when_no_errors() {
3251 let steps = vec![
3252 make_step("t", "action1", "ok"),
3253 make_step("t", "action2", "done"),
3254 ];
3255 let session = make_session(steps, 0);
3256 assert!(!session.has_tool_failures());
3257 }
3258
3259 #[test]
3260 fn test_has_tool_failures_true_when_error_observation() {
3261 let steps = vec![
3262 make_step("t", "action1", "{\"error\": \"timeout\"}"),
3263 ];
3264 let session = make_session(steps, 0);
3265 assert!(session.has_tool_failures());
3266 }
3267
3268 #[test]
3269 fn test_tool_call_rate_zero_for_empty_session() {
3270 let session = make_session(vec![], 0);
3271 assert!((session.tool_call_rate() - 0.0).abs() < 1e-9);
3272 }
3273
3274 #[test]
3275 fn test_tool_call_rate_correct_ratio() {
3276 let steps = vec![
3277 make_step("t", "tool_action", "ok"),
3278 make_step("t", "FINAL_ANSWER: done", ""),
3279 make_step("t", "another_tool", "ok"),
3280 ];
3281 let session = make_session(steps, 0);
3282 assert!((session.tool_call_rate() - 2.0 / 3.0).abs() < 1e-9);
3284 }
3285
3286 #[test]
3287 fn test_step_success_rate_one_for_empty_session() {
3288 let session = make_session(vec![], 0);
3289 assert!((session.step_success_rate() - 1.0).abs() < 1e-9);
3290 }
3291
3292 #[test]
3293 fn test_step_success_rate_less_than_one_when_failures() {
3294 let steps = vec![
3295 make_step("t", "act", "{\"error\": \"fail\"}"),
3296 make_step("t", "act", "success"),
3297 ];
3298 let session = make_session(steps, 0);
3299 assert!((session.step_success_rate() - 0.5).abs() < 1e-9);
3301 }
3302
3303 #[test]
3306 fn test_avg_step_duration_ms_zero_for_empty() {
3307 let session = make_session(vec![], 0);
3308 assert!((session.avg_step_duration_ms() - 0.0).abs() < 1e-9);
3309 }
3310
3311 #[test]
3312 fn test_avg_step_duration_ms_correct_mean() {
3313 let mut s1 = make_step("t", "a", "o");
3314 s1.step_duration_ms = 100;
3315 let mut s2 = make_step("t", "b", "o");
3316 s2.step_duration_ms = 200;
3317 let session = make_session(vec![s1, s2], 0);
3318 assert!((session.avg_step_duration_ms() - 150.0).abs() < 1e-9);
3319 }
3320
3321 #[test]
3322 fn test_longest_step_none_for_empty() {
3323 let session = make_session(vec![], 0);
3324 assert!(session.longest_step().is_none());
3325 }
3326
3327 #[test]
3328 fn test_longest_step_middle_has_max_duration() {
3329 let mut s1 = make_step("t", "a", "o");
3330 s1.step_duration_ms = 10;
3331 let mut s2 = make_step("t", "b", "o");
3332 s2.step_duration_ms = 500;
3333 let mut s3 = make_step("t", "c", "o");
3334 s3.step_duration_ms = 20;
3335 let session = make_session(vec![s1, s2, s3], 0);
3336 assert_eq!(session.longest_step().unwrap().action, "b");
3337 }
3338
3339 #[test]
3340 fn test_unique_tools_used_deduplicates_and_sorts() {
3341 let steps = vec![
3342 make_step("t", "search", "o"),
3343 make_step("t", "lookup", "o"),
3344 make_step("t", "search", "o"),
3345 ];
3346 let session = make_session(steps, 0);
3347 assert_eq!(session.unique_tools_used(), vec!["lookup", "search"]);
3348 }
3349
3350 #[test]
3351 fn test_all_thoughts_collects_in_order() {
3352 let steps = vec![make_step("think1", "a", "o"), make_step("think2", "b", "o")];
3353 let session = make_session(steps, 0);
3354 assert_eq!(session.all_thoughts(), vec!["think1", "think2"]);
3355 }
3356
3357 #[test]
3358 fn test_all_actions_collects_in_order() {
3359 let steps = vec![make_step("t", "act1", "o"), make_step("t", "act2", "o")];
3360 let session = make_session(steps, 0);
3361 assert_eq!(session.all_actions(), vec!["act1", "act2"]);
3362 }
3363
3364 #[test]
3365 fn test_all_observations_collects_in_order() {
3366 let steps = vec![make_step("t", "a", "obs1"), make_step("t", "b", "obs2")];
3367 let session = make_session(steps, 0);
3368 assert_eq!(session.all_observations(), vec!["obs1", "obs2"]);
3369 }
3370
3371 #[test]
3372 fn test_action_counts_returns_frequency_map() {
3373 let steps = vec![
3374 make_step("t", "search", "o"),
3375 make_step("t", "lookup", "o"),
3376 make_step("t", "search", "o"),
3377 ];
3378 let session = make_session(steps, 0);
3379 let counts = session.action_counts();
3380 assert_eq!(counts["search"], 2);
3381 assert_eq!(counts["lookup"], 1);
3382 }
3383
3384 #[test]
3385 fn test_unique_actions_three_with_repeat_yields_two() {
3386 let steps = vec![
3387 make_step("t", "beta", "o"),
3388 make_step("t", "alpha", "o"),
3389 make_step("t", "beta", "o"),
3390 ];
3391 let session = make_session(steps, 0);
3392 assert_eq!(session.unique_actions(), vec!["alpha", "beta"]);
3393 }
3394
3395 #[test]
3396 fn test_action_diversity_zero_for_empty() {
3397 let session = make_session(vec![], 0);
3398 assert!((session.action_diversity() - 0.0).abs() < 1e-9);
3399 }
3400
3401 #[test]
3402 fn test_action_diversity_one_when_all_actions_unique() {
3403 let steps = vec![
3404 make_step("t", "a", "o"),
3405 make_step("t", "b", "o"),
3406 make_step("t", "c", "o"),
3407 ];
3408 let session = make_session(steps, 0);
3409 assert!((session.action_diversity() - 1.0).abs() < 1e-9);
3410 }
3411
3412 #[test]
3413 fn test_action_diversity_fraction_when_repeated() {
3414 let steps = vec![
3415 make_step("t", "x", "o"),
3416 make_step("t", "x", "o"),
3417 ];
3418 let session = make_session(steps, 0);
3419 assert!((session.action_diversity() - 0.5).abs() < 1e-9);
3420 }
3421
3422 #[test]
3423 fn test_has_checkpoint_errors_false_for_new_session() {
3424 let session = make_session(vec![], 0);
3425 assert!(!session.has_checkpoint_errors());
3426 }
3427
3428 #[test]
3429 fn test_has_checkpoint_errors_true_when_errors_present() {
3430 let mut session = make_session(vec![], 0);
3431 session.checkpoint_errors.push("err1".to_string());
3432 assert!(session.has_checkpoint_errors());
3433 }
3434
3435 #[test]
3436 fn test_graph_lookup_count_returns_raw_value() {
3437 let mut session = make_session(vec![make_step("t", "a", "o")], 0);
3438 session.graph_lookups = 7;
3439 assert_eq!(session.graph_lookup_count(), 7);
3440 }
3441
3442 #[test]
3443 fn test_memory_hit_rate_zero_for_empty_session() {
3444 let session = make_session(vec![], 0);
3445 assert!((session.memory_hit_rate() - 0.0).abs() < 1e-9);
3446 }
3447
3448 #[test]
3449 fn test_memory_hit_rate_correct_ratio() {
3450 let steps = vec![
3451 make_step("t", "a", "o"),
3452 make_step("t", "b", "o"),
3453 make_step("t", "c", "o"),
3454 make_step("t", "d", "o"),
3455 ];
3456 let mut session = make_session(steps, 0);
3457 session.memory_hits = 2;
3458 assert!((session.memory_hit_rate() - 0.5).abs() < 1e-9);
3459 }
3460
3461 #[test]
3462 fn test_total_memory_hits_returns_raw_value() {
3463 let mut session = make_session(vec![], 0);
3464 session.memory_hits = 13;
3465 assert_eq!(session.total_memory_hits(), 13);
3466 }
3467
3468 #[cfg(feature = "memory")]
3471 #[test]
3472 fn test_has_memory_false_without_memory() {
3473 let runtime = AgentRuntime::builder()
3474 .with_agent_config(simple_config())
3475 .build();
3476 assert!(!runtime.has_memory());
3477 }
3478
3479 #[cfg(feature = "memory")]
3480 #[test]
3481 fn test_has_memory_true_with_memory() {
3482 let runtime = AgentRuntime::builder()
3483 .with_agent_config(simple_config())
3484 .with_memory(EpisodicStore::new())
3485 .build();
3486 assert!(runtime.has_memory());
3487 }
3488
3489 #[cfg(feature = "graph")]
3490 #[test]
3491 fn test_has_graph_false_without_graph() {
3492 let runtime = AgentRuntime::builder()
3493 .with_agent_config(simple_config())
3494 .build();
3495 assert!(!runtime.has_graph());
3496 }
3497
3498 #[cfg(feature = "graph")]
3499 #[test]
3500 fn test_has_graph_true_with_graph() {
3501 let runtime = AgentRuntime::builder()
3502 .with_agent_config(simple_config())
3503 .with_graph(GraphStore::new())
3504 .build();
3505 assert!(runtime.has_graph());
3506 }
3507
3508 #[cfg(feature = "memory")]
3509 #[test]
3510 fn test_has_working_memory_false_without_working_memory() {
3511 let runtime = AgentRuntime::builder()
3512 .with_agent_config(simple_config())
3513 .build();
3514 assert!(!runtime.has_working_memory());
3515 }
3516
3517 #[cfg(feature = "memory")]
3518 #[test]
3519 fn test_has_working_memory_true_with_working_memory() {
3520 let runtime = AgentRuntime::builder()
3521 .with_agent_config(simple_config())
3522 .with_working_memory(WorkingMemory::new(10).unwrap())
3523 .build();
3524 assert!(runtime.has_working_memory());
3525 }
3526
3527 #[test]
3530 fn test_last_observation_returns_most_recent_nonempty() {
3531 let steps = vec![
3532 make_step("t1", "act", "first obs"),
3533 make_step("t2", "act", ""),
3534 make_step("t3", "act", "last obs"),
3535 ];
3536 let session = make_session(steps, 0);
3537 assert_eq!(session.last_observation(), Some("last obs"));
3538 }
3539
3540 #[test]
3541 fn test_last_observation_skips_empty_steps() {
3542 let steps = vec![
3543 make_step("t1", "act", "only obs"),
3544 make_step("t2", "act", ""),
3545 ];
3546 let session = make_session(steps, 0);
3547 assert_eq!(session.last_observation(), Some("only obs"));
3548 }
3549
3550 #[test]
3551 fn test_last_observation_none_for_empty_session() {
3552 let session = make_session(vec![], 0);
3553 assert!(session.last_observation().is_none());
3554 }
3555
3556 #[test]
3557 fn test_thought_count_counts_nonempty_thoughts() {
3558 let steps = vec![
3559 make_step("think", "act", "obs"),
3560 make_step("", "act", "obs"),
3561 make_step("think again", "act", "obs"),
3562 ];
3563 let session = make_session(steps, 0);
3564 assert_eq!(session.thought_count(), 2);
3565 }
3566
3567 #[test]
3568 fn test_thought_count_zero_for_empty_session() {
3569 let session = make_session(vec![], 0);
3570 assert_eq!(session.thought_count(), 0);
3571 }
3572
3573 #[test]
3574 fn test_observation_rate_correct_fraction() {
3575 let steps = vec![
3576 make_step("t", "a", "obs"),
3577 make_step("t", "a", ""),
3578 make_step("t", "a", "obs"),
3579 make_step("t", "a", ""),
3580 ];
3581 let session = make_session(steps, 0);
3582 assert!((session.observation_rate() - 0.5).abs() < 1e-9);
3583 }
3584
3585 #[test]
3586 fn test_observation_rate_zero_for_empty_session() {
3587 let session = make_session(vec![], 0);
3588 assert!((session.observation_rate() - 0.0).abs() < 1e-9);
3589 }
3590
3591 #[test]
3594 fn test_action_repetition_rate_zero_for_empty_session() {
3595 let session = make_session(vec![], 0);
3596 assert!((session.action_repetition_rate() - 0.0).abs() < 1e-9);
3597 }
3598
3599 #[test]
3600 fn test_action_repetition_rate_zero_for_single_step() {
3601 let session = make_session(vec![make_step("t", "search", "r")], 0);
3602 assert!((session.action_repetition_rate() - 0.0).abs() < 1e-9);
3603 }
3604
3605 #[test]
3606 fn test_action_repetition_rate_one_when_all_same() {
3607 let steps = vec![
3608 make_step("t", "search", "r"),
3609 make_step("t", "search", "r"),
3610 make_step("t", "search", "r"),
3611 ];
3612 let session = make_session(steps, 0);
3613 assert!((session.action_repetition_rate() - 1.0).abs() < 1e-9);
3614 }
3615
3616 #[test]
3617 fn test_action_repetition_rate_partial_repeats() {
3618 let steps = vec![
3620 make_step("t", "search", "r"),
3621 make_step("t", "search", "r"),
3622 make_step("t", "calc", "r"),
3623 ];
3624 let session = make_session(steps, 0);
3625 assert!((session.action_repetition_rate() - 0.5).abs() < 1e-9);
3626 }
3627
3628 #[test]
3629 fn test_max_consecutive_failures_zero_for_no_errors() {
3630 let steps = vec![make_step("t", "a", "ok"), make_step("t", "b", "done")];
3631 let session = make_session(steps, 0);
3632 assert_eq!(session.max_consecutive_failures(), 0);
3633 }
3634
3635 #[test]
3636 fn test_max_consecutive_failures_counts_run() {
3637 let steps = vec![
3638 make_step("t", "a", "ok"),
3639 make_step("t", "b", r#"{"error":"x"}"#),
3640 make_step("t", "c", r#"{"error":"y"}"#),
3641 make_step("t", "d", "ok"),
3642 ];
3643 let session = make_session(steps, 0);
3644 assert_eq!(session.max_consecutive_failures(), 2);
3645 }
3646
3647 #[test]
3648 fn test_avg_thought_length_zero_for_empty_session() {
3649 let session = make_session(vec![], 0);
3650 assert!((session.avg_thought_length() - 0.0).abs() < 1e-9);
3651 }
3652
3653 #[test]
3654 fn test_avg_thought_length_excludes_empty_thoughts() {
3655 let steps = vec![
3656 make_step("hello", "a", "r"), make_step("", "b", "r"), make_step("hi", "c", "r"), ];
3660 let session = make_session(steps, 0);
3662 assert!((session.avg_thought_length() - 3.5).abs() < 1e-9);
3663 }
3664
3665 #[test]
3668 fn test_last_n_observations_empty_session() {
3669 let session = make_session(vec![], 0);
3670 assert!(session.last_n_observations(3).is_empty());
3671 }
3672
3673 #[test]
3674 fn test_last_n_observations_returns_last_n_nonempty() {
3675 let steps = vec![
3676 make_step("t", "a", "obs1"),
3677 make_step("t", "b", ""), make_step("t", "c", "obs2"),
3679 make_step("t", "d", "obs3"),
3680 ];
3681 let session = make_session(steps, 0);
3682 let last2 = session.last_n_observations(2);
3683 assert_eq!(last2, vec!["obs2", "obs3"]);
3684 }
3685
3686 #[test]
3687 fn test_last_n_observations_returns_all_when_fewer_than_n() {
3688 let steps = vec![make_step("t", "a", "only")];
3689 let session = make_session(steps, 0);
3690 assert_eq!(session.last_n_observations(5), vec!["only"]);
3691 }
3692
3693 #[test]
3694 fn test_actions_in_window_empty_session() {
3695 let session = make_session(vec![], 0);
3696 assert!(session.actions_in_window(3).is_empty());
3697 }
3698
3699 #[test]
3700 fn test_actions_in_window_returns_last_n_steps() {
3701 let steps = vec![
3702 make_step("t", "alpha", "r"),
3703 make_step("t", "beta", "r"),
3704 make_step("t", "gamma", "r"),
3705 ];
3706 let session = make_session(steps, 0);
3707 let window = session.actions_in_window(2);
3708 assert_eq!(window, vec!["beta", "gamma"]);
3709 }
3710
3711 #[test]
3712 fn test_actions_in_window_all_when_fewer_than_n() {
3713 let steps = vec![make_step("t", "solo", "r")];
3714 let session = make_session(steps, 0);
3715 assert_eq!(session.actions_in_window(10), vec!["solo"]);
3716 }
3717
3718 #[test]
3721 fn test_observation_at_returns_correct_observation() {
3722 let steps = vec![
3723 make_step("t1", "a1", "obs-zero"),
3724 make_step("t2", "a2", "obs-one"),
3725 ];
3726 let session = make_session(steps, 0);
3727 assert_eq!(session.observation_at(0), Some("obs-zero"));
3728 assert_eq!(session.observation_at(1), Some("obs-one"));
3729 }
3730
3731 #[test]
3732 fn test_observation_at_returns_none_out_of_bounds() {
3733 let session = make_session(vec![], 0);
3734 assert!(session.observation_at(0).is_none());
3735 }
3736
3737 #[test]
3738 fn test_action_at_returns_correct_action() {
3739 let steps = vec![
3740 make_step("t1", "first-action", "obs"),
3741 make_step("t2", "second-action", "obs"),
3742 ];
3743 let session = make_session(steps, 0);
3744 assert_eq!(session.action_at(0), Some("first-action"));
3745 assert_eq!(session.action_at(1), Some("second-action"));
3746 }
3747
3748 #[test]
3749 fn test_action_at_returns_none_out_of_bounds() {
3750 let session = make_session(vec![], 0);
3751 assert!(session.action_at(5).is_none());
3752 }
3753
3754 #[test]
3757 fn test_has_graph_lookups_false_when_zero() {
3758 let session = make_session(vec![], 0);
3759 assert!(!session.has_graph_lookups());
3760 }
3761
3762 #[test]
3763 fn test_has_graph_lookups_true_when_positive() {
3764 let session = AgentSession {
3765 session_id: "s".into(),
3766 agent_id: AgentId::new("a"),
3767 steps: vec![],
3768 memory_hits: 0,
3769 graph_lookups: 5,
3770 duration_ms: 0,
3771 checkpoint_errors: vec![],
3772 };
3773 assert!(session.has_graph_lookups());
3774 }
3775
3776 #[test]
3777 fn test_consecutive_same_action_at_end_empty_session() {
3778 let session = make_session(vec![], 0);
3779 assert_eq!(session.consecutive_same_action_at_end(), 0);
3780 }
3781
3782 #[test]
3783 fn test_consecutive_same_action_at_end_single_step() {
3784 let steps = vec![make_step("t", "act", "obs")];
3785 let session = make_session(steps, 0);
3786 assert_eq!(session.consecutive_same_action_at_end(), 0);
3787 }
3788
3789 #[test]
3790 fn test_consecutive_same_action_at_end_two_same_at_end() {
3791 let steps = vec![
3792 make_step("t", "other", "obs"),
3793 make_step("t", "repeat", "obs"),
3794 make_step("t", "repeat", "obs"),
3795 ];
3796 let session = make_session(steps, 0);
3797 assert_eq!(session.consecutive_same_action_at_end(), 1);
3798 }
3799
3800 #[test]
3801 fn test_consecutive_same_action_at_end_all_same() {
3802 let steps = vec![
3803 make_step("t", "same", "obs"),
3804 make_step("t", "same", "obs"),
3805 make_step("t", "same", "obs"),
3806 ];
3807 let session = make_session(steps, 0);
3808 assert_eq!(session.consecutive_same_action_at_end(), 2);
3809 }
3810
3811 #[test]
3814 fn test_failure_rate_zero_for_empty_session() {
3815 let session = make_session(vec![], 0);
3816 assert!((session.failure_rate() - 0.0).abs() < 1e-9);
3817 }
3818
3819 #[test]
3820 fn test_failure_rate_zero_when_no_failures() {
3821 let steps = vec![
3822 make_step("t", "lookup", "ok"),
3823 make_step("t", "search", "ok"),
3824 ];
3825 let session = make_session(steps, 0);
3826 assert!((session.failure_rate() - 0.0).abs() < 1e-9);
3827 }
3828
3829 #[test]
3830 fn test_unique_action_count_zero_for_empty_session() {
3831 let session = make_session(vec![], 0);
3832 assert_eq!(session.unique_action_count(), 0);
3833 }
3834
3835 #[test]
3836 fn test_unique_action_count_counts_distinct_actions() {
3837 let steps = vec![
3838 make_step("t", "search", "r"),
3839 make_step("t", "lookup", "r"),
3840 make_step("t", "search", "r"), ];
3842 let session = make_session(steps, 0);
3843 assert_eq!(session.unique_action_count(), 2);
3844 }
3845
3846 #[test]
3849 fn test_total_thought_length_zero_for_empty_session() {
3850 let session = make_session(vec![], 0);
3851 assert_eq!(session.total_thought_length(), 0);
3852 }
3853
3854 #[test]
3855 fn test_total_thought_length_sums_all_thoughts() {
3856 let steps = vec![
3857 make_step("hi", "a", "r"), make_step("hello", "b", "r"), ];
3860 let session = make_session(steps, 0);
3861 assert_eq!(session.total_thought_length(), 7);
3862 }
3863
3864 #[test]
3865 fn test_longest_observation_none_for_empty_session() {
3866 let session = make_session(vec![], 0);
3867 assert!(session.longest_observation().is_none());
3868 }
3869
3870 #[test]
3871 fn test_longest_observation_returns_longest() {
3872 let steps = vec![
3873 make_step("t", "a", "short"),
3874 make_step("t", "b", "a much longer observation"),
3875 ];
3876 let session = make_session(steps, 0);
3877 assert_eq!(session.longest_observation(), Some("a much longer observation"));
3878 }
3879
3880 #[test]
3883 fn test_steps_with_empty_observations_zero_when_all_filled() {
3884 let steps = vec![make_step("t", "a", "obs"), make_step("t", "b", "obs2")];
3885 let session = make_session(steps, 0);
3886 assert_eq!(session.steps_with_empty_observations(), 0);
3887 }
3888
3889 #[test]
3890 fn test_steps_with_empty_observations_counts_empty_ones() {
3891 let steps = vec![
3892 make_step("t", "a", ""), make_step("t", "b", "ok"),
3894 make_step("t", "c", ""), ];
3896 let session = make_session(steps, 0);
3897 assert_eq!(session.steps_with_empty_observations(), 2);
3898 }
3899
3900 #[test]
3901 fn test_min_thought_length_zero_for_empty_session() {
3902 let session = make_session(vec![], 0);
3903 assert_eq!(session.min_thought_length(), 0);
3904 }
3905
3906 #[test]
3907 fn test_min_thought_length_returns_shortest_non_empty() {
3908 let steps = vec![
3909 make_step("hi", "a", "r"), make_step("hello", "b", "r"), make_step("", "c", "r"), ];
3913 let session = make_session(steps, 0);
3914 assert_eq!(session.min_thought_length(), 2);
3915 }
3916
3917 #[test]
3920 fn test_observation_lengths_empty_for_empty_session() {
3921 let session = make_session(vec![], 0);
3922 assert!(session.observation_lengths().is_empty());
3923 }
3924
3925 #[test]
3926 fn test_observation_lengths_returns_lengths_in_order() {
3927 let steps = vec![
3928 make_step("t", "a", "hi"), make_step("t", "b", "hello"), ];
3931 let session = make_session(steps, 0);
3932 assert_eq!(session.observation_lengths(), vec![2, 5]);
3933 }
3934
3935 #[test]
3936 fn test_avg_observation_length_zero_for_empty_session() {
3937 let session = make_session(vec![], 0);
3938 assert!((session.avg_observation_length() - 0.0).abs() < 1e-9);
3939 }
3940
3941 #[test]
3942 fn test_avg_observation_length_correct_mean() {
3943 let steps = vec![
3944 make_step("t", "a", "hi"), make_step("t", "b", "hello"), ];
3947 let session = make_session(steps, 0);
3948 assert!((session.avg_observation_length() - 3.5).abs() < 1e-9);
3950 }
3951
3952 #[test]
3953 fn test_duration_secs_converts_ms_to_seconds() {
3954 let session = make_session(vec![], 7000);
3955 assert_eq!(session.duration_secs(), 7);
3956 }
3957
3958 #[test]
3959 fn test_steps_above_thought_length_counts_qualifying_steps() {
3960 let steps = vec![
3961 make_step("hi", "a", "obs"),
3962 make_step("a longer thought here", "b", "obs"),
3963 make_step("medium thought", "c", "obs"),
3964 ];
3965 let session = make_session(steps, 0);
3966 assert_eq!(session.steps_above_thought_length(5), 2);
3968 }
3969
3970 #[test]
3971 fn test_has_final_answer_true_when_step_has_final_answer_action() {
3972 let steps = vec![
3973 make_step("think", "search", "result"),
3974 make_step("done", "FINAL_ANSWER: 42", ""),
3975 ];
3976 let session = make_session(steps, 0);
3977 assert!(session.has_final_answer());
3978 }
3979
3980 #[test]
3981 fn test_has_final_answer_false_when_no_final_answer_step() {
3982 let steps = vec![make_step("think", "search", "result")];
3983 let session = make_session(steps, 0);
3984 assert!(!session.has_final_answer());
3985 }
3986
3987 #[test]
3988 fn test_avg_action_length_correct_mean() {
3989 let steps = vec![
3990 make_step("t", "ab", "o"), make_step("t", "abcd", "o"), ];
3993 let session = make_session(steps, 0);
3994 assert!((session.avg_action_length() - 3.0).abs() < 1e-9);
3995 }
3996
3997 #[test]
3998 fn test_avg_action_length_empty_returns_zero() {
3999 let session = make_session(vec![], 0);
4000 assert_eq!(session.avg_action_length(), 0.0);
4001 }
4002
4003 #[test]
4004 fn test_thought_lengths_returns_lengths_in_order() {
4005 let steps = vec![
4006 make_step("hi", "a", "o"),
4007 make_step("hello", "b", "o"),
4008 ];
4009 let session = make_session(steps, 0);
4010 assert_eq!(session.thought_lengths(), vec![2, 5]);
4011 }
4012
4013 #[test]
4014 fn test_most_common_action_returns_most_frequent() {
4015 let steps = vec![
4016 make_step("t", "search", "o"),
4017 make_step("t", "search", "o"),
4018 make_step("t", "other", "o"),
4019 ];
4020 let session = make_session(steps, 0);
4021 assert_eq!(session.most_common_action(), Some("search"));
4022 }
4023
4024 #[test]
4025 fn test_most_common_action_none_for_empty_session() {
4026 let session = make_session(vec![], 0);
4027 assert!(session.most_common_action().is_none());
4028 }
4029
4030 #[test]
4031 fn test_count_steps_with_action_counts_exact_matches() {
4032 let steps = vec![
4033 make_step("t", "search", "o"),
4034 make_step("t", "search", "o"),
4035 make_step("t", "other", "o"),
4036 ];
4037 let session = make_session(steps, 0);
4038 assert_eq!(session.count_steps_with_action("search"), 2);
4039 assert_eq!(session.count_steps_with_action("other"), 1);
4040 assert_eq!(session.count_steps_with_action("missing"), 0);
4041 }
4042
4043 #[test]
4044 fn test_thought_contains_count_counts_matching_steps() {
4045 let steps = vec![
4046 make_step("search for rust", "a", "o"),
4047 make_step("think about python", "b", "o"),
4048 make_step("rust is great", "c", "o"),
4049 ];
4050 let session = make_session(steps, 0);
4051 assert_eq!(session.thought_contains_count("rust"), 2);
4052 assert_eq!(session.thought_contains_count("python"), 1);
4053 assert_eq!(session.thought_contains_count("java"), 0);
4054 }
4055
4056 #[test]
4057 fn test_count_nonempty_thoughts_counts_steps_with_thoughts() {
4058 let steps = vec![
4059 make_step("hello", "a", "o"),
4060 make_step("", "b", "o"),
4061 make_step("world", "c", "o"),
4062 ];
4063 let session = make_session(steps, 0);
4064 assert_eq!(session.count_nonempty_thoughts(), 2);
4065 }
4066
4067 #[test]
4068 fn test_observation_contains_count_counts_matching_observations() {
4069 let steps = vec![
4070 make_step("t", "a", "result: success"),
4071 make_step("t", "b", "result: failure"),
4072 make_step("t", "c", "no match here"),
4073 ];
4074 let session = make_session(steps, 0);
4075 assert_eq!(session.observation_contains_count("result"), 2);
4076 assert_eq!(session.observation_contains_count("success"), 1);
4077 }
4078
4079 #[test]
4082 fn test_action_lengths_returns_byte_lengths_in_order() {
4083 let steps = vec![
4084 make_step("t", "ab", "o"),
4085 make_step("t", "hello", "o"),
4086 make_step("t", "", "o"),
4087 ];
4088 let session = make_session(steps, 0);
4089 assert_eq!(session.action_lengths(), vec![2, 5, 0]);
4090 }
4091
4092 #[test]
4093 fn test_action_lengths_empty_session_returns_empty_vec() {
4094 let session = make_session(vec![], 0);
4095 assert!(session.action_lengths().is_empty());
4096 }
4097
4098 #[test]
4099 fn test_step_success_count_excludes_failed_steps() {
4100 let steps = vec![
4101 make_step("t", "a", "ok"),
4102 make_step("t", "b", "{\"error\": \"timeout\"}"),
4103 make_step("t", "c", "ok"),
4104 ];
4105 let session = make_session(steps, 0);
4106 assert_eq!(session.step_success_count(), 2);
4107 }
4108
4109 #[test]
4110 fn test_step_success_count_all_success_when_no_failures() {
4111 let steps = vec![make_step("t", "a", "ok"), make_step("t", "b", "ok")];
4112 let session = make_session(steps, 0);
4113 assert_eq!(session.step_success_count(), 2);
4114 }
4115
4116 #[test]
4119 fn test_longest_thought_returns_step_with_most_bytes() {
4120 let steps = vec![
4121 make_step("hi", "a", "o"),
4122 make_step("hello world", "b", "o"),
4123 make_step("hey", "c", "o"),
4124 ];
4125 let session = make_session(steps, 0);
4126 assert_eq!(session.longest_thought(), Some("hello world"));
4127 }
4128
4129 #[test]
4130 fn test_longest_thought_returns_none_for_empty_session() {
4131 let session = make_session(vec![], 0);
4132 assert!(session.longest_thought().is_none());
4133 }
4134
4135 #[test]
4136 fn test_shortest_action_returns_step_with_fewest_bytes() {
4137 let steps = vec![
4138 make_step("t", "search", "o"),
4139 make_step("t", "go", "o"),
4140 make_step("t", "lookup", "o"),
4141 ];
4142 let session = make_session(steps, 0);
4143 assert_eq!(session.shortest_action(), Some("go"));
4144 }
4145
4146 #[test]
4147 fn test_shortest_action_returns_none_for_empty_session() {
4148 let session = make_session(vec![], 0);
4149 assert!(session.shortest_action().is_none());
4150 }
4151
4152 #[test]
4155 fn test_first_step_action_returns_action_of_first_step() {
4156 let steps = vec![
4157 make_step("t", "first", "o"),
4158 make_step("t", "second", "o"),
4159 ];
4160 let session = make_session(steps, 0);
4161 assert_eq!(session.first_step_action(), Some("first"));
4162 }
4163
4164 #[test]
4165 fn test_first_step_action_returns_none_for_empty_session() {
4166 let session = make_session(vec![], 0);
4167 assert!(session.first_step_action().is_none());
4168 }
4169
4170 #[test]
4171 fn test_last_step_action_returns_action_of_last_step() {
4172 let steps = vec![
4173 make_step("t", "first", "o"),
4174 make_step("t", "last_one", "o"),
4175 ];
4176 let session = make_session(steps, 0);
4177 assert_eq!(session.last_step_action(), Some("last_one"));
4178 }
4179
4180 #[test]
4181 fn test_last_step_action_returns_none_for_empty_session() {
4182 let session = make_session(vec![], 0);
4183 assert!(session.last_step_action().is_none());
4184 }
4185
4186 #[test]
4189 fn test_total_thought_bytes_sums_all_thought_lengths() {
4190 let steps = vec![
4191 make_step("hi", "a", "o"), make_step("hello", "b", "o"), ];
4194 let session = make_session(steps, 0);
4195 assert_eq!(session.total_thought_bytes(), 7);
4196 }
4197
4198 #[test]
4199 fn test_total_observation_bytes_sums_all_observation_lengths() {
4200 let steps = vec![
4201 make_step("t", "a", "ok"), make_step("t", "b", "done!"), ];
4204 let session = make_session(steps, 0);
4205 assert_eq!(session.total_observation_bytes(), 7);
4206 }
4207}