use crate::agent::{AgentConfig, ReActLoop, ReActStep, ToolSpec};
use crate::error::AgentRuntimeError;
use crate::metrics::RuntimeMetrics;
use crate::types::AgentId;
#[cfg(feature = "memory")]
use crate::memory::{EpisodicStore, WorkingMemory};
use serde::{Deserialize, Serialize};
use std::fmt::Write as FmtWrite;
use std::marker::PhantomData;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Instant;
#[cfg(feature = "graph")]
use crate::graph::GraphStore;
#[cfg(feature = "orchestrator")]
use crate::orchestrator::BackpressureGuard;
pub struct NeedsConfig;
pub struct HasConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSession {
pub session_id: String,
pub agent_id: AgentId,
pub steps: Vec<ReActStep>,
pub memory_hits: usize,
pub graph_lookups: usize,
pub duration_ms: u64,
#[serde(default)]
pub checkpoint_errors: Vec<String>,
}
impl AgentSession {
pub fn step_count(&self) -> usize {
self.steps.len()
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub fn final_answer(&self) -> Option<String> {
let last = self.steps.last()?;
let upper = last.action.trim().to_ascii_uppercase();
if upper.starts_with("FINAL_ANSWER") {
let answer = last.action.trim()["FINAL_ANSWER".len()..].trim().to_owned();
Some(answer)
} else {
None
}
}
pub fn is_successful(&self) -> bool {
self.final_answer().is_some()
}
pub fn elapsed(&self) -> std::time::Duration {
std::time::Duration::from_millis(self.duration_ms)
}
pub fn tool_calls_made(&self) -> usize {
self.steps
.iter()
.filter(|s| {
!s.action.trim().to_ascii_uppercase().starts_with("FINAL_ANSWER")
&& !s.action.trim().is_empty()
})
.count()
}
pub fn total_step_duration_ms(&self) -> u64 {
self.steps.iter().map(|s| s.step_duration_ms).sum()
}
pub fn average_step_duration_ms(&self) -> u64 {
if self.steps.is_empty() {
return 0;
}
self.total_step_duration_ms() / self.steps.len() as u64
}
pub fn slowest_step(&self) -> Option<&ReActStep> {
self.steps.iter().max_by_key(|s| s.step_duration_ms)
}
pub fn fastest_step(&self) -> Option<&ReActStep> {
self.steps.iter().min_by_key(|s| s.step_duration_ms)
}
pub fn filter_tool_call_steps(&self) -> Vec<&ReActStep> {
self.steps.iter().filter(|s| s.is_tool_call()).collect()
}
pub fn slowest_step_index(&self) -> Option<usize> {
self.steps
.iter()
.enumerate()
.max_by_key(|(_, s)| s.step_duration_ms)
.map(|(i, _)| i)
}
pub fn fastest_step_index(&self) -> Option<usize> {
self.steps
.iter()
.enumerate()
.min_by_key(|(_, s)| s.step_duration_ms)
.map(|(i, _)| i)
}
pub fn last_step(&self) -> Option<&ReActStep> {
self.steps.last()
}
pub fn first_step(&self) -> Option<&ReActStep> {
self.steps.first()
}
pub fn step_at(&self, idx: usize) -> Option<&ReActStep> {
self.steps.get(idx)
}
pub fn observation_at(&self, idx: usize) -> Option<&str> {
self.steps.get(idx).map(|s| s.observation.as_str())
}
pub fn action_at(&self, idx: usize) -> Option<&str> {
self.steps.get(idx).map(|s| s.action.as_str())
}
pub fn observations_matching(&self, pattern: &str) -> Vec<&ReActStep> {
let lower = pattern.to_ascii_lowercase();
self.steps
.iter()
.filter(|s| s.observation.to_ascii_lowercase().contains(&lower))
.collect()
}
pub fn thoughts_containing(&self, pattern: &str) -> Vec<&ReActStep> {
let lower = pattern.to_ascii_lowercase();
self.steps
.iter()
.filter(|s| s.thought.to_ascii_lowercase().contains(&lower))
.collect()
}
pub fn has_action(&self, action_name: &str) -> bool {
self.steps.iter().any(|s| s.action == action_name)
}
pub fn thought_at(&self, idx: usize) -> Option<&str> {
self.steps.get(idx).map(|s| s.thought.as_str())
}
pub fn step_count_for_action(&self, action_name: &str) -> usize {
self.steps.iter().filter(|s| s.action == action_name).count()
}
pub fn observations(&self) -> Vec<&str> {
self.steps.iter().map(|s| s.observation.as_str()).collect()
}
pub fn observation_count(&self) -> usize {
self.steps.iter().filter(|s| !s.observation.is_empty()).count()
}
pub fn last_n_observations(&self, n: usize) -> Vec<&str> {
let all: Vec<&str> = self
.steps
.iter()
.filter(|s| !s.observation.is_empty())
.map(|s| s.observation.as_str())
.collect();
let skip = all.len().saturating_sub(n);
all[skip..].to_vec()
}
pub fn actions_in_window(&self, n: usize) -> Vec<&str> {
let skip = self.steps.len().saturating_sub(n);
self.steps[skip..]
.iter()
.map(|s| s.action.as_str())
.collect()
}
pub fn steps_without_observation(&self) -> usize {
self.steps.iter().filter(|s| s.observation.is_empty()).count()
}
pub fn first_thought(&self) -> Option<&str> {
self.steps.first().map(|s| s.thought.as_str())
}
pub fn last_thought(&self) -> Option<&str> {
self.steps.last().map(|s| s.thought.as_str())
}
pub fn first_action(&self) -> Option<&str> {
self.steps.first().map(|s| s.action.as_str())
}
pub fn last_action(&self) -> Option<&str> {
self.steps.last().map(|s| s.action.as_str())
}
pub fn last_n_steps(&self, n: usize) -> &[crate::agent::ReActStep] {
let len = self.steps.len();
let start = len.saturating_sub(n);
&self.steps[start..]
}
pub fn first_n_steps(&self, n: usize) -> &[crate::agent::ReActStep] {
let end = n.min(self.steps.len());
&self.steps[..end]
}
pub fn steps_with_tool<'a>(&'a self, tool_name: &str) -> Vec<&'a crate::agent::ReActStep> {
self.steps
.iter()
.filter(|s| s.action.contains(tool_name) && !s.is_final_answer())
.collect()
}
pub fn total_chars(&self) -> usize {
self.steps
.iter()
.map(|s| s.thought.len() + s.action.len() + s.observation.len())
.sum()
}
pub fn step_durations_ms(&self) -> Vec<u64> {
self.steps.iter().map(|s| s.step_duration_ms).collect()
}
pub fn total_latency_ms(&self) -> u64 {
self.steps.iter().map(|s| s.step_duration_ms).sum()
}
pub fn avg_step_duration_ms(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
self.total_latency_ms() as f64 / self.steps.len() as f64
}
pub fn longest_step(&self) -> Option<&crate::agent::ReActStep> {
self.steps.iter().max_by_key(|s| s.step_duration_ms)
}
pub fn shortest_step(&self) -> Option<&crate::agent::ReActStep> {
self.steps.iter().min_by_key(|s| s.step_duration_ms)
}
pub fn action_sequence(&self) -> Vec<String> {
self.steps.iter().map(|s| s.action.clone()).collect()
}
pub fn unique_tools_used(&self) -> Vec<String> {
let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();
for step in &self.steps {
let action = step.action.trim();
if action.is_empty() || action.to_ascii_uppercase().starts_with("FINAL_ANSWER") {
continue;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(action) {
if let Some(name) = v.get("tool").and_then(|n| n.as_str()) {
names.insert(name.to_owned());
continue;
}
}
names.insert(action.to_owned());
}
let mut sorted: Vec<String> = names.into_iter().collect();
sorted.sort_unstable();
sorted
}
pub fn all_thoughts(&self) -> Vec<&str> {
self.steps.iter().map(|s| s.thought.as_str()).collect()
}
pub fn all_actions(&self) -> Vec<&str> {
self.steps.iter().map(|s| s.action.as_str()).collect()
}
pub fn all_observations(&self) -> Vec<&str> {
self.steps.iter().map(|s| s.observation.as_str()).collect()
}
pub fn failed_steps(&self) -> Vec<&crate::agent::ReActStep> {
self.steps
.iter()
.filter(|s| {
let obs = s.observation.trim();
obs.starts_with("{\"error\"")
|| obs.to_ascii_lowercase().contains("\"error\"")
})
.collect()
}
pub fn failed_tool_call_count(&self) -> usize {
self.steps
.iter()
.filter(|s| {
let obs = s.observation.trim();
obs.starts_with("{\"error\"")
|| obs.to_ascii_lowercase().contains("\"error\"")
})
.count()
}
pub fn action_counts(&self) -> std::collections::HashMap<String, usize> {
let mut counts = std::collections::HashMap::new();
for step in &self.steps {
*counts.entry(step.action.clone()).or_insert(0) += 1;
}
counts
}
pub fn unique_actions(&self) -> Vec<String> {
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for step in &self.steps {
seen.insert(step.action.clone());
}
seen.into_iter().collect()
}
pub fn has_duplicate_actions(&self) -> bool {
let mut seen = std::collections::HashSet::new();
self.steps.iter().any(|s| !seen.insert(s.action.as_str()))
}
pub fn step_indices_with_tool(&self, tool_name: &str) -> Vec<usize> {
self.steps
.iter()
.enumerate()
.filter(|(_, s)| !s.is_final_answer() && s.action.contains(tool_name))
.map(|(i, _)| i)
.collect()
}
pub fn most_used_action(&self) -> Option<String> {
let counts = self.action_counts();
counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(name, _)| name)
}
pub fn last_observation(&self) -> Option<&str> {
self.steps
.iter()
.rev()
.find(|s| !s.observation.is_empty())
.map(|s| s.observation.as_str())
}
pub fn thought_count(&self) -> usize {
self.steps.iter().filter(|s| !s.thought.is_empty()).count()
}
pub fn observation_rate(&self) -> f64 {
let n = self.steps.len();
if n == 0 {
return 0.0;
}
let with_obs = self
.steps
.iter()
.filter(|s| !s.observation.is_empty())
.count();
with_obs as f64 / n as f64
}
pub fn has_graph_lookups(&self) -> bool {
self.graph_lookups > 0
}
pub fn consecutive_same_action_at_end(&self) -> usize {
let n = self.steps.len();
if n == 0 {
return 0;
}
let last_action = &self.steps[n - 1].action;
self.steps
.iter()
.rev()
.take_while(|s| &s.action == last_action)
.count()
.saturating_sub(1) }
pub fn action_repetition_rate(&self) -> f64 {
let n = self.steps.len();
if n < 2 {
return 0.0;
}
let repeats = self
.steps
.windows(2)
.filter(|w| w[0].action == w[1].action)
.count();
repeats as f64 / (n - 1) as f64
}
pub fn max_consecutive_failures(&self) -> usize {
let mut max_run = 0usize;
let mut current = 0usize;
for step in &self.steps {
let obs = step.observation.trim();
if obs.starts_with("{\"error\"") || obs.to_ascii_lowercase().contains("\"error\"") {
current += 1;
if current > max_run {
max_run = current;
}
} else {
current = 0;
}
}
max_run
}
pub fn avg_thought_length(&self) -> f64 {
let thoughts: Vec<_> = self
.steps
.iter()
.filter(|s| !s.thought.is_empty())
.collect();
if thoughts.is_empty() {
return 0.0;
}
let total: usize = thoughts.iter().map(|s| s.thought.len()).sum();
total as f64 / thoughts.len() as f64
}
pub fn graph_lookup_rate(&self) -> f64 {
let steps = self.steps.len();
if steps == 0 {
return 0.0;
}
self.graph_lookups as f64 / steps as f64
}
pub fn has_checkpoint_errors(&self) -> bool {
!self.checkpoint_errors.is_empty()
}
pub fn checkpoint_error_count(&self) -> usize {
self.checkpoint_errors.len()
}
pub fn graph_lookup_count(&self) -> usize {
self.graph_lookups
}
pub fn memory_hit_rate(&self) -> f64 {
let steps = self.steps.len();
if steps == 0 {
return 0.0;
}
self.memory_hits as f64 / steps as f64
}
pub fn total_memory_hits(&self) -> usize {
self.memory_hits
}
pub fn throughput_steps_per_sec(&self) -> f64 {
if self.duration_ms == 0 {
return 0.0;
}
self.steps.len() as f64 / (self.duration_ms as f64 / 1000.0)
}
pub fn duration_secs(&self) -> u64 {
self.duration_ms / 1000
}
pub fn steps_above_thought_length(&self, threshold: usize) -> usize {
self.steps.iter().filter(|s| s.thought.len() > threshold).count()
}
pub fn has_final_answer(&self) -> bool {
self.steps
.iter()
.any(|s| s.action.to_ascii_uppercase().starts_with("FINAL_ANSWER"))
}
pub fn avg_action_length(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.action.len()).sum();
total as f64 / self.steps.len() as f64
}
pub fn has_tool_failures(&self) -> bool {
self.failed_tool_call_count() > 0
}
pub fn tool_call_rate(&self) -> f64 {
let total = self.steps.len();
if total == 0 {
return 0.0;
}
self.tool_calls_made() as f64 / total as f64
}
pub fn step_success_rate(&self) -> f64 {
let total = self.steps.len();
if total == 0 {
return 1.0;
}
1.0 - (self.failed_tool_call_count() as f64 / total as f64)
}
pub fn action_diversity(&self) -> f64 {
let total = self.steps.len();
if total == 0 {
return 0.0;
}
let unique: std::collections::HashSet<&str> =
self.steps.iter().map(|s| s.action.as_str()).collect();
unique.len() as f64 / total as f64
}
pub fn total_thought_length(&self) -> usize {
self.steps.iter().map(|s| s.thought.len()).sum()
}
pub fn steps_with_empty_observations(&self) -> usize {
self.steps.iter().filter(|s| s.observation.is_empty()).count()
}
pub fn observation_lengths(&self) -> Vec<usize> {
self.steps.iter().map(|s| s.observation.len()).collect()
}
pub fn avg_observation_length(&self) -> f64 {
let n = self.steps.len();
if n == 0 {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.observation.len()).sum();
total as f64 / n as f64
}
pub fn min_thought_length(&self) -> usize {
self.steps
.iter()
.filter(|s| !s.thought.is_empty())
.map(|s| s.thought.len())
.min()
.unwrap_or(0)
}
pub fn longest_observation(&self) -> Option<&str> {
self.steps
.iter()
.max_by_key(|s| s.observation.len())
.map(|s| s.observation.as_str())
}
pub fn thought_lengths(&self) -> Vec<usize> {
self.steps.iter().map(|s| s.thought.len()).collect()
}
pub fn most_common_action(&self) -> Option<&str> {
if self.steps.is_empty() {
return None;
}
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for s in &self.steps {
*counts.entry(s.action.as_str()).or_insert(0) += 1;
}
counts.into_iter().max_by_key(|(_, c)| *c).map(|(a, _)| a)
}
pub fn action_lengths(&self) -> Vec<usize> {
self.steps.iter().map(|s| s.action.len()).collect()
}
pub fn step_success_count(&self) -> usize {
self.steps.len() - self.failed_tool_call_count()
}
pub fn longest_thought(&self) -> Option<&str> {
self.steps
.iter()
.max_by_key(|s| s.thought.len())
.map(|s| s.thought.as_str())
}
pub fn shortest_action(&self) -> Option<&str> {
self.steps
.iter()
.min_by_key(|s| s.action.len())
.map(|s| s.action.as_str())
}
pub fn total_thought_bytes(&self) -> usize {
self.steps.iter().map(|s| s.thought.len()).sum()
}
pub fn total_observation_bytes(&self) -> usize {
self.steps.iter().map(|s| s.observation.len()).sum()
}
pub fn first_step_action(&self) -> Option<&str> {
self.steps.first().map(|s| s.action.as_str())
}
pub fn last_step_action(&self) -> Option<&str> {
self.steps.last().map(|s| s.action.as_str())
}
pub fn count_nonempty_thoughts(&self) -> usize {
self.steps.iter().filter(|s| !s.thought.is_empty()).count()
}
pub fn observation_contains_count(&self, substring: &str) -> usize {
self.steps.iter().filter(|s| s.observation.contains(substring)).count()
}
pub fn count_steps_with_action(&self, action: &str) -> usize {
self.steps.iter().filter(|s| s.action == action).count()
}
pub fn thought_contains_count(&self, substring: &str) -> usize {
self.steps.iter().filter(|s| s.thought.contains(substring)).count()
}
pub fn failure_rate(&self) -> f64 {
let total = self.steps.len();
if total == 0 {
return 0.0;
}
self.failed_tool_call_count() as f64 / total as f64
}
pub fn unique_action_count(&self) -> usize {
let unique: std::collections::HashSet<&str> =
self.steps.iter().map(|s| s.action.as_str()).collect();
unique.len()
}
pub fn steps_in_range(&self, start: usize, end: usize) -> Vec<&ReActStep> {
let clamped_end = end.min(self.steps.len());
if start >= clamped_end {
return Vec::new();
}
self.steps[start..clamped_end].iter().collect()
}
pub fn median_step_duration_ms(&self) -> u64 {
if self.steps.is_empty() {
return 0;
}
let mut durations: Vec<u64> = self.steps.iter().map(|s| s.step_duration_ms).collect();
durations.sort_unstable();
durations[durations.len() / 2]
}
pub fn p95_step_duration_ms(&self) -> u64 {
if self.steps.is_empty() {
return 0;
}
let mut durations: Vec<u64> = self.steps.iter().map(|s| s.step_duration_ms).collect();
durations.sort_unstable();
let idx = ((durations.len() as f64 * 0.95).ceil() as usize)
.saturating_sub(1)
.min(durations.len() - 1);
durations[idx]
}
pub fn p99_step_duration_ms(&self) -> u64 {
if self.steps.is_empty() {
return 0;
}
let mut durations: Vec<u64> = self.steps.iter().map(|s| s.step_duration_ms).collect();
durations.sort_unstable();
let idx = ((durations.len() as f64 * 0.99).ceil() as usize)
.saturating_sub(1)
.min(durations.len() - 1);
durations[idx]
}
pub fn step_count_above_duration_ms(&self, threshold_ms: u64) -> usize {
self.steps
.iter()
.filter(|s| s.step_duration_ms > threshold_ms)
.count()
}
pub fn min_step_duration_ms(&self) -> u64 {
self.steps.iter().map(|s| s.step_duration_ms).min().unwrap_or(0)
}
pub fn max_step_duration_ms(&self) -> u64 {
self.steps.iter().map(|s| s.step_duration_ms).max().unwrap_or(0)
}
pub fn total_action_bytes(&self) -> usize {
self.steps.iter().map(|s| s.action.len()).sum()
}
pub fn step_duration_variance_ms(&self) -> f64 {
let n = self.steps.len();
if n < 2 {
return 0.0;
}
let mean = self.average_step_duration_ms() as f64;
let sum_sq: f64 = self
.steps
.iter()
.map(|s| {
let diff = s.step_duration_ms as f64 - mean;
diff * diff
})
.sum();
sum_sq / n as f64
}
pub fn steps_with_errors(&self) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.observation.to_ascii_lowercase().contains("error"))
.collect()
}
pub fn steps_with_long_observations(&self, threshold_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.observation.len() > threshold_bytes)
.collect()
}
pub fn observations_above_bytes(&self, min_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.observation.len() > min_bytes)
.collect()
}
pub fn total_step_chars(&self) -> usize {
self.steps
.iter()
.map(|s| {
s.thought.chars().count()
+ s.action.chars().count()
+ s.observation.chars().count()
})
.sum()
}
pub fn unique_observations_count(&self) -> usize {
let unique: std::collections::HashSet<&str> =
self.steps.iter().map(|s| s.observation.as_str()).collect();
unique.len()
}
pub fn thought_max_bytes(&self) -> usize {
self.steps.iter().map(|s| s.thought.len()).max().unwrap_or(0)
}
pub fn observation_max_bytes(&self) -> usize {
self.steps.iter().map(|s| s.observation.len()).max().unwrap_or(0)
}
pub fn step_count_below_duration_ms(&self, threshold_ms: u64) -> usize {
self.steps
.iter()
.filter(|s| s.step_duration_ms < threshold_ms)
.count()
}
pub fn max_action_bytes(&self) -> usize {
self.steps.iter().map(|s| s.action.len()).max().unwrap_or(0)
}
pub fn min_action_bytes(&self) -> usize {
self.steps.iter().map(|s| s.action.len()).min().unwrap_or(0)
}
pub fn proportion_tool_calls(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let tool_calls = self.steps.iter().filter(|s| s.is_tool_call()).count();
tool_calls as f64 / self.steps.len() as f64
}
pub fn thought_density(&self) -> f64 {
let thought_bytes: usize = self.steps.iter().map(|s| s.thought.len()).sum();
let total_bytes: usize = self
.steps
.iter()
.map(|s| s.thought.len() + s.action.len() + s.observation.len())
.sum();
if total_bytes == 0 {
return 0.0;
}
thought_bytes as f64 / total_bytes as f64
}
pub fn step_throughput_per_sec(&self) -> f64 {
if self.duration_ms == 0 || self.steps.is_empty() {
return 0.0;
}
self.steps.len() as f64 / (self.duration_ms as f64 / 1000.0)
}
pub fn avg_action_bytes(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.action.len()).sum();
total as f64 / self.steps.len() as f64
}
pub fn avg_observation_bytes(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.observation.len()).sum();
total as f64 / self.steps.len() as f64
}
pub fn total_observation_count(&self) -> usize {
self.steps.iter().filter(|s| !s.observation.is_empty()).count()
}
pub fn actions_containing<'a>(&'a self, substring: &str) -> Vec<&'a ReActStep> {
self.steps
.iter()
.filter(|s| s.action.contains(substring))
.collect()
}
pub fn avg_thought_bytes(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.thought.len()).sum();
total as f64 / self.steps.len() as f64
}
pub fn steps_above_action_bytes(&self, min_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.action.len() > min_bytes)
.collect()
}
pub fn steps_between(&self, start: usize, end: usize) -> Vec<&ReActStep> {
let clamped_end = end.min(self.steps.len());
if start >= clamped_end {
return Vec::new();
}
self.steps[start..clamped_end].iter().collect()
}
pub fn step_observation_rate(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let count = self.steps.iter().filter(|s| !s.observation.is_empty()).count();
count as f64 / self.steps.len() as f64
}
pub fn steps_below_thought_bytes(&self, max_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.thought.len() < max_bytes)
.collect()
}
pub fn steps_with_duplicate_thoughts(&self) -> Vec<&ReActStep> {
let mut seen = std::collections::HashSet::new();
self.steps
.iter()
.filter(|s| !seen.insert(s.thought.as_str()))
.collect()
}
pub fn max_thought_bytes(&self) -> usize {
self.steps.iter().map(|s| s.thought.len()).max().unwrap_or(0)
}
pub fn steps_by_action_prefix<'a>(&'a self, prefix: &str) -> Vec<&'a ReActStep> {
self.steps
.iter()
.filter(|s| s.action.starts_with(prefix))
.collect()
}
pub fn action_count(&self) -> usize {
self.steps.iter().filter(|s| s.is_tool_call()).count()
}
pub fn steps_above_observation_bytes(&self, min_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.observation.len() > min_bytes)
.collect()
}
pub fn steps_matching_observation<'a>(&'a self, substr: &str) -> Vec<&'a ReActStep> {
self.steps
.iter()
.filter(|s| s.observation.contains(substr))
.collect()
}
pub fn step_action_lengths(&self) -> Vec<usize> {
self.steps.iter().map(|s| s.action.len()).collect()
}
pub fn has_thought_starting_with(&self, prefix: &str) -> bool {
self.steps.iter().any(|s| s.thought.starts_with(prefix))
}
pub fn step_count_above_action_bytes(&self, min_bytes: usize) -> usize {
self.steps.iter().filter(|s| s.action.len() > min_bytes).count()
}
pub fn steps_with_empty_action(&self) -> Vec<&ReActStep> {
self.steps.iter().filter(|s| s.action.is_empty()).collect()
}
pub fn has_action_containing(&self, substr: &str) -> bool {
self.steps.iter().any(|s| s.action.contains(substr))
}
pub fn max_observation_chars(&self) -> usize {
self.steps
.iter()
.map(|s| s.observation.chars().count())
.max()
.unwrap_or(0)
}
pub fn step_index_of_longest_thought(&self) -> Option<usize> {
self.steps
.iter()
.enumerate()
.max_by_key(|(_, s)| s.thought.chars().count())
.map(|(i, _)| i)
}
pub fn observation_word_counts(&self) -> Vec<usize> {
self.steps
.iter()
.map(|s| s.observation.split_whitespace().count())
.collect()
}
pub fn observation_starts_with_any(&self, prefixes: &[&str]) -> bool {
self.steps
.iter()
.any(|s| prefixes.iter().any(|p| s.observation.starts_with(p)))
}
pub fn has_repeated_actions(&self) -> bool {
let mut seen = std::collections::HashSet::new();
self.steps
.iter()
.filter(|s| !s.action.is_empty())
.any(|s| !seen.insert(s.action.as_str()))
}
pub fn thought_starts_with_any(&self, prefixes: &[&str]) -> bool {
self.steps
.iter()
.any(|s| prefixes.iter().any(|p| s.thought.starts_with(p)))
}
pub fn action_word_count(&self) -> usize {
self.steps
.iter()
.map(|s| s.action.split_whitespace().count())
.sum()
}
pub fn steps_above_thought_chars(&self, min: usize) -> usize {
self.steps
.iter()
.filter(|s| s.thought.chars().count() > min)
.count()
}
pub fn steps_with_non_empty_observation(&self) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| !s.observation.is_empty())
.collect()
}
pub fn observations_containing(&self, substr: &str) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.observation.contains(substr))
.collect()
}
pub fn thought_observation_ratio(&self) -> f64 {
let obs: usize = self.steps.iter().map(|s| s.observation.chars().count()).sum();
if obs == 0 {
return 0.0;
}
let thoughts: usize = self.steps.iter().map(|s| s.thought.chars().count()).sum();
thoughts as f64 / obs as f64
}
pub fn steps_matching_thought(&self, substr: &str) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.thought.contains(substr))
.collect()
}
pub fn median_observation_chars(&self) -> usize {
if self.steps.is_empty() {
return 0;
}
let mut lens: Vec<usize> = self
.steps
.iter()
.map(|s| s.observation.chars().count())
.collect();
lens.sort_unstable();
lens[lens.len() / 2]
}
pub fn cumulative_thought_chars(&self) -> Vec<usize> {
let mut total = 0usize;
self.steps
.iter()
.map(|s| {
total += s.thought.chars().count();
total
})
.collect()
}
pub fn count_steps_with_thought_containing(&self, substr: &str) -> usize {
self.steps
.iter()
.filter(|s| s.thought.contains(substr))
.count()
}
pub fn min_observation_bytes(&self) -> usize {
self.steps
.iter()
.map(|s| s.observation.len())
.filter(|&n| n > 0)
.min()
.unwrap_or(0)
}
pub fn min_thought_bytes(&self) -> usize {
self.steps
.iter()
.map(|s| s.thought.len())
.filter(|&n| n > 0)
.min()
.unwrap_or(0)
}
pub fn proportion_empty_thoughts(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let empty = self.steps.iter().filter(|s| s.thought.is_empty()).count();
empty as f64 / self.steps.len() as f64
}
pub fn has_failed_steps(&self) -> bool {
self.steps
.iter()
.any(|s| s.observation.starts_with("[error]"))
}
pub fn total_thought_chars(&self) -> usize {
self.steps.iter().map(|s| s.thought.chars().count()).sum()
}
pub fn total_action_chars(&self) -> usize {
self.steps.iter().map(|s| s.action.chars().count()).sum()
}
pub fn total_observation_chars(&self) -> usize {
self.steps.iter().map(|s| s.observation.chars().count()).sum()
}
pub fn action_byte_variance(&self) -> f64 {
if self.steps.len() < 2 {
return 0.0;
}
let lengths: Vec<f64> = self.steps.iter().map(|s| s.action.len() as f64).collect();
let mean = lengths.iter().sum::<f64>() / lengths.len() as f64;
lengths.iter().map(|&l| (l - mean).powi(2)).sum::<f64>() / lengths.len() as f64
}
pub fn non_empty_action_count(&self) -> usize {
self.steps.iter().filter(|s| !s.action.is_empty()).count()
}
pub fn total_step_bytes(&self) -> usize {
self.steps
.iter()
.map(|s| s.thought.len() + s.action.len() + s.observation.len())
.sum()
}
pub fn last_thought_bytes(&self) -> usize {
self.steps.last().map_or(0, |s| s.thought.len())
}
pub fn first_observation_bytes(&self) -> usize {
self.steps.first().map_or(0, |s| s.observation.len())
}
pub fn has_step_with_empty_observation(&self) -> bool {
self.steps.iter().any(|s| s.observation.is_empty())
}
pub fn thought_to_action_byte_ratio(&self) -> f64 {
let thought_bytes: usize = self.steps.iter().map(|s| s.thought.len()).sum();
let action_bytes: usize = self.steps.iter().map(|s| s.action.len()).sum();
if action_bytes == 0 {
return 0.0;
}
thought_bytes as f64 / action_bytes as f64
}
pub fn observation_above_bytes_count(&self, min_bytes: usize) -> usize {
self.steps
.iter()
.filter(|s| s.observation.len() > min_bytes)
.count()
}
pub fn steps_with_both_thought_and_action(&self) -> usize {
self.steps
.iter()
.filter(|s| !s.thought.is_empty() && !s.action.is_empty())
.count()
}
pub fn steps_with_observation_prefix(&self, prefix: &str) -> usize {
self.steps
.iter()
.filter(|s| s.observation.starts_with(prefix))
.count()
}
pub fn observation_bytes_total(&self) -> usize {
self.steps.iter().map(|s| s.observation.len()).sum()
}
pub fn first_thought_chars(&self) -> usize {
self.steps.first().map_or(0, |s| s.thought.chars().count())
}
pub fn last_observation_chars(&self) -> usize {
self.steps.last().map_or(0, |s| s.observation.chars().count())
}
pub fn observation_word_count_total(&self) -> usize {
self.steps
.iter()
.map(|s| s.observation.split_whitespace().count())
.sum()
}
pub fn action_ends_with_count(&self, suffix: &str) -> usize {
self.steps
.iter()
.filter(|s| s.action.ends_with(suffix))
.count()
}
pub fn avg_observation_words(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self
.steps
.iter()
.map(|s| s.observation.split_whitespace().count())
.sum();
total as f64 / self.steps.len() as f64
}
pub fn thought_byte_variance(&self) -> f64 {
if self.steps.len() < 2 {
return 0.0;
}
let lengths: Vec<f64> = self.steps.iter().map(|s| s.thought.len() as f64).collect();
let mean = lengths.iter().sum::<f64>() / lengths.len() as f64;
lengths.iter().map(|&l| (l - mean).powi(2)).sum::<f64>() / lengths.len() as f64
}
pub fn steps_above_thought_bytes(&self, min_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.thought.len() > min_bytes)
.collect()
}
pub fn total_empty_steps(&self) -> usize {
self.steps
.iter()
.filter(|s| s.thought.is_empty() && s.action.is_empty() && s.observation.is_empty())
.count()
}
pub fn action_starts_with_count(&self, prefix: &str) -> usize {
self.steps
.iter()
.filter(|s| s.action.starts_with(prefix))
.count()
}
pub fn longest_action(&self) -> Option<&str> {
self.steps
.iter()
.max_by_key(|s| s.action.len())
.map(|s| s.action.as_str())
}
pub fn thought_completeness(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let non_empty = self.steps.iter().filter(|s| !s.thought.is_empty()).count();
non_empty as f64 / self.steps.len() as f64
}
pub fn final_answer_step_index(&self) -> Option<usize> {
self.steps.iter().position(|s| s.is_final_answer())
}
pub fn step_duration_range_ms(&self) -> (u64, u64) {
if self.steps.is_empty() {
return (0, 0);
}
let min = self.steps.iter().map(|s| s.step_duration_ms).min().unwrap_or(0);
let max = self.steps.iter().map(|s| s.step_duration_ms).max().unwrap_or(0);
(min, max)
}
pub fn count_unique_thoughts(&self) -> usize {
let unique: std::collections::HashSet<&str> =
self.steps.iter().map(|s| s.thought.as_str()).collect();
unique.len()
}
pub fn steps_with_empty_thoughts(&self) -> Vec<&ReActStep> {
self.steps.iter().filter(|s| s.thought.is_empty()).collect()
}
pub fn steps_with_long_thoughts(&self, threshold_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.thought.len() > threshold_bytes)
.collect()
}
pub fn action_count_containing(&self, substring: &str) -> usize {
self.steps.iter().filter(|s| s.action.contains(substring)).count()
}
pub fn total_thought_count(&self) -> usize {
self.steps.iter().filter(|s| !s.thought.is_empty()).count()
}
pub fn has_thought_containing(&self, substring: &str) -> bool {
self.steps.iter().any(|s| s.thought.contains(substring))
}
pub fn steps_with_action_length_above(&self, min_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.action.len() > min_bytes)
.collect()
}
#[cfg(feature = "persistence")]
pub async fn save_checkpoint(
&self,
backend: &dyn crate::persistence::PersistenceBackend,
) -> Result<(), AgentRuntimeError> {
let key = format!("session:{}", self.session_id);
let bytes = serde_json::to_vec(self)
.map_err(|e| AgentRuntimeError::Persistence(format!("serialize: {e}")))?;
backend.save(&key, &bytes).await
}
#[cfg(feature = "persistence")]
pub async fn load_checkpoint(
backend: &dyn crate::persistence::PersistenceBackend,
session_id: &str,
) -> Result<Option<AgentSession>, AgentRuntimeError> {
let key = format!("session:{session_id}");
match backend.load(&key).await? {
None => Ok(None),
Some(bytes) => {
let session = serde_json::from_slice(&bytes)
.map_err(|e| AgentRuntimeError::Persistence(format!("deserialize: {e}")))?;
Ok(Some(session))
}
}
}
#[cfg(feature = "persistence")]
#[deprecated(since = "1.1.0", note = "Use load_checkpoint_at_step instead")]
pub async fn load_step_checkpoint(
backend: &dyn crate::persistence::PersistenceBackend,
session_id: &str,
step: usize,
) -> Result<Option<AgentSession>, AgentRuntimeError> {
Self::load_checkpoint_at_step(backend, session_id, step).await
}
#[cfg(feature = "persistence")]
pub async fn load_checkpoint_at_step(
backend: &dyn crate::persistence::PersistenceBackend,
session_id: &str,
step: usize,
) -> Result<Option<AgentSession>, AgentRuntimeError> {
let key = format!("session:{session_id}:step:{step}");
match backend.load(&key).await? {
None => Ok(None),
Some(bytes) => {
let session = serde_json::from_slice(&bytes)
.map_err(|e| AgentRuntimeError::Persistence(format!("deserialize: {e}")))?;
Ok(Some(session))
}
}
}
pub fn into_steps(self) -> Vec<crate::agent::ReActStep> {
self.steps
}
pub fn iter_steps(&self) -> std::slice::Iter<'_, crate::agent::ReActStep> {
self.steps.iter()
}
pub fn has_at_least_steps(&self, n: usize) -> bool {
self.steps.len() >= n
}
pub fn all_observations_non_empty(&self) -> bool {
self.steps.iter().all(|s| !s.observation.is_empty())
}
pub fn avg_combined_step_bytes(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.combined_byte_length()).sum();
total as f64 / self.steps.len() as f64
}
pub fn shortest_observation_step(&self) -> Option<&ReActStep> {
self.steps.iter().min_by_key(|s| s.observation.len())
}
pub fn unique_observation_count(&self) -> usize {
self.steps
.iter()
.map(|s| s.observation.as_str())
.collect::<std::collections::HashSet<_>>()
.len()
}
pub fn avg_thought_word_count(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self
.steps
.iter()
.map(|s| s.thought.split_whitespace().count())
.sum();
total as f64 / self.steps.len() as f64
}
pub fn observation_contains_any(&self, terms: &[&str]) -> bool {
if terms.is_empty() {
return false;
}
self.steps
.iter()
.any(|s| terms.iter().any(|t| s.observation.contains(t)))
}
pub fn step_at_index(&self, index: usize) -> Option<&ReActStep> {
self.steps.get(index)
}
pub fn thought_contains_all(&self, terms: &[&str]) -> bool {
if terms.is_empty() {
return false;
}
self.steps
.iter()
.any(|s| terms.iter().all(|t| s.thought.contains(t)))
}
pub fn action_contains_any(&self, terms: &[&str]) -> bool {
if terms.is_empty() {
return false;
}
self.steps
.iter()
.any(|s| terms.iter().any(|t| s.action.contains(t)))
}
pub fn max_thought_chars(&self) -> usize {
self.steps
.iter()
.map(|s| s.thought.chars().count())
.max()
.unwrap_or(0)
}
pub fn min_thought_chars(&self) -> usize {
self.steps
.iter()
.map(|s| s.thought.chars().count())
.filter(|&n| n > 0)
.min()
.unwrap_or(0)
}
pub fn avg_action_chars(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.action.chars().count()).sum();
total as f64 / self.steps.len() as f64
}
pub fn avg_observation_chars(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.observation.chars().count()).sum();
total as f64 / self.steps.len() as f64
}
pub fn step_with_longest_action(&self) -> Option<&ReActStep> {
self.steps.iter().max_by_key(|s| s.action.chars().count())
}
pub fn action_ends_with(&self, suffix: &str) -> bool {
self.steps.iter().any(|s| s.action.ends_with(suffix))
}
pub fn thought_ends_with(&self, suffix: &str) -> bool {
self.steps.iter().any(|s| s.thought.ends_with(suffix))
}
pub fn has_step_with_both(&self, thought_term: &str, action_term: &str) -> bool {
self.steps
.iter()
.any(|s| s.thought.contains(thought_term) && s.action.contains(action_term))
}
pub fn step_count_with_observation_longer_than(&self, min_bytes: usize) -> usize {
self.steps
.iter()
.filter(|s| s.observation.len() > min_bytes)
.count()
}
pub fn thought_word_counts(&self) -> Vec<usize> {
self.steps
.iter()
.map(|s| s.thought.split_whitespace().count())
.collect()
}
pub fn steps_sorted_by_thought_len(&self) -> Vec<&ReActStep> {
let mut sorted: Vec<&ReActStep> = self.steps.iter().collect();
sorted.sort_by_key(|s| s.thought.len());
sorted
}
pub fn steps_with_thought_longer_than(&self, min_bytes: usize) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.thought.len() > min_bytes)
.collect()
}
pub fn steps_with_action_containing(&self, substr: &str) -> Vec<&ReActStep> {
self.steps
.iter()
.filter(|s| s.action.contains(substr))
.collect()
}
pub fn observation_max_chars(&self) -> usize {
self.steps
.iter()
.map(|s| s.observation.chars().count())
.max()
.unwrap_or(0)
}
pub fn observation_min_chars(&self) -> usize {
self.steps
.iter()
.map(|s| s.observation.chars().count())
.filter(|&n| n > 0)
.min()
.unwrap_or(0)
}
pub fn action_word_counts(&self) -> Vec<usize> {
self.steps
.iter()
.map(|s| s.action.split_whitespace().count())
.collect()
}
pub fn thought_avg_chars(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let total: usize = self.steps.iter().map(|s| s.thought.chars().count()).sum();
total as f64 / self.steps.len() as f64
}
pub fn thought_byte_range(&self) -> (usize, usize) {
if self.steps.is_empty() {
return (0, 0);
}
let min = self.steps.iter().map(|s| s.thought.len()).min().unwrap_or(0);
let max = self.steps.iter().map(|s| s.thought.len()).max().unwrap_or(0);
(min, max)
}
}
pub struct AgentRuntimeBuilder<S = NeedsConfig> {
#[cfg(feature = "memory")]
memory: Option<EpisodicStore>,
#[cfg(feature = "memory")]
working: Option<WorkingMemory>,
#[cfg(feature = "graph")]
graph: Option<GraphStore>,
#[cfg(feature = "orchestrator")]
backpressure: Option<BackpressureGuard>,
agent_config: Option<AgentConfig>,
tools: Vec<Arc<ToolSpec>>,
metrics: Arc<RuntimeMetrics>,
#[cfg(feature = "persistence")]
checkpoint_backend: Option<Arc<dyn crate::persistence::PersistenceBackend>>,
token_estimator: Option<Arc<dyn TokenEstimator>>,
_state: PhantomData<S>,
}
trait DebugBuilderState {
const NAME: &'static str;
const HAS_CONFIG: bool;
}
impl DebugBuilderState for NeedsConfig {
const NAME: &'static str = "AgentRuntimeBuilder<NeedsConfig>";
const HAS_CONFIG: bool = false;
}
impl DebugBuilderState for HasConfig {
const NAME: &'static str = "AgentRuntimeBuilder<HasConfig>";
const HAS_CONFIG: bool = true;
}
impl<S: DebugBuilderState> std::fmt::Debug for AgentRuntimeBuilder<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct(S::NAME);
#[cfg(feature = "memory")]
{
s.field("memory", &self.memory.is_some())
.field("working", &self.working.is_some());
}
#[cfg(feature = "graph")]
s.field("graph", &self.graph.is_some());
#[cfg(feature = "orchestrator")]
s.field("backpressure", &self.backpressure.is_some());
if S::HAS_CONFIG {
s.field("agent_config", &self.agent_config.is_some());
}
s.field("tools", &self.tools.len()).finish()
}
}
impl Default for AgentRuntimeBuilder<NeedsConfig> {
fn default() -> Self {
Self {
#[cfg(feature = "memory")]
memory: None,
#[cfg(feature = "memory")]
working: None,
#[cfg(feature = "graph")]
graph: None,
#[cfg(feature = "orchestrator")]
backpressure: None,
agent_config: None,
tools: Vec::new(),
metrics: RuntimeMetrics::new(),
#[cfg(feature = "persistence")]
checkpoint_backend: None,
token_estimator: None,
_state: PhantomData,
}
}
}
impl<S> AgentRuntimeBuilder<S> {
#[cfg(feature = "memory")]
pub fn with_memory(mut self, store: EpisodicStore) -> Self {
self.memory = Some(store);
self
}
#[cfg(feature = "memory")]
pub fn with_working_memory(mut self, wm: WorkingMemory) -> Self {
self.working = Some(wm);
self
}
#[cfg(feature = "graph")]
pub fn with_graph(mut self, graph: GraphStore) -> Self {
self.graph = Some(graph);
self
}
#[cfg(feature = "orchestrator")]
pub fn with_backpressure(mut self, guard: BackpressureGuard) -> Self {
self.backpressure = Some(guard);
self
}
pub fn register_tool(mut self, spec: ToolSpec) -> Self {
self.tools.push(Arc::new(spec));
self
}
pub fn register_tools(mut self, specs: impl IntoIterator<Item = ToolSpec>) -> Self {
for spec in specs {
self.tools.push(Arc::new(spec));
}
self
}
pub fn with_metrics(mut self, metrics: Arc<RuntimeMetrics>) -> Self {
self.metrics = metrics;
self
}
#[cfg(feature = "persistence")]
pub fn with_checkpoint_backend(
mut self,
backend: Arc<dyn crate::persistence::PersistenceBackend>,
) -> Self {
self.checkpoint_backend = Some(backend);
self
}
pub fn with_token_estimator(mut self, estimator: Arc<dyn TokenEstimator>) -> Self {
self.token_estimator = Some(estimator);
self
}
}
impl AgentRuntimeBuilder<NeedsConfig> {
pub fn new() -> Self {
Self::default()
}
pub fn with_agent_config(self, config: AgentConfig) -> AgentRuntimeBuilder<HasConfig> {
AgentRuntimeBuilder {
memory: self.memory,
working: self.working,
#[cfg(feature = "graph")]
graph: self.graph,
#[cfg(feature = "orchestrator")]
backpressure: self.backpressure,
agent_config: Some(config),
tools: self.tools,
metrics: self.metrics,
#[cfg(feature = "persistence")]
checkpoint_backend: self.checkpoint_backend,
token_estimator: self.token_estimator,
_state: PhantomData,
}
}
}
impl AgentRuntimeBuilder<HasConfig> {
pub fn build(self) -> AgentRuntime {
#[allow(clippy::unwrap_used)]
let agent_config = self.agent_config.unwrap();
AgentRuntime {
#[cfg(feature = "memory")]
memory: self.memory,
#[cfg(feature = "memory")]
working: self.working,
#[cfg(feature = "graph")]
graph: self.graph,
#[cfg(feature = "orchestrator")]
backpressure: self.backpressure,
agent_config,
tools: self.tools,
metrics: self.metrics,
token_estimator: self
.token_estimator
.unwrap_or_else(|| Arc::new(CharDivTokenEstimator)),
#[cfg(feature = "persistence")]
checkpoint_backend: self.checkpoint_backend,
}
}
}
pub trait TokenEstimator: Send + Sync {
fn count_tokens(&self, text: &str) -> usize;
}
pub struct CharDivTokenEstimator;
impl TokenEstimator for CharDivTokenEstimator {
fn count_tokens(&self, text: &str) -> usize {
(text.len() / 4).max(1)
}
}
pub struct AgentRuntime {
#[cfg(feature = "memory")]
memory: Option<EpisodicStore>,
#[cfg(feature = "memory")]
working: Option<WorkingMemory>,
#[cfg(feature = "graph")]
graph: Option<GraphStore>,
#[cfg(feature = "orchestrator")]
backpressure: Option<BackpressureGuard>,
agent_config: AgentConfig,
tools: Vec<Arc<ToolSpec>>,
metrics: Arc<RuntimeMetrics>,
#[cfg(feature = "persistence")]
checkpoint_backend: Option<Arc<dyn crate::persistence::PersistenceBackend>>,
token_estimator: Arc<dyn TokenEstimator>,
}
impl std::fmt::Debug for AgentRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("AgentRuntime");
s.field("memory", &self.memory.is_some())
.field("working", &self.working.is_some());
#[cfg(feature = "graph")]
s.field("graph", &self.graph.is_some());
#[cfg(feature = "orchestrator")]
s.field("backpressure", &self.backpressure.is_some());
s.field("tools", &self.tools.len());
#[cfg(feature = "persistence")]
s.field("checkpoint_backend", &self.checkpoint_backend.is_some());
s.finish()
}
}
impl AgentRuntime {
pub fn builder() -> AgentRuntimeBuilder<NeedsConfig> {
AgentRuntimeBuilder::new()
}
pub fn quick(max_iterations: usize, model: impl Into<String>) -> Self {
AgentRuntime::builder()
.with_agent_config(AgentConfig::new(max_iterations, model))
.build()
}
pub fn metrics(&self) -> Arc<RuntimeMetrics> {
Arc::clone(&self.metrics)
}
#[tracing::instrument(skip(self, infer), fields(agent_id = %agent_id))]
pub async fn run_agent<F, Fut>(
&self,
agent_id: AgentId,
prompt: &str,
infer: F,
) -> Result<AgentSession, AgentRuntimeError>
where
F: FnMut(String) -> Fut,
Fut: std::future::Future<Output = String>,
{
#[cfg(feature = "orchestrator")]
{
let backpressure_result = if let Some(ref guard) = self.backpressure {
guard.try_acquire()
} else {
Ok(())
};
if let Err(e) = backpressure_result {
tracing::warn!(agent_id = %agent_id, error = %e, "backpressure shed: rejecting session");
self.metrics
.backpressure_shed_count
.fetch_add(1, Ordering::Relaxed);
return Err(e);
}
}
self.metrics.total_sessions.fetch_add(1, Ordering::Relaxed);
self.metrics.active_sessions.fetch_add(1, Ordering::Relaxed);
tracing::info!(agent_id = %agent_id, "agent session starting");
let outcome = self.run_agent_inner(agent_id.clone(), prompt, infer).await;
#[cfg(feature = "orchestrator")]
if let Some(ref guard) = self.backpressure {
let _ = guard.release();
}
let _ = self.metrics.active_sessions.fetch_update(
Ordering::Relaxed,
Ordering::Relaxed,
|v| Some(v.saturating_sub(1)),
);
match &outcome {
Ok(session) => {
tracing::info!(
agent_id = %agent_id,
session_id = %session.session_id,
steps = session.step_count(),
duration_ms = session.duration_ms,
"agent session completed"
);
self.metrics
.total_steps
.fetch_add(session.step_count() as u64, Ordering::Relaxed);
}
Err(e) => {
tracing::error!(agent_id = %agent_id, error = %e, "agent session failed");
}
}
outcome
}
#[tracing::instrument(skip(self, infer), fields(agent_id = %agent_id, session_id = tracing::field::Empty))]
async fn run_agent_inner<F, Fut>(
&self,
agent_id: AgentId,
prompt: &str,
infer: F,
) -> Result<AgentSession, AgentRuntimeError>
where
F: FnMut(String) -> Fut,
Fut: std::future::Future<Output = String>,
{
let start = Instant::now();
let session_id = uuid::Uuid::new_v4().to_string();
let mut memory_hits = 0usize;
let mut graph_lookups = 0usize;
#[cfg(feature = "memory")]
let enriched_prompt = if let Some(ref store) = self.memory {
let memories = store.recall(&agent_id, self.agent_config.max_memory_recalls)?;
let memories = if let Some(token_budget) = self.agent_config.max_memory_tokens {
let mut used = 0usize;
memories
.into_iter()
.filter(|m| {
let tokens = self.token_estimator.count_tokens(&m.content);
if used + tokens <= token_budget {
used += tokens;
true
} else {
false
}
})
.collect::<Vec<_>>()
} else {
memories
};
memory_hits = memories.len();
self.metrics
.memory_recall_count
.fetch_add(1, Ordering::Relaxed);
if let Some(budget) = self.agent_config.max_memory_tokens {
tracing::debug!(
"memory token budget: {budget}, injecting {} items",
memory_hits
);
} else {
tracing::debug!("enriched prompt with {} memory items", memory_hits);
}
if memories.is_empty() {
prompt.to_owned()
} else {
let mut enriched =
String::with_capacity(prompt.len() + memories.len() * 64 + 32);
enriched.push_str("Relevant memories:\n");
for m in &memories {
let _ = writeln!(enriched, "- {}", m.content);
}
let _ = write!(enriched, "\nCurrent prompt: {prompt}");
enriched
}
} else {
prompt.to_owned()
};
#[cfg(not(feature = "memory"))]
let enriched_prompt = prompt.to_owned();
#[cfg(feature = "memory")]
let enriched_prompt = if let Some(ref wm) = self.working {
let entries = wm.entries()?;
if entries.is_empty() {
enriched_prompt
} else {
let mut out = String::with_capacity(
enriched_prompt.len() + entries.len() * 32 + 32,
);
out.push_str(&enriched_prompt);
out.push_str("\n\nCurrent working state:\n");
for (k, v) in &entries {
let _ = writeln!(out, " {k}: {v}");
}
if out.ends_with('\n') {
out.pop();
}
out
}
} else {
enriched_prompt
};
#[cfg(feature = "graph")]
if let Some(ref graph) = self.graph {
graph_lookups = graph.entity_count()?;
tracing::debug!("graph has {} entities", graph_lookups);
}
let mut react_loop = ReActLoop::new(self.agent_config.clone())
.with_metrics(Arc::clone(&self.metrics));
#[cfg(feature = "persistence")]
if let Some(ref backend) = self.checkpoint_backend {
react_loop = react_loop
.with_step_checkpoint(Arc::clone(backend), session_id.clone());
}
for tool in &self.tools {
let tool_arc = Arc::clone(tool);
let required_fields = tool_arc.required_fields.clone();
#[cfg(feature = "orchestrator")]
let circuit_breaker = tool_arc.circuit_breaker.clone();
let mut spec = ToolSpec::new_async(
tool_arc.name.clone(),
tool_arc.description.clone(),
move |args| {
let t = Arc::clone(&tool_arc);
Box::pin(async move { t.call(args).await })
},
)
.with_required_fields(required_fields);
#[cfg(feature = "orchestrator")]
if let Some(cb) = circuit_breaker {
spec = spec.with_circuit_breaker(cb);
}
react_loop.register_tool(spec);
}
tracing::Span::current().record("session_id", &session_id.as_str());
let steps = react_loop.run(&enriched_prompt, infer).await?;
let duration_ms = start.elapsed().as_millis() as u64;
#[cfg(feature = "persistence")]
let mut ckpt_errors: Vec<String> = Vec::new();
#[cfg(feature = "persistence")]
if let Some(ref backend) = self.checkpoint_backend {
tracing::info!(session_id = %session_id, "saving session checkpoint");
let tmp = AgentSession {
session_id: session_id.clone(),
agent_id: agent_id.clone(),
steps: steps.clone(),
memory_hits,
graph_lookups,
duration_ms,
checkpoint_errors: vec![],
};
tmp.save_checkpoint(backend.as_ref()).await?;
for i in 1..=steps.len() {
let partial = AgentSession {
session_id: session_id.clone(),
agent_id: agent_id.clone(),
steps: steps[..i].to_vec(),
memory_hits,
graph_lookups,
duration_ms,
checkpoint_errors: vec![],
};
let key = format!("session:{session_id}:step:{i}");
match serde_json::to_vec(&partial) {
Ok(bytes) => {
if let Err(e) = backend.save(&key, &bytes).await {
let msg = format!("session:{session_id} step:{i} save: {e}");
tracing::warn!("{}", msg);
ckpt_errors.push(msg);
}
}
Err(e) => {
let msg =
format!("session:{session_id} step:{i} serialise: {e}");
tracing::warn!("{}", msg);
ckpt_errors.push(msg);
}
}
}
}
let session = AgentSession {
session_id,
agent_id,
steps,
memory_hits,
graph_lookups,
duration_ms,
#[cfg(feature = "persistence")]
checkpoint_errors: ckpt_errors,
#[cfg(not(feature = "persistence"))]
checkpoint_errors: vec![],
};
Ok(session)
}
#[cfg(feature = "memory")]
pub fn memory(&self) -> Option<&EpisodicStore> {
self.memory.as_ref()
}
#[cfg(feature = "graph")]
pub fn graph(&self) -> Option<&GraphStore> {
self.graph.as_ref()
}
#[cfg(feature = "memory")]
pub fn working_memory(&self) -> Option<&WorkingMemory> {
self.working.as_ref()
}
#[cfg(feature = "memory")]
pub fn has_memory(&self) -> bool {
self.memory.is_some()
}
#[cfg(feature = "graph")]
pub fn has_graph(&self) -> bool {
self.graph.is_some()
}
#[cfg(feature = "memory")]
pub fn has_working_memory(&self) -> bool {
self.working.is_some()
}
pub fn has_active_sessions(&self) -> bool {
self.metrics
.active_sessions
.load(std::sync::atomic::Ordering::Relaxed)
> 0
}
pub fn tool_count(&self) -> usize {
self.tools.len()
}
pub fn tool_names(&self) -> Vec<&str> {
let mut names: Vec<&str> = self.tools.iter().map(|t| t.name.as_str()).collect();
names.sort_unstable();
names
}
pub fn registered_tool_names(&self) -> Vec<String> {
let mut names: Vec<String> =
self.tools.iter().map(|t| t.name.clone()).collect();
names.sort_unstable();
names
}
pub fn config(&self) -> &AgentConfig {
&self.agent_config
}
pub fn model_name(&self) -> &str {
&self.agent_config.model
}
pub fn session_max_iterations(&self) -> usize {
self.agent_config.max_iterations
}
pub fn is_registered_tool(&self, name: &str) -> bool {
self.tools.iter().any(|t| t.name == name)
}
pub async fn shutdown(&self) {
tracing::info!("AgentRuntime shutting down");
tracing::info!(
active_sessions = self.metrics.active_sessions(),
total_sessions = self.metrics.total_sessions(),
total_steps = self.metrics.total_steps(),
total_tool_calls = self.metrics.total_tool_calls(),
failed_tool_calls = self.metrics.failed_tool_calls(),
"final metrics snapshot on shutdown"
);
#[cfg(feature = "persistence")]
if let Some(ref backend) = self.checkpoint_backend {
let ts = chrono::Utc::now().to_rfc3339();
match backend.save("runtime:shutdown", ts.as_bytes()).await {
Ok(()) => tracing::debug!("shutdown sentinel saved"),
Err(e) => tracing::warn!(error = %e, "failed to save shutdown sentinel"),
}
}
tracing::info!("AgentRuntime shutdown complete");
}
#[cfg(feature = "providers")]
pub async fn run_agent_with_provider(
&self,
agent_id: AgentId,
prompt: &str,
provider: std::sync::Arc<dyn crate::providers::LlmProvider>,
) -> Result<AgentSession, AgentRuntimeError> {
let model = self.agent_config.model.clone();
self.run_agent(agent_id, prompt, |ctx| {
let provider = provider.clone();
let model = model.clone();
async move {
provider
.complete(&ctx, &model)
.await
.unwrap_or_else(|e| format!("FINAL ANSWER: inference error: {e}"))
}
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{Entity, GraphStore, Relationship};
use crate::memory::{EpisodicStore, WorkingMemory};
fn simple_config() -> AgentConfig {
AgentConfig::new(5, "test")
}
async fn final_answer_infer(_ctx: String) -> String {
"Thought: done\nAction: FINAL_ANSWER 42".into()
}
#[tokio::test]
async fn test_builder_with_config_compiles() {
let _runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
}
#[tokio::test]
async fn test_builder_succeeds_with_minimal_config() {
let _runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
}
#[tokio::test]
async fn test_builder_with_all_subsystems() {
let _runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_memory(EpisodicStore::new())
.with_graph(GraphStore::new())
.with_working_memory(WorkingMemory::new(10).unwrap())
.with_backpressure(BackpressureGuard::new(5).unwrap())
.build();
}
#[tokio::test]
async fn test_builder_produces_runtime_with_config() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
let session = runtime
.run_agent(AgentId::new("agent-x"), "hello", final_answer_infer)
.await
.unwrap();
assert!(session.step_count() >= 1);
assert!(!session.session_id.is_empty());
}
#[tokio::test]
async fn test_run_agent_returns_session_with_steps() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
let session = runtime
.run_agent(AgentId::new("agent-1"), "hello", final_answer_infer)
.await
.unwrap();
assert_eq!(session.step_count(), 1);
}
#[tokio::test]
async fn test_run_agent_session_has_agent_id() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
let session = runtime
.run_agent(AgentId::new("agent-42"), "hello", final_answer_infer)
.await
.unwrap();
assert_eq!(session.agent_id.0, "agent-42");
}
#[tokio::test]
async fn test_run_agent_session_duration_is_set() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
let session = runtime
.run_agent(AgentId::new("a"), "hello", final_answer_infer)
.await
.unwrap();
let _ = session.duration_ms; }
#[tokio::test]
async fn test_run_agent_session_has_session_id() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
let session = runtime
.run_agent(AgentId::new("a"), "hello", final_answer_infer)
.await
.unwrap();
assert!(!session.session_id.is_empty());
assert_eq!(session.session_id.len(), 36); }
#[tokio::test]
async fn test_run_agent_memory_hits_zero_without_memory() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
let session = runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await
.unwrap();
assert_eq!(session.memory_hits, 0);
}
#[tokio::test]
async fn test_run_agent_memory_hits_counts_recalled_items() {
let store = EpisodicStore::new();
let agent = AgentId::new("mem-agent");
store
.add_episode(agent.clone(), "remembered fact", 0.8)
.unwrap();
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_memory(store)
.build();
let session = runtime
.run_agent(agent, "prompt", final_answer_infer)
.await
.unwrap();
assert_eq!(session.memory_hits, 1);
}
#[tokio::test]
async fn test_run_agent_graph_lookups_counts_entities() {
let graph = GraphStore::new();
graph.add_entity(Entity::new("e1", "Node")).unwrap();
graph.add_entity(Entity::new("e2", "Node")).unwrap();
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_graph(graph)
.build();
let session = runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await
.unwrap();
assert_eq!(session.graph_lookups, 2);
}
#[tokio::test]
async fn test_run_agent_backpressure_released_after_run() {
let guard = BackpressureGuard::new(3).unwrap();
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_backpressure(guard.clone())
.build();
runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await
.unwrap();
assert_eq!(guard.depth().unwrap(), 0);
}
#[tokio::test]
async fn test_run_agent_backpressure_sheds_when_full() {
let guard = BackpressureGuard::new(1).unwrap();
guard.try_acquire().unwrap();
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_backpressure(guard)
.build();
let result = runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await;
assert!(matches!(
result,
Err(AgentRuntimeError::BackpressureShed { .. })
));
}
#[tokio::test]
async fn test_run_agent_max_iterations_error_propagated() {
let cfg = AgentConfig::new(2, "model");
let runtime = AgentRuntime::builder().with_agent_config(cfg).build();
let result = runtime
.run_agent(AgentId::new("a"), "prompt", |_ctx: String| async {
"Thought: looping\nAction: FINAL_ANSWER done".to_string()
})
.await;
assert!(result.is_ok()); }
#[tokio::test]
async fn test_agent_session_step_count_matches_steps() {
let session = AgentSession {
session_id: "test-session-id".into(),
agent_id: AgentId::new("a"),
steps: vec![
ReActStep {
thought: "t".into(),
action: "a".into(),
observation: "o".into(),
step_duration_ms: 0,
},
ReActStep {
thought: "t2".into(),
action: "FINAL_ANSWER".into(),
observation: "done".into(),
step_duration_ms: 0,
},
],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 10,
checkpoint_errors: vec![],
};
assert_eq!(session.step_count(), 2);
}
#[tokio::test]
async fn test_runtime_memory_accessor_returns_none_when_not_configured() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
assert!(runtime.memory().is_none());
}
#[tokio::test]
async fn test_runtime_memory_accessor_returns_some_when_configured() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_memory(EpisodicStore::new())
.build();
assert!(runtime.memory().is_some());
}
#[tokio::test]
async fn test_runtime_graph_accessor_returns_none_when_not_configured() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
assert!(runtime.graph().is_none());
}
#[tokio::test]
async fn test_runtime_graph_accessor_returns_some_when_configured() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_graph(GraphStore::new())
.build();
assert!(runtime.graph().is_some());
}
#[tokio::test]
async fn test_runtime_working_memory_accessor() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_working_memory(WorkingMemory::new(5).unwrap())
.build();
assert!(runtime.working_memory().is_some());
}
#[tokio::test]
async fn test_runtime_with_tool_registered() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.register_tool(ToolSpec::new("calc", "math", |_| serde_json::json!(99)))
.build();
let mut call_count = 0;
let session = runtime
.run_agent(AgentId::new("a"), "compute", move |_ctx: String| {
call_count += 1;
let count = call_count;
async move {
if count == 1 {
"Thought: use calc\nAction: calc {}".into()
} else {
"Thought: done\nAction: FINAL_ANSWER result".into()
}
}
})
.await
.unwrap();
assert!(session.step_count() >= 1);
}
#[tokio::test]
async fn test_run_agent_with_graph_relationship_lookup() {
let graph = GraphStore::new();
graph.add_entity(Entity::new("a", "X")).unwrap();
graph.add_entity(Entity::new("b", "Y")).unwrap();
graph
.add_relationship(Relationship::new("a", "b", "LINKS", 1.0))
.unwrap();
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_graph(graph)
.build();
let session = runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await
.unwrap();
assert_eq!(session.graph_lookups, 2); }
#[tokio::test]
async fn test_metrics_active_sessions_decrements_after_run() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await
.unwrap();
assert_eq!(runtime.metrics().active_sessions(), 0);
}
#[tokio::test]
async fn test_metrics_total_sessions_increments() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await
.unwrap();
runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await
.unwrap();
assert_eq!(runtime.metrics().total_sessions(), 2);
}
#[tokio::test]
async fn test_metrics_backpressure_shed_increments_on_shed() {
let guard = BackpressureGuard::new(1).unwrap();
guard.try_acquire().unwrap();
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_backpressure(guard)
.build();
let _ = runtime
.run_agent(AgentId::new("a"), "prompt", final_answer_infer)
.await;
assert_eq!(runtime.metrics().backpressure_shed_count(), 1);
}
#[tokio::test]
async fn test_metrics_memory_recall_count_increments() {
let store = EpisodicStore::new();
let agent = AgentId::new("a");
store.add_episode(agent.clone(), "fact", 0.9).unwrap();
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_memory(store)
.build();
runtime
.run_agent(agent, "prompt", final_answer_infer)
.await
.unwrap();
assert_eq!(runtime.metrics().memory_recall_count(), 1);
}
#[tokio::test]
async fn test_agent_config_max_memory_tokens_limits_injection() {
let store = EpisodicStore::new();
let agent = AgentId::new("budget-agent");
for i in 0..5 {
let content = format!("{:0>100}", i); store.add_episode(agent.clone(), content, 0.9).unwrap();
}
let cfg = AgentConfig::new(5, "test").with_max_memory_tokens(10);
let runtime = AgentRuntime::builder()
.with_agent_config(cfg)
.with_memory(store)
.build();
let session = runtime
.run_agent(agent, "prompt", final_answer_infer)
.await
.unwrap();
assert!(
session.memory_hits <= 1,
"expected at most 1 memory hit with tight token budget, got {}",
session.memory_hits
);
}
#[tokio::test]
async fn test_working_memory_injected_into_prompt() {
let wm = WorkingMemory::new(10).unwrap();
wm.set("task", "write tests").unwrap();
wm.set("status", "in progress").unwrap();
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_working_memory(wm)
.build();
let mut captured_ctx: Option<String> = None;
let captured_ref = &mut captured_ctx;
runtime
.run_agent(AgentId::new("a"), "do stuff", |ctx: String| {
*captured_ref = Some(ctx.clone());
async move { "Thought: done\nAction: FINAL_ANSWER ok".to_string() }
})
.await
.unwrap();
let ctx = captured_ctx.expect("infer should have been called");
assert!(
ctx.contains("Current working state:"),
"expected working memory injection in context, got: {ctx}"
);
assert!(ctx.contains("task: write tests"));
assert!(ctx.contains("status: in progress"));
}
#[tokio::test]
async fn test_token_budget_zero_returns_no_memories() {
let store = EpisodicStore::new();
let agent = AgentId::new("budget-agent");
store.add_episode(agent.clone(), "short", 0.9).unwrap();
let mut config = AgentConfig::new(5, "test-model");
config.max_memory_tokens = Some(0);
config.max_memory_recalls = 10;
let runtime = AgentRuntime::builder()
.with_memory(store)
.with_agent_config(config)
.build();
let steps = runtime
.run_agent(
agent,
"test",
|_ctx| async { "Thought: ok\nAction: FINAL_ANSWER done".to_string() },
)
.await
.unwrap();
assert_eq!(steps.steps.len(), 1);
}
#[tokio::test]
async fn test_token_budget_smaller_than_smallest_item_returns_no_memories() {
let store = EpisodicStore::new();
let agent = AgentId::new("budget-agent2");
store
.add_episode(agent.clone(), "a".repeat(40), 0.9)
.unwrap();
let mut config = AgentConfig::new(5, "test-model");
config.max_memory_tokens = Some(1);
config.max_memory_recalls = 10;
let runtime = AgentRuntime::builder()
.with_memory(store)
.with_agent_config(config)
.build();
let session = runtime
.run_agent(
agent,
"test",
|_ctx| async { "Thought: ok\nAction: FINAL_ANSWER done".to_string() },
)
.await
.unwrap();
assert_eq!(session.memory_hits, 0);
}
#[tokio::test]
async fn test_agent_runtime_quick_runs_agent() {
let runtime = AgentRuntime::quick(5, "test-model");
let agent = AgentId::new("quick-agent");
let session = runtime
.run_agent(agent, "hello", |_ctx| async {
"Thought: done\nAction: FINAL_ANSWER ok".to_string()
})
.await
.unwrap();
assert_eq!(session.step_count(), 1);
}
#[test]
fn test_final_answer_extracts_text() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![ReActStep {
thought: "done".into(),
action: "FINAL_ANSWER Paris".into(),
observation: "".into(),
step_duration_ms: 0,
}],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(session.final_answer(), Some("Paris".to_string()));
}
#[test]
fn test_final_answer_returns_none_without_final_step() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![ReActStep {
thought: "thinking".into(),
action: "search {}".into(),
observation: "result".into(),
step_duration_ms: 0,
}],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(session.final_answer(), None);
let empty_session = AgentSession {
session_id: "s2".into(),
agent_id: AgentId::new("a"),
steps: vec![],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(empty_session.final_answer(), None);
}
#[test]
fn test_all_actions_returns_actions_in_order() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![
ReActStep::new("think1", "search {}", "result"),
ReActStep::new("think2", "FINAL_ANSWER done", ""),
],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 10,
checkpoint_errors: vec![],
};
assert_eq!(session.all_actions(), vec!["search {}", "FINAL_ANSWER done"]);
}
#[test]
fn test_has_checkpoint_errors_false_when_empty() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert!(!session.has_checkpoint_errors());
}
#[test]
fn test_has_checkpoint_errors_true_when_non_empty() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec!["err".into()],
};
assert!(session.has_checkpoint_errors());
}
#[test]
fn test_memory_hit_rate_zero_with_no_steps() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![],
memory_hits: 5,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(session.memory_hit_rate(), 0.0);
}
#[test]
fn test_memory_hit_rate_correct_proportion() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![
ReActStep::new("t", "a", "o"),
ReActStep::new("t", "a", "o"),
ReActStep::new("t", "a", "o"),
ReActStep::new("t", "a", "o"),
],
memory_hits: 2,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert!((session.memory_hit_rate() - 0.5).abs() < 1e-9);
}
#[test]
fn test_filter_tool_call_steps_excludes_final_answer() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![
ReActStep::new("t1", "search {}", "res"),
ReActStep::new("t2", "FINAL_ANSWER done", ""),
],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
let tool_steps = session.filter_tool_call_steps();
assert_eq!(tool_steps.len(), 1);
assert_eq!(tool_steps[0].action, "search {}");
}
#[test]
fn test_slowest_step_index() {
let mut s0 = ReActStep::new("t", "a", "o");
s0.step_duration_ms = 5;
let mut s1 = ReActStep::new("t", "a", "o");
s1.step_duration_ms = 100;
let mut s2 = ReActStep::new("t", "a", "o");
s2.step_duration_ms = 10;
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![s0, s1, s2],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(session.slowest_step_index(), Some(1));
assert_eq!(session.fastest_step_index(), Some(0));
}
#[test]
fn test_slowest_step_index_none_when_empty() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(session.slowest_step_index(), None);
assert_eq!(session.fastest_step_index(), None);
}
#[test]
fn test_last_step_returns_last() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![
ReActStep::new("t1", "a1", "o1"),
ReActStep::new("t2", "FINAL_ANSWER done", ""),
],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(session.last_step().map(|s| s.action.as_str()), Some("FINAL_ANSWER done"));
}
#[test]
fn test_last_step_none_when_empty() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert!(session.last_step().is_none());
}
#[test]
fn test_step_at_returns_correct_step() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![
ReActStep::new("t0", "a0", "o0"),
ReActStep::new("t1", "a1", "o1"),
],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(session.step_at(1).map(|s| s.thought.as_str()), Some("t1"));
assert!(session.step_at(99).is_none());
}
#[test]
fn test_failed_steps_returns_steps_with_error_observation() {
use crate::agent::ReActStep;
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![
ReActStep::new("t", "tool_a {}", r#"{"error":"bad input","ok":false}"#),
ReActStep::new("t", "tool_b {}", r#"{"result":"ok","ok":true}"#),
],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
let failed = session.failed_steps();
assert_eq!(failed.len(), 1);
assert!(failed[0].observation.contains("bad input"));
}
#[test]
fn test_failed_steps_empty_when_no_errors() {
use crate::agent::ReActStep;
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![ReActStep::new("t", "FINAL_ANSWER done", "")],
memory_hits: 0,
graph_lookups: 0,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert!(session.failed_steps().is_empty());
}
fn make_step(thought: &str, action: &str, observation: &str) -> ReActStep {
ReActStep::new(thought, action, observation)
}
fn make_session(steps: Vec<ReActStep>, duration_ms: u64) -> AgentSession {
AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps,
memory_hits: 0,
graph_lookups: 0,
duration_ms,
checkpoint_errors: vec![],
}
}
#[test]
fn test_step_count_returns_number_of_steps() {
let s = make_session(vec![ReActStep::new("t", "a", "o"), ReActStep::new("t", "a", "o")], 0);
assert_eq!(s.step_count(), 2);
}
#[test]
fn test_is_empty_true_for_no_steps() {
let s = make_session(vec![], 0);
assert!(s.is_empty());
}
#[test]
fn test_is_empty_false_with_steps() {
let s = make_session(vec![ReActStep::new("t", "a", "o")], 0);
assert!(!s.is_empty());
}
#[test]
fn test_is_successful_true_with_final_answer() {
let s = make_session(vec![ReActStep::new("t", "FINAL_ANSWER yes", "")], 0);
assert!(s.is_successful());
}
#[test]
fn test_is_successful_false_without_final_answer() {
let s = make_session(vec![ReActStep::new("t", "search {}", "result")], 0);
assert!(!s.is_successful());
}
#[test]
fn test_elapsed_returns_duration_from_duration_ms() {
let s = make_session(vec![], 500);
assert_eq!(s.elapsed(), std::time::Duration::from_millis(500));
}
#[test]
fn test_tool_calls_made_excludes_final_answer() {
let s = make_session(vec![
ReActStep::new("t", "search {}", "res"),
ReActStep::new("t", "lookup {}", "res"),
ReActStep::new("t", "FINAL_ANSWER done", ""),
], 0);
assert_eq!(s.tool_calls_made(), 2);
}
#[test]
fn test_total_step_duration_ms_sums_all_steps() {
let mut s1 = ReActStep::new("t", "a", "o"); s1.step_duration_ms = 10;
let mut s2 = ReActStep::new("t", "a", "o"); s2.step_duration_ms = 30;
let s = make_session(vec![s1, s2], 0);
assert_eq!(s.total_step_duration_ms(), 40);
}
#[test]
fn test_average_step_duration_ms() {
let mut s1 = ReActStep::new("t", "a", "o"); s1.step_duration_ms = 20;
let mut s2 = ReActStep::new("t", "a", "o"); s2.step_duration_ms = 40;
let s = make_session(vec![s1, s2], 0);
assert_eq!(s.average_step_duration_ms(), 30);
}
#[test]
fn test_all_thoughts_returns_thoughts_in_order() {
let s = make_session(vec![
ReActStep::new("first thought", "a1", "o1"),
ReActStep::new("second thought", "a2", "o2"),
], 0);
assert_eq!(s.all_thoughts(), vec!["first thought", "second thought"]);
}
#[test]
fn test_all_observations_returns_observations_in_order() {
let s = make_session(vec![
ReActStep::new("t1", "a1", "obs one"),
ReActStep::new("t2", "a2", "obs two"),
], 0);
assert_eq!(s.all_observations(), vec!["obs one", "obs two"]);
}
#[test]
fn test_observations_matching_finds_matching_steps() {
let s = make_session(vec![
ReActStep::new("t1", "a1", "found the answer"),
ReActStep::new("t2", "a2", "nothing relevant"),
], 0);
let matching = s.observations_matching("answer");
assert_eq!(matching.len(), 1);
assert!(matching[0].observation.contains("answer"));
}
#[test]
fn test_first_step_returns_first() {
let s = make_session(vec![
ReActStep::new("first", "a1", "o1"),
ReActStep::new("second", "a2", "o2"),
], 0);
assert_eq!(s.first_step().map(|s| s.thought.as_str()), Some("first"));
}
#[test]
fn test_first_step_none_when_empty() {
let s = make_session(vec![], 0);
assert!(s.first_step().is_none());
}
#[test]
fn test_graph_lookup_count_returns_field() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![],
memory_hits: 0,
graph_lookups: 7,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert_eq!(session.graph_lookup_count(), 7usize);
}
#[test]
fn test_action_counts_counts_each_action() {
let session = make_session(
vec![
ReActStep::new("t1", "search", "r1"),
ReActStep::new("t2", "search", "r2"),
ReActStep::new("t3", "FINAL_ANSWER", "done"),
],
0,
);
let counts = session.action_counts();
assert_eq!(counts.get("search").copied().unwrap_or(0), 2);
assert_eq!(counts.get("FINAL_ANSWER").copied().unwrap_or(0), 1);
}
#[test]
fn test_unique_actions_returns_sorted_deduped() {
let session = make_session(
vec![
ReActStep::new("t", "b_action", "r"),
ReActStep::new("t", "a_action", "r"),
ReActStep::new("t", "b_action", "r"),
],
0,
);
assert_eq!(session.unique_actions(), vec!["a_action", "b_action"]);
}
#[test]
fn test_unique_actions_empty_when_no_steps() {
let session = make_session(vec![], 0);
assert!(session.unique_actions().is_empty());
}
#[test]
fn test_total_latency_ms_sums_step_durations() {
let mut steps = vec![
ReActStep::new("t1", "a1", "o1"),
ReActStep::new("t2", "a2", "o2"),
];
steps[0].step_duration_ms = 100;
steps[1].step_duration_ms = 250;
let session = make_session(steps, 350);
assert_eq!(session.total_latency_ms(), 350);
}
#[test]
fn test_total_latency_ms_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_latency_ms(), 0);
}
#[test]
fn test_action_sequence_returns_actions_in_order() {
let session = make_session(
vec![
ReActStep::new("t", "search", "r"),
ReActStep::new("t", "FINAL_ANSWER", "done"),
],
0,
);
assert_eq!(session.action_sequence(), vec!["search", "FINAL_ANSWER"]);
}
#[test]
fn test_has_action_returns_true_for_present_action() {
let session = make_session(
vec![ReActStep::new("t", "search", "r")],
0,
);
assert!(session.has_action("search"));
}
#[test]
fn test_has_action_returns_false_for_absent_action() {
let session = make_session(
vec![ReActStep::new("t", "search", "r")],
0,
);
assert!(!session.has_action("compute"));
}
#[test]
fn test_thought_at_returns_thought_for_valid_index() {
let session = make_session(
vec![
ReActStep::new("first thought", "a1", "r1"),
ReActStep::new("second thought", "a2", "r2"),
],
0,
);
assert_eq!(session.thought_at(0), Some("first thought"));
assert_eq!(session.thought_at(1), Some("second thought"));
}
#[test]
fn test_thought_at_returns_none_for_out_of_bounds_index() {
let session = make_session(vec![ReActStep::new("t", "a", "r")], 0);
assert!(session.thought_at(99).is_none());
}
#[test]
fn test_step_count_for_action_counts_correctly() {
let session = make_session(
vec![
ReActStep::new("t", "search", "r1"),
ReActStep::new("t", "search", "r2"),
ReActStep::new("t", "FINAL_ANSWER", "done"),
],
0,
);
assert_eq!(session.step_count_for_action("search"), 2);
assert_eq!(session.step_count_for_action("FINAL_ANSWER"), 1);
assert_eq!(session.step_count_for_action("unknown"), 0);
}
#[test]
fn test_observations_returns_all_observation_strings() {
let session = make_session(
vec![
ReActStep::new("t1", "a", "obs_one"),
ReActStep::new("t2", "b", "obs_two"),
],
0,
);
let obs = session.observations();
assert_eq!(obs, vec!["obs_one", "obs_two"]);
}
#[test]
fn test_observations_empty_for_no_steps() {
let session = make_session(vec![], 0);
assert!(session.observations().is_empty());
}
#[test]
fn test_unique_tools_used_deduplicates_actions() {
let session = make_session(
vec![
ReActStep::new("t", "search", "r1"),
ReActStep::new("t", "lookup", "r2"),
ReActStep::new("t", "search", "r3"),
],
0,
);
let tools = session.unique_tools_used();
assert_eq!(tools.len(), 2);
assert!(tools.contains(&"search".to_string()));
assert!(tools.contains(&"lookup".to_string()));
}
#[test]
fn test_unique_tools_used_excludes_final_answer() {
let session = make_session(
vec![
ReActStep::new("t", "search", "r1"),
ReActStep::new("t", "FINAL_ANSWER: done", "r2"),
],
0,
);
let tools = session.unique_tools_used();
assert_eq!(tools.len(), 1);
assert!(tools.contains(&"search".to_string()));
}
#[test]
fn test_unique_tools_used_empty_for_no_steps() {
let session = make_session(vec![], 0);
assert!(session.unique_tools_used().is_empty());
}
#[test]
fn test_avg_step_duration_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.avg_step_duration_ms() - 0.0).abs() < 1e-9);
}
#[test]
fn test_avg_step_duration_single_step() {
let mut step = ReActStep::new("t", "a", "r");
step.step_duration_ms = 100;
let session = make_session(vec![step], 0);
assert!((session.avg_step_duration_ms() - 100.0).abs() < 1e-9);
}
#[test]
fn test_avg_step_duration_multiple_steps() {
let mut s1 = ReActStep::new("t1", "a", "r");
s1.step_duration_ms = 100;
let mut s2 = ReActStep::new("t2", "b", "r");
s2.step_duration_ms = 200;
let session = make_session(vec![s1, s2], 0);
assert!((session.avg_step_duration_ms() - 150.0).abs() < 1e-9);
}
#[test]
fn test_longest_step_returns_step_with_max_duration() {
let mut s1 = ReActStep::new("t1", "a", "r");
s1.step_duration_ms = 50;
let mut s2 = ReActStep::new("t2", "b", "r");
s2.step_duration_ms = 200;
let session = make_session(vec![s1, s2], 0);
assert_eq!(session.longest_step().map(|s| s.step_duration_ms), Some(200));
}
#[test]
fn test_longest_step_returns_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.longest_step().is_none());
}
#[test]
fn test_shortest_step_returns_step_with_min_duration() {
let mut s1 = ReActStep::new("t1", "a", "r");
s1.step_duration_ms = 50;
let mut s2 = ReActStep::new("t2", "b", "r");
s2.step_duration_ms = 200;
let session = make_session(vec![s1, s2], 0);
assert_eq!(session.shortest_step().map(|s| s.step_duration_ms), Some(50));
}
#[test]
fn test_first_thought_returns_thought_from_first_step() {
let session = make_session(
vec![
ReActStep::new("alpha", "a1", "r1"),
ReActStep::new("beta", "a2", "r2"),
],
0,
);
assert_eq!(session.first_thought(), Some("alpha"));
}
#[test]
fn test_last_thought_returns_thought_from_last_step() {
let session = make_session(
vec![
ReActStep::new("alpha", "a1", "r1"),
ReActStep::new("beta", "a2", "r2"),
],
0,
);
assert_eq!(session.last_thought(), Some("beta"));
}
#[test]
fn test_first_thought_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.first_thought().is_none());
}
#[test]
fn test_last_thought_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.last_thought().is_none());
}
#[test]
fn test_first_action_returns_action_from_first_step() {
let session = make_session(
vec![
ReActStep::new("t1", "search", "r1"),
ReActStep::new("t2", "FINAL_ANSWER", "r2"),
],
0,
);
assert_eq!(session.first_action(), Some("search"));
}
#[test]
fn test_last_action_returns_action_from_last_step() {
let session = make_session(
vec![
ReActStep::new("t1", "search", "r1"),
ReActStep::new("t2", "FINAL_ANSWER", "r2"),
],
0,
);
assert_eq!(session.last_action(), Some("FINAL_ANSWER"));
}
#[test]
fn test_first_action_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.first_action().is_none());
}
#[test]
fn test_last_action_equals_first_action_for_single_step() {
let session = make_session(vec![ReActStep::new("t", "calc", "r")], 0);
assert_eq!(session.first_action(), session.last_action());
}
#[test]
fn test_checkpoint_error_count_zero_when_none() {
let session = make_session(vec![], 0);
assert_eq!(session.checkpoint_error_count(), 0);
}
#[test]
fn test_checkpoint_error_count_reflects_errors() {
let mut session = make_session(vec![], 0);
session.checkpoint_errors.push("save failed".into());
session.checkpoint_errors.push("disk full".into());
assert_eq!(session.checkpoint_error_count(), 2);
}
#[test]
fn test_failed_tool_call_count_zero_when_no_errors() {
let step = ReActStep::new("think", "search", "results found");
let session = make_session(vec![step], 0);
assert_eq!(session.failed_tool_call_count(), 0);
}
#[test]
fn test_failed_tool_call_count_matches_failed_steps() {
let ok_step = ReActStep::new("ok", "search", "all good");
let err_step = ReActStep::new("err", "lookup", "{\"error\": \"not found\"}");
let session = make_session(vec![ok_step, err_step], 0);
assert_eq!(session.failed_tool_call_count(), session.failed_steps().len());
assert_eq!(session.failed_tool_call_count(), 1);
}
#[test]
fn test_failed_tool_call_count_counts_all_errors() {
let err1 = ReActStep::new("e1", "a", "{\"error\": \"bad\"}");
let err2 = ReActStep::new("e2", "b", "some \"error\" text");
let ok = ReActStep::new("ok", "c", "success");
let session = make_session(vec![err1, err2, ok], 0);
assert_eq!(session.failed_tool_call_count(), 2);
}
#[test]
fn test_total_memory_hits_returns_memory_hits_field() {
let mut session = make_session(vec![], 0);
session.memory_hits = 7;
assert_eq!(session.total_memory_hits(), 7);
}
#[test]
fn test_total_memory_hits_zero_by_default() {
let session = make_session(vec![], 0);
assert_eq!(session.total_memory_hits(), 0);
}
#[test]
fn test_action_diversity_all_unique_is_one() {
let steps = vec![
ReActStep::new("t", "search", "r"),
ReActStep::new("t", "calc", "r"),
ReActStep::new("t", "lookup", "r"),
];
let session = make_session(steps, 0);
assert!((session.action_diversity() - 1.0).abs() < 1e-9);
}
#[test]
fn test_action_diversity_all_same_is_fraction() {
let steps = vec![
ReActStep::new("t", "search", "r"),
ReActStep::new("t", "search", "r"),
ReActStep::new("t", "search", "r"),
];
let session = make_session(steps, 0);
assert!((session.action_diversity() - 1.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_action_diversity_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.action_diversity() - 0.0).abs() < 1e-9);
}
#[test]
fn test_last_n_steps_returns_last_n() {
let steps = vec![
ReActStep::new("t1", "a", "r1"),
ReActStep::new("t2", "b", "r2"),
ReActStep::new("t3", "c", "r3"),
];
let session = make_session(steps, 0);
let last2 = session.last_n_steps(2);
assert_eq!(last2.len(), 2);
assert_eq!(last2[0].action, "b");
assert_eq!(last2[1].action, "c");
}
#[test]
fn test_last_n_steps_returns_all_when_n_exceeds_count() {
let steps = vec![
ReActStep::new("t1", "a", "r1"),
ReActStep::new("t2", "b", "r2"),
];
let session = make_session(steps, 0);
assert_eq!(session.last_n_steps(10).len(), 2);
}
#[test]
fn test_last_n_steps_empty_for_no_steps() {
let session = make_session(vec![], 0);
assert!(session.last_n_steps(3).is_empty());
}
#[test]
fn test_last_n_steps_zero_returns_empty() {
let steps = vec![ReActStep::new("t1", "a", "r1")];
let session = make_session(steps, 0);
assert!(session.last_n_steps(0).is_empty());
}
#[test]
fn test_observation_count_counts_non_empty() {
let steps = vec![
ReActStep::new("t", "a", "result"),
ReActStep::new("t", "b", ""),
ReActStep::new("t", "c", "data"),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_count(), 2);
}
#[test]
fn test_observation_count_zero_when_all_empty() {
let steps = vec![
ReActStep::new("t", "a", ""),
ReActStep::new("t", "b", ""),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_count(), 0);
}
#[test]
fn test_steps_without_observation_counts_empty_obs() {
let steps = vec![
ReActStep::new("t", "a", ""),
ReActStep::new("t", "b", "data"),
ReActStep::new("t", "c", ""),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_without_observation(), 2);
}
#[test]
fn test_steps_without_observation_zero_when_all_filled() {
let steps = vec![
ReActStep::new("t", "a", "r1"),
ReActStep::new("t", "b", "r2"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_without_observation(), 0);
}
#[test]
fn test_throughput_steps_per_sec_correct_ratio() {
let steps = vec![
ReActStep::new("t", "a", "r"),
ReActStep::new("t", "b", "r"),
];
let session = make_session(steps, 1000);
assert!((session.throughput_steps_per_sec() - 2.0).abs() < 1e-9);
}
#[test]
fn test_throughput_steps_per_sec_zero_when_no_duration() {
let steps = vec![ReActStep::new("t", "a", "r")];
let session = make_session(steps, 0);
assert!((session.throughput_steps_per_sec() - 0.0).abs() < 1e-9);
}
#[test]
fn test_thoughts_containing_returns_matching_steps() {
let session = make_session(
vec![
ReActStep::new("I need to search", "search", "found"),
ReActStep::new("Let me calculate", "calc", "done"),
ReActStep::new("search again", "search", "ok"),
],
0,
);
let matches = session.thoughts_containing("search");
assert_eq!(matches.len(), 2);
}
#[test]
fn test_thoughts_containing_is_case_insensitive() {
let session = make_session(
vec![ReActStep::new("SEARCH the web", "search", "r")],
0,
);
assert_eq!(session.thoughts_containing("search").len(), 1);
}
#[test]
fn test_thoughts_containing_empty_when_no_match() {
let session = make_session(vec![ReActStep::new("think about x", "a", "r")], 0);
assert!(session.thoughts_containing("zebra").is_empty());
}
#[test]
fn test_step_durations_ms_returns_all_durations() {
let mut steps = vec![
ReActStep::new("t", "a", "r"),
ReActStep::new("t", "b", "r"),
];
steps[0].step_duration_ms = 100;
steps[1].step_duration_ms = 200;
let session = make_session(steps, 300);
assert_eq!(session.step_durations_ms(), vec![100, 200]);
}
#[test]
fn test_fastest_step_index_returns_index_of_shortest_step() {
let mut steps = vec![
ReActStep::new("t", "a", "r"),
ReActStep::new("t", "b", "r"),
ReActStep::new("t", "c", "r"),
];
steps[0].step_duration_ms = 300;
steps[1].step_duration_ms = 50;
steps[2].step_duration_ms = 200;
let session = make_session(steps, 550);
assert_eq!(session.fastest_step_index(), Some(1));
}
#[test]
fn test_fastest_step_index_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.fastest_step_index().is_none());
}
#[test]
fn test_most_used_action_returns_most_frequent() {
let steps = vec![
ReActStep::new("t", "search", "r"),
ReActStep::new("t", "calc", "r"),
ReActStep::new("t", "search", "r"),
];
let session = make_session(steps, 0);
assert_eq!(session.most_used_action().as_deref(), Some("search"));
}
#[test]
fn test_most_used_action_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.most_used_action().is_none());
}
#[test]
fn test_graph_lookup_rate_correct_ratio() {
let steps = vec![
ReActStep::new("t", "a", "r"),
ReActStep::new("t", "b", "r"),
ReActStep::new("t", "c", "r"),
ReActStep::new("t", "d", "r"),
];
let mut session = make_session(steps, 0);
session.graph_lookups = 2;
assert!((session.graph_lookup_rate() - 0.5).abs() < 1e-9);
}
#[test]
fn test_graph_lookup_rate_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.graph_lookup_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_has_tool_failures_false_when_no_errors() {
let steps = vec![
make_step("t", "action1", "ok"),
make_step("t", "action2", "done"),
];
let session = make_session(steps, 0);
assert!(!session.has_tool_failures());
}
#[test]
fn test_has_tool_failures_true_when_error_observation() {
let steps = vec![
make_step("t", "action1", "{\"error\": \"timeout\"}"),
];
let session = make_session(steps, 0);
assert!(session.has_tool_failures());
}
#[test]
fn test_tool_call_rate_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.tool_call_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_tool_call_rate_correct_ratio() {
let steps = vec![
make_step("t", "tool_action", "ok"),
make_step("t", "FINAL_ANSWER: done", ""),
make_step("t", "another_tool", "ok"),
];
let session = make_session(steps, 0);
assert!((session.tool_call_rate() - 2.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_step_success_rate_one_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.step_success_rate() - 1.0).abs() < 1e-9);
}
#[test]
fn test_step_success_rate_less_than_one_when_failures() {
let steps = vec![
make_step("t", "act", "{\"error\": \"fail\"}"),
make_step("t", "act", "success"),
];
let session = make_session(steps, 0);
assert!((session.step_success_rate() - 0.5).abs() < 1e-9);
}
#[test]
fn test_avg_step_duration_ms_zero_for_empty() {
let session = make_session(vec![], 0);
assert!((session.avg_step_duration_ms() - 0.0).abs() < 1e-9);
}
#[test]
fn test_avg_step_duration_ms_correct_mean() {
let mut s1 = make_step("t", "a", "o");
s1.step_duration_ms = 100;
let mut s2 = make_step("t", "b", "o");
s2.step_duration_ms = 200;
let session = make_session(vec![s1, s2], 0);
assert!((session.avg_step_duration_ms() - 150.0).abs() < 1e-9);
}
#[test]
fn test_longest_step_none_for_empty() {
let session = make_session(vec![], 0);
assert!(session.longest_step().is_none());
}
#[test]
fn test_longest_step_middle_has_max_duration() {
let mut s1 = make_step("t", "a", "o");
s1.step_duration_ms = 10;
let mut s2 = make_step("t", "b", "o");
s2.step_duration_ms = 500;
let mut s3 = make_step("t", "c", "o");
s3.step_duration_ms = 20;
let session = make_session(vec![s1, s2, s3], 0);
assert_eq!(session.longest_step().unwrap().action, "b");
}
#[test]
fn test_unique_tools_used_deduplicates_and_sorts() {
let steps = vec![
make_step("t", "search", "o"),
make_step("t", "lookup", "o"),
make_step("t", "search", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.unique_tools_used(), vec!["lookup", "search"]);
}
#[test]
fn test_all_thoughts_collects_in_order() {
let steps = vec![make_step("think1", "a", "o"), make_step("think2", "b", "o")];
let session = make_session(steps, 0);
assert_eq!(session.all_thoughts(), vec!["think1", "think2"]);
}
#[test]
fn test_all_actions_collects_in_order() {
let steps = vec![make_step("t", "act1", "o"), make_step("t", "act2", "o")];
let session = make_session(steps, 0);
assert_eq!(session.all_actions(), vec!["act1", "act2"]);
}
#[test]
fn test_all_observations_collects_in_order() {
let steps = vec![make_step("t", "a", "obs1"), make_step("t", "b", "obs2")];
let session = make_session(steps, 0);
assert_eq!(session.all_observations(), vec!["obs1", "obs2"]);
}
#[test]
fn test_action_counts_returns_frequency_map() {
let steps = vec![
make_step("t", "search", "o"),
make_step("t", "lookup", "o"),
make_step("t", "search", "o"),
];
let session = make_session(steps, 0);
let counts = session.action_counts();
assert_eq!(counts["search"], 2);
assert_eq!(counts["lookup"], 1);
}
#[test]
fn test_unique_actions_three_with_repeat_yields_two() {
let steps = vec![
make_step("t", "beta", "o"),
make_step("t", "alpha", "o"),
make_step("t", "beta", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.unique_actions(), vec!["alpha", "beta"]);
}
#[test]
fn test_action_diversity_zero_for_empty() {
let session = make_session(vec![], 0);
assert!((session.action_diversity() - 0.0).abs() < 1e-9);
}
#[test]
fn test_action_diversity_one_when_all_actions_unique() {
let steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
];
let session = make_session(steps, 0);
assert!((session.action_diversity() - 1.0).abs() < 1e-9);
}
#[test]
fn test_action_diversity_fraction_when_repeated() {
let steps = vec![
make_step("t", "x", "o"),
make_step("t", "x", "o"),
];
let session = make_session(steps, 0);
assert!((session.action_diversity() - 0.5).abs() < 1e-9);
}
#[test]
fn test_has_checkpoint_errors_false_for_new_session() {
let session = make_session(vec![], 0);
assert!(!session.has_checkpoint_errors());
}
#[test]
fn test_has_checkpoint_errors_true_when_errors_present() {
let mut session = make_session(vec![], 0);
session.checkpoint_errors.push("err1".to_string());
assert!(session.has_checkpoint_errors());
}
#[test]
fn test_graph_lookup_count_returns_raw_value() {
let mut session = make_session(vec![make_step("t", "a", "o")], 0);
session.graph_lookups = 7;
assert_eq!(session.graph_lookup_count(), 7);
}
#[test]
fn test_memory_hit_rate_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.memory_hit_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_memory_hit_rate_correct_ratio() {
let steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
make_step("t", "d", "o"),
];
let mut session = make_session(steps, 0);
session.memory_hits = 2;
assert!((session.memory_hit_rate() - 0.5).abs() < 1e-9);
}
#[test]
fn test_total_memory_hits_returns_raw_value() {
let mut session = make_session(vec![], 0);
session.memory_hits = 13;
assert_eq!(session.total_memory_hits(), 13);
}
#[cfg(feature = "memory")]
#[test]
fn test_has_memory_false_without_memory() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
assert!(!runtime.has_memory());
}
#[cfg(feature = "memory")]
#[test]
fn test_has_memory_true_with_memory() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_memory(EpisodicStore::new())
.build();
assert!(runtime.has_memory());
}
#[cfg(feature = "graph")]
#[test]
fn test_has_graph_false_without_graph() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
assert!(!runtime.has_graph());
}
#[cfg(feature = "graph")]
#[test]
fn test_has_graph_true_with_graph() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_graph(GraphStore::new())
.build();
assert!(runtime.has_graph());
}
#[cfg(feature = "memory")]
#[test]
fn test_has_working_memory_false_without_working_memory() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.build();
assert!(!runtime.has_working_memory());
}
#[cfg(feature = "memory")]
#[test]
fn test_has_working_memory_true_with_working_memory() {
let runtime = AgentRuntime::builder()
.with_agent_config(simple_config())
.with_working_memory(WorkingMemory::new(10).unwrap())
.build();
assert!(runtime.has_working_memory());
}
#[test]
fn test_last_observation_returns_most_recent_nonempty() {
let steps = vec![
make_step("t1", "act", "first obs"),
make_step("t2", "act", ""),
make_step("t3", "act", "last obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.last_observation(), Some("last obs"));
}
#[test]
fn test_last_observation_skips_empty_steps() {
let steps = vec![
make_step("t1", "act", "only obs"),
make_step("t2", "act", ""),
];
let session = make_session(steps, 0);
assert_eq!(session.last_observation(), Some("only obs"));
}
#[test]
fn test_last_observation_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.last_observation().is_none());
}
#[test]
fn test_thought_count_counts_nonempty_thoughts() {
let steps = vec![
make_step("think", "act", "obs"),
make_step("", "act", "obs"),
make_step("think again", "act", "obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.thought_count(), 2);
}
#[test]
fn test_thought_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.thought_count(), 0);
}
#[test]
fn test_observation_rate_correct_fraction() {
let steps = vec![
make_step("t", "a", "obs"),
make_step("t", "a", ""),
make_step("t", "a", "obs"),
make_step("t", "a", ""),
];
let session = make_session(steps, 0);
assert!((session.observation_rate() - 0.5).abs() < 1e-9);
}
#[test]
fn test_observation_rate_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.observation_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_action_repetition_rate_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.action_repetition_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_action_repetition_rate_zero_for_single_step() {
let session = make_session(vec![make_step("t", "search", "r")], 0);
assert!((session.action_repetition_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_action_repetition_rate_one_when_all_same() {
let steps = vec![
make_step("t", "search", "r"),
make_step("t", "search", "r"),
make_step("t", "search", "r"),
];
let session = make_session(steps, 0);
assert!((session.action_repetition_rate() - 1.0).abs() < 1e-9);
}
#[test]
fn test_action_repetition_rate_partial_repeats() {
let steps = vec![
make_step("t", "search", "r"),
make_step("t", "search", "r"),
make_step("t", "calc", "r"),
];
let session = make_session(steps, 0);
assert!((session.action_repetition_rate() - 0.5).abs() < 1e-9);
}
#[test]
fn test_max_consecutive_failures_zero_for_no_errors() {
let steps = vec![make_step("t", "a", "ok"), make_step("t", "b", "done")];
let session = make_session(steps, 0);
assert_eq!(session.max_consecutive_failures(), 0);
}
#[test]
fn test_max_consecutive_failures_counts_run() {
let steps = vec![
make_step("t", "a", "ok"),
make_step("t", "b", r#"{"error":"x"}"#),
make_step("t", "c", r#"{"error":"y"}"#),
make_step("t", "d", "ok"),
];
let session = make_session(steps, 0);
assert_eq!(session.max_consecutive_failures(), 2);
}
#[test]
fn test_avg_thought_length_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.avg_thought_length() - 0.0).abs() < 1e-9);
}
#[test]
fn test_avg_thought_length_excludes_empty_thoughts() {
let steps = vec![
make_step("hello", "a", "r"), make_step("", "b", "r"), make_step("hi", "c", "r"), ];
let session = make_session(steps, 0);
assert!((session.avg_thought_length() - 3.5).abs() < 1e-9);
}
#[test]
fn test_last_n_observations_empty_session() {
let session = make_session(vec![], 0);
assert!(session.last_n_observations(3).is_empty());
}
#[test]
fn test_last_n_observations_returns_last_n_nonempty() {
let steps = vec![
make_step("t", "a", "obs1"),
make_step("t", "b", ""), make_step("t", "c", "obs2"),
make_step("t", "d", "obs3"),
];
let session = make_session(steps, 0);
let last2 = session.last_n_observations(2);
assert_eq!(last2, vec!["obs2", "obs3"]);
}
#[test]
fn test_last_n_observations_returns_all_when_fewer_than_n() {
let steps = vec![make_step("t", "a", "only")];
let session = make_session(steps, 0);
assert_eq!(session.last_n_observations(5), vec!["only"]);
}
#[test]
fn test_actions_in_window_empty_session() {
let session = make_session(vec![], 0);
assert!(session.actions_in_window(3).is_empty());
}
#[test]
fn test_actions_in_window_returns_last_n_steps() {
let steps = vec![
make_step("t", "alpha", "r"),
make_step("t", "beta", "r"),
make_step("t", "gamma", "r"),
];
let session = make_session(steps, 0);
let window = session.actions_in_window(2);
assert_eq!(window, vec!["beta", "gamma"]);
}
#[test]
fn test_actions_in_window_all_when_fewer_than_n() {
let steps = vec![make_step("t", "solo", "r")];
let session = make_session(steps, 0);
assert_eq!(session.actions_in_window(10), vec!["solo"]);
}
#[test]
fn test_observation_at_returns_correct_observation() {
let steps = vec![
make_step("t1", "a1", "obs-zero"),
make_step("t2", "a2", "obs-one"),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_at(0), Some("obs-zero"));
assert_eq!(session.observation_at(1), Some("obs-one"));
}
#[test]
fn test_observation_at_returns_none_out_of_bounds() {
let session = make_session(vec![], 0);
assert!(session.observation_at(0).is_none());
}
#[test]
fn test_action_at_returns_correct_action() {
let steps = vec![
make_step("t1", "first-action", "obs"),
make_step("t2", "second-action", "obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.action_at(0), Some("first-action"));
assert_eq!(session.action_at(1), Some("second-action"));
}
#[test]
fn test_action_at_returns_none_out_of_bounds() {
let session = make_session(vec![], 0);
assert!(session.action_at(5).is_none());
}
#[test]
fn test_has_graph_lookups_false_when_zero() {
let session = make_session(vec![], 0);
assert!(!session.has_graph_lookups());
}
#[test]
fn test_has_graph_lookups_true_when_positive() {
let session = AgentSession {
session_id: "s".into(),
agent_id: AgentId::new("a"),
steps: vec![],
memory_hits: 0,
graph_lookups: 5,
duration_ms: 0,
checkpoint_errors: vec![],
};
assert!(session.has_graph_lookups());
}
#[test]
fn test_consecutive_same_action_at_end_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.consecutive_same_action_at_end(), 0);
}
#[test]
fn test_consecutive_same_action_at_end_single_step() {
let steps = vec![make_step("t", "act", "obs")];
let session = make_session(steps, 0);
assert_eq!(session.consecutive_same_action_at_end(), 0);
}
#[test]
fn test_consecutive_same_action_at_end_two_same_at_end() {
let steps = vec![
make_step("t", "other", "obs"),
make_step("t", "repeat", "obs"),
make_step("t", "repeat", "obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.consecutive_same_action_at_end(), 1);
}
#[test]
fn test_consecutive_same_action_at_end_all_same() {
let steps = vec![
make_step("t", "same", "obs"),
make_step("t", "same", "obs"),
make_step("t", "same", "obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.consecutive_same_action_at_end(), 2);
}
#[test]
fn test_failure_rate_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.failure_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_failure_rate_zero_when_no_failures() {
let steps = vec![
make_step("t", "lookup", "ok"),
make_step("t", "search", "ok"),
];
let session = make_session(steps, 0);
assert!((session.failure_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_unique_action_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.unique_action_count(), 0);
}
#[test]
fn test_unique_action_count_counts_distinct_actions() {
let steps = vec![
make_step("t", "search", "r"),
make_step("t", "lookup", "r"),
make_step("t", "search", "r"), ];
let session = make_session(steps, 0);
assert_eq!(session.unique_action_count(), 2);
}
#[test]
fn test_total_thought_length_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_thought_length(), 0);
}
#[test]
fn test_total_thought_length_sums_all_thoughts() {
let steps = vec![
make_step("hi", "a", "r"), make_step("hello", "b", "r"), ];
let session = make_session(steps, 0);
assert_eq!(session.total_thought_length(), 7);
}
#[test]
fn test_longest_observation_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.longest_observation().is_none());
}
#[test]
fn test_longest_observation_returns_longest() {
let steps = vec![
make_step("t", "a", "short"),
make_step("t", "b", "a much longer observation"),
];
let session = make_session(steps, 0);
assert_eq!(session.longest_observation(), Some("a much longer observation"));
}
#[test]
fn test_steps_with_empty_observations_zero_when_all_filled() {
let steps = vec![make_step("t", "a", "obs"), make_step("t", "b", "obs2")];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_empty_observations(), 0);
}
#[test]
fn test_steps_with_empty_observations_counts_empty_ones() {
let steps = vec![
make_step("t", "a", ""), make_step("t", "b", "ok"),
make_step("t", "c", ""), ];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_empty_observations(), 2);
}
#[test]
fn test_min_thought_length_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.min_thought_length(), 0);
}
#[test]
fn test_min_thought_length_returns_shortest_non_empty() {
let steps = vec![
make_step("hi", "a", "r"), make_step("hello", "b", "r"), make_step("", "c", "r"), ];
let session = make_session(steps, 0);
assert_eq!(session.min_thought_length(), 2);
}
#[test]
fn test_observation_lengths_empty_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.observation_lengths().is_empty());
}
#[test]
fn test_observation_lengths_returns_lengths_in_order() {
let steps = vec![
make_step("t", "a", "hi"), make_step("t", "b", "hello"), ];
let session = make_session(steps, 0);
assert_eq!(session.observation_lengths(), vec![2, 5]);
}
#[test]
fn test_avg_observation_length_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.avg_observation_length() - 0.0).abs() < 1e-9);
}
#[test]
fn test_avg_observation_length_correct_mean() {
let steps = vec![
make_step("t", "a", "hi"), make_step("t", "b", "hello"), ];
let session = make_session(steps, 0);
assert!((session.avg_observation_length() - 3.5).abs() < 1e-9);
}
#[test]
fn test_duration_secs_converts_ms_to_seconds() {
let session = make_session(vec![], 7000);
assert_eq!(session.duration_secs(), 7);
}
#[test]
fn test_steps_above_thought_length_counts_qualifying_steps() {
let steps = vec![
make_step("hi", "a", "obs"),
make_step("a longer thought here", "b", "obs"),
make_step("medium thought", "c", "obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_above_thought_length(5), 2);
}
#[test]
fn test_has_final_answer_true_when_step_has_final_answer_action() {
let steps = vec![
make_step("think", "search", "result"),
make_step("done", "FINAL_ANSWER: 42", ""),
];
let session = make_session(steps, 0);
assert!(session.has_final_answer());
}
#[test]
fn test_has_final_answer_false_when_no_final_answer_step() {
let steps = vec![make_step("think", "search", "result")];
let session = make_session(steps, 0);
assert!(!session.has_final_answer());
}
#[test]
fn test_avg_action_length_correct_mean() {
let steps = vec![
make_step("t", "ab", "o"), make_step("t", "abcd", "o"), ];
let session = make_session(steps, 0);
assert!((session.avg_action_length() - 3.0).abs() < 1e-9);
}
#[test]
fn test_avg_action_length_empty_returns_zero() {
let session = make_session(vec![], 0);
assert_eq!(session.avg_action_length(), 0.0);
}
#[test]
fn test_thought_lengths_returns_lengths_in_order() {
let steps = vec![
make_step("hi", "a", "o"),
make_step("hello", "b", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.thought_lengths(), vec![2, 5]);
}
#[test]
fn test_most_common_action_returns_most_frequent() {
let steps = vec![
make_step("t", "search", "o"),
make_step("t", "search", "o"),
make_step("t", "other", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.most_common_action(), Some("search"));
}
#[test]
fn test_most_common_action_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.most_common_action().is_none());
}
#[test]
fn test_count_steps_with_action_counts_exact_matches() {
let steps = vec![
make_step("t", "search", "o"),
make_step("t", "search", "o"),
make_step("t", "other", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.count_steps_with_action("search"), 2);
assert_eq!(session.count_steps_with_action("other"), 1);
assert_eq!(session.count_steps_with_action("missing"), 0);
}
#[test]
fn test_thought_contains_count_counts_matching_steps() {
let steps = vec![
make_step("search for rust", "a", "o"),
make_step("think about python", "b", "o"),
make_step("rust is great", "c", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.thought_contains_count("rust"), 2);
assert_eq!(session.thought_contains_count("python"), 1);
assert_eq!(session.thought_contains_count("java"), 0);
}
#[test]
fn test_count_nonempty_thoughts_counts_steps_with_thoughts() {
let steps = vec![
make_step("hello", "a", "o"),
make_step("", "b", "o"),
make_step("world", "c", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.count_nonempty_thoughts(), 2);
}
#[test]
fn test_observation_contains_count_counts_matching_observations() {
let steps = vec![
make_step("t", "a", "result: success"),
make_step("t", "b", "result: failure"),
make_step("t", "c", "no match here"),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_contains_count("result"), 2);
assert_eq!(session.observation_contains_count("success"), 1);
}
#[test]
fn test_action_lengths_returns_byte_lengths_in_order() {
let steps = vec![
make_step("t", "ab", "o"),
make_step("t", "hello", "o"),
make_step("t", "", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.action_lengths(), vec![2, 5, 0]);
}
#[test]
fn test_action_lengths_empty_session_returns_empty_vec() {
let session = make_session(vec![], 0);
assert!(session.action_lengths().is_empty());
}
#[test]
fn test_step_success_count_excludes_failed_steps() {
let steps = vec![
make_step("t", "a", "ok"),
make_step("t", "b", "{\"error\": \"timeout\"}"),
make_step("t", "c", "ok"),
];
let session = make_session(steps, 0);
assert_eq!(session.step_success_count(), 2);
}
#[test]
fn test_step_success_count_all_success_when_no_failures() {
let steps = vec![make_step("t", "a", "ok"), make_step("t", "b", "ok")];
let session = make_session(steps, 0);
assert_eq!(session.step_success_count(), 2);
}
#[test]
fn test_longest_thought_returns_step_with_most_bytes() {
let steps = vec![
make_step("hi", "a", "o"),
make_step("hello world", "b", "o"),
make_step("hey", "c", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.longest_thought(), Some("hello world"));
}
#[test]
fn test_longest_thought_returns_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.longest_thought().is_none());
}
#[test]
fn test_shortest_action_returns_step_with_fewest_bytes() {
let steps = vec![
make_step("t", "search", "o"),
make_step("t", "go", "o"),
make_step("t", "lookup", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.shortest_action(), Some("go"));
}
#[test]
fn test_shortest_action_returns_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.shortest_action().is_none());
}
#[test]
fn test_first_step_action_returns_action_of_first_step() {
let steps = vec![
make_step("t", "first", "o"),
make_step("t", "second", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.first_step_action(), Some("first"));
}
#[test]
fn test_first_step_action_returns_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.first_step_action().is_none());
}
#[test]
fn test_last_step_action_returns_action_of_last_step() {
let steps = vec![
make_step("t", "first", "o"),
make_step("t", "last_one", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.last_step_action(), Some("last_one"));
}
#[test]
fn test_last_step_action_returns_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.last_step_action().is_none());
}
#[test]
fn test_total_thought_bytes_sums_all_thought_lengths() {
let steps = vec![
make_step("hi", "a", "o"), make_step("hello", "b", "o"), ];
let session = make_session(steps, 0);
assert_eq!(session.total_thought_bytes(), 7);
}
#[test]
fn test_total_observation_bytes_sums_all_observation_lengths() {
let steps = vec![
make_step("t", "a", "ok"), make_step("t", "b", "done!"), ];
let session = make_session(steps, 0);
assert_eq!(session.total_observation_bytes(), 7);
}
#[test]
fn test_steps_in_range_returns_correct_slice() {
let steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
];
let session = make_session(steps, 0);
let slice = session.steps_in_range(1, 3);
assert_eq!(slice.len(), 2);
assert_eq!(slice[0].action, "b");
assert_eq!(slice[1].action, "c");
}
#[test]
fn test_steps_in_range_returns_empty_for_out_of_bounds_start() {
let steps = vec![make_step("t", "a", "o")];
let session = make_session(steps, 0);
assert!(session.steps_in_range(5, 10).is_empty());
}
#[test]
fn test_median_step_duration_ms_odd_count() {
let mut steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
];
steps[0].step_duration_ms = 10;
steps[1].step_duration_ms = 50;
steps[2].step_duration_ms = 30;
let session = make_session(steps, 0);
assert_eq!(session.median_step_duration_ms(), 30);
}
#[test]
fn test_median_step_duration_ms_returns_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.median_step_duration_ms(), 0);
}
#[test]
fn test_into_steps_consumes_session_and_returns_owned_vec() {
let steps = vec![
make_step("think", "act", "obs"),
make_step("think2", "act2", "obs2"),
];
let session = make_session(steps, 0);
let owned = session.into_steps();
assert_eq!(owned.len(), 2);
assert_eq!(owned[0].thought, "think");
assert_eq!(owned[1].action, "act2");
}
#[test]
fn test_into_steps_returns_empty_vec_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.into_steps().is_empty());
}
#[test]
fn test_iter_steps_iterates_in_order() {
let steps = vec![
make_step("t1", "a1", "o1"),
make_step("t2", "a2", "o2"),
];
let session = make_session(steps, 0);
let thoughts: Vec<&str> = session.iter_steps().map(|s| s.thought.as_str()).collect();
assert_eq!(thoughts, vec!["t1", "t2"]);
}
#[test]
fn test_has_at_least_steps_true_when_enough_steps() {
let session = make_session(vec![make_step("t", "a", "o"), make_step("t", "a", "o")], 0);
assert!(session.has_at_least_steps(2));
assert!(session.has_at_least_steps(1));
}
#[test]
fn test_has_at_least_steps_false_when_too_few() {
let session = make_session(vec![make_step("t", "a", "o")], 0);
assert!(!session.has_at_least_steps(2));
}
#[test]
fn test_has_at_least_steps_zero_always_true() {
let session = make_session(vec![], 0);
assert!(session.has_at_least_steps(0));
}
#[test]
fn test_p95_step_duration_ms_returns_high_percentile() {
let mut steps: Vec<ReActStep> = (1u64..=20)
.map(|ms| ReActStep::new("t", "a", "o").with_duration(ms))
.collect();
steps.reverse();
let session = make_session(steps, 0);
assert_eq!(session.p95_step_duration_ms(), 19);
}
#[test]
fn test_p95_step_duration_ms_returns_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.p95_step_duration_ms(), 0);
}
#[test]
fn test_p99_step_duration_ms_returns_highest_for_small_set() {
let steps: Vec<ReActStep> = (1u64..=10)
.map(|ms| ReActStep::new("t", "a", "o").with_duration(ms))
.collect();
let session = make_session(steps, 0);
assert_eq!(session.p99_step_duration_ms(), 10);
}
#[test]
fn test_p99_step_duration_ms_returns_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.p99_step_duration_ms(), 0);
}
#[test]
fn test_step_count_above_duration_ms_counts_slow_steps() {
let steps = vec![
ReActStep::new("t", "a", "o").with_duration(10),
ReActStep::new("t", "b", "o").with_duration(200),
ReActStep::new("t", "c", "o").with_duration(50),
ReActStep::new("t", "d", "o").with_duration(300),
];
let session = make_session(steps, 0);
assert_eq!(session.step_count_above_duration_ms(100), 2);
assert_eq!(session.step_count_above_duration_ms(500), 0);
}
#[test]
fn test_step_count_above_duration_ms_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.step_count_above_duration_ms(0), 0);
}
#[test]
fn test_total_action_bytes_sums_action_lengths() {
let steps = vec![
make_step("t", "ab", "o"), make_step("t", "cde", "o"), ];
let session = make_session(steps, 0);
assert_eq!(session.total_action_bytes(), 5);
}
#[test]
fn test_total_action_bytes_empty_session_returns_zero() {
let session = make_session(vec![], 0);
assert_eq!(session.total_action_bytes(), 0);
}
#[test]
fn test_step_duration_variance_ms_computed_correctly() {
let mut steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
];
steps[0].step_duration_ms = 10;
steps[1].step_duration_ms = 20;
let session = make_session(steps, 0);
assert!((session.step_duration_variance_ms() - 25.0).abs() < 1e-9);
}
#[test]
fn test_step_duration_variance_ms_zero_for_single_step() {
let session = make_session(vec![make_step("t", "a", "o")], 0);
assert_eq!(session.step_duration_variance_ms(), 0.0);
}
#[test]
fn test_steps_with_errors_returns_steps_containing_error() {
let steps = vec![
make_step("t", "a", "success"),
make_step("t", "b", "error: timeout"),
make_step("t", "c", "ok"),
make_step("t", "d", "Error: not found"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_errors().len(), 2);
}
#[test]
fn test_steps_with_errors_empty_when_no_errors() {
let steps = vec![make_step("t", "a", "ok"), make_step("t", "b", "done")];
let session = make_session(steps, 0);
assert!(session.steps_with_errors().is_empty());
}
#[test]
fn test_min_step_duration_ms_returns_minimum() {
let mut steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
];
steps[0].step_duration_ms = 50;
steps[1].step_duration_ms = 10;
steps[2].step_duration_ms = 30;
let session = make_session(steps, 0);
assert_eq!(session.min_step_duration_ms(), 10);
}
#[test]
fn test_min_step_duration_ms_empty_returns_zero() {
let session = make_session(vec![], 0);
assert_eq!(session.min_step_duration_ms(), 0);
}
#[test]
fn test_max_step_duration_ms_returns_maximum() {
let mut steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
];
steps[0].step_duration_ms = 50;
steps[1].step_duration_ms = 10;
steps[2].step_duration_ms = 30;
let session = make_session(steps, 0);
assert_eq!(session.max_step_duration_ms(), 50);
}
#[test]
fn test_max_step_duration_ms_empty_returns_zero() {
let session = make_session(vec![], 0);
assert_eq!(session.max_step_duration_ms(), 0);
}
#[test]
fn test_steps_with_long_observations_returns_steps_above_threshold() {
let steps = vec![
make_step("t", "a", "short"), make_step("t", "b", "this is a long observation"), ];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_long_observations(10).len(), 1);
assert_eq!(session.steps_with_long_observations(4).len(), 2);
}
#[test]
fn test_steps_with_long_observations_empty_for_high_threshold() {
let steps = vec![make_step("t", "a", "hi")];
let session = make_session(steps, 0);
assert!(session.steps_with_long_observations(1000).is_empty());
}
#[test]
fn test_unique_observations_count_counts_distinct_values() {
let steps = vec![
make_step("t", "a", "ok"),
make_step("t", "b", "ok"),
make_step("t", "c", "done"),
];
let session = make_session(steps, 0);
assert_eq!(session.unique_observations_count(), 2);
}
#[test]
fn test_unique_observations_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.unique_observations_count(), 0);
}
#[test]
fn test_thought_max_bytes_returns_max_thought_length() {
let steps = vec![
make_step("hi", "a", "o"),
make_step("hello world", "b", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.thought_max_bytes(), 11);
}
#[test]
fn test_thought_max_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.thought_max_bytes(), 0);
}
#[test]
fn test_observation_max_bytes_returns_max_observation_length() {
let steps = vec![
make_step("t", "a", "short"),
make_step("t", "b", "much longer observation"),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_max_bytes(), "much longer observation".len());
}
#[test]
fn test_step_count_below_duration_ms_counts_fast_steps() {
let mut steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
];
steps[0].step_duration_ms = 5;
steps[1].step_duration_ms = 50;
steps[2].step_duration_ms = 500;
let session = make_session(steps, 0);
assert_eq!(session.step_count_below_duration_ms(100), 2);
assert_eq!(session.step_count_below_duration_ms(6), 1);
}
#[test]
fn test_step_count_below_duration_ms_zero_for_empty() {
let session = make_session(vec![], 0);
assert_eq!(session.step_count_below_duration_ms(100), 0);
}
#[test]
fn test_total_observation_count_counts_non_empty_observations() {
let steps = vec![
make_step("t", "a", "result"),
make_step("t", "b", ""),
make_step("t", "c", "output"),
];
let session = make_session(steps, 0);
assert_eq!(session.total_observation_count(), 2);
}
#[test]
fn test_total_observation_count_zero_when_all_empty() {
let steps = vec![make_step("t", "a", ""), make_step("t", "b", "")];
let session = make_session(steps, 0);
assert_eq!(session.total_observation_count(), 0);
}
#[test]
fn test_actions_containing_returns_matching_steps() {
let steps = vec![
make_step("t", "search(query)", "r"),
make_step("t", "write(data)", "r"),
make_step("t", "search(other)", "r"),
];
let session = make_session(steps, 0);
assert_eq!(session.actions_containing("search").len(), 2);
}
#[test]
fn test_actions_containing_empty_when_no_match() {
let steps = vec![make_step("t", "write(x)", "r")];
let session = make_session(steps, 0);
assert!(session.actions_containing("read").is_empty());
}
#[test]
fn test_step_duration_range_ms_returns_min_max() {
let mut steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
];
steps[0].step_duration_ms = 10;
steps[1].step_duration_ms = 50;
steps[2].step_duration_ms = 30;
let session = make_session(steps, 0);
assert_eq!(session.step_duration_range_ms(), (10, 50));
}
#[test]
fn test_step_duration_range_ms_zero_zero_for_empty() {
let session = make_session(vec![], 0);
assert_eq!(session.step_duration_range_ms(), (0, 0));
}
#[test]
fn test_count_unique_thoughts_counts_distinct_strings() {
let steps = vec![
make_step("alpha", "a", "o"),
make_step("beta", "b", "o"),
make_step("alpha", "c", "o"), ];
let session = make_session(steps, 0);
assert_eq!(session.count_unique_thoughts(), 2);
}
#[test]
fn test_count_unique_thoughts_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.count_unique_thoughts(), 0);
}
#[test]
fn test_steps_with_empty_thoughts_returns_matching_steps() {
let steps = vec![
make_step("", "a", "o"),
make_step("thought", "b", "o"),
make_step("", "c", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_empty_thoughts().len(), 2);
}
#[test]
fn test_steps_with_empty_thoughts_returns_empty_when_all_have_thoughts() {
let steps = vec![make_step("t1", "a", "o"), make_step("t2", "b", "o")];
let session = make_session(steps, 0);
assert!(session.steps_with_empty_thoughts().is_empty());
}
#[test]
fn test_max_action_bytes_returns_longest_action() {
let steps = vec![
make_step("t", "short", "o"),
make_step("t", "much longer action string", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.max_action_bytes(), "much longer action string".len());
}
#[test]
fn test_max_action_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.max_action_bytes(), 0);
}
#[test]
fn test_min_action_bytes_returns_shortest_action() {
let steps = vec![
make_step("t", "ab", "o"),
make_step("t", "abcde", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.min_action_bytes(), 2);
}
#[test]
fn test_min_action_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.min_action_bytes(), 0);
}
#[test]
fn test_step_throughput_per_sec_computes_ratio() {
let steps = vec![make_step("t", "a", "o"), make_step("t", "b", "o")];
let session = make_session(steps, 2000); assert!((session.step_throughput_per_sec() - 1.0).abs() < 1e-9);
}
#[test]
fn test_step_throughput_per_sec_zero_for_zero_duration() {
let steps = vec![make_step("t", "a", "o")];
let session = make_session(steps, 0);
assert_eq!(session.step_throughput_per_sec(), 0.0);
}
#[test]
fn test_final_answer_step_index_returns_correct_index() {
let steps = vec![
make_step("think", "search(x)", "result"),
make_step("think2", "FINAL_ANSWER: done", ""),
];
let session = make_session(steps, 0);
assert_eq!(session.final_answer_step_index(), Some(1));
}
#[test]
fn test_final_answer_step_index_returns_none_when_no_final_answer() {
let steps = vec![make_step("t", "search(x)", "result")];
let session = make_session(steps, 0);
assert_eq!(session.final_answer_step_index(), None);
}
#[test]
fn test_final_answer_step_index_returns_none_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.final_answer_step_index(), None);
}
#[test]
fn test_final_answer_step_index_returns_first_occurrence() {
let steps = vec![
make_step("t", "FINAL_ANSWER: first", ""),
make_step("t", "FINAL_ANSWER: second", ""),
];
let session = make_session(steps, 0);
assert_eq!(session.final_answer_step_index(), Some(0));
}
#[test]
fn test_first_n_steps_returns_first_n() {
let steps = vec![
make_step("t1", "a1", "o1"),
make_step("t2", "a2", "o2"),
make_step("t3", "a3", "o3"),
];
let session = make_session(steps, 0);
assert_eq!(session.first_n_steps(2).len(), 2);
assert_eq!(session.first_n_steps(2)[0].thought, "t1");
}
#[test]
fn test_first_n_steps_returns_all_when_n_exceeds_count() {
let steps = vec![make_step("t", "a", "o")];
let session = make_session(steps, 0);
assert_eq!(session.first_n_steps(10).len(), 1);
}
#[test]
fn test_first_n_steps_empty_for_n_zero() {
let steps = vec![make_step("t", "a", "o")];
let session = make_session(steps, 0);
assert!(session.first_n_steps(0).is_empty());
}
#[test]
fn test_steps_with_tool_returns_matching_steps() {
let steps = vec![
make_step("t", "search(query)", "result"),
make_step("t", "write(data)", "ok"),
make_step("t", "search(more)", "more"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_tool("search").len(), 2);
assert_eq!(session.steps_with_tool("write").len(), 1);
}
#[test]
fn test_steps_with_tool_excludes_final_answer() {
let steps = vec![
make_step("t", "FINAL_ANSWER: search done", ""),
];
let session = make_session(steps, 0);
assert!(session.steps_with_tool("search").is_empty());
}
#[test]
fn test_total_chars_sums_all_strings() {
let steps = vec![
make_step("abc", "de", "f"), make_step("g", "hi", "jkl"), ];
let session = make_session(steps, 0);
assert_eq!(session.total_chars(), 12);
}
#[test]
fn test_total_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_chars(), 0);
}
#[test]
fn test_avg_action_bytes_computes_mean() {
let steps = vec![
make_step("t", "ab", "o"), make_step("t", "abcd", "o"), ];
let session = make_session(steps, 0);
assert!((session.avg_action_bytes() - 3.0).abs() < 1e-9);
}
#[test]
fn test_avg_action_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.avg_action_bytes(), 0.0);
}
#[test]
fn test_avg_observation_bytes_computes_mean() {
let steps = vec![
make_step("t", "a", "hi"), make_step("t", "b", "world"), ];
let session = make_session(steps, 0);
assert!((session.avg_observation_bytes() - 3.5).abs() < 1e-9);
}
#[test]
fn test_avg_observation_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.avg_observation_bytes(), 0.0);
}
#[test]
fn test_steps_with_long_thoughts_returns_steps_exceeding_threshold() {
let steps = vec![
make_step("short", "a", "o"),
make_step("this is a much longer thought string", "b", "o"),
make_step("hi", "c", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_long_thoughts(10).len(), 1);
}
#[test]
fn test_steps_with_long_thoughts_empty_when_none_exceed() {
let steps = vec![make_step("hi", "a", "o")];
let session = make_session(steps, 0);
assert!(session.steps_with_long_thoughts(100).is_empty());
}
#[test]
fn test_action_count_containing_counts_matching_steps() {
let steps = vec![
make_step("t", "search(query)", "o"),
make_step("t", "write(data)", "o"),
make_step("t", "search(other)", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.action_count_containing("search"), 2);
}
#[test]
fn test_action_count_containing_zero_when_no_match() {
let steps = vec![make_step("t", "write(x)", "o")];
let session = make_session(steps, 0);
assert_eq!(session.action_count_containing("read"), 0);
}
#[test]
fn test_total_thought_count_counts_non_empty_thoughts() {
let steps = vec![
make_step("thought", "a", "o"),
make_step("", "b", "o"),
make_step("another", "c", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.total_thought_count(), 2);
}
#[test]
fn test_total_thought_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_thought_count(), 0);
}
#[test]
fn test_has_thought_containing_true_when_substring_found() {
let steps = vec![make_step("think about this", "act", "obs")];
let session = make_session(steps, 0);
assert!(session.has_thought_containing("think"));
}
#[test]
fn test_has_thought_containing_false_when_not_found() {
let steps = vec![make_step("unrelated", "act", "obs")];
let session = make_session(steps, 0);
assert!(!session.has_thought_containing("xyz"));
}
#[test]
fn test_has_thought_containing_false_for_empty_session() {
let session = make_session(vec![], 0);
assert!(!session.has_thought_containing("any"));
}
#[test]
fn test_steps_with_action_length_above_returns_matching_steps() {
let steps = vec![
make_step("t", "hi", "o"), make_step("t", "hello world", "o"), ];
let session = make_session(steps, 0);
let result = session.steps_with_action_length_above(5);
assert_eq!(result.len(), 1);
assert_eq!(result[0].action, "hello world");
}
#[test]
fn test_steps_with_action_length_above_empty_when_none_qualify() {
let steps = vec![make_step("t", "hi", "o")];
let session = make_session(steps, 0);
assert!(session.steps_with_action_length_above(100).is_empty());
}
#[test]
fn test_avg_thought_bytes_computes_mean() {
let steps = vec![
make_step("ab", "a", "o"), make_step("abcdef", "b", "o"), ];
let session = make_session(steps, 0);
assert!((session.avg_thought_bytes() - 4.0).abs() < 1e-9);
}
#[test]
fn test_avg_thought_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.avg_thought_bytes(), 0.0);
}
#[test]
fn test_steps_above_action_bytes_filters_correctly() {
let steps = vec![
make_step("t", "ab", "o"), make_step("t", "abcdefgh", "o"), make_step("t", "abc", "o"), ];
let session = make_session(steps, 0);
assert_eq!(session.steps_above_action_bytes(3).len(), 1);
}
#[test]
fn test_steps_above_action_bytes_empty_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.steps_above_action_bytes(0).is_empty());
}
#[test]
fn test_steps_between_returns_subslice() {
let steps = vec![
make_step("t", "a", "o"),
make_step("t", "b", "o"),
make_step("t", "c", "o"),
make_step("t", "d", "o"),
];
let session = make_session(steps, 0);
let between = session.steps_between(1, 3);
assert_eq!(between.len(), 2);
assert_eq!(between[0].action, "b");
assert_eq!(between[1].action, "c");
}
#[test]
fn test_steps_between_empty_when_start_ge_end() {
let steps = vec![make_step("t", "a", "o"), make_step("t", "b", "o")];
let session = make_session(steps, 0);
assert!(session.steps_between(2, 1).is_empty());
}
#[test]
fn test_steps_with_duplicate_thoughts_returns_duplicates_only() {
let steps = vec![
make_step("alpha", "a", "o"),
make_step("beta", "b", "o"),
make_step("alpha", "c", "o"), ];
let session = make_session(steps, 0);
let dupes = session.steps_with_duplicate_thoughts();
assert_eq!(dupes.len(), 1);
assert_eq!(dupes[0].action, "c");
}
#[test]
fn test_steps_with_duplicate_thoughts_empty_when_all_unique() {
let steps = vec![
make_step("t1", "a", "o"),
make_step("t2", "b", "o"),
];
let session = make_session(steps, 0);
assert!(session.steps_with_duplicate_thoughts().is_empty());
}
#[test]
fn test_step_observation_rate_returns_fraction_with_observations() {
let steps = vec![
make_step("t", "a", "obs"),
make_step("t", "b", ""),
make_step("t", "c", "obs2"),
];
let session = make_session(steps, 0);
let rate = session.step_observation_rate();
assert!((rate - 2.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_step_observation_rate_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.step_observation_rate(), 0.0);
}
#[test]
fn test_steps_below_thought_bytes_filters_by_threshold() {
let steps = vec![
make_step("hi", "a", "o"),
make_step("hello world", "b", "o"),
];
let session = make_session(steps, 0);
let below = session.steps_below_thought_bytes(6);
assert_eq!(below.len(), 1);
assert_eq!(below[0].action, "a");
}
#[test]
fn test_steps_below_thought_bytes_empty_when_all_exceed() {
let steps = vec![make_step("long thought text", "a", "o")];
let session = make_session(steps, 0);
assert!(session.steps_below_thought_bytes(3).is_empty());
}
#[test]
fn test_agent_runtime_tool_count_reflects_registered_tools() {
let rt = AgentRuntime::quick(1, "model");
assert_eq!(rt.tool_count(), 0);
}
#[test]
fn test_max_thought_bytes_returns_longest_thought_length() {
let steps = vec![
make_step("hi", "a", "o"),
make_step("hello world", "b", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.max_thought_bytes(), 11);
}
#[test]
fn test_max_thought_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.max_thought_bytes(), 0);
}
#[test]
fn test_steps_above_observation_bytes_filters_by_threshold() {
let steps = vec![
make_step("t", "a", "tiny"),
make_step("t", "b", "a much longer observation"),
];
let session = make_session(steps, 0);
let above = session.steps_above_observation_bytes(5);
assert_eq!(above.len(), 1);
assert_eq!(above[0].action, "b");
}
#[test]
fn test_steps_above_observation_bytes_empty_when_all_below() {
let steps = vec![make_step("t", "a", "hi")];
let session = make_session(steps, 0);
assert!(session.steps_above_observation_bytes(100).is_empty());
}
#[test]
fn test_agent_runtime_tool_names_empty_when_no_tools() {
let rt = AgentRuntime::quick(1, "model");
assert!(rt.tool_names().is_empty());
}
#[test]
fn test_steps_between_returns_correct_slice() {
let steps = vec![
make_step("t0", "a0", "o0"),
make_step("t1", "a1", "o1"),
make_step("t2", "a2", "o2"),
make_step("t3", "a3", "o3"),
];
let session = make_session(steps, 0);
let slice = session.steps_between(1, 3);
assert_eq!(slice.len(), 2);
assert_eq!(slice[0].thought, "t1");
assert_eq!(slice[1].thought, "t2");
}
#[test]
fn test_steps_between_returns_empty_when_start_ge_end() {
let steps = vec![make_step("t", "a", "o"), make_step("t2", "a2", "o2")];
let session = make_session(steps, 0);
assert!(session.steps_between(2, 1).is_empty());
assert!(session.steps_between(1, 1).is_empty());
}
#[test]
fn test_steps_between_clamps_to_step_count() {
let steps = vec![make_step("t", "a", "o")];
let session = make_session(steps, 0);
let slice = session.steps_between(0, 100);
assert_eq!(slice.len(), 1);
}
#[test]
fn test_has_duplicate_actions_true_when_repeated() {
let steps = vec![
make_step("t", "search[foo]", "o"),
make_step("t", "search[foo]", "o"),
];
let session = make_session(steps, 0);
assert!(session.has_duplicate_actions());
}
#[test]
fn test_has_duplicate_actions_false_when_all_unique() {
let steps = vec![
make_step("t", "search[foo]", "o"),
make_step("t", "lookup[bar]", "o"),
];
let session = make_session(steps, 0);
assert!(!session.has_duplicate_actions());
}
#[test]
fn test_has_duplicate_actions_false_for_empty_session() {
let session = make_session(vec![], 0);
assert!(!session.has_duplicate_actions());
}
#[test]
fn test_step_indices_with_tool_returns_correct_indices() {
let steps = vec![
make_step("t", "search[x]", "o"),
make_step("t", "lookup[y]", "o"),
make_step("t", "search[z]", "o"),
];
let session = make_session(steps, 0);
let indices = session.step_indices_with_tool("search");
assert_eq!(indices, vec![0, 2]);
}
#[test]
fn test_step_indices_with_tool_empty_when_no_match() {
let steps = vec![make_step("t", "lookup[x]", "o")];
let session = make_session(steps, 0);
assert!(session.step_indices_with_tool("search").is_empty());
}
#[test]
fn test_observations_above_bytes_returns_matching_steps() {
let steps = vec![
make_step("t", "a", "hi"), make_step("t", "a", "hello world"), ];
let session = make_session(steps, 0);
let result = session.observations_above_bytes(5);
assert_eq!(result.len(), 1);
assert_eq!(result[0].observation, "hello world");
}
#[test]
fn test_observations_above_bytes_empty_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.observations_above_bytes(0).is_empty());
}
#[test]
fn test_total_step_chars_sums_all_fields() {
let steps = vec![
make_step("ab", "cd", "ef"), make_step("x", "y", "z"), ];
let session = make_session(steps, 0);
assert_eq!(session.total_step_chars(), 9);
}
#[test]
fn test_total_step_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_step_chars(), 0);
}
#[test]
fn test_steps_by_action_prefix_returns_matching_steps() {
let steps = vec![
make_step("t", "search_web", "o"),
make_step("t", "search_db", "o"),
make_step("t", "write_file", "o"),
];
let session = make_session(steps, 0);
let result = session.steps_by_action_prefix("search");
assert_eq!(result.len(), 2);
}
#[test]
fn test_steps_by_action_prefix_empty_when_no_match() {
let steps = vec![make_step("t", "write_file", "o")];
let session = make_session(steps, 0);
assert!(session.steps_by_action_prefix("search").is_empty());
}
#[test]
fn test_action_count_counts_tool_call_steps() {
let steps = vec![
make_step("t", "search_web", "o"),
make_step("t", "FINAL_ANSWER: done", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.action_count(), 1);
}
#[test]
fn test_action_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.action_count(), 0);
}
#[test]
fn test_total_thought_bytes_sums_thought_lengths() {
let steps = vec![
make_step("ab", "a", "o"), make_step("abcde", "b", "o"), ];
let session = make_session(steps, 0);
assert_eq!(session.total_thought_bytes(), 7);
}
#[test]
fn test_total_thought_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_thought_bytes(), 0);
}
#[test]
fn test_total_observation_bytes_sums_observation_lengths() {
let steps = vec![
make_step("t", "a", "hello"), make_step("t", "b", "world"), ];
let session = make_session(steps, 0);
assert_eq!(session.total_observation_bytes(), 10);
}
#[test]
fn test_total_observation_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_observation_bytes(), 0);
}
#[test]
fn test_proportion_tool_calls_all_tool_calls() {
let steps = vec![
make_step("t", "search[x]", "o"),
make_step("t", "lookup[y]", "o"),
];
let session = make_session(steps, 0);
assert!((session.proportion_tool_calls() - 1.0).abs() < 1e-9);
}
#[test]
fn test_proportion_tool_calls_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.proportion_tool_calls(), 0.0);
}
#[test]
fn test_thought_density_returns_thought_fraction_of_total_bytes() {
let steps = vec![make_step("ab", "cd", "ef")]; let session = make_session(steps, 0);
let density = session.thought_density();
assert!((density - 1.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_thought_density_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.thought_density(), 0.0);
}
#[test]
fn test_steps_matching_observation_returns_matching_steps() {
let steps = vec![
make_step("t", "a", "found: result"),
make_step("t", "b", "no match here"),
make_step("t", "c", "found: another"),
];
let session = make_session(steps, 0);
let result = session.steps_matching_observation("found:");
assert_eq!(result.len(), 2);
}
#[test]
fn test_steps_matching_observation_empty_when_no_match() {
let steps = vec![make_step("t", "a", "nothing")];
let session = make_session(steps, 0);
assert!(session.steps_matching_observation("found:").is_empty());
}
#[test]
fn test_step_action_lengths_returns_lengths_in_order() {
let steps = vec![
make_step("t", "ab", "o"),
make_step("t", "cdef", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.step_action_lengths(), vec![2, 4]);
}
#[test]
fn test_step_action_lengths_empty_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.step_action_lengths().is_empty());
}
#[test]
fn test_has_thought_starting_with_true_when_match() {
let steps = vec![
make_step("Plan: do something", "act", "obs"),
];
let session = make_session(steps, 0);
assert!(session.has_thought_starting_with("Plan:"));
}
#[test]
fn test_has_thought_starting_with_false_when_no_match() {
let steps = vec![make_step("think", "act", "obs")];
let session = make_session(steps, 0);
assert!(!session.has_thought_starting_with("Plan:"));
}
#[test]
fn test_has_thought_starting_with_false_for_empty_session() {
let session = make_session(vec![], 0);
assert!(!session.has_thought_starting_with("Plan:"));
}
#[test]
fn test_step_count_above_action_bytes_counts_correctly() {
let steps = vec![
make_step("t", "short", "o"),
make_step("t", "a_very_long_action_string", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.step_count_above_action_bytes(5), 1);
}
#[test]
fn test_step_count_above_action_bytes_zero_when_all_small() {
let steps = vec![make_step("t", "ab", "o")];
let session = make_session(steps, 0);
assert_eq!(session.step_count_above_action_bytes(100), 0);
}
#[test]
fn test_runtime_config_returns_agent_config() {
let rt = AgentRuntime::quick(3, "test-model");
assert_eq!(rt.config().max_iterations, 3);
}
#[test]
fn test_total_thought_chars_sums_all_thoughts() {
let steps = vec![
make_step("ab", "x", "y"),
make_step("cde", "x", "y"),
];
let session = make_session(steps, 0);
assert_eq!(session.total_thought_chars(), 5);
}
#[test]
fn test_total_thought_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_thought_chars(), 0);
}
#[test]
fn test_total_action_chars_sums_all_actions() {
let steps = vec![
make_step("t", "hello", "o"),
make_step("t", "world", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.total_action_chars(), 10);
}
#[test]
fn test_total_observation_chars_sums_all_observations() {
let steps = vec![
make_step("t", "a", "abc"),
make_step("t", "a", "de"),
];
let session = make_session(steps, 0);
assert_eq!(session.total_observation_chars(), 5);
}
#[test]
fn test_model_name_returns_configured_model() {
let rt = AgentRuntime::quick(5, "gpt-4o");
assert_eq!(rt.model_name(), "gpt-4o");
}
#[test]
fn test_min_observation_bytes_returns_smallest_nonempty() {
let steps = vec![
make_step("t", "a", "hello"),
make_step("t", "a", "hi"),
make_step("t", "a", ""),
];
let session = make_session(steps, 0);
assert_eq!(session.min_observation_bytes(), 2);
}
#[test]
fn test_min_observation_bytes_zero_when_all_empty() {
let steps = vec![make_step("t", "a", "")];
let session = make_session(steps, 0);
assert_eq!(session.min_observation_bytes(), 0);
}
#[test]
fn test_min_thought_bytes_returns_smallest_nonempty() {
let steps = vec![
make_step("abc", "a", "o"),
make_step("xy", "a", "o"),
make_step("", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.min_thought_bytes(), 2);
}
#[test]
fn test_min_thought_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.min_thought_bytes(), 0);
}
#[test]
fn test_proportion_empty_thoughts_all_empty() {
let steps = vec![
make_step("", "a", "o"),
make_step("", "a", "o"),
];
let session = make_session(steps, 0);
assert!((session.proportion_empty_thoughts() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_proportion_empty_thoughts_none_empty() {
let steps = vec![make_step("think", "a", "o")];
let session = make_session(steps, 0);
assert!((session.proportion_empty_thoughts()).abs() < f64::EPSILON);
}
#[test]
fn test_proportion_empty_thoughts_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.proportion_empty_thoughts()).abs() < f64::EPSILON);
}
#[test]
fn test_has_failed_steps_true_when_error_observation() {
let steps = vec![make_step("t", "a", "[error] something broke")];
let session = make_session(steps, 0);
assert!(session.has_failed_steps());
}
#[test]
fn test_has_failed_steps_false_when_no_errors() {
let steps = vec![make_step("t", "a", "success")];
let session = make_session(steps, 0);
assert!(!session.has_failed_steps());
}
#[test]
fn test_has_failed_steps_false_for_empty_session() {
let session = make_session(vec![], 0);
assert!(!session.has_failed_steps());
}
#[test]
fn test_all_observations_non_empty_true_when_all_have_obs() {
let steps = vec![
make_step("t1", "a1", "result1"),
make_step("t2", "a2", "result2"),
];
let session = make_session(steps, 0);
assert!(session.all_observations_non_empty());
}
#[test]
fn test_all_observations_non_empty_false_when_one_is_empty() {
let steps = vec![
make_step("t1", "a1", "result1"),
make_step("t2", "a2", ""),
];
let session = make_session(steps, 0);
assert!(!session.all_observations_non_empty());
}
#[test]
fn test_all_observations_non_empty_true_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.all_observations_non_empty());
}
#[test]
fn test_avg_combined_step_bytes_correct() {
let steps = vec![
make_step("hi", "go", "ok"),
make_step("hello", "world", "result"),
];
let session = make_session(steps, 0);
let avg = session.avg_combined_step_bytes();
assert!((avg - 11.0).abs() < 1e-9);
}
#[test]
fn test_avg_combined_step_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.avg_combined_step_bytes(), 0.0);
}
#[test]
fn test_shortest_observation_step_returns_shortest() {
let steps = vec![
make_step("t1", "a1", "longer observation"),
make_step("t2", "a2", "short"),
];
let session = make_session(steps, 0);
let s = session.shortest_observation_step().unwrap();
assert_eq!(s.observation, "short");
}
#[test]
fn test_shortest_observation_step_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.shortest_observation_step().is_none());
}
#[test]
fn test_steps_with_empty_action_returns_matching_steps() {
let steps = vec![
make_step("t", "", "obs"),
make_step("t", "act", "obs"),
make_step("t", "", "obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_empty_action().len(), 2);
}
#[test]
fn test_steps_with_empty_action_empty_when_all_have_actions() {
let steps = vec![make_step("t", "act", "obs")];
let session = make_session(steps, 0);
assert!(session.steps_with_empty_action().is_empty());
}
#[test]
fn test_observation_starts_with_any_true_when_prefix_matches() {
let steps = vec![make_step("t", "a", "ERROR: something went wrong")];
let session = make_session(steps, 0);
assert!(session.observation_starts_with_any(&["ERROR:", "WARN:"]));
}
#[test]
fn test_observation_starts_with_any_false_when_no_match() {
let steps = vec![make_step("t", "a", "success")];
let session = make_session(steps, 0);
assert!(!session.observation_starts_with_any(&["ERROR:", "WARN:"]));
}
#[test]
fn test_has_repeated_actions_true_when_duplicate_exists() {
let steps = vec![
make_step("t", "search", "obs"),
make_step("t", "search", "obs"),
];
let session = make_session(steps, 0);
assert!(session.has_repeated_actions());
}
#[test]
fn test_has_repeated_actions_false_when_all_unique() {
let steps = vec![
make_step("t", "search", "obs"),
make_step("t", "read", "obs"),
];
let session = make_session(steps, 0);
assert!(!session.has_repeated_actions());
}
#[test]
fn test_session_max_iterations_returns_config_value() {
let rt = AgentRuntime::quick(7, "model");
assert_eq!(rt.session_max_iterations(), 7);
}
#[test]
fn test_has_action_containing_true_when_substr_matches() {
let steps = vec![make_step("t", "search[query]", "obs")];
let session = make_session(steps, 0);
assert!(session.has_action_containing("search"));
}
#[test]
fn test_has_action_containing_false_when_no_match() {
let steps = vec![make_step("t", "read_file", "obs")];
let session = make_session(steps, 0);
assert!(!session.has_action_containing("write"));
}
#[test]
fn test_max_observation_chars_returns_longest_observation() {
let steps = vec![
make_step("t", "a", "hi"),
make_step("t", "a", "hello world"),
];
let session = make_session(steps, 0);
assert_eq!(session.max_observation_chars(), 11);
}
#[test]
fn test_max_observation_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.max_observation_chars(), 0);
}
#[test]
fn test_step_index_of_longest_thought_returns_correct_index() {
let steps = vec![
make_step("short", "a", "o"),
make_step("a very long thought string", "a", "o"),
make_step("mid", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.step_index_of_longest_thought(), Some(1));
}
#[test]
fn test_step_index_of_longest_thought_none_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.step_index_of_longest_thought(), None);
}
#[test]
fn test_observation_word_counts_returns_word_counts_in_order() {
let steps = vec![
make_step("t", "a", "one two three"),
make_step("t", "a", "single"),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_word_counts(), vec![3, 1]);
}
#[test]
fn test_action_byte_variance_zero_for_equal_lengths() {
let steps = vec![
make_step("t", "abc", "o"),
make_step("t", "def", "o"),
];
let session = make_session(steps, 0);
assert!((session.action_byte_variance()).abs() < f64::EPSILON);
}
#[test]
fn test_action_byte_variance_nonzero_for_different_lengths() {
let steps = vec![
make_step("t", "a", "o"),
make_step("t", "abcde", "o"),
];
let session = make_session(steps, 0);
assert!(session.action_byte_variance() > 0.0);
}
#[test]
fn test_action_byte_variance_zero_for_single_step() {
let steps = vec![make_step("t", "hello", "o")];
let session = make_session(steps, 0);
assert!((session.action_byte_variance()).abs() < f64::EPSILON);
}
#[test]
fn test_thought_byte_variance_nonzero_for_different_lengths() {
let steps = vec![
make_step("a", "act", "o"),
make_step("abcde", "act", "o"),
];
let session = make_session(steps, 0);
assert!(session.thought_byte_variance() > 0.0);
}
#[test]
fn test_thought_byte_variance_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.thought_byte_variance()).abs() < f64::EPSILON);
}
#[test]
fn test_steps_above_thought_bytes_filters_correctly() {
let steps = vec![
make_step("hi", "a", "o"),
make_step("hello world", "a", "o"),
make_step("x", "a", "o"),
];
let session = make_session(steps, 0);
let above = session.steps_above_thought_bytes(4);
assert_eq!(above.len(), 1);
assert_eq!(above[0].thought, "hello world");
}
#[test]
fn test_steps_above_thought_bytes_empty_when_none_qualify() {
let steps = vec![make_step("hi", "a", "o")];
let session = make_session(steps, 0);
assert!(session.steps_above_thought_bytes(100).is_empty());
}
#[test]
fn test_unique_observation_count_counts_distinct_observations() {
let steps = vec![
make_step("t1", "a1", "result"),
make_step("t2", "a2", "result"),
make_step("t3", "a3", "other"),
];
let session = make_session(steps, 0);
assert_eq!(session.unique_observation_count(), 2);
}
#[test]
fn test_unique_observation_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.unique_observation_count(), 0);
}
#[test]
fn test_avg_thought_word_count_computes_correctly() {
let steps = vec![
make_step("one word", "a", "o"), make_step("three word count", "a", "o"), ];
let session = make_session(steps, 0);
let avg = session.avg_thought_word_count();
assert!((avg - 2.5).abs() < 1e-9);
}
#[test]
fn test_avg_thought_word_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.avg_thought_word_count(), 0.0);
}
#[test]
fn test_thought_starts_with_any_true_when_prefix_matches() {
let steps = vec![make_step("Plan: do it", "act", "obs")];
let session = make_session(steps, 0);
assert!(session.thought_starts_with_any(&["Plan:", "Think:"]));
}
#[test]
fn test_thought_starts_with_any_false_when_no_match() {
let steps = vec![make_step("just thinking", "act", "obs")];
let session = make_session(steps, 0);
assert!(!session.thought_starts_with_any(&["Plan:", "Think:"]));
}
#[test]
fn test_action_word_count_sums_words_across_steps() {
let steps = vec![
make_step("t", "search for answer", "obs"),
make_step("t", "write result", "obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.action_word_count(), 5);
}
#[test]
fn test_action_word_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.action_word_count(), 0);
}
#[test]
fn test_steps_above_thought_chars_counts_correctly() {
let steps = vec![
make_step("short", "a", "o"),
make_step("a very long thought here", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_above_thought_chars(5), 1);
}
#[test]
fn test_steps_above_thought_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.steps_above_thought_chars(0), 0);
}
#[test]
fn test_total_empty_steps_counts_fully_empty_steps() {
let steps = vec![
make_step("", "", ""),
make_step("t", "a", "o"),
make_step("", "", ""),
];
let session = make_session(steps, 0);
assert_eq!(session.total_empty_steps(), 2);
}
#[test]
fn test_total_empty_steps_zero_when_no_empty_steps() {
let steps = vec![make_step("t", "a", "o")];
let session = make_session(steps, 0);
assert_eq!(session.total_empty_steps(), 0);
}
#[test]
fn test_action_starts_with_count_correct() {
let steps = vec![
make_step("t", "search:foo", "o"),
make_step("t", "search:bar", "o"),
make_step("t", "write:baz", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.action_starts_with_count("search"), 2);
}
#[test]
fn test_action_starts_with_count_zero_for_no_match() {
let steps = vec![make_step("t", "write:x", "o")];
let session = make_session(steps, 0);
assert_eq!(session.action_starts_with_count("read"), 0);
}
#[test]
fn test_longest_action_returns_longest() {
let steps = vec![
make_step("t", "short", "o"),
make_step("t", "much_longer_action", "o"),
make_step("t", "mid", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.longest_action(), Some("much_longer_action"));
}
#[test]
fn test_longest_action_none_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.longest_action(), None);
}
#[test]
fn test_thought_completeness_all_non_empty() {
let steps = vec![
make_step("think", "a", "o"),
make_step("also thinking", "a", "o"),
];
let session = make_session(steps, 0);
assert!((session.thought_completeness() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_thought_completeness_half_empty() {
let steps = vec![
make_step("think", "a", "o"),
make_step("", "a", "o"),
];
let session = make_session(steps, 0);
assert!((session.thought_completeness() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_thought_completeness_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert!((session.thought_completeness()).abs() < f64::EPSILON);
}
#[test]
fn test_steps_with_non_empty_observation_returns_matching_steps() {
let steps = vec![
make_step("t", "a", "result"),
make_step("t", "a", ""),
make_step("t", "a", "more"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_non_empty_observation().len(), 2);
}
#[test]
fn test_steps_with_non_empty_observation_empty_for_all_empty() {
let steps = vec![make_step("t", "a", "")];
let session = make_session(steps, 0);
assert!(session.steps_with_non_empty_observation().is_empty());
}
#[test]
fn test_observations_containing_returns_matching_steps() {
let steps = vec![
make_step("t", "a", "found the answer"),
make_step("t", "a", "no match"),
];
let session = make_session(steps, 0);
assert_eq!(session.observations_containing("found").len(), 1);
}
#[test]
fn test_thought_observation_ratio_returns_correct_ratio() {
let steps = vec![make_step("ab", "x", "abcd")];
let session = make_session(steps, 0);
assert_eq!(session.thought_observation_ratio(), 0.5);
}
#[test]
fn test_thought_observation_ratio_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.thought_observation_ratio(), 0.0);
}
#[test]
fn test_non_empty_action_count_correct() {
let steps = vec![
make_step("t", "search", "o"),
make_step("t", "", "o"),
make_step("t", "write", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.non_empty_action_count(), 2);
}
#[test]
fn test_non_empty_action_count_zero_for_all_empty() {
let steps = vec![make_step("t", "", "o"), make_step("t", "", "o")];
let session = make_session(steps, 0);
assert_eq!(session.non_empty_action_count(), 0);
}
#[test]
fn test_total_step_bytes_sums_all_fields() {
let steps = vec![make_step("abc", "de", "f")];
let session = make_session(steps, 0);
assert_eq!(session.total_step_bytes(), 6); }
#[test]
fn test_total_step_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.total_step_bytes(), 0);
}
#[test]
fn test_last_thought_bytes_returns_last_step_thought() {
let steps = vec![
make_step("short", "a", "o"),
make_step("much longer thought", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.last_thought_bytes(), "much longer thought".len());
}
#[test]
fn test_last_thought_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.last_thought_bytes(), 0);
}
#[test]
fn test_first_observation_bytes_returns_first_step_observation() {
let steps = vec![
make_step("t", "a", "first obs"),
make_step("t", "a", "second obs is longer"),
];
let session = make_session(steps, 0);
assert_eq!(session.first_observation_bytes(), "first obs".len());
}
#[test]
fn test_first_observation_bytes_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.first_observation_bytes(), 0);
}
#[test]
fn test_has_step_with_empty_observation_true() {
let steps = vec![make_step("t", "a", "")];
let session = make_session(steps, 0);
assert!(session.has_step_with_empty_observation());
}
#[test]
fn test_has_step_with_empty_observation_false_when_all_nonempty() {
let steps = vec![make_step("t", "a", "obs")];
let session = make_session(steps, 0);
assert!(!session.has_step_with_empty_observation());
}
#[test]
fn test_has_step_with_empty_observation_false_for_empty_session() {
let session = make_session(vec![], 0);
assert!(!session.has_step_with_empty_observation());
}
#[test]
fn test_thought_to_action_byte_ratio_correct() {
let steps = vec![make_step("hello", "hi", "o")];
let session = make_session(steps, 0);
assert!((session.thought_to_action_byte_ratio() - 2.5).abs() < 1e-9);
}
#[test]
fn test_thought_to_action_byte_ratio_zero_when_no_action_bytes() {
let steps = vec![make_step("thought", "", "o")];
let session = make_session(steps, 0);
assert_eq!(session.thought_to_action_byte_ratio(), 0.0);
}
#[test]
fn test_thought_to_action_byte_ratio_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.thought_to_action_byte_ratio(), 0.0);
}
#[test]
fn test_observation_above_bytes_count_correct() {
let steps = vec![
make_step("t", "a", "short"),
make_step("t", "a", "this is quite long"),
make_step("t", "a", "x"),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_above_bytes_count(5), 1);
}
#[test]
fn test_observation_above_bytes_count_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.observation_above_bytes_count(0), 0);
}
#[test]
fn test_steps_with_both_thought_and_action_correct() {
let steps = vec![
make_step("think", "act", "obs"),
make_step("", "act", "obs"),
make_step("think", "", "obs"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_both_thought_and_action(), 1);
}
#[test]
fn test_steps_with_both_thought_and_action_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.steps_with_both_thought_and_action(), 0);
}
#[test]
fn test_steps_with_observation_prefix_correct() {
let steps = vec![
make_step("t", "a", "[error] bad"),
make_step("t", "a", "ok"),
make_step("t", "a", "[error] also bad"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_observation_prefix("[error]"), 2);
}
#[test]
fn test_steps_with_observation_prefix_zero_when_none_match() {
let steps = vec![make_step("t", "a", "ok")];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_observation_prefix("[error]"), 0);
}
#[test]
fn test_observation_bytes_total_sums_all_observations() {
let steps = vec![
make_step("t", "a", "abc"), make_step("t", "a", "de"), ];
let session = make_session(steps, 0);
assert_eq!(session.observation_bytes_total(), 5);
}
#[test]
fn test_observation_bytes_total_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.observation_bytes_total(), 0);
}
#[test]
fn test_first_thought_chars_returns_first_step_count() {
let steps = vec![make_step("héllo", "a", "o"), make_step("ignored", "a", "o")];
let session = make_session(steps, 0);
assert_eq!(session.first_thought_chars(), 5);
}
#[test]
fn test_first_thought_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.first_thought_chars(), 0);
}
#[test]
fn test_last_observation_chars_returns_last_step() {
let steps = vec![
make_step("t", "a", "first"),
make_step("t", "a", "last one"),
];
let session = make_session(steps, 0);
assert_eq!(session.last_observation_chars(), "last one".chars().count());
}
#[test]
fn test_last_observation_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.last_observation_chars(), 0);
}
#[test]
fn test_observation_word_count_total_sums_all_words() {
let steps = vec![
make_step("t", "a", "one two"), make_step("t", "a", "three"), ];
let session = make_session(steps, 0);
assert_eq!(session.observation_word_count_total(), 3);
}
#[test]
fn test_observation_word_count_total_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.observation_word_count_total(), 0);
}
#[test]
fn test_action_ends_with_count_correct() {
let steps = vec![
make_step("t", "search.", "o"),
make_step("t", "browse.", "o"),
make_step("t", "calculate", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.action_ends_with_count("."), 2);
}
#[test]
fn test_action_ends_with_count_zero_when_none_match() {
let steps = vec![make_step("t", "nope", "o")];
let session = make_session(steps, 0);
assert_eq!(session.action_ends_with_count("."), 0);
}
#[test]
fn test_avg_observation_words_correct() {
let steps = vec![
make_step("t", "a", "one two"), make_step("t", "a", "three four five"), ];
let session = make_session(steps, 0);
assert!((session.avg_observation_words() - 2.5).abs() < 1e-9);
}
#[test]
fn test_avg_observation_words_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.avg_observation_words(), 0.0);
}
#[test]
fn test_steps_matching_thought_returns_correct_steps() {
let steps = vec![
make_step("I need to search", "a", "o"),
make_step("I found the answer", "a", "o"),
make_step("no match here", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_matching_thought("I").len(), 2);
}
#[test]
fn test_median_observation_chars_returns_middle_value() {
let steps = vec![
make_step("t", "a", "ab"),
make_step("t", "a", "abcde"),
make_step("t", "a", "abc"),
];
let session = make_session(steps, 0);
assert_eq!(session.median_observation_chars(), 3);
}
#[test]
fn test_median_observation_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.median_observation_chars(), 0);
}
#[test]
fn test_cumulative_thought_chars_accumulates_correctly() {
let steps = vec![
make_step("ab", "a", "o"),
make_step("cde", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.cumulative_thought_chars(), vec![2, 5]);
}
#[test]
fn test_count_steps_with_thought_containing_counts_matches() {
let steps = vec![
make_step("call function foo", "a", "o"),
make_step("call function bar", "a", "o"),
make_step("nothing", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.count_steps_with_thought_containing("function"), 2);
}
#[test]
fn test_observation_contains_any_true_when_term_present() {
let steps = vec![
make_step("t", "a", "result: success"),
make_step("t", "a", "result: failure"),
];
let session = make_session(steps, 0);
assert!(session.observation_contains_any(&["success", "error"]));
}
#[test]
fn test_observation_contains_any_false_when_no_match() {
let steps = vec![make_step("t", "a", "nothing here")];
let session = make_session(steps, 0);
assert!(!session.observation_contains_any(&["success", "error"]));
}
#[test]
fn test_observation_contains_any_false_for_empty_session() {
let session = make_session(vec![], 0);
assert!(!session.observation_contains_any(&["anything"]));
}
#[test]
fn test_observation_contains_any_false_for_empty_terms() {
let steps = vec![make_step("t", "a", "something")];
let session = make_session(steps, 0);
assert!(!session.observation_contains_any(&[]));
}
#[test]
fn test_step_at_index_returns_correct_step() {
let steps = vec![
make_step("first", "a1", "o1"),
make_step("second", "a2", "o2"),
];
let session = make_session(steps, 0);
assert_eq!(session.step_at_index(1).map(|s| s.thought.as_str()), Some("second"));
}
#[test]
fn test_step_at_index_returns_none_out_of_bounds() {
let session = make_session(vec![], 0);
assert!(session.step_at_index(0).is_none());
}
#[test]
fn test_thought_contains_all_true_when_all_present_in_one_step() {
let steps = vec![
make_step("alpha beta gamma", "a", "o"),
make_step("alpha only", "a", "o"),
];
let session = make_session(steps, 0);
assert!(session.thought_contains_all(&["alpha", "beta"]));
}
#[test]
fn test_thought_contains_all_false_when_no_single_step_has_all() {
let steps = vec![
make_step("alpha", "a", "o"),
make_step("beta", "a", "o"),
];
let session = make_session(steps, 0);
assert!(!session.thought_contains_all(&["alpha", "beta"]));
}
#[test]
fn test_action_contains_any_true_when_present() {
let steps = vec![
make_step("t", "search(query)", "o"),
make_step("t", "read(file)", "o"),
];
let session = make_session(steps, 0);
assert!(session.action_contains_any(&["search", "write"]));
}
#[test]
fn test_action_contains_any_false_when_not_present() {
let steps = vec![make_step("t", "read(file)", "o")];
let session = make_session(steps, 0);
assert!(!session.action_contains_any(&["search", "write"]));
}
#[test]
fn test_max_thought_chars_returns_longest() {
let steps = vec![
make_step("hi", "a", "o"),
make_step("hello world", "a", "o"),
make_step("hey", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.max_thought_chars(), 11);
}
#[test]
fn test_max_thought_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.max_thought_chars(), 0);
}
#[test]
fn test_min_thought_chars_returns_shortest_non_empty() {
let steps = vec![
make_step("", "a", "o"),
make_step("ab", "a", "o"),
make_step("abcd", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.min_thought_chars(), 2);
}
#[test]
fn test_min_thought_chars_zero_when_all_empty() {
let steps = vec![make_step("", "a", "o")];
let session = make_session(steps, 0);
assert_eq!(session.min_thought_chars(), 0);
}
#[test]
fn test_is_registered_tool_true_for_registered_tool() {
let spec = crate::agent::ToolSpec::new("calculator", "Does math", |_| {
serde_json::json!("ok")
});
let rt = AgentRuntime::builder()
.with_agent_config(AgentConfig::new(3, "m"))
.register_tool(spec)
.build();
assert!(rt.is_registered_tool("calculator"));
}
#[test]
fn test_is_registered_tool_false_for_unknown_tool() {
let rt = AgentRuntime::quick(3, "m");
assert!(!rt.is_registered_tool("nonexistent"));
}
#[test]
fn test_registered_tool_names_returns_owned_sorted_names() {
let rt = AgentRuntime::builder()
.with_agent_config(AgentConfig::new(3, "test-model"))
.register_tool(crate::agent::ToolSpec::new("beta", "b", |_| {
serde_json::json!("ok")
}))
.register_tool(crate::agent::ToolSpec::new("alpha", "a", |_| {
serde_json::json!("ok")
}))
.build();
let names = rt.registered_tool_names();
assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
}
#[test]
fn test_registered_tool_names_empty_when_no_tools() {
let rt = AgentRuntime::quick(3, "test-model");
assert!(rt.registered_tool_names().is_empty());
}
#[test]
fn test_avg_action_chars_correct() {
let steps = vec![
make_step("t", "ab", "o"),
make_step("t", "abcd", "o"),
];
let session = make_session(steps, 0);
assert!((session.avg_action_chars() - 3.0).abs() < 1e-9);
}
#[test]
fn test_avg_action_chars_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.avg_action_chars(), 0.0);
}
#[test]
fn test_avg_observation_chars_correct() {
let steps = vec![
make_step("t", "a", "hello"),
make_step("t", "a", "hi"),
];
let session = make_session(steps, 0);
assert!((session.avg_observation_chars() - 3.5).abs() < 1e-9);
}
#[test]
fn test_step_with_longest_action_returns_correct_step() {
let steps = vec![
make_step("t", "short", "o"),
make_step("t", "much longer action string", "o"),
make_step("t", "medium act", "o"),
];
let session = make_session(steps, 0);
assert_eq!(
session.step_with_longest_action().map(|s| s.action.as_str()),
Some("much longer action string")
);
}
#[test]
fn test_step_with_longest_action_none_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.step_with_longest_action().is_none());
}
#[test]
fn test_step_count_with_observation_longer_than_counts_correctly() {
let steps = vec![
make_step("t", "a", "short"), make_step("t", "a", "a longer string"), make_step("t", "a", "x"), ];
let session = make_session(steps, 0);
assert_eq!(session.step_count_with_observation_longer_than(5), 1);
}
#[test]
fn test_step_count_with_observation_longer_than_zero_for_empty_session() {
let session = make_session(vec![], 0);
assert_eq!(session.step_count_with_observation_longer_than(0), 0);
}
#[test]
fn test_action_ends_with_true_when_present() {
let steps = vec![make_step("t", "search(query)", "o")];
let session = make_session(steps, 0);
assert!(session.action_ends_with(")"));
}
#[test]
fn test_action_ends_with_false_when_absent() {
let steps = vec![make_step("t", "search(query)", "o")];
let session = make_session(steps, 0);
assert!(!session.action_ends_with("!"));
}
#[test]
fn test_thought_ends_with_true_when_present() {
let steps = vec![make_step("I should search.", "a", "o")];
let session = make_session(steps, 0);
assert!(session.thought_ends_with("."));
}
#[test]
fn test_thought_ends_with_false_for_empty_session() {
let session = make_session(vec![], 0);
assert!(!session.thought_ends_with("x"));
}
#[test]
fn test_has_step_with_both_true_when_step_matches_both() {
let steps = vec![
make_step("need to search", "search(foo)", "o"),
make_step("done", "noop", "o"),
];
let session = make_session(steps, 0);
assert!(session.has_step_with_both("search", "foo"));
}
#[test]
fn test_has_step_with_both_false_when_no_step_matches_both() {
let steps = vec![
make_step("need to search", "noop", "o"),
make_step("done", "search(foo)", "o"),
];
let session = make_session(steps, 0);
assert!(!session.has_step_with_both("search", "foo"));
}
#[test]
fn test_thought_word_counts_returns_per_step_counts() {
let steps = vec![
make_step("one two three", "a", "o"),
make_step("hello", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.thought_word_counts(), vec![3, 1]);
}
#[test]
fn test_thought_word_counts_empty_for_empty_session() {
let session = make_session(vec![], 0);
assert!(session.thought_word_counts().is_empty());
}
#[test]
fn test_steps_sorted_by_thought_len_ascending_order() {
let steps = vec![
make_step("longest thought here", "a", "o"),
make_step("hi", "a", "o"),
make_step("medium thought", "a", "o"),
];
let session = make_session(steps, 0);
let sorted = session.steps_sorted_by_thought_len();
assert!(sorted[0].thought.len() <= sorted[1].thought.len());
assert!(sorted[1].thought.len() <= sorted[2].thought.len());
}
#[test]
fn test_steps_with_thought_longer_than_filters_correctly() {
let steps = vec![
make_step("short", "a", "o"),
make_step("this is a longer thought", "a", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_thought_longer_than(5).len(), 1);
}
#[test]
fn test_steps_with_action_containing_returns_matching_steps() {
let steps = vec![
make_step("t", "search(foo)", "o"),
make_step("t", "read(file)", "o"),
make_step("t", "search(bar)", "o"),
];
let session = make_session(steps, 0);
assert_eq!(session.steps_with_action_containing("search").len(), 2);
}
#[test]
fn test_steps_with_action_containing_empty_when_no_match() {
let steps = vec![make_step("t", "read(file)", "o")];
let session = make_session(steps, 0);
assert!(session.steps_with_action_containing("search").is_empty());
}
#[test]
fn test_observation_max_chars_returns_longest() {
let steps = vec![
make_step("t", "a", "hi"),
make_step("t", "a", "hello world"),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_max_chars(), 11);
}
#[test]
fn test_observation_min_chars_skips_empty_observations() {
let steps = vec![
make_step("t", "a", ""),
make_step("t", "a", "abcd"),
make_step("t", "a", "ab"),
];
let session = make_session(steps, 0);
assert_eq!(session.observation_min_chars(), 2);
}
#[test]
fn test_observation_min_chars_zero_when_all_empty() {
let steps = vec![make_step("t", "a", "")];
let session = make_session(steps, 0);
assert_eq!(session.observation_min_chars(), 0);
}
#[test]
fn test_action_word_counts_returns_per_step_counts() {
let steps = vec![
make_step("t1", "one two three", "o1"),
make_step("t2", "hello", "o2"),
make_step("t3", "a b", "o3"),
];
let session = make_session(steps, 0);
assert_eq!(session.action_word_counts(), vec![3, 1, 2]);
}
#[test]
fn test_action_word_counts_empty_for_no_steps() {
let session = make_session(vec![], 0);
assert!(session.action_word_counts().is_empty());
}
#[test]
fn test_thought_avg_chars_returns_average() {
let steps = vec![
make_step("ab", "a", "o"), make_step("abcd", "a", "o"), ];
let session = make_session(steps, 0);
assert_eq!(session.thought_avg_chars(), 3.0);
}
#[test]
fn test_thought_avg_chars_zero_for_empty() {
let session = make_session(vec![], 0);
assert_eq!(session.thought_avg_chars(), 0.0);
}
#[test]
fn test_thought_byte_range_returns_min_max() {
let steps = vec![
make_step("hi", "a", "o"), make_step("hello", "a", "o"), make_step("hey", "a", "o"), ];
let session = make_session(steps, 0);
assert_eq!(session.thought_byte_range(), (2, 5));
}
#[test]
fn test_thought_byte_range_zero_zero_for_empty() {
let session = make_session(vec![], 0);
assert_eq!(session.thought_byte_range(), (0, 0));
}
}