1use crate::error::AgentRuntimeError;
18use crate::metrics::RuntimeMetrics;
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use std::collections::HashMap;
22use std::fmt::Write as FmtWrite;
23use std::future::Future;
24use std::pin::Pin;
25use std::sync::Arc;
26
27pub type AsyncToolFuture = Pin<Box<dyn Future<Output = Value> + Send>>;
31
32pub type AsyncToolResultFuture = Pin<Box<dyn Future<Output = Result<Value, String>> + Send>>;
34
35pub type AsyncToolHandler = Box<dyn Fn(Value) -> AsyncToolFuture + Send + Sync>;
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub enum Role {
41 System,
43 User,
45 Assistant,
47 Tool,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Message {
54 pub role: Role,
56 pub content: String,
58}
59
60impl Message {
61 pub fn new(role: Role, content: impl Into<String>) -> Self {
67 Self {
68 role,
69 content: content.into(),
70 }
71 }
72
73 pub fn user(content: impl Into<String>) -> Self {
75 Self::new(Role::User, content)
76 }
77
78 pub fn assistant(content: impl Into<String>) -> Self {
80 Self::new(Role::Assistant, content)
81 }
82
83 pub fn system(content: impl Into<String>) -> Self {
85 Self::new(Role::System, content)
86 }
87
88 pub fn role(&self) -> &Role {
90 &self.role
91 }
92
93 pub fn content(&self) -> &str {
95 &self.content
96 }
97
98 pub fn is_user(&self) -> bool {
100 self.role == Role::User
101 }
102
103 pub fn is_assistant(&self) -> bool {
105 self.role == Role::Assistant
106 }
107
108 pub fn is_system(&self) -> bool {
110 self.role == Role::System
111 }
112
113 pub fn is_tool(&self) -> bool {
115 self.role == Role::Tool
116 }
117
118 pub fn is_empty(&self) -> bool {
120 self.content.is_empty()
121 }
122
123 pub fn word_count(&self) -> usize {
125 self.content.split_whitespace().count()
126 }
127
128 pub fn byte_len(&self) -> usize {
133 self.content.len()
134 }
135
136 pub fn content_starts_with(&self, prefix: &str) -> bool {
141 self.content.starts_with(prefix)
142 }
143}
144
145impl std::fmt::Display for Role {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 Role::System => write!(f, "system"),
149 Role::User => write!(f, "user"),
150 Role::Assistant => write!(f, "assistant"),
151 Role::Tool => write!(f, "tool"),
152 }
153 }
154}
155
156impl std::fmt::Display for Message {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 write!(f, "{}: {}", self.role, self.content)
162 }
163}
164
165impl From<(Role, String)> for Message {
166 fn from((role, content): (Role, String)) -> Self {
168 Self::new(role, content)
169 }
170}
171
172impl From<(Role, &str)> for Message {
173 fn from((role, content): (Role, &str)) -> Self {
175 Self::new(role, content)
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ReActStep {
182 pub thought: String,
184 pub action: String,
186 pub observation: String,
188 #[serde(default)]
193 pub step_duration_ms: u64,
194}
195
196impl ReActStep {
197 pub fn new(
202 thought: impl Into<String>,
203 action: impl Into<String>,
204 observation: impl Into<String>,
205 ) -> Self {
206 Self {
207 thought: thought.into(),
208 action: action.into(),
209 observation: observation.into(),
210 step_duration_ms: 0,
211 }
212 }
213
214 pub fn is_final_answer(&self) -> bool {
216 self.action.trim().to_ascii_uppercase().starts_with("FINAL_ANSWER")
217 }
218
219 pub fn is_tool_call(&self) -> bool {
221 !self.is_final_answer() && !self.action.trim().is_empty()
222 }
223
224 pub fn with_duration(mut self, ms: u64) -> Self {
229 self.step_duration_ms = ms;
230 self
231 }
232
233 pub fn is_empty(&self) -> bool {
235 self.thought.is_empty() && self.action.is_empty() && self.observation.is_empty()
236 }
237
238 pub fn observation_is_empty(&self) -> bool {
242 self.observation.is_empty()
243 }
244
245 pub fn thought_word_count(&self) -> usize {
249 self.thought.split_whitespace().count()
250 }
251
252 pub fn observation_word_count(&self) -> usize {
256 self.observation.split_whitespace().count()
257 }
258
259 pub fn thought_is_empty(&self) -> bool {
261 self.thought.trim().is_empty()
262 }
263
264 pub fn summary(&self) -> String {
272 fn preview(s: &str) -> String {
273 if s.len() <= 40 {
274 s.to_owned()
275 } else {
276 format!("{}…", &s[..40])
277 }
278 }
279 let kind = if self.is_final_answer() { "FINAL" } else { "TOOL" };
280 format!(
281 "[{kind}] thought={t} action={a} obs={o}",
282 t = preview(self.thought.trim()),
283 a = preview(self.action.trim()),
284 o = preview(self.observation.trim()),
285 )
286 }
287
288 pub fn combined_byte_length(&self) -> usize {
293 self.thought.len() + self.action.len() + self.observation.len()
294 }
295
296 pub fn action_is_empty(&self) -> bool {
298 self.action.trim().is_empty()
299 }
300
301 pub fn total_word_count(&self) -> usize {
306 self.thought.split_whitespace().count()
307 + self.action.split_whitespace().count()
308 + self.observation.split_whitespace().count()
309 }
310
311 pub fn is_complete(&self) -> bool {
318 !self.thought.is_empty() && !self.action.is_empty() && !self.observation.is_empty()
319 }
320
321 pub fn observation_starts_with(&self, prefix: &str) -> bool {
326 self.observation.starts_with(prefix)
327 }
328
329 pub fn action_word_count(&self) -> usize {
333 self.action.split_whitespace().count()
334 }
335
336 pub fn thought_byte_len(&self) -> usize {
338 self.thought.len()
339 }
340
341 pub fn action_byte_len(&self) -> usize {
343 self.action.len()
344 }
345
346 pub fn has_empty_fields(&self) -> bool {
351 self.thought.is_empty() || self.action.is_empty() || self.observation.is_empty()
352 }
353
354 pub fn observation_byte_len(&self) -> usize {
356 self.observation.len()
357 }
358
359 pub fn all_fields_have_words(&self) -> bool {
362 self.thought.split_whitespace().next().is_some()
363 && self.action.split_whitespace().next().is_some()
364 && self.observation.split_whitespace().next().is_some()
365 }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct AgentConfig {
371 pub max_iterations: usize,
373 pub model: String,
375 pub system_prompt: String,
377 pub max_memory_recalls: usize,
380 pub max_memory_tokens: Option<usize>,
389 pub loop_timeout: Option<std::time::Duration>,
393 pub temperature: Option<f32>,
395 pub max_tokens: Option<usize>,
397 pub request_timeout: Option<std::time::Duration>,
399 pub max_context_chars: Option<usize>,
406 pub stop_sequences: Vec<String>,
411}
412
413impl AgentConfig {
414 pub fn new(max_iterations: usize, model: impl Into<String>) -> Self {
416 Self {
417 max_iterations,
418 model: model.into(),
419 system_prompt: "You are a helpful AI agent.".into(),
420 max_memory_recalls: 3,
421 max_memory_tokens: None,
422 loop_timeout: None,
423 temperature: None,
424 max_tokens: None,
425 request_timeout: None,
426 max_context_chars: None,
427 stop_sequences: vec![],
428 }
429 }
430
431 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
433 self.system_prompt = prompt.into();
434 self
435 }
436
437 pub fn with_max_memory_recalls(mut self, n: usize) -> Self {
439 self.max_memory_recalls = n;
440 self
441 }
442
443 pub fn with_max_memory_tokens(mut self, n: usize) -> Self {
445 self.max_memory_tokens = Some(n);
446 self
447 }
448
449 pub fn with_loop_timeout(mut self, d: std::time::Duration) -> Self {
454 self.loop_timeout = Some(d);
455 self
456 }
457
458 pub fn with_loop_timeout_secs(self, secs: u64) -> Self {
462 self.with_loop_timeout(std::time::Duration::from_secs(secs))
463 }
464
465 pub fn with_loop_timeout_ms(self, ms: u64) -> Self {
469 self.with_loop_timeout(std::time::Duration::from_millis(ms))
470 }
471
472 pub fn with_max_iterations(mut self, n: usize) -> Self {
474 self.max_iterations = n;
475 self
476 }
477
478 pub fn max_iterations(&self) -> usize {
480 self.max_iterations
481 }
482
483 pub fn with_temperature(mut self, t: f32) -> Self {
485 self.temperature = Some(t);
486 self
487 }
488
489 pub fn with_max_tokens(mut self, n: usize) -> Self {
491 self.max_tokens = Some(n);
492 self
493 }
494
495 pub fn with_request_timeout(mut self, d: std::time::Duration) -> Self {
497 self.request_timeout = Some(d);
498 self
499 }
500
501 pub fn with_request_timeout_secs(self, secs: u64) -> Self {
505 self.with_request_timeout(std::time::Duration::from_secs(secs))
506 }
507
508 pub fn with_request_timeout_ms(self, ms: u64) -> Self {
512 self.with_request_timeout(std::time::Duration::from_millis(ms))
513 }
514
515 pub fn with_max_context_chars(mut self, n: usize) -> Self {
521 self.max_context_chars = Some(n);
522 self
523 }
524
525 pub fn with_model(mut self, model: impl Into<String>) -> Self {
527 self.model = model.into();
528 self
529 }
530
531 pub fn clone_with_model(&self, model: impl Into<String>) -> Self {
536 let mut copy = self.clone();
537 copy.model = model.into();
538 copy
539 }
540
541 pub fn clone_with_system_prompt(&self, prompt: impl Into<String>) -> Self {
546 let mut copy = self.clone();
547 copy.system_prompt = prompt.into();
548 copy
549 }
550
551 pub fn clone_with_max_iterations(&self, n: usize) -> Self {
557 let mut copy = self.clone();
558 copy.max_iterations = n;
559 copy
560 }
561
562 pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
567 self.stop_sequences = sequences;
568 self
569 }
570
571 pub fn is_valid(&self) -> bool {
576 self.max_iterations >= 1 && !self.model.is_empty()
577 }
578
579 pub fn validate(&self) -> Result<(), crate::error::AgentRuntimeError> {
587 if self.max_iterations == 0 {
588 return Err(crate::error::AgentRuntimeError::AgentLoop(
589 "AgentConfig: max_iterations must be >= 1".into(),
590 ));
591 }
592 if self.model.is_empty() {
593 return Err(crate::error::AgentRuntimeError::AgentLoop(
594 "AgentConfig: model must not be empty".into(),
595 ));
596 }
597 Ok(())
598 }
599
600 pub fn has_loop_timeout(&self) -> bool {
602 self.loop_timeout.is_some()
603 }
604
605 pub fn has_stop_sequences(&self) -> bool {
607 !self.stop_sequences.is_empty()
608 }
609
610 pub fn stop_sequence_count(&self) -> usize {
612 self.stop_sequences.len()
613 }
614
615 pub fn is_single_shot(&self) -> bool {
621 self.max_iterations == 1
622 }
623
624 pub fn has_temperature(&self) -> bool {
626 self.temperature.is_some()
627 }
628
629 pub fn temperature(&self) -> Option<f32> {
631 self.temperature
632 }
633
634 pub fn max_tokens(&self) -> Option<usize> {
636 self.max_tokens
637 }
638
639 pub fn has_request_timeout(&self) -> bool {
641 self.request_timeout.is_some()
642 }
643
644 pub fn request_timeout(&self) -> Option<std::time::Duration> {
646 self.request_timeout
647 }
648
649 pub fn has_max_context_chars(&self) -> bool {
651 self.max_context_chars.is_some()
652 }
653
654 pub fn max_context_chars(&self) -> Option<usize> {
656 self.max_context_chars
657 }
658
659 pub fn remaining_iterations_after(&self, n: usize) -> usize {
664 self.max_iterations.saturating_sub(n)
665 }
666
667 pub fn system_prompt(&self) -> &str {
669 &self.system_prompt
670 }
671
672 pub fn system_prompt_is_empty(&self) -> bool {
676 self.system_prompt.trim().is_empty()
677 }
678
679 pub fn model(&self) -> &str {
681 &self.model
682 }
683
684 pub fn loop_timeout_ms(&self) -> u64 {
691 self.loop_timeout
692 .map(|d| d.as_millis() as u64)
693 .unwrap_or(0)
694 }
695
696 pub fn total_timeout_ms(&self) -> u64 {
702 let loop_ms = self.loop_timeout_ms();
703 let req_ms = self
704 .request_timeout
705 .map(|d| d.as_millis() as u64)
706 .unwrap_or(0);
707 loop_ms.saturating_add(self.max_iterations as u64 * req_ms)
708 }
709
710 pub fn model_is(&self, name: &str) -> bool {
715 self.model == name
716 }
717
718 pub fn system_prompt_word_count(&self) -> usize {
722 self.system_prompt.split_whitespace().count()
723 }
724
725 pub fn iteration_budget_remaining(&self, steps_done: usize) -> usize {
731 self.max_iterations.saturating_sub(steps_done)
732 }
733
734 pub fn is_minimal(&self) -> bool {
739 self.system_prompt.trim().is_empty() && self.max_iterations == 1
740 }
741
742 pub fn model_starts_with(&self, prefix: &str) -> bool {
748 self.model.starts_with(prefix)
749 }
750
751 pub fn exceeds_iteration_limit(&self, steps_done: usize) -> bool {
755 steps_done >= self.max_iterations
756 }
757
758 pub fn token_budget_configured(&self) -> bool {
764 self.max_tokens.is_some() || self.max_context_chars.is_some()
765 }
766
767 pub fn max_tokens_or_default(&self, default: usize) -> usize {
772 self.max_tokens.unwrap_or(default)
773 }
774
775 pub fn effective_temperature(&self) -> f32 {
780 self.temperature.unwrap_or(1.0)
781 }
782
783 pub fn system_prompt_starts_with(&self, prefix: &str) -> bool {
787 self.system_prompt.starts_with(prefix)
788 }
789
790 pub fn max_iterations_above(&self, n: usize) -> bool {
795 self.max_iterations > n
796 }
797
798 pub fn stop_sequences_contain(&self, s: &str) -> bool {
802 self.stop_sequences.iter().any(|seq| seq == s)
803 }
804
805 pub fn system_prompt_byte_len(&self) -> usize {
810 self.system_prompt.len()
811 }
812
813 pub fn has_valid_temperature(&self) -> bool {
818 self.temperature.map_or(false, |t| (0.0..=2.0).contains(&t))
819 }
820}
821
822pub struct ToolSpec {
826 pub name: String,
828 pub description: String,
830 pub(crate) handler: AsyncToolHandler,
832 pub required_fields: Vec<String>,
835 pub validators: Vec<Box<dyn ToolValidator>>,
838 #[cfg(feature = "orchestrator")]
840 pub circuit_breaker: Option<Arc<crate::orchestrator::CircuitBreaker>>,
841}
842
843impl std::fmt::Debug for ToolSpec {
844 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
845 let mut s = f.debug_struct("ToolSpec");
846 s.field("name", &self.name)
847 .field("description", &self.description)
848 .field("required_fields", &self.required_fields);
849 #[cfg(feature = "orchestrator")]
850 s.field("has_circuit_breaker", &self.circuit_breaker.is_some());
851 s.finish()
852 }
853}
854
855impl ToolSpec {
856 pub fn new(
859 name: impl Into<String>,
860 description: impl Into<String>,
861 handler: impl Fn(Value) -> Value + Send + Sync + 'static,
862 ) -> Self {
863 Self {
864 name: name.into(),
865 description: description.into(),
866 handler: Box::new(move |args| {
867 let result = handler(args);
868 Box::pin(async move { result })
869 }),
870 required_fields: Vec::new(),
871 validators: Vec::new(),
872 #[cfg(feature = "orchestrator")]
873 circuit_breaker: None,
874 }
875 }
876
877 pub fn new_async(
879 name: impl Into<String>,
880 description: impl Into<String>,
881 handler: impl Fn(Value) -> AsyncToolFuture + Send + Sync + 'static,
882 ) -> Self {
883 Self {
884 name: name.into(),
885 description: description.into(),
886 handler: Box::new(handler),
887 required_fields: Vec::new(),
888 validators: Vec::new(),
889 #[cfg(feature = "orchestrator")]
890 circuit_breaker: None,
891 }
892 }
893
894 pub fn new_fallible(
897 name: impl Into<String>,
898 description: impl Into<String>,
899 handler: impl Fn(Value) -> Result<Value, String> + Send + Sync + 'static,
900 ) -> Self {
901 Self {
902 name: name.into(),
903 description: description.into(),
904 handler: Box::new(move |args| {
905 let result = handler(args);
906 let value = match result {
907 Ok(v) => v,
908 Err(msg) => serde_json::json!({"error": msg, "ok": false}),
909 };
910 Box::pin(async move { value })
911 }),
912 required_fields: Vec::new(),
913 validators: Vec::new(),
914 #[cfg(feature = "orchestrator")]
915 circuit_breaker: None,
916 }
917 }
918
919 pub fn new_async_fallible(
922 name: impl Into<String>,
923 description: impl Into<String>,
924 handler: impl Fn(Value) -> AsyncToolResultFuture + Send + Sync + 'static,
925 ) -> Self {
926 Self {
927 name: name.into(),
928 description: description.into(),
929 handler: Box::new(move |args| {
930 let fut = handler(args);
931 Box::pin(async move {
932 match fut.await {
933 Ok(v) => v,
934 Err(msg) => serde_json::json!({"error": msg, "ok": false}),
935 }
936 })
937 }),
938 required_fields: Vec::new(),
939 validators: Vec::new(),
940 #[cfg(feature = "orchestrator")]
941 circuit_breaker: None,
942 }
943 }
944
945 pub fn with_required_fields(
951 mut self,
952 fields: impl IntoIterator<Item = impl Into<String>>,
953 ) -> Self {
954 self.required_fields = fields.into_iter().map(Into::into).collect();
955 self
956 }
957
958 pub fn with_validators(mut self, validators: Vec<Box<dyn ToolValidator>>) -> Self {
963 self.validators = validators;
964 self
965 }
966
967 #[cfg(feature = "orchestrator")]
969 pub fn with_circuit_breaker(mut self, cb: Arc<crate::orchestrator::CircuitBreaker>) -> Self {
970 self.circuit_breaker = Some(cb);
971 self
972 }
973
974 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
976 self.description = desc.into();
977 self
978 }
979
980 pub fn with_name(mut self, name: impl Into<String>) -> Self {
982 self.name = name.into();
983 self
984 }
985
986 pub fn required_field_count(&self) -> usize {
988 self.required_fields.len()
989 }
990
991 pub fn has_required_fields(&self) -> bool {
993 !self.required_fields.is_empty()
994 }
995
996 pub fn has_validators(&self) -> bool {
998 !self.validators.is_empty()
999 }
1000
1001 pub async fn call(&self, args: Value) -> Value {
1003 (self.handler)(args).await
1004 }
1005}
1006
1007pub trait ToolCache: Send + Sync {
1033 fn get(&self, tool_name: &str, args: &serde_json::Value) -> Option<serde_json::Value>;
1035 fn set(&self, tool_name: &str, args: &serde_json::Value, result: serde_json::Value);
1037}
1038
1039struct ToolCacheInner {
1043 map: HashMap<(String, String), serde_json::Value>,
1044 order: std::collections::VecDeque<(String, String)>,
1046}
1047
1048pub struct InMemoryToolCache {
1063 inner: std::sync::Mutex<ToolCacheInner>,
1064 max_entries: Option<usize>,
1065}
1066
1067impl std::fmt::Debug for InMemoryToolCache {
1068 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1069 let len = self.len();
1070 f.debug_struct("InMemoryToolCache")
1071 .field("entries", &len)
1072 .field("max_entries", &self.max_entries)
1073 .finish()
1074 }
1075}
1076
1077impl InMemoryToolCache {
1078 pub fn new() -> Self {
1080 Self {
1081 inner: std::sync::Mutex::new(ToolCacheInner {
1082 map: HashMap::new(),
1083 order: std::collections::VecDeque::new(),
1084 }),
1085 max_entries: None,
1086 }
1087 }
1088
1089 pub fn with_max_entries(max: usize) -> Self {
1091 Self {
1092 inner: std::sync::Mutex::new(ToolCacheInner {
1093 map: HashMap::new(),
1094 order: std::collections::VecDeque::new(),
1095 }),
1096 max_entries: Some(max),
1097 }
1098 }
1099
1100 pub fn clear(&self) {
1102 if let Ok(mut inner) = self.inner.lock() {
1103 inner.map.clear();
1104 inner.order.clear();
1105 }
1106 }
1107
1108 pub fn len(&self) -> usize {
1110 self.inner.lock().map(|s| s.map.len()).unwrap_or(0)
1111 }
1112
1113 pub fn is_empty(&self) -> bool {
1115 self.len() == 0
1116 }
1117
1118 pub fn contains(&self, tool_name: &str, args: &serde_json::Value) -> bool {
1120 let key = (tool_name.to_owned(), args.to_string());
1121 self.inner
1122 .lock()
1123 .map(|s| s.map.contains_key(&key))
1124 .unwrap_or(false)
1125 }
1126
1127 pub fn remove(&self, tool_name: &str, args: &serde_json::Value) -> bool {
1129 let key = (tool_name.to_owned(), args.to_string());
1130 if let Ok(mut inner) = self.inner.lock() {
1131 if inner.map.remove(&key).is_some() {
1132 inner.order.retain(|k| k != &key);
1133 return true;
1134 }
1135 }
1136 false
1137 }
1138
1139 pub fn capacity(&self) -> Option<usize> {
1141 self.max_entries
1142 }
1143}
1144
1145impl Default for InMemoryToolCache {
1146 fn default() -> Self {
1147 Self::new()
1148 }
1149}
1150
1151impl ToolCache for InMemoryToolCache {
1152 fn get(&self, tool_name: &str, args: &serde_json::Value) -> Option<serde_json::Value> {
1153 let key = (tool_name.to_owned(), args.to_string());
1154 self.inner.lock().ok()?.map.get(&key).cloned()
1155 }
1156
1157 fn set(&self, tool_name: &str, args: &serde_json::Value, result: serde_json::Value) {
1158 let key = (tool_name.to_owned(), args.to_string());
1159 if let Ok(mut inner) = self.inner.lock() {
1160 if !inner.map.contains_key(&key) {
1161 inner.order.push_back(key.clone());
1162 }
1163 inner.map.insert(key, result);
1164 if let Some(max) = self.max_entries {
1165 while inner.map.len() > max {
1166 if let Some(oldest) = inner.order.pop_front() {
1167 inner.map.remove(&oldest);
1168 }
1169 }
1170 }
1171 }
1172 }
1173}
1174
1175pub struct ToolRegistry {
1179 tools: HashMap<String, ToolSpec>,
1180 cache: Option<Arc<dyn ToolCache>>,
1182}
1183
1184impl std::fmt::Debug for ToolRegistry {
1185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1186 f.debug_struct("ToolRegistry")
1187 .field("tools", &self.tools.keys().collect::<Vec<_>>())
1188 .field("has_cache", &self.cache.is_some())
1189 .finish()
1190 }
1191}
1192
1193impl Default for ToolRegistry {
1194 fn default() -> Self {
1195 Self::new()
1196 }
1197}
1198
1199impl ToolRegistry {
1200 pub fn new() -> Self {
1202 Self {
1203 tools: HashMap::new(),
1204 cache: None,
1205 }
1206 }
1207
1208 pub fn with_cache(mut self, cache: Arc<dyn ToolCache>) -> Self {
1210 self.cache = Some(cache);
1211 self
1212 }
1213
1214 pub fn register(&mut self, spec: ToolSpec) {
1216 self.tools.insert(spec.name.clone(), spec);
1217 }
1218
1219 pub fn register_tools(&mut self, specs: impl IntoIterator<Item = ToolSpec>) {
1226 for spec in specs {
1227 self.register(spec);
1228 }
1229 }
1230
1231 pub fn with_tool(mut self, spec: ToolSpec) -> Self {
1242 self.register(spec);
1243 self
1244 }
1245
1246 #[tracing::instrument(skip_all, fields(tool_name = %name))]
1254 pub async fn call(&self, name: &str, args: Value) -> Result<Value, AgentRuntimeError> {
1255 let spec = self.tools.get(name).ok_or_else(|| {
1256 let mut suggestion = String::new();
1257 let names = self.tool_names();
1258 if !names.is_empty() {
1259 if let Some((closest, dist)) = names
1260 .iter()
1261 .map(|n| (n, levenshtein(name, n)))
1262 .min_by_key(|(_, d)| *d)
1263 {
1264 if dist <= 3 {
1265 suggestion = format!(" (did you mean '{closest}'?)");
1266 }
1267 }
1268 }
1269 AgentRuntimeError::AgentLoop(format!("tool '{name}' not found{suggestion}"))
1270 })?;
1271
1272 if !spec.required_fields.is_empty() {
1274 if let Some(obj) = args.as_object() {
1275 for field in &spec.required_fields {
1276 if !obj.contains_key(field) {
1277 return Err(AgentRuntimeError::AgentLoop(format!(
1278 "tool '{}' missing required field '{}'",
1279 name, field
1280 )));
1281 }
1282 }
1283 } else {
1284 return Err(AgentRuntimeError::AgentLoop(format!(
1285 "tool '{}' requires JSON object args, got {}",
1286 name, args
1287 )));
1288 }
1289 }
1290
1291 for validator in &spec.validators {
1293 validator.validate(&args)?;
1294 }
1295
1296 #[cfg(feature = "orchestrator")]
1298 if let Some(ref cb) = spec.circuit_breaker {
1299 use crate::orchestrator::CircuitState;
1300 if let Ok(CircuitState::Open { .. }) = cb.state() {
1301 return Err(AgentRuntimeError::CircuitOpen {
1302 service: format!("tool:{}", name),
1303 });
1304 }
1305 }
1306
1307 if let Some(ref cache) = self.cache {
1309 if let Some(cached) = cache.get(name, &args) {
1310 return Ok(cached);
1311 }
1312 }
1313
1314 let result = spec.call(args.clone()).await;
1315
1316 #[cfg(feature = "orchestrator")]
1320 if let Some(ref cb) = spec.circuit_breaker {
1321 let is_failure = result
1322 .get("ok")
1323 .and_then(|v| v.as_bool())
1324 .is_some_and(|ok| !ok);
1325 if is_failure {
1326 cb.record_failure();
1327 } else {
1328 cb.record_success();
1329 }
1330 }
1331
1332 if let Some(ref cache) = self.cache {
1334 cache.set(name, &args, result.clone());
1335 }
1336
1337 Ok(result)
1338 }
1339
1340 pub fn get(&self, name: &str) -> Option<&ToolSpec> {
1342 self.tools.get(name)
1343 }
1344
1345 pub fn has_tool(&self, name: &str) -> bool {
1347 self.tools.contains_key(name)
1348 }
1349
1350 pub fn unregister(&mut self, name: &str) -> bool {
1352 self.tools.remove(name).is_some()
1353 }
1354
1355 pub fn tool_names(&self) -> Vec<&str> {
1357 self.tools.keys().map(|s| s.as_str()).collect()
1358 }
1359
1360 pub fn tool_names_owned(&self) -> Vec<String> {
1367 self.tools.keys().cloned().collect()
1368 }
1369
1370 pub fn all_tool_names(&self) -> Vec<String> {
1374 let mut names: Vec<String> = self.tools.keys().cloned().collect();
1375 names.sort();
1376 names
1377 }
1378
1379 pub fn tool_specs(&self) -> Vec<&ToolSpec> {
1381 self.tools.values().collect()
1382 }
1383
1384 pub fn filter_tools<F: Fn(&ToolSpec) -> bool>(&self, pred: F) -> Vec<&ToolSpec> {
1393 self.tools.values().filter(|s| pred(s)).collect()
1394 }
1395
1396 pub fn rename_tool(&mut self, old_name: &str, new_name: impl Into<String>) -> bool {
1403 let Some(mut spec) = self.tools.remove(old_name) else {
1404 return false;
1405 };
1406 let new_name = new_name.into();
1407 spec.name = new_name.clone();
1408 self.tools.insert(new_name, spec);
1409 true
1410 }
1411
1412 pub fn tool_count(&self) -> usize {
1414 self.tools.len()
1415 }
1416
1417 pub fn is_empty(&self) -> bool {
1419 self.tools.is_empty()
1420 }
1421
1422 pub fn clear(&mut self) {
1427 self.tools.clear();
1428 }
1429
1430 pub fn remove(&mut self, name: &str) -> Option<ToolSpec> {
1434 self.tools.remove(name)
1435 }
1436
1437 pub fn contains(&self, name: &str) -> bool {
1439 self.tools.contains_key(name)
1440 }
1441
1442 pub fn descriptions(&self) -> Vec<(&str, &str)> {
1446 let mut pairs: Vec<(&str, &str)> = self
1447 .tools
1448 .values()
1449 .map(|s| (s.name.as_str(), s.description.as_str()))
1450 .collect();
1451 pairs.sort_unstable_by_key(|(name, _)| *name);
1452 pairs
1453 }
1454
1455 pub fn find_by_description_keyword(&self, keyword: &str) -> Vec<&ToolSpec> {
1458 let lower = keyword.to_ascii_lowercase();
1459 self.tools
1460 .values()
1461 .filter(|s| s.description.to_ascii_lowercase().contains(&lower))
1462 .collect()
1463 }
1464
1465 pub fn tool_count_with_required_fields(&self) -> usize {
1467 self.tools.values().filter(|s| s.has_required_fields()).count()
1468 }
1469
1470 pub fn tool_count_with_validators(&self) -> usize {
1477 self.tools.values().filter(|s| s.has_validators()).count()
1478 }
1479
1480 pub fn names(&self) -> Vec<&str> {
1482 let mut names: Vec<&str> = self.tools.keys().map(|k| k.as_str()).collect();
1483 names.sort_unstable();
1484 names
1485 }
1486
1487 pub fn tool_names_starting_with(&self, prefix: &str) -> Vec<&str> {
1490 let mut names: Vec<&str> = self
1491 .tools
1492 .keys()
1493 .filter(|k| k.starts_with(prefix))
1494 .map(|k| k.as_str())
1495 .collect();
1496 names.sort_unstable();
1497 names
1498 }
1499
1500 pub fn description_for(&self, name: &str) -> Option<&str> {
1503 self.tools.get(name).map(|s| s.description.as_str())
1504 }
1505
1506 pub fn count_with_description_containing(&self, keyword: &str) -> usize {
1509 let lower = keyword.to_ascii_lowercase();
1510 self.tools
1511 .values()
1512 .filter(|s| s.description.to_ascii_lowercase().contains(&lower))
1513 .count()
1514 }
1515
1516 pub fn unregister_all(&mut self) {
1518 self.tools.clear();
1519 }
1520
1521 pub fn names_containing(&self, substring: &str) -> Vec<&str> {
1523 let sub = substring.to_ascii_lowercase();
1524 let mut names: Vec<&str> = self
1525 .tools
1526 .keys()
1527 .filter(|name| name.to_ascii_lowercase().contains(&sub))
1528 .map(|s| s.as_str())
1529 .collect();
1530 names.sort_unstable();
1531 names
1532 }
1533
1534 pub fn shortest_description(&self) -> Option<&str> {
1538 self.tools
1539 .values()
1540 .min_by_key(|s| s.description.len())
1541 .map(|s| s.description.as_str())
1542 }
1543
1544 pub fn longest_description(&self) -> Option<&str> {
1548 self.tools
1549 .values()
1550 .max_by_key(|s| s.description.len())
1551 .map(|s| s.description.as_str())
1552 }
1553
1554 pub fn all_descriptions(&self) -> Vec<&str> {
1556 let mut descs: Vec<&str> = self.tools.values().map(|s| s.description.as_str()).collect();
1557 descs.sort_unstable();
1558 descs
1559 }
1560
1561 pub fn tool_names_with_keyword(&self, keyword: &str) -> Vec<&str> {
1563 let kw = keyword.to_ascii_lowercase();
1564 self.tools
1565 .values()
1566 .filter(|s| s.description.to_ascii_lowercase().contains(&kw))
1567 .map(|s| s.name.as_str())
1568 .collect()
1569 }
1570
1571 pub fn avg_description_length(&self) -> f64 {
1575 if self.tools.is_empty() {
1576 return 0.0;
1577 }
1578 let total: usize = self.tools.values().map(|s| s.description.len()).sum();
1579 total as f64 / self.tools.len() as f64
1580 }
1581
1582 pub fn tool_names_sorted(&self) -> Vec<&str> {
1584 let mut names: Vec<&str> = self.tools.keys().map(|k| k.as_str()).collect();
1585 names.sort_unstable();
1586 names
1587 }
1588
1589 pub fn description_contains_count(&self, keyword: &str) -> usize {
1591 let kw = keyword.to_ascii_lowercase();
1592 self.tools
1593 .values()
1594 .filter(|s| s.description.to_ascii_lowercase().contains(&kw))
1595 .count()
1596 }
1597
1598 pub fn total_description_bytes(&self) -> usize {
1603 self.tools.values().map(|s| s.description.len()).sum()
1604 }
1605
1606 pub fn shortest_description_length(&self) -> usize {
1609 self.tools
1610 .values()
1611 .map(|s| s.description.len())
1612 .min()
1613 .unwrap_or(0)
1614 }
1615
1616 pub fn longest_description_length(&self) -> usize {
1619 self.tools
1620 .values()
1621 .map(|s| s.description.len())
1622 .max()
1623 .unwrap_or(0)
1624 }
1625
1626 pub fn tool_count_above_desc_bytes(&self, min_bytes: usize) -> usize {
1632 self.tools
1633 .values()
1634 .filter(|s| s.description.len() > min_bytes)
1635 .count()
1636 }
1637
1638 pub fn tools_with_required_field(&self, field: &str) -> Vec<&ToolSpec> {
1643 self.tools
1644 .values()
1645 .filter(|s| s.required_fields.iter().any(|f| f == field))
1646 .collect()
1647 }
1648
1649 pub fn tools_without_required_fields(&self) -> Vec<&ToolSpec> {
1654 self.tools
1655 .values()
1656 .filter(|s| s.required_fields.is_empty())
1657 .collect()
1658 }
1659
1660 pub fn avg_required_fields_count(&self) -> f64 {
1664 if self.tools.is_empty() {
1665 return 0.0;
1666 }
1667 let total: usize = self.tools.values().map(|s| s.required_fields.len()).sum();
1668 total as f64 / self.tools.len() as f64
1669 }
1670
1671 pub fn tool_descriptions_total_words(&self) -> usize {
1676 self.tools
1677 .values()
1678 .map(|spec| spec.description.split_ascii_whitespace().count())
1679 .sum()
1680 }
1681
1682 pub fn has_tools_with_empty_descriptions(&self) -> bool {
1687 self.tools.values().any(|s| s.description.trim().is_empty())
1688 }
1689
1690 pub fn total_required_fields(&self) -> usize {
1695 self.tools.values().map(|s| s.required_fields.len()).sum()
1696 }
1697
1698 pub fn has_tool_with_description_containing(&self, keyword: &str) -> bool {
1701 self.tools.values().any(|s| s.description.contains(keyword))
1702 }
1703
1704 pub fn tools_with_description_longer_than(&self, min_bytes: usize) -> Vec<&str> {
1709 let mut names: Vec<&str> = self
1710 .tools
1711 .values()
1712 .filter(|s| s.description.len() > min_bytes)
1713 .map(|s| s.name.as_str())
1714 .collect();
1715 names.sort_unstable();
1716 names
1717 }
1718
1719 pub fn max_description_bytes(&self) -> usize {
1721 self.tools.values().map(|s| s.description.len()).max().unwrap_or(0)
1722 }
1723
1724 pub fn min_description_bytes(&self) -> usize {
1726 self.tools.values().map(|s| s.description.len()).min().unwrap_or(0)
1727 }
1728
1729 pub fn description_starts_with_any(&self, prefixes: &[&str]) -> bool {
1735 self.tools
1736 .values()
1737 .any(|s| prefixes.iter().any(|p| s.description.starts_with(p)))
1738 }
1739
1740 pub fn tool_with_most_required_fields(&self) -> Option<&ToolSpec> {
1746 self.tools.values().max_by(|a, b| {
1747 a.required_fields
1748 .len()
1749 .cmp(&b.required_fields.len())
1750 .then_with(|| b.name.cmp(&a.name))
1751 })
1752 }
1753
1754 pub fn tool_by_name(&self, name: &str) -> Option<&ToolSpec> {
1756 self.tools.get(name)
1757 }
1758
1759 pub fn tools_without_validators(&self) -> Vec<&str> {
1766 let mut names: Vec<&str> = self
1767 .tools
1768 .values()
1769 .filter(|s| s.validators.is_empty())
1770 .map(|s| s.name.as_str())
1771 .collect();
1772 names.sort_unstable();
1773 names
1774 }
1775
1776 pub fn tool_names_with_required_fields(&self) -> Vec<&str> {
1782 let mut names: Vec<&str> = self
1783 .tools
1784 .values()
1785 .filter(|s| !s.required_fields.is_empty())
1786 .map(|s| s.name.as_str())
1787 .collect();
1788 names.sort_unstable();
1789 names
1790 }
1791
1792 pub fn has_all_tools(&self, names: &[&str]) -> bool {
1797 names.iter().all(|n| self.tools.contains_key(*n))
1798 }
1799
1800 pub fn tools_with_required_fields_count(&self) -> usize {
1804 self.tools
1805 .values()
1806 .filter(|t| !t.required_fields.is_empty())
1807 .count()
1808 }
1809
1810 pub fn tool_names_with_prefix<'a>(&'a self, prefix: &str) -> Vec<&'a str> {
1815 let mut names: Vec<&str> = self
1816 .tools
1817 .keys()
1818 .filter(|n| n.starts_with(prefix))
1819 .map(|n| n.as_str())
1820 .collect();
1821 names.sort_unstable();
1822 names
1823 }
1824}
1825
1826pub fn parse_react_step(text: &str) -> Result<ReActStep, AgentRuntimeError> {
1837 #[derive(PartialEq)]
1839 enum Section { None, Thought, Action }
1840
1841 let mut thought_lines: Vec<&str> = Vec::new();
1842 let mut action_lines: Vec<&str> = Vec::new();
1843 let mut current = Section::None;
1844
1845 for line in text.lines() {
1846 let trimmed = line.trim();
1847 let lower = trimmed.to_ascii_lowercase();
1848 if lower.starts_with("thought") {
1849 if let Some(colon_pos) = trimmed.find(':') {
1850 current = Section::Thought;
1851 thought_lines.clear();
1852 let first = trimmed[colon_pos + 1..].trim();
1853 if !first.is_empty() {
1854 thought_lines.push(first);
1855 }
1856 continue;
1857 }
1858 } else if lower.starts_with("action") {
1859 if let Some(colon_pos) = trimmed.find(':') {
1860 current = Section::Action;
1861 action_lines.clear();
1862 let first = trimmed[colon_pos + 1..].trim();
1863 if !first.is_empty() {
1864 action_lines.push(first);
1865 }
1866 continue;
1867 }
1868 } else if lower.starts_with("observation") {
1869 current = Section::None;
1871 continue;
1872 }
1873 match current {
1875 Section::Thought => thought_lines.push(trimmed),
1876 Section::Action => action_lines.push(trimmed),
1877 Section::None => {}
1878 }
1879 }
1880
1881 let thought = thought_lines.join(" ");
1882 let action = action_lines.join("\n").trim().to_owned();
1883
1884 if thought.is_empty() && action.is_empty() {
1885 return Err(AgentRuntimeError::AgentLoop(
1886 "could not parse ReAct step from response".into(),
1887 ));
1888 }
1889
1890 Ok(ReActStep {
1891 thought,
1892 action,
1893 observation: String::new(),
1894 step_duration_ms: 0,
1895 })
1896}
1897
1898pub struct ReActLoop {
1900 config: AgentConfig,
1901 registry: ToolRegistry,
1902 metrics: Option<Arc<RuntimeMetrics>>,
1904 #[cfg(feature = "persistence")]
1906 checkpoint_backend: Option<(Arc<dyn crate::persistence::PersistenceBackend>, String)>,
1907 observer: Option<Arc<dyn Observer>>,
1909 action_hook: Option<ActionHook>,
1911}
1912
1913impl std::fmt::Debug for ReActLoop {
1914 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1915 let mut s = f.debug_struct("ReActLoop");
1916 s.field("config", &self.config)
1917 .field("registry", &self.registry)
1918 .field("has_metrics", &self.metrics.is_some())
1919 .field("has_observer", &self.observer.is_some())
1920 .field("has_action_hook", &self.action_hook.is_some());
1921 #[cfg(feature = "persistence")]
1922 s.field("has_checkpoint_backend", &self.checkpoint_backend.is_some());
1923 s.finish()
1924 }
1925}
1926
1927impl ReActLoop {
1928 pub fn new(config: AgentConfig) -> Self {
1930 Self {
1931 config,
1932 registry: ToolRegistry::new(),
1933 metrics: None,
1934 #[cfg(feature = "persistence")]
1935 checkpoint_backend: None,
1936 observer: None,
1937 action_hook: None,
1938 }
1939 }
1940
1941 pub fn with_observer(mut self, observer: Arc<dyn Observer>) -> Self {
1943 self.observer = Some(observer);
1944 self
1945 }
1946
1947 pub fn with_action_hook(mut self, hook: ActionHook) -> Self {
1949 self.action_hook = Some(hook);
1950 self
1951 }
1952
1953 pub fn with_metrics(mut self, metrics: Arc<RuntimeMetrics>) -> Self {
1958 self.metrics = Some(metrics);
1959 self
1960 }
1961
1962 #[cfg(feature = "persistence")]
1968 pub fn with_step_checkpoint(
1969 mut self,
1970 backend: Arc<dyn crate::persistence::PersistenceBackend>,
1971 session_id: impl Into<String>,
1972 ) -> Self {
1973 self.checkpoint_backend = Some((backend, session_id.into()));
1974 self
1975 }
1976
1977 pub fn registry(&self) -> &ToolRegistry {
1979 &self.registry
1980 }
1981
1982 pub fn tool_count(&self) -> usize {
1986 self.registry.tool_count()
1987 }
1988
1989 pub fn unregister_tool(&mut self, name: &str) -> bool {
1991 self.registry.unregister(name)
1992 }
1993
1994 pub fn register_tool(&mut self, spec: ToolSpec) {
1996 self.registry.register(spec);
1997 }
1998
1999 pub fn register_tools(&mut self, specs: impl IntoIterator<Item = ToolSpec>) {
2005 for spec in specs {
2006 self.registry.register(spec);
2007 }
2008 }
2009
2010 fn maybe_trim_context(context: &mut String, max_chars: usize) {
2017 while context.len() > max_chars {
2018 let first = context.find("\nThought:");
2022 let second = first.and_then(|pos| {
2023 context[pos + 1..].find("\nThought:").map(|p| pos + 1 + p)
2024 });
2025 if let Some(drop_until) = second {
2026 context.drain(..drop_until);
2027 } else {
2028 break; }
2030 }
2031 }
2032
2033 fn blocked_observation() -> String {
2035 serde_json::json!({
2036 "ok": false,
2037 "error": "action blocked by reviewer",
2038 "kind": "blocked"
2039 })
2040 .to_string()
2041 }
2042
2043 fn error_observation(_tool_name: &str, e: &AgentRuntimeError) -> String {
2045 let kind = match e {
2046 AgentRuntimeError::AgentLoop(msg) if msg.contains("not found") => "not_found",
2047 #[cfg(feature = "orchestrator")]
2048 AgentRuntimeError::CircuitOpen { .. } => "transient",
2049 _ => "permanent",
2050 };
2051 serde_json::json!({ "ok": false, "error": e.to_string(), "kind": kind }).to_string()
2052 }
2053
2054 #[tracing::instrument(skip(infer))]
2068 pub async fn run<F, Fut>(
2069 &self,
2070 prompt: &str,
2071 mut infer: F,
2072 ) -> Result<Vec<ReActStep>, AgentRuntimeError>
2073 where
2074 F: FnMut(String) -> Fut,
2075 Fut: Future<Output = String>,
2076 {
2077 let mut steps: Vec<ReActStep> = Vec::new();
2078 let mut context = format!("{}\n\nUser: {}\n", self.config.system_prompt, prompt);
2079
2080 let deadline = self
2082 .config
2083 .loop_timeout
2084 .map(|d| std::time::Instant::now() + d);
2085
2086 if let Some(ref obs) = self.observer {
2088 obs.on_loop_start(prompt);
2089 }
2090
2091 for iteration in 0..self.config.max_iterations {
2092 let iter_span = tracing::info_span!(
2093 "react_iteration",
2094 iteration = iteration,
2095 model = %self.config.model,
2096 );
2097 let _iter_guard = iter_span.enter();
2098
2099 if let Some(dl) = deadline {
2101 if std::time::Instant::now() >= dl {
2102 let ms = self
2103 .config
2104 .loop_timeout
2105 .map(|d| d.as_millis())
2106 .unwrap_or(0);
2107 let err = AgentRuntimeError::AgentLoop(format!("loop timeout after {ms} ms"));
2108 if let Some(ref obs) = self.observer {
2109 obs.on_error(&err);
2110 obs.on_loop_end(steps.len());
2111 }
2112 return Err(err);
2113 }
2114 }
2115
2116 let step_start = std::time::Instant::now();
2117 let response = infer(context.clone()).await;
2118 let mut step = parse_react_step(&response)?;
2119
2120 tracing::debug!(
2121 step = iteration,
2122 thought = %step.thought,
2123 action = %step.action,
2124 "ReAct iteration"
2125 );
2126
2127 if step.action.to_ascii_uppercase().starts_with("FINAL_ANSWER") {
2128 step.observation = step.action.clone();
2129 step.step_duration_ms = step_start.elapsed().as_millis() as u64;
2130 if let Some(ref m) = self.metrics {
2131 m.record_step_latency(step.step_duration_ms);
2132 }
2133 if let Some(ref obs) = self.observer {
2134 obs.on_step(iteration, &step);
2135 }
2136 steps.push(step);
2137 tracing::info!(step = iteration, "FINAL_ANSWER reached");
2138 if let Some(ref obs) = self.observer {
2139 obs.on_loop_end(steps.len());
2140 }
2141 return Ok(steps);
2142 }
2143
2144 let (tool_name, args) = parse_tool_call(&step.action)?;
2146
2147 tracing::debug!(
2148 step = iteration,
2149 tool_name = %tool_name,
2150 "dispatching tool call"
2151 );
2152
2153 if let Some(ref hook) = self.action_hook {
2155 if !hook(tool_name.clone(), args.clone()).await {
2156 if let Some(ref obs) = self.observer {
2157 obs.on_action_blocked(&tool_name, &args);
2158 }
2159 if let Some(ref m) = self.metrics {
2160 m.record_tool_call(&tool_name);
2161 m.record_tool_failure(&tool_name);
2162 }
2163 step.observation = Self::blocked_observation();
2164 step.step_duration_ms = step_start.elapsed().as_millis() as u64;
2165 if let Some(ref m) = self.metrics {
2166 m.record_step_latency(step.step_duration_ms);
2167 }
2168 let _ = write!(
2169 context,
2170 "\nThought: {}\nAction: {}\nObservation: {}\n",
2171 step.thought, step.action, step.observation
2172 );
2173 if let Some(max) = self.config.max_context_chars {
2174 Self::maybe_trim_context(&mut context, max);
2175 }
2176 if let Some(ref obs) = self.observer {
2177 obs.on_step(iteration, &step);
2178 }
2179 steps.push(step);
2180 continue;
2181 }
2182 }
2183
2184 if let Some(ref obs) = self.observer {
2186 obs.on_tool_call(&tool_name, &args);
2187 }
2188
2189 if let Some(ref m) = self.metrics {
2191 m.record_tool_call(&tool_name);
2192 }
2193
2194 let tool_span = tracing::info_span!("tool_dispatch", tool = %tool_name);
2196 let _tool_guard = tool_span.enter();
2197 let observation = match self.registry.call(&tool_name, args).await {
2198 Ok(result) => serde_json::json!({ "ok": true, "data": result }).to_string(),
2199 Err(e) => {
2200 if let Some(ref m) = self.metrics {
2202 m.record_tool_failure(&tool_name);
2203 }
2204 Self::error_observation(&tool_name, &e)
2205 }
2206 };
2207
2208 step.observation = observation.clone();
2209 step.step_duration_ms = step_start.elapsed().as_millis() as u64;
2210 if let Some(ref m) = self.metrics {
2211 m.record_step_latency(step.step_duration_ms);
2212 }
2213 let _ = write!(
2214 context,
2215 "\nThought: {}\nAction: {}\nObservation: {}\n",
2216 step.thought, step.action, observation
2217 );
2218 if let Some(max) = self.config.max_context_chars {
2219 Self::maybe_trim_context(&mut context, max);
2220 }
2221 if let Some(ref obs) = self.observer {
2222 obs.on_step(iteration, &step);
2223 }
2224 steps.push(step);
2225
2226 #[cfg(feature = "persistence")]
2228 if let Some((ref backend, ref session_id)) = self.checkpoint_backend {
2229 let step_idx = steps.len();
2230 let key = format!("loop:{session_id}:step:{step_idx}");
2231 match serde_json::to_vec(&steps) {
2232 Ok(bytes) => {
2233 if let Err(e) = backend.save(&key, &bytes).await {
2234 tracing::warn!(
2235 key = %key,
2236 error = %e,
2237 "loop step checkpoint save failed"
2238 );
2239 }
2240 }
2241 Err(e) => {
2242 tracing::warn!(
2243 step = step_idx,
2244 error = %e,
2245 "loop step checkpoint serialisation failed"
2246 );
2247 }
2248 }
2249 }
2250 }
2251
2252 let err = AgentRuntimeError::AgentLoop(format!(
2253 "max iterations ({}) reached without final answer",
2254 self.config.max_iterations
2255 ));
2256 tracing::warn!(
2257 max_iterations = self.config.max_iterations,
2258 "ReAct loop exhausted max iterations without FINAL_ANSWER"
2259 );
2260 if let Some(ref obs) = self.observer {
2261 obs.on_error(&err);
2262 obs.on_loop_end(steps.len());
2263 }
2264 Err(err)
2265 }
2266
2267 #[tracing::instrument(skip(infer_stream))]
2277 pub async fn run_streaming<F, Fut>(
2278 &self,
2279 prompt: &str,
2280 mut infer_stream: F,
2281 ) -> Result<Vec<ReActStep>, AgentRuntimeError>
2282 where
2283 F: FnMut(String) -> Fut,
2284 Fut: Future<
2285 Output = tokio::sync::mpsc::Receiver<Result<String, AgentRuntimeError>>,
2286 >,
2287 {
2288 self.run(prompt, move |ctx| {
2289 let rx_fut = infer_stream(ctx);
2290 async move {
2291 let mut rx = rx_fut.await;
2292 let mut out = String::new();
2293 while let Some(chunk) = rx.recv().await {
2294 match chunk {
2295 Ok(s) => out.push_str(&s),
2296 Err(e) => {
2297 tracing::warn!(error = %e, "streaming chunk error; skipping");
2298 }
2299 }
2300 }
2301 out
2302 }
2303 })
2304 .await
2305 }
2306}
2307
2308pub trait ToolValidator: Send + Sync {
2374 fn validate(&self, args: &Value) -> Result<(), AgentRuntimeError>;
2379}
2380
2381fn levenshtein(a: &str, b: &str) -> usize {
2385 let a: Vec<char> = a.chars().collect();
2386 let b: Vec<char> = b.chars().collect();
2387 let (m, n) = (a.len(), b.len());
2388 let mut dp = vec![vec![0usize; n + 1]; m + 1];
2389 for i in 0..=m {
2390 dp[i][0] = i;
2391 }
2392 for j in 0..=n {
2393 dp[0][j] = j;
2394 }
2395 for i in 1..=m {
2396 for j in 1..=n {
2397 dp[i][j] = if a[i - 1] == b[j - 1] {
2398 dp[i - 1][j - 1]
2399 } else {
2400 1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1])
2401 };
2402 }
2403 }
2404 dp[m][n]
2405}
2406
2407fn parse_tool_call(action: &str) -> Result<(String, Value), AgentRuntimeError> {
2413 let mut parts = action.splitn(2, ' ');
2414 let name = parts.next().unwrap_or("").to_owned();
2415 if name.is_empty() {
2416 return Err(AgentRuntimeError::AgentLoop(
2417 "tool call has an empty tool name".into(),
2418 ));
2419 }
2420 let args_str = parts.next().unwrap_or("{}");
2421 let args: Value = serde_json::from_str(args_str).map_err(|e| {
2422 AgentRuntimeError::AgentLoop(format!(
2423 "invalid JSON args for tool call '{name}': {e} (raw: {args_str})"
2424 ))
2425 })?;
2426 Ok((name, args))
2427}
2428
2429#[derive(Debug, thiserror::Error)]
2433pub enum AgentError {
2434 #[error("Tool '{0}' not found")]
2436 ToolNotFound(String),
2437 #[error("Max iterations exceeded: {0}")]
2439 MaxIterations(usize),
2440 #[error("Parse error: {0}")]
2442 ParseError(String),
2443}
2444
2445impl From<AgentError> for AgentRuntimeError {
2446 fn from(e: AgentError) -> Self {
2447 AgentRuntimeError::AgentLoop(e.to_string())
2448 }
2449}
2450
2451pub trait Observer: Send + Sync {
2458 fn on_step(&self, step_index: usize, step: &ReActStep) {
2460 let _ = (step_index, step);
2461 }
2462 fn on_tool_call(&self, tool_name: &str, args: &serde_json::Value) {
2464 let _ = (tool_name, args);
2465 }
2466 fn on_action_blocked(&self, tool_name: &str, args: &serde_json::Value) {
2471 let _ = (tool_name, args);
2472 }
2473 fn on_loop_start(&self, prompt: &str) {
2475 let _ = prompt;
2476 }
2477 fn on_loop_end(&self, step_count: usize) {
2479 let _ = step_count;
2480 }
2481 fn on_error(&self, error: &crate::error::AgentRuntimeError) {
2486 let _ = error;
2487 }
2488}
2489
2490#[derive(Debug, Clone, PartialEq)]
2494pub enum Action {
2495 FinalAnswer(String),
2497 ToolCall {
2499 name: String,
2501 args: serde_json::Value,
2503 },
2504}
2505
2506impl Action {
2507 pub fn parse(s: &str) -> Result<Action, AgentRuntimeError> {
2512 if s.trim().to_ascii_uppercase().starts_with("FINAL_ANSWER") {
2513 let answer = s.trim()["FINAL_ANSWER".len()..].trim().to_owned();
2514 return Ok(Action::FinalAnswer(answer));
2515 }
2516 let (name, args) = parse_tool_call(s)?;
2517 Ok(Action::ToolCall { name, args })
2518 }
2519}
2520
2521pub type ActionHook = Arc<dyn Fn(String, serde_json::Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send>> + Send + Sync>;
2540
2541pub fn make_action_hook<F, Fut>(f: F) -> ActionHook
2556where
2557 F: Fn(String, serde_json::Value) -> Fut + Send + Sync + 'static,
2558 Fut: std::future::Future<Output = bool> + Send + 'static,
2559{
2560 Arc::new(move |name, args| Box::pin(f(name, args)))
2561}
2562
2563#[cfg(test)]
2566mod tests {
2567 use super::*;
2568
2569 #[tokio::test]
2570 async fn test_final_answer_on_first_step() {
2571 let config = AgentConfig::new(5, "test-model");
2572 let loop_ = ReActLoop::new(config);
2573
2574 let steps = loop_
2575 .run("Say hello", |_ctx| async {
2576 "Thought: I will answer directly\nAction: FINAL_ANSWER hello".to_string()
2577 })
2578 .await
2579 .unwrap();
2580
2581 assert_eq!(steps.len(), 1);
2582 assert!(steps[0]
2583 .action
2584 .to_ascii_uppercase()
2585 .starts_with("FINAL_ANSWER"));
2586 }
2587
2588 #[tokio::test]
2589 async fn test_tool_call_then_final_answer() {
2590 let config = AgentConfig::new(5, "test-model");
2591 let mut loop_ = ReActLoop::new(config);
2592
2593 loop_.register_tool(ToolSpec::new("greet", "Greets someone", |_args| {
2594 serde_json::json!("hello!")
2595 }));
2596
2597 let mut call_count = 0;
2598 let steps = loop_
2599 .run("Say hello", |_ctx| {
2600 call_count += 1;
2601 let count = call_count;
2602 async move {
2603 if count == 1 {
2604 "Thought: I will greet\nAction: greet {}".to_string()
2605 } else {
2606 "Thought: done\nAction: FINAL_ANSWER done".to_string()
2607 }
2608 }
2609 })
2610 .await
2611 .unwrap();
2612
2613 assert_eq!(steps.len(), 2);
2614 assert_eq!(steps[0].action, "greet {}");
2615 assert!(steps[1]
2616 .action
2617 .to_ascii_uppercase()
2618 .starts_with("FINAL_ANSWER"));
2619 }
2620
2621 #[tokio::test]
2622 async fn test_max_iterations_exceeded() {
2623 let config = AgentConfig::new(2, "test-model");
2624 let loop_ = ReActLoop::new(config);
2625
2626 let result = loop_
2627 .run("loop forever", |_ctx| async {
2628 "Thought: thinking\nAction: noop {}".to_string()
2629 })
2630 .await;
2631
2632 assert!(result.is_err());
2633 let err = result.unwrap_err().to_string();
2634 assert!(err.contains("max iterations"));
2635 }
2636
2637 #[tokio::test]
2638 async fn test_parse_react_step_valid() {
2639 let text = "Thought: I should check\nAction: lookup {\"key\":\"val\"}";
2640 let step = parse_react_step(text).unwrap();
2641 assert_eq!(step.thought, "I should check");
2642 assert_eq!(step.action, "lookup {\"key\":\"val\"}");
2643 }
2644
2645 #[tokio::test]
2646 async fn test_parse_react_step_empty_fails() {
2647 let result = parse_react_step("no prefix lines here");
2648 assert!(result.is_err());
2649 }
2650
2651 #[tokio::test]
2652 async fn test_tool_not_found_returns_error_observation() {
2653 let config = AgentConfig::new(3, "test-model");
2654 let loop_ = ReActLoop::new(config);
2655
2656 let mut call_count = 0;
2657 let steps = loop_
2658 .run("test", |_ctx| {
2659 call_count += 1;
2660 let count = call_count;
2661 async move {
2662 if count == 1 {
2663 "Thought: try missing tool\nAction: missing_tool {}".to_string()
2664 } else {
2665 "Thought: done\nAction: FINAL_ANSWER done".to_string()
2666 }
2667 }
2668 })
2669 .await
2670 .unwrap();
2671
2672 assert_eq!(steps.len(), 2);
2673 assert!(steps[0].observation.contains("\"ok\":false"));
2674 }
2675
2676 #[tokio::test]
2677 async fn test_new_async_tool_spec() {
2678 let spec = ToolSpec::new_async("async_tool", "An async tool", |args| {
2679 Box::pin(async move { serde_json::json!({"echo": args}) })
2680 });
2681
2682 let result = spec.call(serde_json::json!({"input": "test"})).await;
2683 assert!(result.get("echo").is_some());
2684 }
2685
2686 #[tokio::test]
2689 async fn test_parse_react_step_case_insensitive() {
2690 let text = "THOUGHT: done\nACTION: FINAL_ANSWER";
2691 let step = parse_react_step(text).unwrap();
2692 assert_eq!(step.thought, "done");
2693 assert_eq!(step.action, "FINAL_ANSWER");
2694 }
2695
2696 #[tokio::test]
2697 async fn test_parse_react_step_space_before_colon() {
2698 let text = "Thought : done\nAction : go";
2699 let step = parse_react_step(text).unwrap();
2700 assert_eq!(step.thought, "done");
2701 assert_eq!(step.action, "go");
2702 }
2703
2704 #[tokio::test]
2707 async fn test_tool_required_fields_missing_returns_error() {
2708 let config = AgentConfig::new(3, "test-model");
2709 let mut loop_ = ReActLoop::new(config);
2710
2711 loop_.register_tool(
2712 ToolSpec::new(
2713 "search",
2714 "Searches for something",
2715 |args| serde_json::json!({ "result": args }),
2716 )
2717 .with_required_fields(vec!["q".to_string()]),
2718 );
2719
2720 let mut call_count = 0;
2721 let steps = loop_
2722 .run("test", |_ctx| {
2723 call_count += 1;
2724 let count = call_count;
2725 async move {
2726 if count == 1 {
2727 "Thought: searching\nAction: search {}".to_string()
2729 } else {
2730 "Thought: done\nAction: FINAL_ANSWER done".to_string()
2731 }
2732 }
2733 })
2734 .await
2735 .unwrap();
2736
2737 assert_eq!(steps.len(), 2);
2738 assert!(
2739 steps[0].observation.contains("missing required field"),
2740 "observation was: {}",
2741 steps[0].observation
2742 );
2743 }
2744
2745 #[tokio::test]
2748 async fn test_tool_error_observation_includes_kind() {
2749 let config = AgentConfig::new(3, "test-model");
2750 let loop_ = ReActLoop::new(config);
2751
2752 let mut call_count = 0;
2753 let steps = loop_
2754 .run("test", |_ctx| {
2755 call_count += 1;
2756 let count = call_count;
2757 async move {
2758 if count == 1 {
2759 "Thought: try missing\nAction: nonexistent_tool {}".to_string()
2760 } else {
2761 "Thought: done\nAction: FINAL_ANSWER done".to_string()
2762 }
2763 }
2764 })
2765 .await
2766 .unwrap();
2767
2768 assert_eq!(steps.len(), 2);
2769 let obs = &steps[0].observation;
2770 assert!(obs.contains("\"ok\":false"), "observation: {obs}");
2771 assert!(obs.contains("\"kind\":\"not_found\""), "observation: {obs}");
2772 }
2773
2774 #[tokio::test]
2777 async fn test_step_duration_ms_is_set() {
2778 let config = AgentConfig::new(5, "test-model");
2779 let loop_ = ReActLoop::new(config);
2780
2781 let steps = loop_
2782 .run("time it", |_ctx| async {
2783 "Thought: done\nAction: FINAL_ANSWER ok".to_string()
2784 })
2785 .await
2786 .unwrap();
2787
2788 let _ = steps[0].step_duration_ms; }
2791
2792 struct RequirePositiveN;
2795 impl ToolValidator for RequirePositiveN {
2796 fn validate(&self, args: &Value) -> Result<(), AgentRuntimeError> {
2797 let n = args.get("n").and_then(|v| v.as_i64()).unwrap_or(0);
2798 if n <= 0 {
2799 return Err(AgentRuntimeError::AgentLoop(
2800 "n must be a positive integer".into(),
2801 ));
2802 }
2803 Ok(())
2804 }
2805 }
2806
2807 #[tokio::test]
2808 async fn test_tool_validator_blocks_invalid_args() {
2809 let mut registry = ToolRegistry::new();
2810 registry.register(
2811 ToolSpec::new("calc", "compute", |args| serde_json::json!({"n": args}))
2812 .with_validators(vec![Box::new(RequirePositiveN)]),
2813 );
2814
2815 let result = registry
2817 .call("calc", serde_json::json!({"n": -1}))
2818 .await;
2819 assert!(result.is_err(), "validator should reject n=-1");
2820 assert!(result.unwrap_err().to_string().contains("positive integer"));
2821 }
2822
2823 #[tokio::test]
2824 async fn test_tool_validator_passes_valid_args() {
2825 let mut registry = ToolRegistry::new();
2826 registry.register(
2827 ToolSpec::new("calc", "compute", |_| serde_json::json!(42))
2828 .with_validators(vec![Box::new(RequirePositiveN)]),
2829 );
2830
2831 let result = registry
2832 .call("calc", serde_json::json!({"n": 5}))
2833 .await;
2834 assert!(result.is_ok(), "validator should accept n=5");
2835 }
2836
2837 #[tokio::test]
2840 async fn test_empty_tool_name_is_rejected() {
2841 let result = parse_tool_call("");
2843 assert!(result.is_err());
2844 assert!(
2845 result.unwrap_err().to_string().contains("empty tool name"),
2846 "expected 'empty tool name' error"
2847 );
2848 }
2849
2850 #[tokio::test]
2853 async fn test_register_tools_bulk() {
2854 let mut registry = ToolRegistry::new();
2855 registry.register_tools(vec![
2856 ToolSpec::new("tool_a", "A", |_| serde_json::json!("a")),
2857 ToolSpec::new("tool_b", "B", |_| serde_json::json!("b")),
2858 ]);
2859 assert!(registry.call("tool_a", serde_json::json!({})).await.is_ok());
2860 assert!(registry.call("tool_b", serde_json::json!({})).await.is_ok());
2861 }
2862
2863 #[tokio::test]
2866 async fn test_run_streaming_parity_with_run() {
2867 use tokio::sync::mpsc;
2868
2869 let config = AgentConfig::new(5, "test-model");
2870 let loop_ = ReActLoop::new(config);
2871
2872 let steps = loop_
2873 .run_streaming("Say hello", |_ctx| async {
2874 let (tx, rx) = mpsc::channel(4);
2875 tokio::spawn(async move {
2877 tx.send(Ok("Thought: done\n".to_string())).await.ok();
2878 tx.send(Ok("Action: FINAL_ANSWER hi".to_string())).await.ok();
2879 });
2880 rx
2881 })
2882 .await
2883 .unwrap();
2884
2885 assert_eq!(steps.len(), 1);
2886 assert!(steps[0]
2887 .action
2888 .to_ascii_uppercase()
2889 .starts_with("FINAL_ANSWER"));
2890 }
2891
2892 #[tokio::test]
2893 async fn test_run_streaming_error_chunk_is_skipped() {
2894 use tokio::sync::mpsc;
2895 use crate::error::AgentRuntimeError;
2896
2897 let config = AgentConfig::new(5, "test-model");
2898 let loop_ = ReActLoop::new(config);
2899
2900 let steps = loop_
2902 .run_streaming("test", |_ctx| async {
2903 let (tx, rx) = mpsc::channel(4);
2904 tokio::spawn(async move {
2905 tx.send(Err(AgentRuntimeError::Provider("stream error".into())))
2906 .await
2907 .ok();
2908 tx.send(Ok("Thought: recovered\nAction: FINAL_ANSWER ok".to_string()))
2909 .await
2910 .ok();
2911 });
2912 rx
2913 })
2914 .await
2915 .unwrap();
2916
2917 assert_eq!(steps.len(), 1);
2918 }
2919
2920 #[cfg(feature = "orchestrator")]
2923 #[tokio::test]
2924 async fn test_tool_with_circuit_breaker_passes_when_closed() {
2925 use std::sync::Arc;
2926
2927 let cb = Arc::new(
2928 crate::orchestrator::CircuitBreaker::new(
2929 "echo-tool",
2930 5,
2931 std::time::Duration::from_secs(30),
2932 )
2933 .unwrap(),
2934 );
2935
2936 let spec = ToolSpec::new(
2937 "echo",
2938 "Echoes args",
2939 |args| serde_json::json!({ "echoed": args }),
2940 )
2941 .with_circuit_breaker(cb);
2942
2943 let registry = {
2944 let mut r = ToolRegistry::new();
2945 r.register(spec);
2946 r
2947 };
2948
2949 let result = registry
2950 .call("echo", serde_json::json!({ "msg": "hi" }))
2951 .await;
2952 assert!(result.is_ok(), "expected Ok, got {:?}", result);
2953 }
2954
2955 #[test]
2958 fn test_agent_config_builder_methods_set_fields() {
2959 let config = AgentConfig::new(3, "model")
2960 .with_temperature(0.7)
2961 .with_max_tokens(512)
2962 .with_request_timeout(std::time::Duration::from_secs(10));
2963 assert_eq!(config.temperature, Some(0.7));
2964 assert_eq!(config.max_tokens, Some(512));
2965 assert_eq!(config.request_timeout, Some(std::time::Duration::from_secs(10)));
2966 }
2967
2968 #[tokio::test]
2971 async fn test_fallible_tool_returns_error_json_on_err() {
2972 let spec = ToolSpec::new_fallible(
2973 "fail",
2974 "always fails",
2975 |_| Err::<Value, String>("something went wrong".to_string()),
2976 );
2977 let result = spec.call(serde_json::json!({})).await;
2978 assert_eq!(result["ok"], serde_json::json!(false));
2979 assert_eq!(result["error"], serde_json::json!("something went wrong"));
2980 }
2981
2982 #[tokio::test]
2983 async fn test_fallible_tool_returns_value_on_ok() {
2984 let spec = ToolSpec::new_fallible(
2985 "succeed",
2986 "always succeeds",
2987 |_| Ok::<Value, String>(serde_json::json!(42)),
2988 );
2989 let result = spec.call(serde_json::json!({})).await;
2990 assert_eq!(result, serde_json::json!(42));
2991 }
2992
2993 #[tokio::test]
2996 async fn test_did_you_mean_suggestion_for_typo() {
2997 let mut registry = ToolRegistry::new();
2998 registry.register(ToolSpec::new("search", "search", |_| serde_json::json!("ok")));
2999 let result = registry.call("searc", serde_json::json!({})).await;
3000 assert!(result.is_err());
3001 let msg = result.unwrap_err().to_string();
3002 assert!(msg.contains("did you mean"), "expected suggestion in: {msg}");
3003 }
3004
3005 #[tokio::test]
3006 async fn test_no_suggestion_for_very_different_name() {
3007 let mut registry = ToolRegistry::new();
3008 registry.register(ToolSpec::new("search", "search", |_| serde_json::json!("ok")));
3009 let result = registry.call("xxxxxxxxxxxxxxx", serde_json::json!({})).await;
3010 assert!(result.is_err());
3011 let msg = result.unwrap_err().to_string();
3012 assert!(!msg.contains("did you mean"), "unexpected suggestion in: {msg}");
3013 }
3014
3015 #[test]
3018 fn test_action_parse_final_answer() {
3019 let action = Action::parse("FINAL_ANSWER hello world").unwrap();
3020 assert_eq!(action, Action::FinalAnswer("hello world".to_string()));
3021 }
3022
3023 #[test]
3024 fn test_action_parse_tool_call() {
3025 let action = Action::parse("search {\"q\": \"rust\"}").unwrap();
3026 match action {
3027 Action::ToolCall { name, args } => {
3028 assert_eq!(name, "search");
3029 assert_eq!(args["q"], "rust");
3030 }
3031 _ => panic!("expected ToolCall"),
3032 }
3033 }
3034
3035 #[test]
3036 fn test_action_parse_invalid_returns_err() {
3037 let result = Action::parse("");
3038 assert!(result.is_err());
3039 }
3040
3041 #[tokio::test]
3044 async fn test_observer_on_step_called_for_each_step() {
3045 use std::sync::{Arc, Mutex};
3046
3047 struct CountingObserver {
3048 step_count: Mutex<usize>,
3049 }
3050 impl Observer for CountingObserver {
3051 fn on_step(&self, _step_index: usize, _step: &ReActStep) {
3052 let mut c = self.step_count.lock().unwrap_or_else(|e| e.into_inner());
3053 *c += 1;
3054 }
3055 }
3056
3057 let obs = Arc::new(CountingObserver { step_count: Mutex::new(0) });
3058 let config = AgentConfig::new(5, "test-model");
3059 let mut loop_ = ReActLoop::new(config).with_observer(obs.clone() as Arc<dyn Observer>);
3060 loop_.register_tool(ToolSpec::new("noop", "noop", |_| serde_json::json!("ok")));
3061
3062 let mut call_count = 0;
3063 let _steps = loop_.run("test", |_ctx| {
3064 call_count += 1;
3065 let count = call_count;
3066 async move {
3067 if count == 1 {
3068 "Thought: call noop\nAction: noop {}".to_string()
3069 } else {
3070 "Thought: done\nAction: FINAL_ANSWER done".to_string()
3071 }
3072 }
3073 }).await.unwrap();
3074
3075 let count = *obs.step_count.lock().unwrap_or_else(|e| e.into_inner());
3076 assert_eq!(count, 2, "observer should have seen 2 steps");
3077 }
3078
3079 #[tokio::test]
3082 async fn test_tool_cache_returns_cached_result_on_second_call() {
3083 use std::collections::HashMap;
3084 use std::sync::Mutex;
3085
3086 struct InMemCache {
3087 map: Mutex<HashMap<String, Value>>,
3088 }
3089 impl ToolCache for InMemCache {
3090 fn get(&self, tool_name: &str, args: &Value) -> Option<Value> {
3091 let key = format!("{tool_name}:{args}");
3092 let map = self.map.lock().unwrap_or_else(|e| e.into_inner());
3093 map.get(&key).cloned()
3094 }
3095 fn set(&self, tool_name: &str, args: &Value, result: Value) {
3096 let key = format!("{tool_name}:{args}");
3097 let mut map = self.map.lock().unwrap_or_else(|e| e.into_inner());
3098 map.insert(key, result);
3099 }
3100 }
3101
3102 let call_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3103 let call_count_clone = call_count.clone();
3104
3105 let cache = Arc::new(InMemCache { map: Mutex::new(HashMap::new()) });
3106 let registry = ToolRegistry::new()
3107 .with_cache(cache as Arc<dyn ToolCache>);
3108 let mut registry = registry;
3109
3110 registry.register(ToolSpec::new("count", "count calls", move |_| {
3111 call_count_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3112 serde_json::json!({"calls": 1})
3113 }));
3114
3115 let args = serde_json::json!({});
3116 let r1 = registry.call("count", args.clone()).await.unwrap();
3117 let r2 = registry.call("count", args.clone()).await.unwrap();
3118
3119 assert_eq!(r1, r2);
3120 assert_eq!(call_count.load(std::sync::atomic::Ordering::Relaxed), 1);
3122 }
3123
3124 #[tokio::test]
3127 async fn test_validators_short_circuit_on_first_failure() {
3128 use std::sync::atomic::{AtomicUsize, Ordering as AOrdering};
3129 use std::sync::Arc;
3130
3131 let second_called = Arc::new(AtomicUsize::new(0));
3132 let second_called_clone = Arc::clone(&second_called);
3133
3134 struct AlwaysFail;
3135 impl ToolValidator for AlwaysFail {
3136 fn validate(&self, _args: &Value) -> Result<(), AgentRuntimeError> {
3137 Err(AgentRuntimeError::AgentLoop("first validator failed".into()))
3138 }
3139 }
3140
3141 struct CountCalls(Arc<AtomicUsize>);
3142 impl ToolValidator for CountCalls {
3143 fn validate(&self, _args: &Value) -> Result<(), AgentRuntimeError> {
3144 self.0.fetch_add(1, AOrdering::SeqCst);
3145 Ok(())
3146 }
3147 }
3148
3149 let mut registry = ToolRegistry::new();
3150 registry.register(
3151 ToolSpec::new("guarded", "A guarded tool", |args| args.clone())
3152 .with_validators(vec![
3153 Box::new(AlwaysFail),
3154 Box::new(CountCalls(second_called_clone)),
3155 ]),
3156 );
3157
3158 let result = registry.call("guarded", serde_json::json!({})).await;
3159 assert!(result.is_err(), "should fail due to first validator");
3160 assert_eq!(
3161 second_called.load(AOrdering::SeqCst),
3162 0,
3163 "second validator must not be called when first fails"
3164 );
3165 }
3166
3167 #[tokio::test]
3170 async fn test_loop_timeout_fires_between_iterations() {
3171 let mut config = AgentConfig::new(100, "test-model");
3172 config.loop_timeout = Some(std::time::Duration::from_millis(30));
3174 let loop_ = ReActLoop::new(config);
3175
3176 let result = loop_
3177 .run("test", |_ctx| async {
3178 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
3180 "Thought: still working\nAction: noop {}".to_string()
3182 })
3183 .await;
3184
3185 assert!(result.is_err(), "loop should time out");
3186 let msg = result.unwrap_err().to_string();
3187 assert!(msg.contains("loop timeout"), "unexpected error: {msg}");
3188 }
3189
3190 #[test]
3195 fn test_react_step_is_final_answer() {
3196 let step = ReActStep {
3197 thought: "".into(),
3198 action: "FINAL_ANSWER done".into(),
3199 observation: "".into(),
3200 step_duration_ms: 0,
3201 };
3202 assert!(step.is_final_answer());
3203 assert!(!step.is_tool_call());
3204 }
3205
3206 #[test]
3207 fn test_react_step_is_tool_call() {
3208 let step = ReActStep {
3209 thought: "".into(),
3210 action: "search {}".into(),
3211 observation: "".into(),
3212 step_duration_ms: 0,
3213 };
3214 assert!(!step.is_final_answer());
3215 assert!(step.is_tool_call());
3216 }
3217
3218 #[test]
3221 fn test_role_display() {
3222 assert_eq!(Role::System.to_string(), "system");
3223 assert_eq!(Role::User.to_string(), "user");
3224 assert_eq!(Role::Assistant.to_string(), "assistant");
3225 assert_eq!(Role::Tool.to_string(), "tool");
3226 }
3227
3228 #[test]
3231 fn test_message_accessors() {
3232 let msg = Message::new(Role::User, "hello");
3233 assert_eq!(msg.role(), &Role::User);
3234 assert_eq!(msg.content(), "hello");
3235 }
3236
3237 #[test]
3240 fn test_action_parse_final_answer_round_trip() {
3241 let step = ReActStep {
3242 thought: "done".into(),
3243 action: "FINAL_ANSWER Paris".into(),
3244 observation: "".into(),
3245 step_duration_ms: 0,
3246 };
3247 assert!(step.is_final_answer());
3248 let action = Action::parse(&step.action).unwrap();
3249 assert!(matches!(action, Action::FinalAnswer(ref s) if s == "Paris"));
3250 }
3251
3252 #[test]
3253 fn test_action_parse_tool_call_round_trip() {
3254 let step = ReActStep {
3255 thought: "searching".into(),
3256 action: "search {\"q\":\"hello\"}".into(),
3257 observation: "".into(),
3258 step_duration_ms: 0,
3259 };
3260 assert!(step.is_tool_call());
3261 let action = Action::parse(&step.action).unwrap();
3262 assert!(matches!(action, Action::ToolCall { ref name, .. } if name == "search"));
3263 }
3264
3265 #[tokio::test]
3268 async fn test_observer_receives_correct_step_indices() {
3269 use std::sync::{Arc, Mutex};
3270
3271 struct IndexCollector(Arc<Mutex<Vec<usize>>>);
3272 impl Observer for IndexCollector {
3273 fn on_step(&self, step_index: usize, _step: &ReActStep) {
3274 self.0.lock().unwrap_or_else(|e| e.into_inner()).push(step_index);
3275 }
3276 }
3277
3278 let indices = Arc::new(Mutex::new(Vec::new()));
3279 let obs = Arc::new(IndexCollector(Arc::clone(&indices)));
3280
3281 let config = AgentConfig::new(5, "test");
3282 let mut loop_ = ReActLoop::new(config).with_observer(obs as Arc<dyn Observer>);
3283 loop_.register_tool(ToolSpec::new("noop", "no-op", |_| serde_json::json!({})));
3284
3285 let mut call_count = 0;
3286 loop_.run("test", |_ctx| {
3287 call_count += 1;
3288 let count = call_count;
3289 async move {
3290 if count == 1 {
3291 "Thought: step1\nAction: noop {}".to_string()
3292 } else {
3293 "Thought: done\nAction: FINAL_ANSWER ok".to_string()
3294 }
3295 }
3296 }).await.unwrap();
3297
3298 let collected = indices.lock().unwrap_or_else(|e| e.into_inner()).clone();
3299 assert_eq!(collected, vec![0, 1], "expected step indices 0 and 1");
3300 }
3301
3302 #[tokio::test]
3303 async fn test_action_hook_blocking_inserts_blocked_observation() {
3304 let hook: ActionHook = Arc::new(|_name, _args| {
3305 Box::pin(async move { false }) });
3307
3308 let config = AgentConfig::new(5, "test-model");
3309 let mut loop_ = ReActLoop::new(config).with_action_hook(hook);
3310 loop_.register_tool(ToolSpec::new("noop", "noop", |_| serde_json::json!("ok")));
3311
3312 let mut call_count = 0;
3313 let steps = loop_.run("test", |_ctx| {
3314 call_count += 1;
3315 let count = call_count;
3316 async move {
3317 if count == 1 {
3318 "Thought: try tool\nAction: noop {}".to_string()
3319 } else {
3320 "Thought: done\nAction: FINAL_ANSWER done".to_string()
3321 }
3322 }
3323 }).await.unwrap();
3324
3325 assert!(steps[0].observation.contains("blocked"), "expected blocked observation, got: {}", steps[0].observation);
3326 }
3327
3328 #[test]
3329 fn test_react_step_new_constructor() {
3330 let s = ReActStep::new("think", "act", "obs");
3331 assert_eq!(s.thought, "think");
3332 assert_eq!(s.action, "act");
3333 assert_eq!(s.observation, "obs");
3334 assert_eq!(s.step_duration_ms, 0);
3335 }
3336
3337 #[test]
3338 fn test_react_step_new_is_tool_call() {
3339 let s = ReActStep::new("think", "search {}", "result");
3340 assert!(s.is_tool_call());
3341 assert!(!s.is_final_answer());
3342 }
3343
3344 #[test]
3345 fn test_react_step_new_is_final_answer() {
3346 let s = ReActStep::new("done", "FINAL_ANSWER 42", "");
3347 assert!(s.is_final_answer());
3348 assert!(!s.is_tool_call());
3349 }
3350
3351 #[test]
3352 fn test_agent_config_is_valid_with_valid_config() {
3353 let cfg = AgentConfig::new(5, "my-model");
3354 assert!(cfg.is_valid());
3355 }
3356
3357 #[test]
3358 fn test_agent_config_is_valid_with_zero_iterations() {
3359 let mut cfg = AgentConfig::new(1, "my-model");
3360 cfg.max_iterations = 0;
3361 assert!(!cfg.is_valid());
3362 }
3363
3364 #[test]
3365 fn test_agent_config_is_valid_with_empty_model() {
3366 let mut cfg = AgentConfig::new(5, "my-model");
3367 cfg.model = String::new();
3368 assert!(!cfg.is_valid());
3369 }
3370
3371 #[test]
3372 fn test_react_loop_tool_count_delegates_to_registry() {
3373 let cfg = AgentConfig::new(5, "model");
3374 let mut loop_ = ReActLoop::new(cfg);
3375 assert_eq!(loop_.tool_count(), 0);
3376 loop_.register_tool(ToolSpec::new("t1", "desc", |_| serde_json::json!("ok")));
3377 loop_.register_tool(ToolSpec::new("t2", "desc", |_| serde_json::json!("ok")));
3378 assert_eq!(loop_.tool_count(), 2);
3379 }
3380
3381 #[test]
3382 fn test_tool_registry_has_tool_returns_true_when_registered() {
3383 let mut reg = ToolRegistry::new();
3384 reg.register(ToolSpec::new("my-tool", "desc", |_| serde_json::json!("ok")));
3385 assert!(reg.has_tool("my-tool"));
3386 assert!(!reg.has_tool("other-tool"));
3387 }
3388
3389 #[test]
3392 fn test_agent_config_validate_ok_for_valid_config() {
3393 let cfg = AgentConfig::new(5, "my-model");
3394 assert!(cfg.validate().is_ok());
3395 }
3396
3397 #[test]
3398 fn test_agent_config_validate_err_for_zero_iterations() {
3399 let cfg = AgentConfig::new(0, "my-model");
3400 let err = cfg.validate().unwrap_err();
3401 assert!(err.to_string().contains("max_iterations"));
3402 }
3403
3404 #[test]
3405 fn test_agent_config_validate_err_for_empty_model() {
3406 let cfg = AgentConfig::new(5, "");
3407 let err = cfg.validate().unwrap_err();
3408 assert!(err.to_string().contains("model"));
3409 }
3410
3411 #[test]
3414 fn test_clone_with_model_produces_new_model_string() {
3415 let cfg = AgentConfig::new(5, "gpt-4");
3416 let new_cfg = cfg.clone_with_model("claude-3");
3417 assert_eq!(new_cfg.model, "claude-3");
3418 assert_eq!(cfg.model, "gpt-4");
3420 }
3421
3422 #[test]
3423 fn test_clone_with_model_preserves_other_fields() {
3424 let cfg = AgentConfig::new(10, "gpt-4").with_stop_sequences(vec!["STOP".to_string()]);
3425 let new_cfg = cfg.clone_with_model("o1");
3426 assert_eq!(new_cfg.max_iterations, 10);
3427 assert_eq!(new_cfg.stop_sequences, cfg.stop_sequences);
3428 }
3429
3430 #[tokio::test]
3431 async fn test_tool_spec_with_name_changes_name() {
3432 let spec = ToolSpec::new("original", "desc", |_| serde_json::json!("ok"))
3433 .with_name("renamed");
3434 assert_eq!(spec.name, "renamed");
3435 }
3436
3437 #[tokio::test]
3438 async fn test_tool_spec_with_name_and_description_chainable() {
3439 let spec = ToolSpec::new("old", "old desc", |_| serde_json::json!("ok"))
3440 .with_name("new")
3441 .with_description("new desc");
3442 assert_eq!(spec.name, "new");
3443 assert_eq!(spec.description, "new desc");
3444 }
3445
3446 #[test]
3449 fn test_message_user_sets_role_and_content() {
3450 let m = Message::user("hello");
3451 assert_eq!(m.content(), "hello");
3452 assert!(m.is_user());
3453 assert!(!m.is_assistant());
3454 }
3455
3456 #[test]
3457 fn test_message_assistant_sets_role() {
3458 let m = Message::assistant("reply");
3459 assert!(m.is_assistant());
3460 assert!(!m.is_user());
3461 assert!(!m.is_system());
3462 }
3463
3464 #[test]
3465 fn test_message_system_sets_role() {
3466 let m = Message::system("system prompt");
3467 assert!(m.is_system());
3468 assert_eq!(m.content(), "system prompt");
3469 }
3470
3471 #[test]
3472 fn test_parse_react_step_valid_input() {
3473 let text = "Thought: I need to search\nAction: search[query]";
3474 let step = parse_react_step(text).unwrap();
3475 assert!(step.thought.contains("search"));
3476 assert!(step.action.contains("search"));
3477 }
3478
3479 #[test]
3480 fn test_parse_react_step_missing_fields_returns_err() {
3481 let text = "no structured content here";
3482 assert!(parse_react_step(text).is_err());
3483 }
3484
3485 #[test]
3488 fn test_react_step_is_final_answer_true() {
3489 let step = ReActStep::new("t", "FINAL_ANSWER Paris", "");
3490 assert!(step.is_final_answer());
3491 assert!(!step.is_tool_call());
3492 }
3493
3494 #[test]
3495 fn test_react_step_is_tool_call_true() {
3496 let step = ReActStep::new("t", "search {}", "result");
3497 assert!(step.is_tool_call());
3498 assert!(!step.is_final_answer());
3499 }
3500
3501 #[test]
3502 fn test_tool_registry_unregister_returns_true_when_present() {
3503 let mut reg = ToolRegistry::new();
3504 reg.register(ToolSpec::new("tool-x", "desc", |_| serde_json::json!("ok")));
3505 assert!(reg.unregister("tool-x"));
3506 assert!(!reg.has_tool("tool-x"));
3507 }
3508
3509 #[test]
3510 fn test_tool_registry_unregister_returns_false_when_absent() {
3511 let mut reg = ToolRegistry::new();
3512 assert!(!reg.unregister("ghost"));
3513 }
3514
3515 #[test]
3516 fn test_tool_registry_contains_matches_has_tool() {
3517 let mut reg = ToolRegistry::new();
3518 reg.register(ToolSpec::new("alpha", "desc", |_| serde_json::json!("ok")));
3519 assert!(reg.contains("alpha"));
3520 assert!(!reg.contains("beta"));
3521 }
3522
3523 #[test]
3524 fn test_agent_config_with_system_prompt() {
3525 let cfg = AgentConfig::new(5, "model")
3526 .with_system_prompt("You are helpful.");
3527 assert_eq!(cfg.system_prompt, "You are helpful.");
3528 }
3529
3530 #[test]
3531 fn test_agent_config_with_temperature_and_max_tokens() {
3532 let cfg = AgentConfig::new(3, "model")
3533 .with_temperature(0.7)
3534 .with_max_tokens(512);
3535 assert!((cfg.temperature.unwrap() - 0.7).abs() < 1e-6);
3536 assert_eq!(cfg.max_tokens, Some(512));
3537 }
3538
3539 #[test]
3540 fn test_agent_config_clone_with_model() {
3541 let orig = AgentConfig::new(5, "gpt-4");
3542 let cloned = orig.clone_with_model("claude-3");
3543 assert_eq!(cloned.model, "claude-3");
3544 assert_eq!(cloned.max_iterations, 5);
3545 }
3546
3547 #[test]
3550 fn test_agent_config_with_loop_timeout_secs() {
3551 let cfg = AgentConfig::new(5, "model").with_loop_timeout_secs(30);
3552 assert_eq!(cfg.loop_timeout, Some(std::time::Duration::from_secs(30)));
3553 }
3554
3555 #[test]
3556 fn test_agent_config_with_max_context_chars() {
3557 let cfg = AgentConfig::new(5, "model").with_max_context_chars(4096);
3558 assert_eq!(cfg.max_context_chars, Some(4096));
3559 }
3560
3561 #[test]
3562 fn test_agent_config_with_stop_sequences() {
3563 let cfg = AgentConfig::new(5, "model")
3564 .with_stop_sequences(vec!["STOP".to_string(), "END".to_string()]);
3565 assert_eq!(cfg.stop_sequences, vec!["STOP", "END"]);
3566 }
3567
3568 #[test]
3569 fn test_message_is_tool_false_for_non_tool_roles() {
3570 assert!(!Message::user("hi").is_tool());
3571 assert!(!Message::assistant("reply").is_tool());
3572 assert!(!Message::system("prompt").is_tool());
3573 }
3574
3575 #[test]
3578 fn test_agent_config_with_max_iterations() {
3579 let cfg = AgentConfig::new(5, "m").with_max_iterations(20);
3580 assert_eq!(cfg.max_iterations, 20);
3581 }
3582
3583 #[test]
3584 fn test_tool_registry_tool_names_owned_returns_strings() {
3585 let mut reg = ToolRegistry::new();
3586 reg.register(ToolSpec::new("alpha", "d", |_| serde_json::json!("ok")));
3587 reg.register(ToolSpec::new("beta", "d", |_| serde_json::json!("ok")));
3588 let mut names = reg.tool_names_owned();
3589 names.sort();
3590 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
3591 }
3592
3593 #[test]
3594 fn test_tool_registry_tool_names_owned_empty_when_no_tools() {
3595 let reg = ToolRegistry::new();
3596 assert!(reg.tool_names_owned().is_empty());
3597 }
3598
3599 #[test]
3602 fn test_tool_registry_tool_specs_returns_all_specs() {
3603 let mut reg = ToolRegistry::new();
3604 reg.register(ToolSpec::new("t1", "desc1", |_| serde_json::json!("ok")));
3605 reg.register(ToolSpec::new("t2", "desc2", |_| serde_json::json!("ok")));
3606 let specs = reg.tool_specs();
3607 assert_eq!(specs.len(), 2);
3608 }
3609
3610 #[test]
3611 fn test_tool_registry_tool_specs_empty_when_no_tools() {
3612 let reg = ToolRegistry::new();
3613 assert!(reg.tool_specs().is_empty());
3614 }
3615
3616 #[test]
3619 fn test_rename_tool_updates_name_and_key() {
3620 let mut reg = ToolRegistry::new();
3621 reg.register(ToolSpec::new("old", "desc", |_| serde_json::json!("ok")));
3622 assert!(reg.rename_tool("old", "new"));
3623 assert!(reg.has_tool("new"));
3624 assert!(!reg.has_tool("old"));
3625 let spec = reg.get("new").unwrap();
3626 assert_eq!(spec.name, "new");
3627 }
3628
3629 #[test]
3630 fn test_rename_tool_returns_false_for_unknown_name() {
3631 let mut reg = ToolRegistry::new();
3632 assert!(!reg.rename_tool("ghost", "other"));
3633 }
3634
3635 #[test]
3638 fn test_filter_tools_returns_matching_specs() {
3639 let mut reg = ToolRegistry::new();
3640 reg.register(ToolSpec::new("short_desc", "hi", |_| serde_json::json!({})));
3641 reg.register(ToolSpec::new("long_desc", "a longer description here", |_| serde_json::json!({})));
3642 let long_ones = reg.filter_tools(|s| s.description.len() > 10);
3643 assert_eq!(long_ones.len(), 1);
3644 assert_eq!(long_ones[0].name, "long_desc");
3645 }
3646
3647 #[test]
3648 fn test_filter_tools_returns_empty_when_none_match() {
3649 let mut reg = ToolRegistry::new();
3650 reg.register(ToolSpec::new("t1", "desc", |_| serde_json::json!({})));
3651 let none: Vec<_> = reg.filter_tools(|_| false);
3652 assert!(none.is_empty());
3653 }
3654
3655 #[test]
3656 fn test_filter_tools_returns_all_when_predicate_always_true() {
3657 let mut reg = ToolRegistry::new();
3658 reg.register(ToolSpec::new("a", "d1", |_| serde_json::json!({})));
3659 reg.register(ToolSpec::new("b", "d2", |_| serde_json::json!({})));
3660 let all = reg.filter_tools(|_| true);
3661 assert_eq!(all.len(), 2);
3662 }
3663
3664 #[test]
3667 fn test_agent_config_max_iterations_getter_returns_configured_value() {
3668 let cfg = AgentConfig::new(5, "model-x");
3669 assert_eq!(cfg.max_iterations(), 5);
3670 }
3671
3672 #[test]
3673 fn test_agent_config_with_max_iterations_updates_getter() {
3674 let cfg = AgentConfig::new(3, "m").with_max_iterations(10);
3675 assert_eq!(cfg.max_iterations(), 10);
3676 }
3677
3678 #[test]
3681 fn test_tool_registry_is_empty_true_when_new() {
3682 let reg = ToolRegistry::new();
3683 assert!(reg.is_empty());
3684 }
3685
3686 #[test]
3687 fn test_tool_registry_is_empty_false_after_register() {
3688 let mut reg = ToolRegistry::new();
3689 reg.register(ToolSpec::new("t", "d", |_| serde_json::json!({})));
3690 assert!(!reg.is_empty());
3691 }
3692
3693 #[test]
3694 fn test_tool_registry_clear_empties_registry() {
3695 let mut reg = ToolRegistry::new();
3696 reg.register(ToolSpec::new("t1", "d", |_| serde_json::json!({})));
3697 reg.register(ToolSpec::new("t2", "d", |_| serde_json::json!({})));
3698 reg.clear();
3699 assert!(reg.is_empty());
3700 assert_eq!(reg.tool_count(), 0);
3701 }
3702
3703 #[test]
3704 fn test_tool_registry_remove_returns_spec_and_decrements_count() {
3705 let mut reg = ToolRegistry::new();
3706 reg.register(ToolSpec::new("myTool", "desc", |_| serde_json::json!({})));
3707 assert_eq!(reg.tool_count(), 1);
3708 let removed = reg.remove("myTool");
3709 assert!(removed.is_some());
3710 assert_eq!(reg.tool_count(), 0);
3711 }
3712
3713 #[test]
3714 fn test_tool_registry_remove_returns_none_for_absent_tool() {
3715 let mut reg = ToolRegistry::new();
3716 assert!(reg.remove("ghost").is_none());
3717 }
3718
3719 #[test]
3722 fn test_all_tool_names_returns_sorted_names() {
3723 let mut reg = ToolRegistry::new();
3724 reg.register(ToolSpec::new("zebra", "d", |_| serde_json::json!({})));
3725 reg.register(ToolSpec::new("apple", "d", |_| serde_json::json!({})));
3726 reg.register(ToolSpec::new("mango", "d", |_| serde_json::json!({})));
3727 let names = reg.all_tool_names();
3728 assert_eq!(names, vec!["apple", "mango", "zebra"]);
3729 }
3730
3731 #[test]
3732 fn test_all_tool_names_empty_for_empty_registry() {
3733 let reg = ToolRegistry::new();
3734 assert!(reg.all_tool_names().is_empty());
3735 }
3736
3737 #[test]
3740 fn test_remaining_iterations_after_full_budget() {
3741 let cfg = AgentConfig::new(10, "m");
3742 assert_eq!(cfg.remaining_iterations_after(0), 10);
3743 }
3744
3745 #[test]
3746 fn test_remaining_iterations_after_partial_use() {
3747 let cfg = AgentConfig::new(10, "m");
3748 assert_eq!(cfg.remaining_iterations_after(3), 7);
3749 }
3750
3751 #[test]
3752 fn test_remaining_iterations_after_saturates_at_zero() {
3753 let cfg = AgentConfig::new(5, "m");
3754 assert_eq!(cfg.remaining_iterations_after(10), 0);
3755 }
3756
3757 #[test]
3758 fn test_tool_spec_required_field_count_zero_by_default() {
3759 let spec = ToolSpec::new("t", "d", |_| serde_json::json!({}));
3760 assert_eq!(spec.required_field_count(), 0);
3761 }
3762
3763 #[test]
3764 fn test_tool_spec_required_field_count_after_adding() {
3765 let spec = ToolSpec::new("t", "d", |_| serde_json::json!({}))
3766 .with_required_fields(["query", "limit"]);
3767 assert_eq!(spec.required_field_count(), 2);
3768 }
3769
3770 #[test]
3771 fn test_tool_spec_has_required_fields_false_by_default() {
3772 let spec = ToolSpec::new("t", "d", |_| serde_json::json!({}));
3773 assert!(!spec.has_required_fields());
3774 }
3775
3776 #[test]
3777 fn test_tool_spec_has_required_fields_true_after_adding() {
3778 let spec = ToolSpec::new("t", "d", |_| serde_json::json!({}))
3779 .with_required_fields(["key"]);
3780 assert!(spec.has_required_fields());
3781 }
3782
3783 #[test]
3784 fn test_tool_spec_has_validators_false_by_default() {
3785 let spec = ToolSpec::new("t", "d", |_| serde_json::json!({}));
3786 assert!(!spec.has_validators());
3787 }
3788
3789 #[test]
3792 fn test_tool_registry_contains_true_for_registered_tool() {
3793 let mut reg = ToolRegistry::new();
3794 reg.register(ToolSpec::new("search", "d", |_| serde_json::json!({})));
3795 assert!(reg.contains("search"));
3796 }
3797
3798 #[test]
3799 fn test_tool_registry_contains_false_for_unknown_tool() {
3800 let reg = ToolRegistry::new();
3801 assert!(!reg.contains("missing"));
3802 }
3803
3804 #[test]
3805 fn test_tool_registry_descriptions_sorted_by_name() {
3806 let mut reg = ToolRegistry::new();
3807 reg.register(ToolSpec::new("zebra", "z-desc", |_| serde_json::json!({})));
3808 reg.register(ToolSpec::new("apple", "a-desc", |_| serde_json::json!({})));
3809 let descs = reg.descriptions();
3810 assert_eq!(descs[0], ("apple", "a-desc"));
3811 assert_eq!(descs[1], ("zebra", "z-desc"));
3812 }
3813
3814 #[test]
3815 fn test_tool_registry_descriptions_empty_when_no_tools() {
3816 let reg = ToolRegistry::new();
3817 assert!(reg.descriptions().is_empty());
3818 }
3819
3820 #[test]
3821 fn test_tool_registry_tool_count_increments_on_register() {
3822 let mut reg = ToolRegistry::new();
3823 assert_eq!(reg.tool_count(), 0);
3824 reg.register(ToolSpec::new("t1", "d", |_| serde_json::json!({})));
3825 assert_eq!(reg.tool_count(), 1);
3826 reg.register(ToolSpec::new("t2", "d", |_| serde_json::json!({})));
3827 assert_eq!(reg.tool_count(), 2);
3828 }
3829
3830 #[test]
3833 fn test_observation_is_empty_true_for_empty_string() {
3834 let step = ReActStep::new("think", "search", "");
3835 assert!(step.observation_is_empty());
3836 }
3837
3838 #[test]
3839 fn test_observation_is_empty_false_for_non_empty() {
3840 let step = ReActStep::new("think", "search", "found results");
3841 assert!(!step.observation_is_empty());
3842 }
3843
3844 #[test]
3847 fn test_agent_config_temperature_getter_none_by_default() {
3848 let cfg = AgentConfig::new(5, "gpt-4");
3849 assert!(cfg.temperature().is_none());
3850 }
3851
3852 #[test]
3853 fn test_agent_config_temperature_getter_some_when_set() {
3854 let cfg = AgentConfig::new(5, "gpt-4").with_temperature(0.7);
3855 assert!((cfg.temperature().unwrap() - 0.7).abs() < 1e-5);
3856 }
3857
3858 #[test]
3859 fn test_agent_config_max_tokens_getter_none_by_default() {
3860 let cfg = AgentConfig::new(5, "gpt-4");
3861 assert!(cfg.max_tokens().is_none());
3862 }
3863
3864 #[test]
3865 fn test_agent_config_max_tokens_getter_some_when_set() {
3866 let cfg = AgentConfig::new(5, "gpt-4").with_max_tokens(512);
3867 assert_eq!(cfg.max_tokens(), Some(512));
3868 }
3869
3870 #[test]
3871 fn test_agent_config_request_timeout_getter_none_by_default() {
3872 let cfg = AgentConfig::new(5, "gpt-4");
3873 assert!(cfg.request_timeout().is_none());
3874 }
3875
3876 #[test]
3877 fn test_agent_config_request_timeout_getter_some_when_set() {
3878 let cfg = AgentConfig::new(5, "gpt-4")
3879 .with_request_timeout(std::time::Duration::from_secs(10));
3880 assert_eq!(cfg.request_timeout(), Some(std::time::Duration::from_secs(10)));
3881 }
3882
3883 #[test]
3886 fn test_agent_config_has_max_context_chars_false_by_default() {
3887 let cfg = AgentConfig::new(5, "gpt-4");
3888 assert!(!cfg.has_max_context_chars());
3889 }
3890
3891 #[test]
3892 fn test_agent_config_has_max_context_chars_true_after_setting() {
3893 let cfg = AgentConfig::new(5, "gpt-4").with_max_context_chars(8192);
3894 assert!(cfg.has_max_context_chars());
3895 }
3896
3897 #[test]
3898 fn test_agent_config_max_context_chars_none_by_default() {
3899 let cfg = AgentConfig::new(5, "gpt-4");
3900 assert_eq!(cfg.max_context_chars(), None);
3901 }
3902
3903 #[test]
3904 fn test_agent_config_max_context_chars_some_after_setting() {
3905 let cfg = AgentConfig::new(5, "gpt-4").with_max_context_chars(4096);
3906 assert_eq!(cfg.max_context_chars(), Some(4096));
3907 }
3908
3909 #[test]
3910 fn test_agent_config_system_prompt_returns_configured_prompt() {
3911 let cfg = AgentConfig::new(5, "gpt-4").with_system_prompt("Be concise.");
3912 assert_eq!(cfg.system_prompt(), "Be concise.");
3913 }
3914
3915 #[test]
3916 fn test_agent_config_model_returns_configured_model() {
3917 let cfg = AgentConfig::new(5, "claude-3");
3918 assert_eq!(cfg.model(), "claude-3");
3919 }
3920
3921 #[test]
3924 fn test_message_is_system_true_for_system_role() {
3925 let m = Message::system("context");
3926 assert!(m.is_system());
3927 }
3928
3929 #[test]
3930 fn test_message_is_system_false_for_user_role() {
3931 let m = Message::user("hello");
3932 assert!(!m.is_system());
3933 }
3934
3935 #[test]
3936 fn test_message_word_count_counts_whitespace_words() {
3937 let m = Message::user("hello world foo");
3938 assert_eq!(m.word_count(), 3);
3939 }
3940
3941 #[test]
3942 fn test_message_word_count_zero_for_empty_content() {
3943 let m = Message::user("");
3944 assert_eq!(m.word_count(), 0);
3945 }
3946
3947 #[test]
3948 fn test_agent_config_has_loop_timeout_false_by_default() {
3949 let cfg = AgentConfig::new(5, "m");
3950 assert!(!cfg.has_loop_timeout());
3951 }
3952
3953 #[test]
3954 fn test_agent_config_has_loop_timeout_true_after_setting() {
3955 let cfg = AgentConfig::new(5, "m")
3956 .with_loop_timeout(std::time::Duration::from_secs(30));
3957 assert!(cfg.has_loop_timeout());
3958 }
3959
3960 #[test]
3961 fn test_agent_config_has_stop_sequences_false_by_default() {
3962 let cfg = AgentConfig::new(5, "m");
3963 assert!(!cfg.has_stop_sequences());
3964 }
3965
3966 #[test]
3967 fn test_agent_config_has_stop_sequences_true_after_adding() {
3968 let cfg = AgentConfig::new(5, "m").with_stop_sequences(vec!["STOP".to_string()]);
3969 assert!(cfg.has_stop_sequences());
3970 }
3971
3972 #[test]
3973 fn test_agent_config_is_single_shot_true_when_max_iterations_one() {
3974 let cfg = AgentConfig::new(1, "m");
3975 assert!(cfg.is_single_shot());
3976 }
3977
3978 #[test]
3979 fn test_agent_config_is_single_shot_false_when_max_iterations_gt_one() {
3980 let cfg = AgentConfig::new(5, "m");
3981 assert!(!cfg.is_single_shot());
3982 }
3983
3984 #[test]
3985 fn test_agent_config_has_temperature_false_by_default() {
3986 let cfg = AgentConfig::new(5, "m");
3987 assert!(!cfg.has_temperature());
3988 }
3989
3990 #[test]
3991 fn test_agent_config_has_temperature_true_after_setting() {
3992 let cfg = AgentConfig::new(5, "m").with_temperature(0.7);
3993 assert!(cfg.has_temperature());
3994 }
3995
3996 #[test]
3999 fn test_tool_spec_new_fallible_returns_ok_value() {
4000 let rt = tokio::runtime::Runtime::new().unwrap();
4001 let tool = ToolSpec::new_fallible(
4002 "add",
4003 "adds numbers",
4004 |_args| Ok(serde_json::json!({"result": 42})),
4005 );
4006 let result = rt.block_on(tool.call(serde_json::json!({})));
4007 assert_eq!(result["result"], 42);
4008 }
4009
4010 #[test]
4011 fn test_tool_spec_new_fallible_wraps_error_as_json() {
4012 let rt = tokio::runtime::Runtime::new().unwrap();
4013 let tool = ToolSpec::new_fallible(
4014 "fail",
4015 "always fails",
4016 |_| Err("bad input".to_string()),
4017 );
4018 let result = rt.block_on(tool.call(serde_json::json!({})));
4019 assert_eq!(result["error"], "bad input");
4020 assert_eq!(result["ok"], false);
4021 }
4022
4023 #[test]
4024 fn test_tool_spec_new_async_fallible_wraps_error() {
4025 let rt = tokio::runtime::Runtime::new().unwrap();
4026 let tool = ToolSpec::new_async_fallible(
4027 "async_fail",
4028 "async error",
4029 |_| Box::pin(async { Err("async bad".to_string()) }),
4030 );
4031 let result = rt.block_on(tool.call(serde_json::json!({})));
4032 assert_eq!(result["error"], "async bad");
4033 }
4034
4035 #[test]
4036 fn test_tool_spec_with_required_fields_sets_fields() {
4037 let tool = ToolSpec::new("t", "d", |_| serde_json::json!({}))
4038 .with_required_fields(["name", "value"]);
4039 assert_eq!(tool.required_field_count(), 2);
4040 }
4041
4042 #[test]
4043 fn test_tool_spec_with_description_overrides_description() {
4044 let tool = ToolSpec::new("t", "original", |_| serde_json::json!({}))
4045 .with_description("updated description");
4046 assert_eq!(tool.description, "updated description");
4047 }
4048
4049 #[test]
4052 fn test_agent_config_stop_sequence_count_zero_by_default() {
4053 let cfg = AgentConfig::new(5, "gpt-4");
4054 assert_eq!(cfg.stop_sequence_count(), 0);
4055 }
4056
4057 #[test]
4058 fn test_agent_config_stop_sequence_count_reflects_configured_count() {
4059 let cfg = AgentConfig::new(5, "gpt-4")
4060 .with_stop_sequences(vec!["STOP".to_string(), "END".to_string()]);
4061 assert_eq!(cfg.stop_sequence_count(), 2);
4062 }
4063
4064 #[test]
4065 fn test_tool_registry_find_by_description_keyword_empty_when_no_match() {
4066 let mut reg = ToolRegistry::new();
4067 reg.register(ToolSpec::new("calc", "Performs arithmetic", |_| serde_json::json!({})));
4068 let results = reg.find_by_description_keyword("weather");
4069 assert!(results.is_empty());
4070 }
4071
4072 #[test]
4073 fn test_tool_registry_find_by_description_keyword_case_insensitive() {
4074 let mut reg = ToolRegistry::new();
4075 reg.register(ToolSpec::new("calc", "Performs ARITHMETIC operations", |_| serde_json::json!({})));
4076 reg.register(ToolSpec::new("search", "Searches the web", |_| serde_json::json!({})));
4077 let results = reg.find_by_description_keyword("arithmetic");
4078 assert_eq!(results.len(), 1);
4079 assert_eq!(results[0].name, "calc");
4080 }
4081
4082 #[test]
4083 fn test_tool_registry_find_by_description_keyword_multiple_matches() {
4084 let mut reg = ToolRegistry::new();
4085 reg.register(ToolSpec::new("t1", "query the database", |_| serde_json::json!({})));
4086 reg.register(ToolSpec::new("t2", "query the cache", |_| serde_json::json!({})));
4087 reg.register(ToolSpec::new("t3", "send a message", |_| serde_json::json!({})));
4088 let results = reg.find_by_description_keyword("query");
4089 assert_eq!(results.len(), 2);
4090 }
4091
4092 #[test]
4096 fn test_message_is_user_true_for_user_role_r31() {
4097 let msg = Message::user("hello");
4098 assert!(msg.is_user());
4099 assert!(!msg.is_assistant());
4100 }
4101
4102 #[test]
4103 fn test_message_is_assistant_true_for_assistant_role_r31() {
4104 let msg = Message::assistant("hi there");
4105 assert!(msg.is_assistant());
4106 assert!(!msg.is_user());
4107 }
4108
4109 #[test]
4110 fn test_agent_config_stop_sequence_count_zero_for_new_config() {
4111 let cfg = AgentConfig::new(5, "model");
4112 assert_eq!(cfg.stop_sequence_count(), 0);
4113 }
4114
4115 #[test]
4116 fn test_agent_config_stop_sequence_count_after_setting() {
4117 let cfg = AgentConfig::new(5, "model")
4118 .with_stop_sequences(vec!["<stop>".to_string(), "END".to_string()]);
4119 assert_eq!(cfg.stop_sequence_count(), 2);
4120 }
4121
4122 #[test]
4123 fn test_agent_config_has_request_timeout_false_by_default() {
4124 let cfg = AgentConfig::new(5, "model");
4125 assert!(!cfg.has_request_timeout());
4126 }
4127
4128 #[test]
4129 fn test_agent_config_has_request_timeout_true_after_setting() {
4130 let cfg = AgentConfig::new(5, "model")
4131 .with_request_timeout(std::time::Duration::from_secs(30));
4132 assert!(cfg.has_request_timeout());
4133 }
4134
4135 #[test]
4138 fn test_react_loop_unregister_tool_removes_registered_tool() {
4139 let mut agent = ReActLoop::new(AgentConfig::new(5, "m"));
4140 agent.register_tool(ToolSpec::new("t1", "desc", |_| serde_json::json!({})));
4141 assert!(agent.unregister_tool("t1"));
4142 assert_eq!(agent.tool_count(), 0);
4143 }
4144
4145 #[test]
4146 fn test_react_loop_unregister_tool_returns_false_for_unknown() {
4147 let mut agent = ReActLoop::new(AgentConfig::new(5, "m"));
4148 assert!(!agent.unregister_tool("nonexistent"));
4149 }
4150
4151 #[test]
4154 fn test_tool_count_with_required_fields_zero_when_empty() {
4155 let reg = ToolRegistry::new();
4156 assert_eq!(reg.tool_count_with_required_fields(), 0);
4157 }
4158
4159 #[test]
4160 fn test_tool_count_with_required_fields_excludes_tools_without_fields() {
4161 let mut reg = ToolRegistry::new();
4162 reg.register(ToolSpec::new("t1", "d", |_| serde_json::json!({})));
4163 assert_eq!(reg.tool_count_with_required_fields(), 0);
4164 }
4165
4166 #[test]
4167 fn test_tool_count_with_required_fields_counts_only_tools_with_fields() {
4168 let mut reg = ToolRegistry::new();
4169 reg.register(
4170 ToolSpec::new("t1", "d", |_| serde_json::json!({}))
4171 .with_required_fields(["query"]),
4172 );
4173 reg.register(ToolSpec::new("t2", "d", |_| serde_json::json!({}))); reg.register(
4175 ToolSpec::new("t3", "d", |_| serde_json::json!({}))
4176 .with_required_fields(["url", "method"]),
4177 );
4178 assert_eq!(reg.tool_count_with_required_fields(), 2);
4179 }
4180
4181 #[test]
4184 fn test_tool_registry_names_empty_when_no_tools() {
4185 let reg = ToolRegistry::new();
4186 assert!(reg.names().is_empty());
4187 }
4188
4189 #[test]
4190 fn test_tool_registry_names_sorted_alphabetically() {
4191 let mut reg = ToolRegistry::new();
4192 reg.register(ToolSpec::new("zebra", "d", |_| serde_json::json!({})));
4193 reg.register(ToolSpec::new("alpha", "d", |_| serde_json::json!({})));
4194 reg.register(ToolSpec::new("mango", "d", |_| serde_json::json!({})));
4195 assert_eq!(reg.names(), vec!["alpha", "mango", "zebra"]);
4196 }
4197
4198 #[test]
4201 fn test_tool_names_starting_with_empty_when_no_match() {
4202 let mut reg = ToolRegistry::new();
4203 reg.register(ToolSpec::new("search", "d", |_| serde_json::json!({})));
4204 assert!(reg.tool_names_starting_with("calc").is_empty());
4205 }
4206
4207 #[test]
4208 fn test_tool_names_starting_with_returns_sorted_matches() {
4209 let mut reg = ToolRegistry::new();
4210 reg.register(ToolSpec::new("db_write", "d", |_| serde_json::json!({})));
4211 reg.register(ToolSpec::new("db_read", "d", |_| serde_json::json!({})));
4212 reg.register(ToolSpec::new("cache_get", "d", |_| serde_json::json!({})));
4213 let results = reg.tool_names_starting_with("db_");
4214 assert_eq!(results, vec!["db_read", "db_write"]);
4215 }
4216
4217 #[test]
4220 fn test_tool_registry_description_for_none_when_missing() {
4221 let reg = ToolRegistry::new();
4222 assert!(reg.description_for("unknown").is_none());
4223 }
4224
4225 #[test]
4226 fn test_tool_registry_description_for_returns_description() {
4227 let mut reg = ToolRegistry::new();
4228 reg.register(ToolSpec::new("search", "Find web results", |_| serde_json::json!({})));
4229 assert_eq!(reg.description_for("search"), Some("Find web results"));
4230 }
4231
4232 #[test]
4235 fn test_count_with_description_containing_zero_when_no_match() {
4236 let mut reg = ToolRegistry::new();
4237 reg.register(ToolSpec::new("t1", "database query", |_| serde_json::json!({})));
4238 assert_eq!(reg.count_with_description_containing("weather"), 0);
4239 }
4240
4241 #[test]
4242 fn test_count_with_description_containing_case_insensitive() {
4243 let mut reg = ToolRegistry::new();
4244 reg.register(ToolSpec::new("t1", "Search the WEB", |_| serde_json::json!({})));
4245 reg.register(ToolSpec::new("t2", "web scraper tool", |_| serde_json::json!({})));
4246 reg.register(ToolSpec::new("t3", "database lookup", |_| serde_json::json!({})));
4247 assert_eq!(reg.count_with_description_containing("web"), 2);
4248 }
4249
4250 #[test]
4251 fn test_unregister_all_clears_all_tools() {
4252 let mut reg = ToolRegistry::new();
4253 reg.register(ToolSpec::new("t1", "tool one", |_| serde_json::json!({})));
4254 reg.register(ToolSpec::new("t2", "tool two", |_| serde_json::json!({})));
4255 assert_eq!(reg.tool_count(), 2);
4256 reg.unregister_all();
4257 assert_eq!(reg.tool_count(), 0);
4258 }
4259
4260 #[test]
4261 fn test_tool_names_with_keyword_returns_matching_tool_names() {
4262 let mut reg = ToolRegistry::new();
4263 reg.register(ToolSpec::new("search", "search the web for info", |_| serde_json::json!({})));
4264 reg.register(ToolSpec::new("db", "query database records", |_| serde_json::json!({})));
4265 reg.register(ToolSpec::new("web-fetch", "fetch a WEB page", |_| serde_json::json!({})));
4266 let mut names = reg.tool_names_with_keyword("web");
4267 names.sort_unstable();
4268 assert_eq!(names, vec!["search", "web-fetch"]);
4269 }
4270
4271 #[test]
4272 fn test_tool_names_with_keyword_no_match_returns_empty() {
4273 let mut reg = ToolRegistry::new();
4274 reg.register(ToolSpec::new("t", "some tool", |_| serde_json::json!({})));
4275 assert!(reg.tool_names_with_keyword("missing").is_empty());
4276 }
4277
4278 #[test]
4279 fn test_all_descriptions_returns_sorted_descriptions() {
4280 let mut reg = ToolRegistry::new();
4281 reg.register(ToolSpec::new("t1", "z description", |_| serde_json::json!({})));
4282 reg.register(ToolSpec::new("t2", "a description", |_| serde_json::json!({})));
4283 assert_eq!(reg.all_descriptions(), vec!["a description", "z description"]);
4284 }
4285
4286 #[test]
4287 fn test_all_descriptions_empty_registry_returns_empty() {
4288 let reg = ToolRegistry::new();
4289 assert!(reg.all_descriptions().is_empty());
4290 }
4291
4292 #[test]
4293 fn test_longest_description_returns_longest() {
4294 let mut reg = ToolRegistry::new();
4295 reg.register(ToolSpec::new("t1", "short", |_| serde_json::json!({})));
4296 reg.register(ToolSpec::new("t2", "a much longer description here", |_| serde_json::json!({})));
4297 assert_eq!(reg.longest_description(), Some("a much longer description here"));
4298 }
4299
4300 #[test]
4301 fn test_longest_description_empty_registry_returns_none() {
4302 let reg = ToolRegistry::new();
4303 assert!(reg.longest_description().is_none());
4304 }
4305
4306 #[test]
4307 fn test_names_containing_returns_sorted_matching_names() {
4308 let mut reg = ToolRegistry::new();
4309 reg.register(ToolSpec::new("search-web", "search tool", |_| serde_json::json!({})));
4310 reg.register(ToolSpec::new("web-fetch", "fetch tool", |_| serde_json::json!({})));
4311 reg.register(ToolSpec::new("db-query", "database tool", |_| serde_json::json!({})));
4312 let names = reg.names_containing("web");
4313 assert_eq!(names, vec!["search-web", "web-fetch"]);
4314 }
4315
4316 #[test]
4317 fn test_names_containing_no_match_returns_empty() {
4318 let mut reg = ToolRegistry::new();
4319 reg.register(ToolSpec::new("t", "tool", |_| serde_json::json!({})));
4320 assert!(reg.names_containing("missing").is_empty());
4321 }
4322
4323 #[test]
4326 fn test_avg_description_length_returns_mean_byte_length() {
4327 let mut reg = ToolRegistry::new();
4328 reg.register(ToolSpec::new("a", "ab", |_| serde_json::json!({}))); reg.register(ToolSpec::new("b", "abcd", |_| serde_json::json!({}))); let avg = reg.avg_description_length();
4331 assert!((avg - 3.0).abs() < 1e-9);
4332 }
4333
4334 #[test]
4335 fn test_avg_description_length_returns_zero_when_empty() {
4336 let reg = ToolRegistry::new();
4337 assert_eq!(reg.avg_description_length(), 0.0);
4338 }
4339
4340 #[test]
4343 fn test_shortest_description_returns_shortest_string() {
4344 let mut reg = ToolRegistry::new();
4345 reg.register(ToolSpec::new("a", "hello world", |_| serde_json::json!({})));
4346 reg.register(ToolSpec::new("b", "hi", |_| serde_json::json!({})));
4347 reg.register(ToolSpec::new("c", "greetings", |_| serde_json::json!({})));
4348 assert_eq!(reg.shortest_description(), Some("hi"));
4349 }
4350
4351 #[test]
4352 fn test_shortest_description_returns_none_when_empty() {
4353 let reg = ToolRegistry::new();
4354 assert!(reg.shortest_description().is_none());
4355 }
4356
4357 #[test]
4360 fn test_tool_names_sorted_returns_names_in_alphabetical_order() {
4361 let mut reg = ToolRegistry::new();
4362 reg.register(ToolSpec::new("zap", "z tool", |_| serde_json::json!({})));
4363 reg.register(ToolSpec::new("alpha", "a tool", |_| serde_json::json!({})));
4364 reg.register(ToolSpec::new("middle", "m tool", |_| serde_json::json!({})));
4365 assert_eq!(reg.tool_names_sorted(), vec!["alpha", "middle", "zap"]);
4366 }
4367
4368 #[test]
4369 fn test_tool_names_sorted_empty_returns_empty() {
4370 let reg = ToolRegistry::new();
4371 assert!(reg.tool_names_sorted().is_empty());
4372 }
4373
4374 #[test]
4377 fn test_description_contains_count_counts_matching_descriptions() {
4378 let mut reg = ToolRegistry::new();
4379 reg.register(ToolSpec::new("a", "search the web", |_| serde_json::json!({})));
4380 reg.register(ToolSpec::new("b", "write to disk", |_| serde_json::json!({})));
4381 reg.register(ToolSpec::new("c", "search and filter", |_| serde_json::json!({})));
4382 assert_eq!(reg.description_contains_count("search"), 2);
4383 assert_eq!(reg.description_contains_count("SEARCH"), 2);
4384 assert_eq!(reg.description_contains_count("missing"), 0);
4385 }
4386
4387 #[test]
4388 fn test_description_contains_count_zero_when_empty() {
4389 let reg = ToolRegistry::new();
4390 assert_eq!(reg.description_contains_count("anything"), 0);
4391 }
4392
4393 #[test]
4396 fn test_react_step_summary_tool_kind() {
4397 let step = ReActStep::new("I need to search", r#"{"tool":"search","q":"rust"}"#, "results");
4398 let s = step.summary();
4399 assert!(s.starts_with("[TOOL]"));
4400 assert!(s.contains("I need to search"));
4401 assert!(s.contains("results"));
4402 }
4403
4404 #[test]
4405 fn test_react_step_summary_final_kind() {
4406 let step = ReActStep::new("Done", "FINAL_ANSWER hello", "");
4407 let s = step.summary();
4408 assert!(s.starts_with("[FINAL]"));
4409 assert!(s.contains("FINAL_ANSWER hello"));
4410 }
4411
4412 #[test]
4413 fn test_react_step_summary_truncates_long_fields() {
4414 let long = "a".repeat(100);
4415 let step = ReActStep::new(long.clone(), long.clone(), long.clone());
4416 let s = step.summary();
4417 assert!(s.contains('…'));
4419 }
4420
4421 #[test]
4422 fn test_react_step_summary_empty_fields() {
4423 let step = ReActStep::new("", "", "");
4424 let s = step.summary();
4425 assert!(s.contains("[TOOL]"));
4426 }
4427
4428 #[test]
4431 fn test_tool_registry_total_description_bytes_sums_correctly() {
4432 let mut reg = ToolRegistry::new();
4433 reg.register(ToolSpec::new("a", "hello", |_| serde_json::json!({}))); reg.register(ToolSpec::new("b", "world!", |_| serde_json::json!({}))); assert_eq!(reg.total_description_bytes(), 11);
4436 }
4437
4438 #[test]
4439 fn test_tool_registry_total_description_bytes_empty_returns_zero() {
4440 let reg = ToolRegistry::new();
4441 assert_eq!(reg.total_description_bytes(), 0);
4442 }
4443
4444 #[test]
4447 fn test_react_step_thought_word_count_counts_words() {
4448 let step = ReActStep::new("hello world foo", "act", "obs");
4449 assert_eq!(step.thought_word_count(), 3);
4450 }
4451
4452 #[test]
4453 fn test_react_step_thought_word_count_empty_thought_returns_zero() {
4454 let step = ReActStep::new("", "act", "obs");
4455 assert_eq!(step.thought_word_count(), 0);
4456 }
4457
4458 #[test]
4459 fn test_agent_config_clone_with_system_prompt_changes_only_prompt() {
4460 let original = AgentConfig::new(5, "gpt-4");
4461 let cloned = original.clone_with_system_prompt("Custom prompt.");
4462 assert_eq!(cloned.system_prompt, "Custom prompt.");
4463 assert_eq!(cloned.model, "gpt-4");
4464 assert_eq!(cloned.max_iterations, 5);
4465 }
4466
4467 #[test]
4468 fn test_agent_config_clone_with_system_prompt_leaves_original_unchanged() {
4469 let original = AgentConfig::new(3, "claude").with_system_prompt("Original.");
4470 let _cloned = original.clone_with_system_prompt("New.");
4471 assert_eq!(original.system_prompt, "Original.");
4472 }
4473
4474 #[test]
4475 fn test_agent_config_clone_with_max_iterations_changes_only_iterations() {
4476 let original = AgentConfig::new(5, "claude-3");
4477 let cloned = original.clone_with_max_iterations(20);
4478 assert_eq!(cloned.max_iterations, 20);
4479 assert_eq!(cloned.model, "claude-3");
4480 }
4481
4482 #[test]
4483 fn test_agent_config_clone_with_max_iterations_leaves_original_unchanged() {
4484 let original = AgentConfig::new(5, "claude-3");
4485 let _cloned = original.clone_with_max_iterations(10);
4486 assert_eq!(original.max_iterations, 5);
4487 }
4488
4489 #[test]
4492 fn test_message_display_user_role() {
4493 let m = Message::user("hello world");
4494 assert_eq!(m.to_string(), "user: hello world");
4495 }
4496
4497 #[test]
4498 fn test_message_display_assistant_role() {
4499 let m = Message::assistant("I can help");
4500 assert_eq!(m.to_string(), "assistant: I can help");
4501 }
4502
4503 #[test]
4504 fn test_message_display_system_role() {
4505 let m = Message::system("Be helpful");
4506 assert_eq!(m.to_string(), "system: Be helpful");
4507 }
4508
4509 #[test]
4510 fn test_message_from_role_string_tuple() {
4511 let m = Message::from((Role::User, "hello".to_owned()));
4512 assert_eq!(m.role, Role::User);
4513 assert_eq!(m.content, "hello");
4514 }
4515
4516 #[test]
4517 fn test_message_from_role_str_ref_tuple() {
4518 let m = Message::from((Role::Assistant, "ok"));
4519 assert_eq!(m.role, Role::Assistant);
4520 assert_eq!(m.content, "ok");
4521 }
4522
4523 #[test]
4524 fn test_message_into_from_system_tuple() {
4525 let m: Message = (Role::System, "sys prompt").into();
4526 assert!(m.is_system());
4527 assert_eq!(m.content(), "sys prompt");
4528 }
4529
4530 #[test]
4533 fn test_tool_registry_shortest_description_length_returns_min_bytes() {
4534 let mut reg = ToolRegistry::new();
4535 reg.register(ToolSpec::new("a", "hello world", |_| serde_json::json!({}))); reg.register(ToolSpec::new("b", "hi", |_| serde_json::json!({}))); reg.register(ToolSpec::new("c", "greetings!", |_| serde_json::json!({}))); assert_eq!(reg.shortest_description_length(), 2);
4539 }
4540
4541 #[test]
4542 fn test_tool_registry_shortest_description_length_empty_returns_zero() {
4543 let reg = ToolRegistry::new();
4544 assert_eq!(reg.shortest_description_length(), 0);
4545 }
4546
4547 struct AlwaysOk;
4550 impl ToolValidator for AlwaysOk {
4551 fn validate(&self, _args: &Value) -> Result<(), AgentRuntimeError> {
4552 Ok(())
4553 }
4554 }
4555
4556 #[test]
4557 fn test_tool_count_with_validators_counts_tools_that_have_validators() {
4558 let mut reg = ToolRegistry::new();
4559 reg.register(ToolSpec::new("a", "desc", |_| serde_json::json!({}))
4560 .with_validators(vec![Box::new(AlwaysOk)]));
4561 reg.register(ToolSpec::new("b", "desc", |_| serde_json::json!({}))); reg.register(ToolSpec::new("c", "desc", |_| serde_json::json!({}))
4563 .with_validators(vec![Box::new(AlwaysOk)]));
4564 assert_eq!(reg.tool_count_with_validators(), 2);
4565 }
4566
4567 #[test]
4568 fn test_tool_count_with_validators_zero_when_none_have_validators() {
4569 let mut reg = ToolRegistry::new();
4570 reg.register(ToolSpec::new("a", "desc", |_| serde_json::json!({})));
4571 assert_eq!(reg.tool_count_with_validators(), 0);
4572 }
4573
4574 #[test]
4575 fn test_tool_count_with_validators_zero_for_empty_registry() {
4576 let reg = ToolRegistry::new();
4577 assert_eq!(reg.tool_count_with_validators(), 0);
4578 }
4579
4580 #[test]
4583 fn test_longest_description_length_returns_max_bytes() {
4584 let mut reg = ToolRegistry::new();
4585 reg.register(ToolSpec::new("a", "hi", |_| serde_json::json!({}))); reg.register(ToolSpec::new("b", "hello world", |_| serde_json::json!({}))); reg.register(ToolSpec::new("c", "yo", |_| serde_json::json!({}))); assert_eq!(reg.longest_description_length(), 11);
4589 }
4590
4591 #[test]
4592 fn test_longest_description_length_zero_for_empty_registry() {
4593 let reg = ToolRegistry::new();
4594 assert_eq!(reg.longest_description_length(), 0);
4595 }
4596
4597 #[test]
4600 fn test_tools_with_required_field_returns_matching_tools() {
4601 let mut reg = ToolRegistry::new();
4602 reg.register(
4603 ToolSpec::new("a", "desc", |_| serde_json::json!({}))
4604 .with_required_fields(vec!["query".to_string()]),
4605 );
4606 reg.register(ToolSpec::new("b", "desc", |_| serde_json::json!({}))); reg.register(
4608 ToolSpec::new("c", "desc", |_| serde_json::json!({}))
4609 .with_required_fields(vec!["query".to_string(), "limit".to_string()]),
4610 );
4611 let result = reg.tools_with_required_field("query");
4612 assert_eq!(result.len(), 2);
4613 assert!(result.iter().any(|t| t.name == "a"));
4614 assert!(result.iter().any(|t| t.name == "c"));
4615 }
4616
4617 #[test]
4618 fn test_tools_with_required_field_empty_when_no_match() {
4619 let mut reg = ToolRegistry::new();
4620 reg.register(
4621 ToolSpec::new("a", "desc", |_| serde_json::json!({}))
4622 .with_required_fields(vec!["x".to_string()]),
4623 );
4624 assert!(reg.tools_with_required_field("missing").is_empty());
4625 }
4626
4627 #[test]
4628 fn test_tools_with_required_field_empty_registry_returns_empty() {
4629 let reg = ToolRegistry::new();
4630 assert!(reg.tools_with_required_field("any").is_empty());
4631 }
4632
4633 #[test]
4636 fn test_observation_word_count_counts_words() {
4637 let step = ReActStep {
4638 thought: "t".into(),
4639 action: "a".into(),
4640 observation: "hello world foo".into(),
4641 step_duration_ms: 0,
4642 };
4643 assert_eq!(step.observation_word_count(), 3);
4644 }
4645
4646 #[test]
4647 fn test_observation_word_count_zero_for_empty() {
4648 let step = ReActStep {
4649 thought: "t".into(),
4650 action: "a".into(),
4651 observation: "".into(),
4652 step_duration_ms: 0,
4653 };
4654 assert_eq!(step.observation_word_count(), 0);
4655 }
4656
4657 #[test]
4658 fn test_tool_with_most_required_fields_returns_correct_tool() {
4659 let mut reg = ToolRegistry::new();
4660 reg.register(
4661 ToolSpec::new("few", "d", |_| serde_json::json!({}))
4662 .with_required_fields(vec!["a".to_string()]),
4663 );
4664 reg.register(
4665 ToolSpec::new("many", "d", |_| serde_json::json!({}))
4666 .with_required_fields(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
4667 );
4668 let winner = reg.tool_with_most_required_fields().unwrap();
4669 assert_eq!(winner.name, "many");
4670 }
4671
4672 #[test]
4673 fn test_tool_with_most_required_fields_returns_none_for_empty_registry() {
4674 let reg = ToolRegistry::new();
4675 assert!(reg.tool_with_most_required_fields().is_none());
4676 }
4677
4678 #[test]
4681 fn test_tool_count_above_desc_bytes_counts_tools_with_long_descriptions() {
4682 let mut reg = ToolRegistry::new();
4683 reg.register(ToolSpec::new("short", "hi", |_| serde_json::json!({})));
4684 reg.register(ToolSpec::new("long", "a much longer description here", |_| serde_json::json!({})));
4685 assert_eq!(reg.tool_count_above_desc_bytes(2), 1);
4686 }
4687
4688 #[test]
4689 fn test_tool_count_above_desc_bytes_zero_when_none_exceed() {
4690 let mut reg = ToolRegistry::new();
4691 reg.register(ToolSpec::new("t", "ab", |_| serde_json::json!({})));
4692 assert_eq!(reg.tool_count_above_desc_bytes(100), 0);
4693 }
4694
4695 #[test]
4696 fn test_tool_count_above_desc_bytes_zero_for_empty_registry() {
4697 let reg = ToolRegistry::new();
4698 assert_eq!(reg.tool_count_above_desc_bytes(0), 0);
4699 }
4700
4701 #[test]
4704 fn test_tool_names_with_required_fields_returns_sorted_names() {
4705 let mut reg = ToolRegistry::new();
4706 reg.register(
4707 ToolSpec::new("b", "desc", |_| serde_json::json!({}))
4708 .with_required_fields(vec!["x".to_string()]),
4709 );
4710 reg.register(
4711 ToolSpec::new("a", "desc", |_| serde_json::json!({}))
4712 .with_required_fields(vec!["y".to_string()]),
4713 );
4714 reg.register(ToolSpec::new("c", "desc", |_| serde_json::json!({}))); assert_eq!(reg.tool_names_with_required_fields(), vec!["a", "b"]);
4716 }
4717
4718 #[test]
4719 fn test_tool_names_with_required_fields_empty_when_none_have_fields() {
4720 let mut reg = ToolRegistry::new();
4721 reg.register(ToolSpec::new("a", "desc", |_| serde_json::json!({})));
4722 assert!(reg.tool_names_with_required_fields().is_empty());
4723 }
4724
4725 #[test]
4726 fn test_tool_names_with_required_fields_empty_for_empty_registry() {
4727 let reg = ToolRegistry::new();
4728 assert!(reg.tool_names_with_required_fields().is_empty());
4729 }
4730
4731 #[test]
4734 fn test_tools_without_required_fields_returns_tools_with_no_required_fields() {
4735 let mut reg = ToolRegistry::new();
4736 reg.register(ToolSpec::new("no-req", "desc", |_| serde_json::json!({})));
4737 reg.register(
4738 ToolSpec::new("with-req", "desc", |_| serde_json::json!({}))
4739 .with_required_fields(vec!["x".to_string()]),
4740 );
4741 let result = reg.tools_without_required_fields();
4742 assert_eq!(result.len(), 1);
4743 assert_eq!(result[0].name, "no-req");
4744 }
4745
4746 #[test]
4747 fn test_tools_without_required_fields_empty_for_empty_registry() {
4748 let reg = ToolRegistry::new();
4749 assert!(reg.tools_without_required_fields().is_empty());
4750 }
4751
4752 #[test]
4755 fn test_avg_required_fields_count_computes_mean() {
4756 let mut reg = ToolRegistry::new();
4757 reg.register(ToolSpec::new("t1", "d", |_| serde_json::json!({})));
4758 reg.register(
4759 ToolSpec::new("t2", "d", |_| serde_json::json!({}))
4760 .with_required_fields(vec!["a".to_string(), "b".to_string()]),
4761 );
4762 assert!((reg.avg_required_fields_count() - 1.0).abs() < 1e-9);
4764 }
4765
4766 #[test]
4767 fn test_avg_required_fields_count_zero_for_empty_registry() {
4768 let reg = ToolRegistry::new();
4769 assert_eq!(reg.avg_required_fields_count(), 0.0);
4770 }
4771
4772 #[test]
4775 fn test_thought_is_empty_true_for_empty_thought() {
4776 let step = ReActStep::new("", "action", "obs");
4777 assert!(step.thought_is_empty());
4778 }
4779
4780 #[test]
4781 fn test_thought_is_empty_true_for_whitespace_only() {
4782 let step = ReActStep::new(" ", "action", "obs");
4783 assert!(step.thought_is_empty());
4784 }
4785
4786 #[test]
4787 fn test_thought_is_empty_false_for_nonempty_thought() {
4788 let step = ReActStep::new("I need to search", "action", "obs");
4789 assert!(!step.thought_is_empty());
4790 }
4791
4792 #[test]
4793 fn test_model_is_true_for_matching_name() {
4794 let config = AgentConfig::new(10, "claude-sonnet-4-6");
4795 assert!(config.model_is("claude-sonnet-4-6"));
4796 }
4797
4798 #[test]
4799 fn test_model_is_false_for_different_name() {
4800 let config = AgentConfig::new(10, "claude-opus-4-6");
4801 assert!(!config.model_is("claude-sonnet-4-6"));
4802 }
4803
4804 #[test]
4805 fn test_loop_timeout_ms_returns_zero_when_not_configured() {
4806 let config = AgentConfig::new(10, "m");
4807 assert_eq!(config.loop_timeout_ms(), 0);
4808 }
4809
4810 #[test]
4811 fn test_loop_timeout_ms_returns_millis_when_configured() {
4812 let config = AgentConfig::new(10, "m")
4813 .with_loop_timeout(std::time::Duration::from_millis(5000));
4814 assert_eq!(config.loop_timeout_ms(), 5000);
4815 }
4816
4817 #[test]
4818 fn test_total_timeout_ms_zero_when_neither_configured() {
4819 let config = AgentConfig::new(10, "m");
4820 assert_eq!(config.total_timeout_ms(), 0);
4821 }
4822
4823 #[test]
4824 fn test_total_timeout_ms_includes_loop_and_request_budgets() {
4825 let config = AgentConfig::new(4, "m")
4826 .with_loop_timeout(std::time::Duration::from_millis(1000))
4827 .with_request_timeout(std::time::Duration::from_millis(500));
4828 assert_eq!(config.total_timeout_ms(), 3000);
4830 }
4831
4832 #[test]
4835 fn test_tool_descriptions_total_words_sums_words() {
4836 let mut reg = ToolRegistry::new();
4837 reg.register(ToolSpec::new("t1", "one two three", |_| serde_json::json!({})));
4838 reg.register(ToolSpec::new("t2", "four five", |_| serde_json::json!({})));
4839 assert_eq!(reg.tool_descriptions_total_words(), 5);
4840 }
4841
4842 #[test]
4843 fn test_tool_descriptions_total_words_zero_for_empty_registry() {
4844 let reg = ToolRegistry::new();
4845 assert_eq!(reg.tool_descriptions_total_words(), 0);
4846 }
4847
4848 #[test]
4851 fn test_content_starts_with_true_for_matching_prefix() {
4852 let msg = Message::user("Hello, world!");
4853 assert!(msg.content_starts_with("Hello"));
4854 }
4855
4856 #[test]
4857 fn test_content_starts_with_false_for_non_matching_prefix() {
4858 let msg = Message::user("Hello, world!");
4859 assert!(!msg.content_starts_with("World"));
4860 }
4861
4862 #[test]
4863 fn test_content_starts_with_empty_prefix_always_true() {
4864 let msg = Message::assistant("anything");
4865 assert!(msg.content_starts_with(""));
4866 }
4867
4868 #[test]
4869 fn test_system_prompt_is_empty_true_for_blank_prompt() {
4870 let cfg = AgentConfig::new(5, "m").with_system_prompt("");
4871 assert!(cfg.system_prompt_is_empty());
4872 }
4873
4874 #[test]
4875 fn test_system_prompt_is_empty_false_when_set() {
4876 let cfg = AgentConfig::new(5, "m").with_system_prompt("You are helpful.");
4877 assert!(!cfg.system_prompt_is_empty());
4878 }
4879
4880 #[test]
4883 fn test_has_tools_with_empty_descriptions_true_when_blank_present() {
4884 let mut reg = ToolRegistry::new();
4885 reg.register(ToolSpec::new("t1", " ", |_| serde_json::json!({})));
4886 assert!(reg.has_tools_with_empty_descriptions());
4887 }
4888
4889 #[test]
4890 fn test_has_tools_with_empty_descriptions_false_when_all_filled() {
4891 let mut reg = ToolRegistry::new();
4892 reg.register(ToolSpec::new("t1", "desc", |_| serde_json::json!({})));
4893 assert!(!reg.has_tools_with_empty_descriptions());
4894 }
4895
4896 #[test]
4897 fn test_total_required_fields_sums_across_tools() {
4898 let mut reg = ToolRegistry::new();
4899 reg.register(
4900 ToolSpec::new("t1", "d", |_| serde_json::json!({}))
4901 .with_required_fields(vec!["a".to_string(), "b".to_string()]),
4902 );
4903 reg.register(
4904 ToolSpec::new("t2", "d", |_| serde_json::json!({}))
4905 .with_required_fields(vec!["c".to_string()]),
4906 );
4907 assert_eq!(reg.total_required_fields(), 3);
4908 }
4909
4910 #[test]
4911 fn test_total_required_fields_zero_for_empty_registry() {
4912 let reg = ToolRegistry::new();
4913 assert_eq!(reg.total_required_fields(), 0);
4914 }
4915
4916 #[test]
4919 fn test_tools_with_description_longer_than_returns_matching_tools() {
4920 let mut reg = ToolRegistry::new();
4921 reg.register(ToolSpec::new("short", "hi", |_| serde_json::json!({})));
4922 reg.register(ToolSpec::new("long", "a much longer description", |_| serde_json::json!({})));
4923 let names = reg.tools_with_description_longer_than(5);
4924 assert_eq!(names, vec!["long"]);
4925 }
4926
4927 #[test]
4928 fn test_max_description_bytes_returns_longest() {
4929 let mut reg = ToolRegistry::new();
4930 reg.register(ToolSpec::new("t1", "hi", |_| serde_json::json!({})));
4931 reg.register(ToolSpec::new("t2", "hello world", |_| serde_json::json!({})));
4932 assert_eq!(reg.max_description_bytes(), 11);
4933 }
4934
4935 #[test]
4936 fn test_min_description_bytes_returns_shortest() {
4937 let mut reg = ToolRegistry::new();
4938 reg.register(ToolSpec::new("t1", "hi", |_| serde_json::json!({})));
4939 reg.register(ToolSpec::new("t2", "hello world", |_| serde_json::json!({})));
4940 assert_eq!(reg.min_description_bytes(), 2);
4941 }
4942
4943 #[test]
4944 fn test_max_description_bytes_zero_for_empty_registry() {
4945 let reg = ToolRegistry::new();
4946 assert_eq!(reg.max_description_bytes(), 0);
4947 }
4948
4949 #[test]
4952 fn test_has_tool_with_description_containing_true_when_keyword_found() {
4953 let mut reg = ToolRegistry::new();
4954 reg.register(ToolSpec::new("search", "search the web", |_| serde_json::json!({})));
4955 assert!(reg.has_tool_with_description_containing("web"));
4956 }
4957
4958 #[test]
4959 fn test_has_tool_with_description_containing_false_when_keyword_absent() {
4960 let mut reg = ToolRegistry::new();
4961 reg.register(ToolSpec::new("search", "search the web", |_| serde_json::json!({})));
4962 assert!(!reg.has_tool_with_description_containing("database"));
4963 }
4964
4965 #[test]
4966 fn test_has_tool_with_description_containing_false_for_empty_registry() {
4967 let reg = ToolRegistry::new();
4968 assert!(!reg.has_tool_with_description_containing("anything"));
4969 }
4970
4971 #[test]
4974 fn test_system_prompt_word_count_counts_words() {
4975 let cfg = AgentConfig::new(10, "m")
4976 .with_system_prompt("You are a helpful AI agent.");
4977 assert_eq!(cfg.system_prompt_word_count(), 6);
4978 }
4979
4980 #[test]
4981 fn test_system_prompt_word_count_zero_for_empty_prompt() {
4982 let cfg = AgentConfig::new(10, "m").with_system_prompt("");
4983 assert_eq!(cfg.system_prompt_word_count(), 0);
4984 }
4985
4986 #[test]
4989 fn test_description_starts_with_any_true_when_prefix_matches() {
4990 let mut reg = ToolRegistry::new();
4991 reg.register(ToolSpec::new("t1", "Search the web", |_| serde_json::json!({})));
4992 assert!(reg.description_starts_with_any(&["Search", "Write"]));
4993 }
4994
4995 #[test]
4996 fn test_description_starts_with_any_false_when_no_prefix_matches() {
4997 let mut reg = ToolRegistry::new();
4998 reg.register(ToolSpec::new("t1", "Read a file", |_| serde_json::json!({})));
4999 assert!(!reg.description_starts_with_any(&["Search", "Write"]));
5000 }
5001
5002 #[test]
5003 fn test_description_starts_with_any_false_for_empty_registry() {
5004 let reg = ToolRegistry::new();
5005 assert!(!reg.description_starts_with_any(&["Search"]));
5006 }
5007
5008 #[test]
5011 fn test_combined_byte_length_sums_all_fields() {
5012 let step = ReActStep::new("hello", "search", "result");
5013 assert_eq!(step.combined_byte_length(), 5 + 6 + 6);
5014 }
5015
5016 #[test]
5017 fn test_combined_byte_length_zero_for_empty_step() {
5018 let step = ReActStep::new("", "", "");
5019 assert_eq!(step.combined_byte_length(), 0);
5020 }
5021
5022 #[test]
5023 fn test_iteration_budget_remaining_full_when_no_steps_done() {
5024 let cfg = AgentConfig::new(10, "m");
5025 assert_eq!(cfg.iteration_budget_remaining(0), 10);
5026 }
5027
5028 #[test]
5029 fn test_iteration_budget_remaining_decreases_with_steps() {
5030 let cfg = AgentConfig::new(10, "m");
5031 assert_eq!(cfg.iteration_budget_remaining(7), 3);
5032 }
5033
5034 #[test]
5035 fn test_iteration_budget_remaining_saturates_at_zero() {
5036 let cfg = AgentConfig::new(5, "m");
5037 assert_eq!(cfg.iteration_budget_remaining(10), 0);
5038 }
5039
5040 #[test]
5041 fn test_has_all_tools_true_when_all_registered() {
5042 let mut reg = ToolRegistry::new();
5043 reg.register(ToolSpec::new("search", "Search", |_| serde_json::json!({})));
5044 reg.register(ToolSpec::new("write", "Write", |_| serde_json::json!({})));
5045 assert!(reg.has_all_tools(&["search", "write"]));
5046 }
5047
5048 #[test]
5049 fn test_has_all_tools_false_when_one_missing() {
5050 let mut reg = ToolRegistry::new();
5051 reg.register(ToolSpec::new("search", "Search", |_| serde_json::json!({})));
5052 assert!(!reg.has_all_tools(&["search", "write"]));
5053 }
5054
5055 #[test]
5056 fn test_has_all_tools_true_for_empty_slice() {
5057 let reg = ToolRegistry::new();
5058 assert!(reg.has_all_tools(&[]));
5059 }
5060
5061 #[test]
5064 fn test_tool_by_name_returns_tool_when_present() {
5065 let mut reg = ToolRegistry::new();
5066 reg.register(ToolSpec::new("search", "Search the web", |_| serde_json::json!({})));
5067 assert!(reg.tool_by_name("search").is_some());
5068 assert_eq!(reg.tool_by_name("search").unwrap().name, "search");
5069 }
5070
5071 #[test]
5072 fn test_tool_by_name_returns_none_when_absent() {
5073 let reg = ToolRegistry::new();
5074 assert!(reg.tool_by_name("missing").is_none());
5075 }
5076
5077 #[test]
5078 fn test_tools_without_validators_returns_unvalidated_tools() {
5079 let mut reg = ToolRegistry::new();
5080 reg.register(ToolSpec::new("a", "Tool A", |_| serde_json::json!({})));
5081 reg.register(ToolSpec::new("b", "Tool B", |_| serde_json::json!({})));
5082 let names = reg.tools_without_validators();
5083 assert!(names.contains(&"a"));
5084 assert!(names.contains(&"b"));
5085 }
5086
5087 #[test]
5088 fn test_tools_without_validators_empty_for_empty_registry() {
5089 let reg = ToolRegistry::new();
5090 assert!(reg.tools_without_validators().is_empty());
5091 }
5092
5093 #[test]
5096 fn test_action_is_empty_true_for_empty_action() {
5097 let step = ReActStep::new("thought", "", "obs");
5098 assert!(step.action_is_empty());
5099 }
5100
5101 #[test]
5102 fn test_action_is_empty_false_for_nonempty_action() {
5103 let step = ReActStep::new("thought", "search", "obs");
5104 assert!(!step.action_is_empty());
5105 }
5106
5107 #[test]
5108 fn test_action_is_empty_true_for_whitespace_only() {
5109 let step = ReActStep::new("thought", " ", "obs");
5110 assert!(step.action_is_empty());
5111 }
5112
5113 #[test]
5114 fn test_is_minimal_true_for_single_iteration_no_prompt() {
5115 let cfg = AgentConfig::new(1, "m").with_system_prompt("");
5116 assert!(cfg.is_minimal());
5117 }
5118
5119 #[test]
5120 fn test_is_minimal_false_when_max_iterations_above_one() {
5121 let cfg = AgentConfig::new(5, "m");
5122 assert!(!cfg.is_minimal());
5123 }
5124
5125 #[test]
5126 fn test_is_minimal_false_when_system_prompt_set() {
5127 let cfg = AgentConfig::new(1, "m").with_system_prompt("prompt");
5128 assert!(!cfg.is_minimal());
5129 }
5130
5131 #[test]
5134 fn test_model_starts_with_true_when_prefix_matches() {
5135 let cfg = AgentConfig::new(3, "claude-3-opus");
5136 assert!(cfg.model_starts_with("claude"));
5137 }
5138
5139 #[test]
5140 fn test_model_starts_with_false_when_prefix_differs() {
5141 let cfg = AgentConfig::new(3, "gpt-4o");
5142 assert!(!cfg.model_starts_with("claude"));
5143 }
5144
5145 #[test]
5146 fn test_tools_with_required_fields_count_correct() {
5147 let mut registry = ToolRegistry::new();
5148 registry.register(ToolSpec::new(
5149 "search",
5150 "desc",
5151 |_| serde_json::json!("ok"),
5152 ).with_required_fields(vec!["query".to_string()]));
5153 registry.register(ToolSpec::new(
5154 "noop",
5155 "desc",
5156 |_| serde_json::json!("ok"),
5157 ));
5158 assert_eq!(registry.tools_with_required_fields_count(), 1);
5159 }
5160
5161 #[test]
5162 fn test_tools_with_required_fields_count_zero_for_empty_registry() {
5163 let registry = ToolRegistry::new();
5164 assert_eq!(registry.tools_with_required_fields_count(), 0);
5165 }
5166
5167 #[test]
5170 fn test_tool_names_with_prefix_returns_matching_names() {
5171 let mut reg = ToolRegistry::new();
5172 reg.register(ToolSpec::new("search_web", "desc", |_| serde_json::json!({})));
5173 reg.register(ToolSpec::new("search_code", "desc", |_| serde_json::json!({})));
5174 reg.register(ToolSpec::new("write_file", "desc", |_| serde_json::json!({})));
5175 let names = reg.tool_names_with_prefix("search_");
5176 assert_eq!(names, vec!["search_code", "search_web"]);
5177 }
5178
5179 #[test]
5180 fn test_tool_names_with_prefix_empty_when_no_match() {
5181 let mut reg = ToolRegistry::new();
5182 reg.register(ToolSpec::new("write_file", "desc", |_| serde_json::json!({})));
5183 assert!(reg.tool_names_with_prefix("search_").is_empty());
5184 }
5185
5186 #[test]
5187 fn test_exceeds_iteration_limit_true_when_at_limit() {
5188 let cfg = AgentConfig::new(5, "m");
5189 assert!(cfg.exceeds_iteration_limit(5));
5190 assert!(cfg.exceeds_iteration_limit(10));
5191 }
5192
5193 #[test]
5194 fn test_exceeds_iteration_limit_false_when_below_limit() {
5195 let cfg = AgentConfig::new(5, "m");
5196 assert!(!cfg.exceeds_iteration_limit(4));
5197 assert!(!cfg.exceeds_iteration_limit(0));
5198 }
5199
5200 #[test]
5203 fn test_total_word_count_sums_all_fields() {
5204 let step = ReActStep::new("one two", "three", "four five six");
5205 assert_eq!(step.total_word_count(), 6);
5206 }
5207
5208 #[test]
5209 fn test_total_word_count_zero_for_empty_step() {
5210 let step = ReActStep::new("", "", "");
5211 assert_eq!(step.total_word_count(), 0);
5212 }
5213
5214 #[test]
5215 fn test_token_budget_configured_true_when_max_tokens_set() {
5216 let cfg = AgentConfig::new(3, "m").with_max_tokens(100);
5217 assert!(cfg.token_budget_configured());
5218 }
5219
5220 #[test]
5221 fn test_token_budget_configured_true_when_max_context_chars_set() {
5222 let cfg = AgentConfig::new(3, "m").with_max_context_chars(200);
5223 assert!(cfg.token_budget_configured());
5224 }
5225
5226 #[test]
5227 fn test_token_budget_configured_false_when_neither_set() {
5228 let cfg = AgentConfig::new(3, "m");
5229 assert!(!cfg.token_budget_configured());
5230 }
5231
5232 #[test]
5235 fn test_is_complete_true_when_all_fields_nonempty() {
5236 let step = ReActStep::new("thought", "action", "observation");
5237 assert!(step.is_complete());
5238 }
5239
5240 #[test]
5241 fn test_is_complete_false_when_observation_empty() {
5242 let step = ReActStep::new("thought", "action", "");
5243 assert!(!step.is_complete());
5244 }
5245
5246 #[test]
5247 fn test_is_complete_false_when_action_empty() {
5248 let step = ReActStep::new("thought", "", "obs");
5249 assert!(!step.is_complete());
5250 }
5251
5252 #[test]
5253 fn test_max_tokens_or_default_returns_value_when_set() {
5254 let cfg = AgentConfig::new(3, "m").with_max_tokens(512);
5255 assert_eq!(cfg.max_tokens_or_default(100), 512);
5256 }
5257
5258 #[test]
5259 fn test_max_tokens_or_default_returns_default_when_unset() {
5260 let cfg = AgentConfig::new(3, "m");
5261 assert_eq!(cfg.max_tokens_or_default(256), 256);
5262 }
5263
5264 #[test]
5267 fn test_observation_starts_with_true_for_matching_prefix() {
5268 let step = ReActStep::new("t", "a", "Result: ok");
5269 assert!(step.observation_starts_with("Result:"));
5270 }
5271
5272 #[test]
5273 fn test_observation_starts_with_false_for_non_matching_prefix() {
5274 let step = ReActStep::new("t", "a", "Error: failed");
5275 assert!(!step.observation_starts_with("Result:"));
5276 }
5277
5278 #[test]
5279 fn test_effective_temperature_returns_configured_value() {
5280 let cfg = AgentConfig::new(3, "m").with_temperature(0.5);
5281 assert!((cfg.effective_temperature() - 0.5_f32).abs() < 1e-6);
5282 }
5283
5284 #[test]
5285 fn test_effective_temperature_returns_default_when_unset() {
5286 let cfg = AgentConfig::new(3, "m");
5287 assert!((cfg.effective_temperature() - 1.0_f32).abs() < 1e-6);
5288 }
5289
5290 #[test]
5293 fn test_action_word_count_returns_words_in_action() {
5294 let step = ReActStep::new("think", "do this now", "ok");
5295 assert_eq!(step.action_word_count(), 3);
5296 }
5297
5298 #[test]
5299 fn test_action_word_count_zero_for_empty_action() {
5300 let step = ReActStep::new("think", "", "ok");
5301 assert_eq!(step.action_word_count(), 0);
5302 }
5303
5304 #[test]
5305 fn test_thought_byte_len_matches_string_len() {
5306 let step = ReActStep::new("hello", "act", "obs");
5307 assert_eq!(step.thought_byte_len(), "hello".len());
5308 }
5309
5310 #[test]
5311 fn test_action_byte_len_matches_string_len() {
5312 let step = ReActStep::new("think", "do it", "obs");
5313 assert_eq!(step.action_byte_len(), "do it".len());
5314 }
5315
5316 #[test]
5317 fn test_has_empty_fields_true_when_observation_empty() {
5318 let step = ReActStep::new("think", "act", "");
5319 assert!(step.has_empty_fields());
5320 }
5321
5322 #[test]
5323 fn test_has_empty_fields_false_when_all_populated() {
5324 let step = ReActStep::new("think", "act", "obs");
5325 assert!(!step.has_empty_fields());
5326 }
5327
5328 #[test]
5331 fn test_system_prompt_starts_with_true_for_matching_prefix() {
5332 let cfg = AgentConfig::new(3, "m").with_system_prompt("You are a helpful assistant.");
5333 assert!(cfg.system_prompt_starts_with("You are"));
5334 }
5335
5336 #[test]
5337 fn test_system_prompt_starts_with_false_for_non_matching_prefix() {
5338 let cfg = AgentConfig::new(3, "m").with_system_prompt("Hello world");
5339 assert!(!cfg.system_prompt_starts_with("Goodbye"));
5340 }
5341
5342 #[test]
5343 fn test_max_iterations_above_true_when_greater() {
5344 let cfg = AgentConfig::new(5, "m");
5345 assert!(cfg.max_iterations_above(4));
5346 }
5347
5348 #[test]
5349 fn test_max_iterations_above_false_when_equal() {
5350 let cfg = AgentConfig::new(5, "m");
5351 assert!(!cfg.max_iterations_above(5));
5352 }
5353
5354 #[test]
5355 fn test_stop_sequences_contain_true_for_present_sequence() {
5356 let cfg = AgentConfig::new(3, "m")
5357 .with_stop_sequences(vec!["STOP".to_string(), "END".to_string()]);
5358 assert!(cfg.stop_sequences_contain("STOP"));
5359 }
5360
5361 #[test]
5362 fn test_stop_sequences_contain_false_for_absent_sequence() {
5363 let cfg = AgentConfig::new(3, "m")
5364 .with_stop_sequences(vec!["STOP".to_string()]);
5365 assert!(!cfg.stop_sequences_contain("END"));
5366 }
5367
5368 #[test]
5369 fn test_stop_sequences_contain_false_for_empty_config() {
5370 let cfg = AgentConfig::new(3, "m");
5371 assert!(!cfg.stop_sequences_contain("STOP"));
5372 }
5373
5374 #[test]
5377 fn test_observation_byte_len_matches_string_len() {
5378 let step = ReActStep::new("t", "a", "result");
5379 assert_eq!(step.observation_byte_len(), "result".len());
5380 }
5381
5382 #[test]
5383 fn test_observation_byte_len_zero_for_empty() {
5384 let step = ReActStep::new("t", "a", "");
5385 assert_eq!(step.observation_byte_len(), 0);
5386 }
5387
5388 #[test]
5389 fn test_all_fields_have_words_true_when_all_populated() {
5390 let step = ReActStep::new("think", "act", "obs");
5391 assert!(step.all_fields_have_words());
5392 }
5393
5394 #[test]
5395 fn test_all_fields_have_words_false_when_action_empty() {
5396 let step = ReActStep::new("think", "", "obs");
5397 assert!(!step.all_fields_have_words());
5398 }
5399
5400 #[test]
5401 fn test_system_prompt_byte_len_returns_length() {
5402 let cfg = AgentConfig::new(3, "m").with_system_prompt("Hello!");
5403 assert_eq!(cfg.system_prompt_byte_len(), "Hello!".len());
5404 }
5405
5406 #[test]
5407 fn test_system_prompt_byte_len_default_is_nonzero() {
5408 let cfg = AgentConfig::new(3, "m");
5409 assert_eq!(cfg.system_prompt_byte_len(), "You are a helpful AI agent.".len());
5411 }
5412
5413 #[test]
5414 fn test_has_valid_temperature_true_for_in_range() {
5415 let cfg = AgentConfig::new(3, "m").with_temperature(0.7);
5416 assert!(cfg.has_valid_temperature());
5417 }
5418
5419 #[test]
5420 fn test_has_valid_temperature_false_when_unset() {
5421 let cfg = AgentConfig::new(3, "m");
5422 assert!(!cfg.has_valid_temperature());
5423 }
5424
5425 #[test]
5426 fn test_has_valid_temperature_true_at_boundaries() {
5427 assert!(AgentConfig::new(3, "m").with_temperature(0.0).has_valid_temperature());
5428 assert!(AgentConfig::new(3, "m").with_temperature(2.0).has_valid_temperature());
5429 }
5430}