1#![cfg(feature = "llm")]
17
18use std::{
19 collections::{BTreeMap, HashMap, HashSet, VecDeque},
20 future::Future,
21 sync::{Arc, Mutex, OnceLock, RwLock},
22 time::Duration,
23};
24
25use tokio_util::sync::CancellationToken;
26
27use crate::{
28 Agent, AgentContext, AgentData, AgentError, AgentOutput, AgentSpec, AgentStatus, AgentValue,
29 AsAgent, Message, MessageContent, ModularAgent, SharedAgent, ToolCall, async_trait,
30 modular_agent,
31};
32#[cfg(feature = "image")]
33use crate::{ContentBlock, value::IMAGE_BASE64_PREFIX};
34use futures_util::{StreamExt, stream};
35use im::{Vector, vector};
36use regex::RegexSet;
37use tokio::sync::oneshot;
38
39const CATEGORY: &str = "Core/Tool";
40
41const PORT_LIMIT_EXCEEDED: &str = "limit_exceeded";
42const PORT_MESSAGE: &str = "message";
43const PORT_PATTERNS: &str = "patterns";
44const PORT_TOOLS: &str = "tools";
45const PORT_TOOL_CALL: &str = "tool_call";
46const PORT_TOOL_IN: &str = "tool_in";
47const PORT_TOOL_OUT: &str = "tool_out";
48const PORT_VALUE: &str = "value";
49
50const CONFIG_TOOLS: &str = "tools";
51const CONFIG_TOOL_NAME: &str = "name";
52const CONFIG_TOOL_DESCRIPTION: &str = "description";
53const CONFIG_TOOL_PARAMETERS: &str = "parameters";
54const CONFIG_TIMEOUT_SECS: &str = "timeout_secs";
55const CONFIG_MAX_ITERATIONS: &str = "max_iterations";
56const CONFIG_MAX_CONCURRENCY: &str = "max_concurrency";
57
58const DEFAULT_TIMEOUT_SECS: i64 = 60;
60
61const DEFAULT_MAX_ITERATIONS: i64 = 25;
63
64const DEFAULT_MAX_CONCURRENCY: i64 = 8;
67
68const ABORTED_TOOL_RESULT: &str = "Operation aborted";
72
73async fn run_unless_cancelled<T>(
78 cancel: Option<&CancellationToken>,
79 fut: impl Future<Output = T>,
80) -> Option<T> {
81 match cancel {
82 Some(token) => {
83 tokio::select! {
84 biased;
85 _ = token.cancelled() => None,
86 r = fut => Some(r),
87 }
88 }
89 None => Some(fut.await),
90 }
91}
92
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub enum ExecutionMode {
102 #[default]
104 Sequential,
105 Parallel,
107}
108
109#[derive(Clone, Debug)]
114pub struct ToolInfo {
115 pub name: String,
117
118 pub description: String,
127
128 pub parameters: serde_json::Value,
133
134 pub execution_mode: ExecutionMode,
138}
139
140impl ToolInfo {
141 pub fn new(
147 name: impl Into<String>,
148 description: impl Into<String>,
149 parameters: Option<serde_json::Value>,
150 ) -> Self {
151 Self {
152 name: name.into(),
153 description: description.into(),
154 parameters: parameters
155 .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}})),
156 execution_mode: ExecutionMode::default(),
157 }
158 }
159
160 pub fn with_execution_mode(mut self, mode: ExecutionMode) -> Self {
163 self.execution_mode = mode;
164 self
165 }
166}
167
168#[async_trait]
196pub trait Tool {
197 fn info(&self) -> &ToolInfo;
199
200 async fn call(&self, ctx: AgentContext, args: AgentValue) -> Result<AgentValue, AgentError>;
211}
212
213impl From<ToolInfo> for AgentValue {
214 fn from(info: ToolInfo) -> Self {
215 let mut obj: BTreeMap<String, AgentValue> = BTreeMap::new();
216 obj.insert("name".to_string(), AgentValue::from(info.name));
217 obj.insert(
218 "description".to_string(),
219 AgentValue::from(info.description),
220 );
221 if let Ok(params_value) = AgentValue::from_serialize(&info.parameters) {
222 obj.insert("parameters".to_string(), params_value);
223 }
224 AgentValue::object(obj.into())
225 }
226}
227
228#[derive(Clone)]
230struct ToolEntry {
231 info: ToolInfo,
232 tool: Arc<Box<dyn Tool + Send + Sync>>,
233}
234
235impl ToolEntry {
236 fn new<T: Tool + Send + Sync + 'static>(tool: T) -> Self {
238 Self {
239 info: tool.info().clone(),
240 tool: Arc::new(Box::new(tool)),
241 }
242 }
243}
244
245struct ToolRegistry {
247 tools: HashMap<String, ToolEntry>,
248}
249
250impl ToolRegistry {
251 fn new() -> Self {
253 Self {
254 tools: HashMap::new(),
255 }
256 }
257
258 fn register_tool<T: Tool + Send + Sync + 'static>(&mut self, tool: T) {
260 let info = tool.info();
261 if let Some(warning) = description_warning(&info.name, &info.description) {
262 log::warn!("{}", warning);
263 }
264 let name = info.name.to_string();
265 let entry = ToolEntry::new(tool);
266 self.tools.insert(name, entry);
267 }
268
269 fn unregister_tool(&mut self, name: &str) {
271 self.tools.remove(name);
272 }
273
274 fn get_tool(&self, name: &str) -> Option<Arc<Box<dyn Tool + Send + Sync>>> {
276 self.tools.get(name).map(|entry| entry.tool.clone())
277 }
278}
279
280static TOOL_REGISTRY: OnceLock<RwLock<ToolRegistry>> = OnceLock::new();
282
283fn registry() -> &'static RwLock<ToolRegistry> {
285 TOOL_REGISTRY.get_or_init(|| RwLock::new(ToolRegistry::new()))
286}
287
288pub fn register_tool<T: Tool + Send + Sync + 'static>(tool: T) {
301 registry().write().unwrap().register_tool(tool);
302}
303
304const MIN_DESCRIPTION_CHARS: usize = 10;
306
307fn description_warning(name: &str, description: &str) -> Option<String> {
311 let trimmed = description.trim();
312 if trimmed.is_empty() {
313 Some(format!(
314 "Tool '{}' is registered without a description; the description is \
315 the most important factor for tool-calling performance and should \
316 explain what the tool does, when to use it, and what its \
317 parameters mean",
318 name
319 ))
320 } else if trimmed.chars().count() < MIN_DESCRIPTION_CHARS {
321 Some(format!(
322 "Tool '{}' has a very short description {:?}; detailed descriptions \
323 (3-4+ sentences) significantly improve tool-calling performance",
324 name, trimmed
325 ))
326 } else {
327 None
328 }
329}
330
331fn is_valid_tool_name(name: &str) -> bool {
334 let len = name.len();
335 (1..=64).contains(&len)
336 && name
337 .bytes()
338 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
339}
340
341pub fn unregister_tool(name: &str) {
347 registry().write().unwrap().unregister_tool(name);
348}
349
350pub fn list_tool_infos() -> Vec<ToolInfo> {
356 registry()
357 .read()
358 .unwrap()
359 .tools
360 .values()
361 .map(|entry| entry.info.clone())
362 .collect()
363}
364
365pub fn list_tool_infos_patterns(patterns: &str) -> Result<Vec<ToolInfo>, regex::Error> {
382 let patterns = patterns
384 .lines()
385 .map(|line| line.trim())
386 .filter(|line| !line.is_empty())
387 .collect::<Vec<&str>>();
388 let reg_set = RegexSet::new(&patterns)?;
389 let tool_names = registry()
390 .read()
391 .unwrap()
392 .tools
393 .values()
394 .filter_map(|entry| {
395 if reg_set.is_match(&entry.info.name) {
396 Some(entry.info.clone())
397 } else {
398 None
399 }
400 })
401 .collect();
402 Ok(tool_names)
403}
404
405pub fn get_tool(name: &str) -> Option<Arc<Box<dyn Tool + Send + Sync>>> {
415 registry().read().unwrap().get_tool(name)
416}
417
418pub async fn call_tool(
430 ctx: AgentContext,
431 name: &str,
432 args: AgentValue,
433) -> Result<AgentValue, AgentError> {
434 if ctx.is_cancelled() {
435 return Err(AgentError::Cancelled);
436 }
437
438 let tool = {
439 let guard = registry().read().unwrap();
440 guard.get_tool(name)
441 };
442
443 let Some(tool) = tool else {
444 return Err(AgentError::Other(format!("Tool '{}' not found", name)));
445 };
446
447 tool.call(ctx, args).await
448}
449
450pub fn error_tool_result(call: &ToolCall, e: impl ToString) -> Message {
457 let mut msg = Message::tool(call.function.name.clone(), e.to_string());
458 msg.id = call.function.id.clone();
459 msg.is_error = Some(true);
460 msg
461}
462
463fn schema_accepts(schema: &serde_json::Value, value: &serde_json::Value) -> bool {
466 jsonschema::validator_for(schema)
467 .map(|v| v.is_valid(value))
468 .unwrap_or(false)
469}
470
471fn coerce_by_schema(value: &mut serde_json::Value, schema: &serde_json::Value) {
487 let Some(schema_obj) = schema.as_object() else {
488 return;
489 };
490
491 for key in ["anyOf", "oneOf"] {
492 let Some(variants) = schema_obj.get(key).and_then(|v| v.as_array()) else {
493 continue;
494 };
495 if !variants.iter().any(|v| schema_accepts(v, value)) {
496 for variant in variants {
497 let mut candidate = value.clone();
498 coerce_by_schema(&mut candidate, variant);
499 if schema_accepts(variant, &candidate) {
500 *value = candidate;
501 break;
502 }
503 }
504 }
505 break;
506 }
507
508 match schema_obj.get("type").and_then(|t| t.as_str()) {
509 Some("integer") => {
510 if let Some(s) = value.as_str()
513 && let Ok(i) = s.parse::<i64>()
514 {
515 *value = serde_json::Value::from(i);
516 }
517 }
518 Some("number") => {
519 if let Some(s) = value.as_str()
521 && let Ok(f) = s.parse::<f64>()
522 && let Some(n) = serde_json::Number::from_f64(f)
523 {
524 *value = serde_json::Value::Number(n);
525 }
526 }
527 Some("boolean") => match value.as_str() {
528 Some("true") => *value = serde_json::Value::Bool(true),
529 Some("false") => *value = serde_json::Value::Bool(false),
530 _ => {}
531 },
532 _ => {}
533 }
534
535 if let Some(props) = schema_obj.get("properties").and_then(|p| p.as_object())
536 && let Some(map) = value.as_object_mut()
537 {
538 for (name, prop_schema) in props {
539 if let Some(v) = map.get_mut(name) {
540 coerce_by_schema(v, prop_schema);
541 }
542 }
543 }
544
545 if let Some(items) = schema_obj.get("items")
546 && let Some(arr) = value.as_array_mut()
547 {
548 for v in arr {
549 coerce_by_schema(v, items);
550 }
551 }
552}
553
554fn validate_tool_args(
564 schema: &serde_json::Value,
565 args: &mut serde_json::Value,
566) -> Result<(), String> {
567 let validator = match jsonschema::validator_for(schema) {
568 Ok(v) => v,
569 Err(e) => {
570 log::warn!(
571 "Tool parameter schema failed to compile; skipping argument validation: {}",
572 e
573 );
574 return Ok(());
575 }
576 };
577
578 coerce_by_schema(args, schema);
579
580 let errors = validator
581 .iter_errors(args)
582 .map(|e| format!("at '{}': {}", e.instance_path(), e))
583 .collect::<Vec<_>>();
584 if errors.is_empty() {
585 Ok(())
586 } else {
587 Err(errors.join("; "))
588 }
589}
590
591async fn execute_tool_call(ctx: &AgentContext, call: &ToolCall) -> Message {
595 if let Some(err) = &call.function.parse_error {
598 return error_tool_result(
599 call,
600 format!(
601 "Tool call arguments could not be parsed as JSON; the call was \
602 not executed. Re-issue the call with valid JSON arguments. {}",
603 err
604 ),
605 );
606 }
607 let mut parameters = call.function.parameters.clone();
611 if let Some(tool) = get_tool(call.function.name.as_str())
612 && let Err(msg) = validate_tool_args(&tool.info().parameters, &mut parameters)
613 {
614 return error_tool_result(
615 call,
616 format!(
617 "Tool call arguments failed schema validation; the call was \
618 not executed. Re-issue the call with corrected arguments. \
619 Errors: {}",
620 msg
621 ),
622 );
623 }
624 let args = match AgentValue::from_json(parameters) {
625 Ok(args) => args,
626 Err(e) => {
627 return error_tool_result(call, format!("Failed to parse tool call parameters: {}", e));
628 }
629 };
630 match call_tool(ctx.clone(), call.function.name.as_str(), args).await {
631 Ok(tool_resp) => {
632 let mut msg = Message::tool_with_content(
633 call.function.name.clone(),
634 tool_result_content(&tool_resp),
635 );
636 msg.id = call.function.id.clone();
637 msg
638 }
639 Err(e) => error_tool_result(call, e),
640 }
641}
642
643fn tool_result_content(resp: &AgentValue) -> MessageContent {
652 #[cfg(feature = "image")]
653 match resp {
654 AgentValue::Image(img) => {
655 return MessageContent::Blocks(vec![image_block(img)]);
656 }
657 AgentValue::Array(arr) if arr.iter().any(|v| matches!(v, AgentValue::Image(_))) => {
658 let blocks = arr
659 .iter()
660 .map(|v| match v {
661 AgentValue::Image(img) => image_block(img),
662 other => ContentBlock::Text {
663 text: other.to_json().to_string(),
664 },
665 })
666 .collect();
667 return MessageContent::Blocks(blocks);
668 }
669 _ => {}
670 }
671 MessageContent::Text(resp.to_json().to_string())
672}
673
674#[cfg(feature = "image")]
679fn image_block(img: &photon_rs::PhotonImage) -> ContentBlock {
680 ContentBlock::Image {
681 data: img
682 .get_base64()
683 .trim_start_matches(IMAGE_BASE64_PREFIX)
684 .to_string(),
685 mime_type: "image/png".to_string(),
686 }
687}
688
689async fn flush_parallel_batch(
698 ctx: &AgentContext,
699 batch: &mut Vec<&ToolCall>,
700 max_concurrency: usize,
701 out: &mut Vec<Message>,
702 cancel: Option<&CancellationToken>,
703) -> bool {
704 if batch.is_empty() {
705 return true;
706 }
707 let mut futures = Vec::with_capacity(batch.len());
711 for (index, &call) in batch.iter().enumerate() {
712 futures.push(async move { (index, execute_tool_call(ctx, call).await) });
713 }
714 let mut results = stream::iter(futures).buffer_unordered(max_concurrency);
718 let mut slots: Vec<Option<Message>> = Vec::new();
719 slots.resize_with(batch.len(), || None);
720 let mut aborted = false;
721 loop {
722 match run_unless_cancelled(cancel, results.next()).await {
723 Some(Some((index, msg))) => slots[index] = Some(msg),
724 Some(None) => break,
725 None => {
726 aborted = true;
727 break;
728 }
729 }
730 }
731 if aborted {
732 use futures_util::FutureExt;
735 while let Some(Some((index, msg))) = results.next().now_or_never() {
736 slots[index] = Some(msg);
737 }
738 }
739 drop(results);
740 for (slot, call) in slots.iter_mut().zip(batch.drain(..)) {
741 out.push(match slot.take() {
742 Some(msg) => msg,
743 None => error_tool_result(call, ABORTED_TOOL_RESULT),
744 });
745 }
746 !aborted
747}
748
749pub async fn call_tools(
790 ctx: &AgentContext,
791 tool_calls: &Vector<ToolCall>,
792 max_concurrency: usize,
793) -> Result<Vector<Message>, AgentError> {
794 if tool_calls.is_empty() {
795 return Ok(vector![]);
796 };
797 let max_concurrency = max_concurrency.max(1);
798 let cancel = ctx.cancel_token();
799
800 let mut aborted = false;
805 let mut resp_messages = Vec::with_capacity(tool_calls.len());
806 let mut parallel_batch: Vec<&ToolCall> = Vec::new();
807 for call in tool_calls {
808 let mode = get_tool(call.function.name.as_str())
811 .map(|tool| tool.info().execution_mode)
812 .unwrap_or_default();
813 if mode == ExecutionMode::Parallel {
814 parallel_batch.push(call);
815 continue;
816 }
817 if !flush_parallel_batch(
820 ctx,
821 &mut parallel_batch,
822 max_concurrency,
823 &mut resp_messages,
824 cancel,
825 )
826 .await
827 {
828 aborted = true;
829 break;
830 }
831 match run_unless_cancelled(cancel, execute_tool_call(ctx, call)).await {
832 Some(msg) => resp_messages.push(msg),
833 None => {
834 aborted = true;
835 break;
836 }
837 }
838 }
839 if !aborted
840 && !flush_parallel_batch(
841 ctx,
842 &mut parallel_batch,
843 max_concurrency,
844 &mut resp_messages,
845 cancel,
846 )
847 .await
848 {
849 aborted = true;
850 }
851
852 if aborted {
857 for call in tool_calls.iter().skip(resp_messages.len()) {
858 resp_messages.push(error_tool_result(call, ABORTED_TOOL_RESULT));
859 }
860 }
861
862 Ok(resp_messages.into())
863}
864
865#[modular_agent(
882 title="List Tools",
883 category=CATEGORY,
884 inputs=[PORT_PATTERNS],
885 outputs=[PORT_TOOLS],
886)]
887pub struct ListToolsAgent {
888 data: AgentData,
889}
890
891#[async_trait]
892impl AsAgent for ListToolsAgent {
893 fn new(ma: ModularAgent, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
894 Ok(Self {
895 data: AgentData::new(ma, id, spec),
896 })
897 }
898
899 async fn process(
900 &mut self,
901 ctx: AgentContext,
902 _port: String,
903 value: AgentValue,
904 ) -> Result<(), AgentError> {
905 let Some(patterns) = value.as_str() else {
906 return Err(AgentError::InvalidValue(
907 "patterns input must be a string".to_string(),
908 ));
909 };
910
911 let tools = if !patterns.is_empty() {
912 list_tool_infos_patterns(patterns)
913 .map_err(|e| AgentError::InvalidValue(format!("Invalid regex patterns: {}", e)))?
914 } else {
915 list_tool_infos()
916 };
917 let tools = tools
918 .into_iter()
919 .map(|tool| tool.into())
920 .collect::<Vector<AgentValue>>();
921 let tools_array = AgentValue::array(tools);
922
923 self.output(ctx, PORT_TOOLS, tools_array).await?;
924
925 Ok(())
926 }
927}
928
929#[modular_agent(
949 title="Preset Tool",
950 category=CATEGORY,
951 inputs=[PORT_TOOL_OUT],
952 outputs=[PORT_TOOL_IN],
953 string_config(name=CONFIG_TOOL_NAME),
954 text_config(name=CONFIG_TOOL_DESCRIPTION),
955 object_config(name=CONFIG_TOOL_PARAMETERS),
956 integer_config(name=CONFIG_TIMEOUT_SECS, default=60),
957)]
958pub struct PresetToolAgent {
959 data: AgentData,
960 name: String,
961 description: String,
962 parameters: Option<serde_json::Value>,
963 pending: Arc<Mutex<HashMap<usize, oneshot::Sender<AgentValue>>>>,
965}
966
967impl PresetToolAgent {
968 fn start_tool_call(
973 &mut self,
974 ctx: AgentContext,
975 args: AgentValue,
976 ) -> Result<oneshot::Receiver<AgentValue>, AgentError> {
977 let (tx, rx) = oneshot::channel();
978
979 self.pending.lock().unwrap().insert(ctx.id(), tx);
980 if let Err(e) = self.try_output(ctx.clone(), PORT_TOOL_IN, args) {
981 self.pending.lock().unwrap().remove(&ctx.id());
985 return Err(e);
986 }
987
988 Ok(rx)
989 }
990}
991
992#[async_trait]
993impl AsAgent for PresetToolAgent {
994 fn new(ma: ModularAgent, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
995 let def_name = spec.def_name.clone();
996 let configs = spec.configs.clone();
997 let name = configs
998 .as_ref()
999 .and_then(|c| c.get_string(CONFIG_TOOL_NAME).ok())
1000 .unwrap_or_else(|| def_name.clone());
1001 let description = configs
1002 .as_ref()
1003 .and_then(|c| c.get_string(CONFIG_TOOL_DESCRIPTION).ok())
1004 .unwrap_or_default();
1005 let parameters = configs
1006 .as_ref()
1007 .and_then(|c| c.get(CONFIG_TOOL_PARAMETERS).ok())
1008 .and_then(|v| serde_json::to_value(v).ok());
1009 Ok(Self {
1010 data: AgentData::new(ma, id, spec),
1011 name,
1012 description,
1013 parameters,
1014 pending: Arc::new(Mutex::new(HashMap::new())),
1015 })
1016 }
1017
1018 fn configs_changed(&mut self) -> Result<(), AgentError> {
1019 let old_name = self.name.clone();
1020 self.name = self.configs()?.get_string_or_default(CONFIG_TOOL_NAME);
1021 self.description = self
1022 .configs()?
1023 .get_string_or_default(CONFIG_TOOL_DESCRIPTION);
1024 self.parameters = self
1025 .configs()?
1026 .get(CONFIG_TOOL_PARAMETERS)
1027 .ok()
1028 .and_then(|v| serde_json::to_value(v).ok());
1029
1030 if self.data.status == AgentStatus::Start {
1033 if !is_valid_tool_name(&self.name) {
1034 log::warn!(
1035 "PresetToolAgent {} has invalid tool name {:?}; \
1036 tool names must match ^[a-zA-Z0-9_-]{{1,64}}$",
1037 self.id(),
1038 self.name
1039 );
1040 }
1041 let agent_handle = self
1042 .ma()
1043 .get_agent(self.id())
1044 .ok_or_else(|| AgentError::AgentNotFound(self.id().to_string()))?;
1045 let tool = PresetTool::new(
1046 self.name.clone(),
1047 self.description.clone(),
1048 self.parameters.clone(),
1049 agent_handle,
1050 );
1051 register_tool(tool);
1057 if old_name != self.name {
1058 unregister_tool(&old_name);
1059 }
1060 }
1061
1062 Ok(())
1063 }
1064
1065 async fn start(&mut self) -> Result<(), AgentError> {
1066 if !is_valid_tool_name(&self.name) {
1069 log::warn!(
1070 "PresetToolAgent {} has invalid tool name {:?}; \
1071 tool names must match ^[a-zA-Z0-9_-]{{1,64}}$",
1072 self.id(),
1073 self.name
1074 );
1075 }
1076 let agent_handle = self
1077 .ma()
1078 .get_agent(self.id())
1079 .ok_or_else(|| AgentError::AgentNotFound(self.id().to_string()))?;
1080 let tool = PresetTool::new(
1081 self.name.clone(),
1082 self.description.clone(),
1083 self.parameters.clone(),
1084 agent_handle,
1085 );
1086 register_tool(tool);
1087 Ok(())
1088 }
1089
1090 async fn stop(&mut self) -> Result<(), AgentError> {
1091 unregister_tool(&self.name);
1092 self.pending.lock().unwrap().clear();
1093 Ok(())
1094 }
1095
1096 async fn process(
1097 &mut self,
1098 ctx: AgentContext,
1099 _port: String,
1100 value: AgentValue,
1101 ) -> Result<(), AgentError> {
1102 if let Some(tx) = self.pending.lock().unwrap().remove(&ctx.id()) {
1103 let _ = tx.send(value);
1104 }
1105 Ok(())
1106 }
1107}
1108
1109struct PresetTool {
1111 info: ToolInfo,
1112 agent: SharedAgent,
1113}
1114
1115impl PresetTool {
1116 fn new(
1118 name: String,
1119 description: String,
1120 parameters: Option<serde_json::Value>,
1121 agent: SharedAgent,
1122 ) -> Self {
1123 Self {
1124 info: ToolInfo::new(name, description, parameters),
1125 agent,
1126 }
1127 }
1128
1129 async fn tool_call(
1139 &self,
1140 ctx: AgentContext,
1141 args: AgentValue,
1142 ) -> Result<AgentValue, AgentError> {
1143 if ctx.is_cancelled() {
1144 return Err(AgentError::Cancelled);
1145 }
1146
1147 let ctx_id = ctx.id();
1149 let cancel = ctx.cancel_token().cloned();
1150 let (rx, timeout_secs, pending) = {
1151 let mut guard = self.agent.lock().await;
1152 let Some(preset_tool_agent) = guard.as_agent_mut::<PresetToolAgent>() else {
1153 return Err(AgentError::Other(
1154 "Agent is not PresetToolAgent".to_string(),
1155 ));
1156 };
1157 if ctx.is_cancelled() {
1161 return Err(AgentError::Cancelled);
1162 }
1163 let timeout_secs = preset_tool_agent
1164 .configs()
1165 .map(|c| c.get_integer_or(CONFIG_TIMEOUT_SECS, DEFAULT_TIMEOUT_SECS))
1166 .unwrap_or(DEFAULT_TIMEOUT_SECS);
1167 let pending = preset_tool_agent.pending.clone();
1168 let rx = preset_tool_agent.start_tool_call(ctx, args)?;
1169 (rx, timeout_secs, pending)
1170 };
1171
1172 struct PendingGuard {
1185 pending: Arc<Mutex<HashMap<usize, oneshot::Sender<AgentValue>>>>,
1186 ctx_id: usize,
1187 }
1188 impl Drop for PendingGuard {
1189 fn drop(&mut self) {
1190 self.pending.lock().unwrap().remove(&self.ctx_id);
1191 }
1192 }
1193 let _guard = PendingGuard { pending, ctx_id };
1194
1195 let wait = async {
1196 let rx = async {
1197 rx.await
1198 .map_err(|_| AgentError::Other("tool_out dropped".to_string()))
1199 };
1200 if timeout_secs <= 0 {
1201 rx.await
1202 } else {
1203 match tokio::time::timeout(Duration::from_secs(timeout_secs as u64), rx).await {
1204 Ok(result) => result,
1205 Err(_) => Err(AgentError::Timeout(format!(
1206 "Tool call timed out after {} seconds",
1207 timeout_secs
1208 ))),
1209 }
1210 }
1211 };
1212 run_unless_cancelled(cancel.as_ref(), wait)
1213 .await
1214 .unwrap_or(Err(AgentError::Cancelled))
1215 }
1216}
1217
1218#[async_trait]
1219impl Tool for PresetTool {
1220 fn info(&self) -> &ToolInfo {
1221 &self.info
1222 }
1223
1224 async fn call(&self, ctx: AgentContext, args: AgentValue) -> Result<AgentValue, AgentError> {
1225 self.tool_call(ctx, args).await
1226 }
1227}
1228
1229#[modular_agent(
1248 title="Call Tool Message",
1249 category=CATEGORY,
1250 inputs=[PORT_MESSAGE],
1251 outputs=[PORT_MESSAGE],
1252 string_config(name=CONFIG_TOOLS),
1253 integer_config(name=CONFIG_MAX_CONCURRENCY, default=8),
1254)]
1255pub struct CallToolMessageAgent {
1256 data: AgentData,
1257 executed: BTreeMap<String, HashSet<String>>,
1261 ctx_key_order: VecDeque<String>,
1263}
1264
1265const MAX_TRACKED_CTX_KEYS: usize = 1024;
1267
1268impl CallToolMessageAgent {
1269 fn is_executed(&self, ctx_key: &str, id: &str) -> bool {
1270 self.executed
1271 .get(ctx_key)
1272 .is_some_and(|ids| ids.contains(id))
1273 }
1274
1275 fn mark_executed(&mut self, ctx_key: &str, id: String) {
1276 if !self.executed.contains_key(ctx_key) {
1277 if self.ctx_key_order.len() >= MAX_TRACKED_CTX_KEYS
1278 && let Some(oldest) = self.ctx_key_order.pop_front()
1279 {
1280 self.executed.remove(&oldest);
1281 }
1282 self.ctx_key_order.push_back(ctx_key.to_string());
1283 }
1284 self.executed
1285 .entry(ctx_key.to_string())
1286 .or_default()
1287 .insert(id);
1288 }
1289}
1290
1291#[async_trait]
1292impl AsAgent for CallToolMessageAgent {
1293 fn new(ma: ModularAgent, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
1294 Ok(Self {
1295 data: AgentData::new(ma, id, spec),
1296 executed: BTreeMap::new(),
1297 ctx_key_order: VecDeque::new(),
1298 })
1299 }
1300
1301 async fn stop(&mut self) -> Result<(), AgentError> {
1302 self.executed.clear();
1303 self.ctx_key_order.clear();
1304 Ok(())
1305 }
1306
1307 async fn process(
1308 &mut self,
1309 ctx: AgentContext,
1310 _port: String,
1311 value: AgentValue,
1312 ) -> Result<(), AgentError> {
1313 let Some(message) = value.as_message() else {
1314 return Ok(());
1315 };
1316 if message.streaming {
1319 return Ok(());
1320 }
1321 let Some(mut tool_calls) = message.tool_calls.clone() else {
1322 return Ok(());
1323 };
1324
1325 let config_tools = self.configs()?.get_string_or_default(CONFIG_TOOLS);
1327 if !config_tools.is_empty() {
1328 let tools = list_tool_infos_patterns(&config_tools)
1329 .map_err(|e| AgentError::InvalidValue(format!("Invalid regex patterns: {}", e)))?;
1330 let allowed_tool_names: HashSet<String> = tools.into_iter().map(|t| t.name).collect();
1332 tool_calls = tool_calls
1333 .iter()
1334 .filter(|call| allowed_tool_names.contains(&call.function.name))
1335 .cloned()
1336 .collect();
1337 }
1338
1339 let ctx_key = ctx.ctx_key()?;
1342 tool_calls = tool_calls
1343 .iter()
1344 .filter(|call| match &call.function.id {
1345 Some(id) => !self.is_executed(&ctx_key, id),
1346 None => true,
1347 })
1348 .cloned()
1349 .collect();
1350
1351 for call in &tool_calls {
1354 if let Some(id) = &call.function.id {
1355 self.mark_executed(&ctx_key, id.clone());
1356 }
1357 }
1358
1359 if message.stop_reason.as_deref() == Some("length") {
1364 for call in &tool_calls {
1365 let resp_msg = error_tool_result(
1366 call,
1367 format!(
1368 "Tool call \"{}\" was not executed: output hit the token \
1369 limit; arguments may be truncated. Re-issue with complete \
1370 arguments.",
1371 call.function.name
1372 ),
1373 );
1374 self.output(ctx.clone(), PORT_MESSAGE, AgentValue::message(resp_msg))
1375 .await?;
1376 }
1377 return Ok(());
1378 }
1379
1380 let max_concurrency = self
1383 .configs()?
1384 .get_integer_or(CONFIG_MAX_CONCURRENCY, DEFAULT_MAX_CONCURRENCY)
1385 .max(1) as usize;
1386
1387 let resp_messages = call_tools(&ctx, &tool_calls, max_concurrency).await?;
1388 for resp_msg in resp_messages {
1389 self.output(ctx.clone(), PORT_MESSAGE, AgentValue::message(resp_msg))
1390 .await?;
1391 }
1392 Ok(())
1393 }
1394}
1395
1396#[modular_agent(
1429 title="Loop Control",
1430 category=CATEGORY,
1431 inputs=[PORT_MESSAGE],
1432 outputs=[PORT_MESSAGE, PORT_LIMIT_EXCEEDED],
1433 integer_config(name=CONFIG_MAX_ITERATIONS, default=25),
1434)]
1435pub struct LoopControlAgent {
1436 data: AgentData,
1437 counts: BTreeMap<String, (u32, Option<String>)>,
1440 ctx_key_order: VecDeque<String>,
1442}
1443
1444impl LoopControlAgent {
1445 fn record_count(&mut self, ctx_key: &str, count: u32, id: Option<String>) {
1446 if !self.counts.contains_key(ctx_key) {
1447 if self.ctx_key_order.len() >= MAX_TRACKED_CTX_KEYS
1448 && let Some(oldest) = self.ctx_key_order.pop_front()
1449 {
1450 self.counts.remove(&oldest);
1451 }
1452 self.ctx_key_order.push_back(ctx_key.to_string());
1453 }
1454 self.counts.insert(ctx_key.to_string(), (count, id));
1455 }
1456}
1457
1458#[async_trait]
1459impl AsAgent for LoopControlAgent {
1460 fn new(ma: ModularAgent, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
1461 Ok(Self {
1462 data: AgentData::new(ma, id, spec),
1463 counts: BTreeMap::new(),
1464 ctx_key_order: VecDeque::new(),
1465 })
1466 }
1467
1468 async fn stop(&mut self) -> Result<(), AgentError> {
1469 self.counts.clear();
1470 self.ctx_key_order.clear();
1471 Ok(())
1472 }
1473
1474 async fn process(
1475 &mut self,
1476 ctx: AgentContext,
1477 _port: String,
1478 value: AgentValue,
1479 ) -> Result<(), AgentError> {
1480 let countable = value.as_message().is_some_and(|m| {
1481 m.role == "assistant"
1482 && !m.streaming
1483 && m.tool_calls.as_ref().is_some_and(|calls| !calls.is_empty())
1484 });
1485 if !countable {
1486 return self.output(ctx, PORT_MESSAGE, value).await;
1490 }
1491 let message_id = value.as_message().and_then(|m| m.id.clone());
1492
1493 let max_iterations = self
1494 .configs()?
1495 .get_integer_or(CONFIG_MAX_ITERATIONS, DEFAULT_MAX_ITERATIONS);
1496 let ctx_key = ctx.ctx_key()?;
1497 let (count, last_counted_id) = self.counts.get(&ctx_key).cloned().unwrap_or((0, None));
1498
1499 if let Some(id) = &message_id
1505 && last_counted_id.as_deref() == Some(id)
1506 {
1507 if max_iterations > 0 && i64::from(count) > max_iterations {
1508 return Ok(());
1509 }
1510 return self.output(ctx, PORT_MESSAGE, value).await;
1511 }
1512
1513 let count = count.saturating_add(1);
1514 self.record_count(&ctx_key, count, message_id);
1515
1516 if max_iterations > 0 && i64::from(count) > max_iterations {
1517 let notice = Message::assistant(format!(
1518 "Loop limit reached: the tool-call cycle exceeded the configured max_iterations of {} and has been stopped.",
1519 max_iterations
1520 ));
1521 return self
1522 .output(ctx, PORT_LIMIT_EXCEEDED, AgentValue::message(notice))
1523 .await;
1524 }
1525
1526 self.output(ctx, PORT_MESSAGE, value).await
1527 }
1528}
1529
1530#[modular_agent(
1540 title="Call Tool",
1541 category=CATEGORY,
1542 inputs=[PORT_TOOL_CALL],
1543 outputs=[PORT_VALUE],
1544)]
1545pub struct CallToolAgent {
1546 data: AgentData,
1547}
1548
1549#[async_trait]
1550impl AsAgent for CallToolAgent {
1551 fn new(ma: ModularAgent, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
1552 Ok(Self {
1553 data: AgentData::new(ma, id, spec),
1554 })
1555 }
1556
1557 async fn process(
1558 &mut self,
1559 ctx: AgentContext,
1560 _port: String,
1561 value: AgentValue,
1562 ) -> Result<(), AgentError> {
1563 let obj = value.as_object().ok_or_else(|| {
1564 AgentError::InvalidValue("tool_call input must be an object".to_string())
1565 })?;
1566 let tool_name = obj.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
1567 AgentError::InvalidValue("tool_call.name must be a string".to_string())
1568 })?;
1569 let tool_parameters = obj.get("parameters").cloned().unwrap_or(AgentValue::unit());
1570
1571 let resp = call_tool(ctx.clone(), tool_name, tool_parameters).await?;
1572 self.output(ctx, PORT_VALUE, resp).await?;
1573
1574 Ok(())
1575 }
1576}
1577
1578#[cfg(test)]
1579mod tests {
1580 use super::*;
1581 use crate::ToolCallFunction;
1582
1583 #[test]
1584 fn test_tool_info_new_parameters_default() {
1585 let info = ToolInfo::new("t", "d", None);
1586 assert_eq!(
1587 info.parameters,
1588 serde_json::json!({"type": "object", "properties": {}})
1589 );
1590
1591 let schema = serde_json::json!({
1592 "type": "object",
1593 "properties": {"x": {"type": "string"}},
1594 "required": ["x"]
1595 });
1596 let info = ToolInfo::new("t", "d", Some(schema.clone()));
1597 assert_eq!(info.parameters, schema);
1598 }
1599
1600 #[test]
1601 fn test_error_tool_result_shape() {
1602 let call = ToolCall {
1603 function: ToolCallFunction {
1604 name: "my_tool".to_string(),
1605 parameters: serde_json::json!({}),
1606 id: Some("call42".to_string()),
1607 parse_error: None,
1608 },
1609 };
1610 let msg = error_tool_result(&call, "something went wrong");
1611
1612 assert_eq!(msg.role, "tool");
1613 assert_eq!(msg.tool_name.as_deref(), Some("my_tool"));
1614 assert_eq!(msg.id.as_deref(), Some("call42"));
1615 assert_eq!(msg.is_error, Some(true));
1616 assert_eq!(msg.text(), "something went wrong");
1617 }
1618
1619 #[test]
1620 fn test_is_valid_tool_name_accepts_valid() {
1621 assert!(is_valid_tool_name("a"));
1622 assert!(is_valid_tool_name("my_tool"));
1623 assert!(is_valid_tool_name("my-tool"));
1624 assert!(is_valid_tool_name("Tool_123-ABC"));
1625 assert!(is_valid_tool_name(&"x".repeat(64)));
1626 }
1627
1628 #[test]
1629 fn test_preset_tool_timeout_config_default() {
1630 let def = PresetToolAgent::agent_definition();
1631 let specs = def
1632 .configs
1633 .as_ref()
1634 .expect("PresetToolAgent should have config specs");
1635 let spec = specs
1636 .get(CONFIG_TIMEOUT_SECS)
1637 .expect("timeout_secs config should be present");
1638 assert_eq!(spec.value, AgentValue::integer(DEFAULT_TIMEOUT_SECS));
1639 }
1640
1641 #[test]
1642 fn test_description_warning_flags_weak_descriptions() {
1643 let warning = description_warning("my_tool", "").expect("empty should warn");
1644 assert!(warning.contains("my_tool"));
1645
1646 let warning = description_warning("my_tool", " \t\n ").expect("whitespace should warn");
1647 assert!(warning.contains("my_tool"));
1648
1649 let warning = description_warning("my_tool", "too short").expect("short should warn");
1651 assert!(warning.contains("my_tool"));
1652 assert!(warning.contains("too short"));
1653 }
1654
1655 #[test]
1656 fn test_description_warning_accepts_adequate_descriptions() {
1657 assert!(description_warning("t", "0123456789").is_none());
1658 assert!(
1659 description_warning("t", "Fetches the current weather for a given city.").is_none()
1660 );
1661 assert!(description_warning("t", "ツールの詳細な説明です。").is_none());
1663 }
1664
1665 #[test]
1666 fn test_is_valid_tool_name_rejects_invalid() {
1667 assert!(!is_valid_tool_name(""));
1668 assert!(!is_valid_tool_name(&"x".repeat(65)));
1669 assert!(!is_valid_tool_name("my tool"));
1670 assert!(!is_valid_tool_name("my.tool"));
1671 assert!(!is_valid_tool_name("ツール"));
1672 assert!(!is_valid_tool_name("tool@1"));
1673 }
1674
1675 mod arg_validation {
1676 use super::*;
1677 use serde_json::json;
1678
1679 struct EchoTool {
1682 info: ToolInfo,
1683 }
1684
1685 #[async_trait]
1686 impl Tool for EchoTool {
1687 fn info(&self) -> &ToolInfo {
1688 &self.info
1689 }
1690
1691 async fn call(
1692 &self,
1693 _ctx: AgentContext,
1694 args: AgentValue,
1695 ) -> Result<AgentValue, AgentError> {
1696 Ok(args)
1697 }
1698 }
1699
1700 fn register_echo(name: &str, schema: Option<serde_json::Value>) {
1701 register_tool(EchoTool {
1702 info: ToolInfo::new(name, "echoes args", schema),
1703 });
1704 }
1705
1706 fn call(name: &str, id: &str, params: serde_json::Value) -> ToolCall {
1707 ToolCall {
1708 function: ToolCallFunction {
1709 name: name.to_string(),
1710 parameters: params,
1711 id: Some(id.to_string()),
1712 parse_error: None,
1713 },
1714 }
1715 }
1716
1717 fn content_json(msg: &Message) -> serde_json::Value {
1718 serde_json::from_str(&msg.text()).unwrap()
1719 }
1720
1721 #[test]
1722 fn coerce_does_not_truncate_float_string_to_integer() {
1723 let schema = json!({"type": "integer"});
1724 let mut v = json!("42.5");
1725 coerce_by_schema(&mut v, &schema);
1726 assert_eq!(v, json!("42.5"));
1727 }
1728
1729 #[test]
1730 fn coerce_number_accepts_float_string_and_keeps_integer() {
1731 let schema = json!({"type": "number"});
1732 let mut v = json!("42.5");
1733 coerce_by_schema(&mut v, &schema);
1734 assert_eq!(v, json!(42.5));
1735
1736 let mut v = json!(3);
1738 coerce_by_schema(&mut v, &schema);
1739 assert_eq!(v, json!(3));
1740 }
1741
1742 #[test]
1743 fn coerce_array_items() {
1744 let schema = json!({"type": "array", "items": {"type": "integer"}});
1745 let mut v = json!(["1", 2, "3"]);
1746 coerce_by_schema(&mut v, &schema);
1747 assert_eq!(v, json!([1, 2, 3]));
1748 }
1749
1750 #[tokio::test]
1751 async fn string_primitives_coerced_before_tool_call() {
1752 let name = "p14_coerce_prims";
1753 register_echo(
1754 name,
1755 Some(json!({
1756 "type": "object",
1757 "properties": {
1758 "count": {"type": "integer"},
1759 "flag": {"type": "boolean"}
1760 },
1761 "required": ["count", "flag"]
1762 })),
1763 );
1764
1765 let calls = vector![call(name, "c1", json!({"count": "42", "flag": "true"}))];
1766 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1767 unregister_tool(name);
1768
1769 assert_eq!(msgs.len(), 1);
1770 assert_eq!(msgs[0].is_error, None);
1771 assert_eq!(msgs[0].id.as_deref(), Some("c1"));
1772 assert_eq!(content_json(&msgs[0]), json!({"count": 42, "flag": true}));
1773 }
1774
1775 #[tokio::test]
1776 async fn nested_object_property_coerced() {
1777 let name = "p14_coerce_nested";
1778 register_echo(
1779 name,
1780 Some(json!({
1781 "type": "object",
1782 "properties": {
1783 "outer": {
1784 "type": "object",
1785 "properties": {"n": {"type": "integer"}}
1786 }
1787 }
1788 })),
1789 );
1790
1791 let calls = vector![call(name, "c1", json!({"outer": {"n": "7"}}))];
1792 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1793 unregister_tool(name);
1794
1795 assert_eq!(msgs.len(), 1);
1796 assert_eq!(msgs[0].is_error, None);
1797 assert_eq!(content_json(&msgs[0]), json!({"outer": {"n": 7}}));
1798 }
1799
1800 #[tokio::test]
1801 async fn validation_failure_reports_error_and_batch_continues() {
1802 let name = "p14_validation_failure";
1803 register_echo(
1804 name,
1805 Some(json!({
1806 "type": "object",
1807 "properties": {"count": {"type": "integer"}},
1808 "required": ["count"]
1809 })),
1810 );
1811
1812 let calls = vector![
1813 call(name, "bad", json!({"count": "abc"})),
1814 call(name, "good", json!({"count": "5"})),
1815 ];
1816 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1817 unregister_tool(name);
1818
1819 assert_eq!(msgs.len(), 2);
1820 assert_eq!(msgs[0].is_error, Some(true));
1821 assert_eq!(msgs[0].id.as_deref(), Some("bad"));
1822 assert!(msgs[0].text().contains("not executed"));
1823 assert!(msgs[0].text().contains("/count"));
1824
1825 assert_eq!(msgs[1].is_error, None);
1827 assert_eq!(msgs[1].id.as_deref(), Some("good"));
1828 assert_eq!(content_json(&msgs[1]), json!({"count": 5}));
1829 }
1830
1831 #[tokio::test]
1832 async fn validation_failure_collects_all_errors() {
1833 let name = "p14_all_errors";
1834 register_echo(
1835 name,
1836 Some(json!({
1837 "type": "object",
1838 "properties": {
1839 "count": {"type": "integer"},
1840 "flag": {"type": "boolean"}
1841 },
1842 "required": ["count", "flag"]
1843 })),
1844 );
1845
1846 let calls = vector![call(name, "bad", json!({"count": "abc", "flag": "xyz"}))];
1847 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1848 unregister_tool(name);
1849
1850 assert_eq!(msgs.len(), 1);
1851 assert_eq!(msgs[0].is_error, Some(true));
1852 assert!(msgs[0].text().contains("/count"));
1854 assert!(msgs[0].text().contains("/flag"));
1855 }
1856
1857 #[tokio::test]
1858 async fn default_empty_object_schema_accepts_arbitrary_args() {
1859 let name = "p14_default_schema";
1860 register_echo(name, None);
1861
1862 let args = json!({"anything": [1, 2, 3], "nested": {"x": "y"}});
1863 let calls = vector![call(name, "c1", args.clone())];
1864 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1865 unregister_tool(name);
1866
1867 assert_eq!(msgs.len(), 1);
1868 assert_eq!(msgs[0].is_error, None);
1869 assert_eq!(content_json(&msgs[0]), args);
1870 }
1871
1872 #[tokio::test]
1873 async fn uncompilable_schema_skips_validation_and_executes() {
1874 let name = "p14_broken_schema";
1875 register_echo(name, Some(json!({"type": 123})));
1876
1877 let args = json!({"x": "1"});
1878 let calls = vector![call(name, "c1", args.clone())];
1879 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1880 unregister_tool(name);
1881
1882 assert_eq!(msgs.len(), 1);
1883 assert_eq!(msgs[0].is_error, None);
1884 assert_eq!(content_json(&msgs[0]), args);
1886 }
1887
1888 #[tokio::test]
1889 async fn any_of_coercion_picks_validating_variant() {
1890 let name = "p14_any_of";
1891 register_echo(
1892 name,
1893 Some(json!({
1894 "type": "object",
1895 "properties": {
1896 "v": {"anyOf": [{"type": "integer"}, {"type": "boolean"}]}
1897 }
1898 })),
1899 );
1900
1901 let calls = vector![
1902 call(name, "c1", json!({"v": "true"})),
1903 call(name, "c2", json!({"v": "7"})),
1904 call(name, "c3", json!({"v": 5})),
1905 ];
1906 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1907 unregister_tool(name);
1908
1909 assert_eq!(msgs.len(), 3);
1910 assert_eq!(content_json(&msgs[0]), json!({"v": true}));
1912 assert_eq!(content_json(&msgs[1]), json!({"v": 7}));
1913 assert_eq!(content_json(&msgs[2]), json!({"v": 5}));
1915 }
1916
1917 #[tokio::test]
1918 async fn any_of_with_sibling_properties_still_coerces() {
1919 let name = "p14_any_of_siblings";
1920 register_echo(
1924 name,
1925 Some(json!({
1926 "type": "object",
1927 "properties": {
1928 "a": {"type": "integer"},
1929 "b": {"type": "integer"}
1930 },
1931 "anyOf": [{"required": ["a"]}, {"required": ["b"]}]
1932 })),
1933 );
1934
1935 let calls = vector![call(name, "c1", json!({"a": "5"}))];
1936 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1937 unregister_tool(name);
1938
1939 assert_eq!(msgs.len(), 1);
1940 assert_eq!(msgs[0].is_error, None);
1941 assert_eq!(content_json(&msgs[0]), json!({"a": 5}));
1942 }
1943 }
1944
1945 mod result_content {
1946 use super::*;
1947
1948 struct FixedTool {
1950 info: ToolInfo,
1951 value: AgentValue,
1952 }
1953
1954 #[async_trait]
1955 impl Tool for FixedTool {
1956 fn info(&self) -> &ToolInfo {
1957 &self.info
1958 }
1959
1960 async fn call(
1961 &self,
1962 _ctx: AgentContext,
1963 _args: AgentValue,
1964 ) -> Result<AgentValue, AgentError> {
1965 Ok(self.value.clone())
1966 }
1967 }
1968
1969 fn register_fixed(name: &str, value: AgentValue) {
1970 register_tool(FixedTool {
1971 info: ToolInfo::new(name, "returns a fixed value for tests", None),
1972 value,
1973 });
1974 }
1975
1976 fn call(name: &str, id: &str) -> ToolCall {
1977 ToolCall {
1978 function: ToolCallFunction {
1979 name: name.to_string(),
1980 parameters: serde_json::json!({}),
1981 id: Some(id.to_string()),
1982 parse_error: None,
1983 },
1984 }
1985 }
1986
1987 async fn run_single(name: &str) -> Message {
1988 let calls = vector![call(name, "c1")];
1989 let mut msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
1990 unregister_tool(name);
1991 assert_eq!(msgs.len(), 1);
1992 msgs.remove(0)
1993 }
1994
1995 #[cfg(feature = "image")]
1996 #[tokio::test]
1997 async fn image_result_becomes_single_image_block() {
1998 let name = "result_content_image";
1999 register_fixed(name, AgentValue::image_default());
2000
2001 let msg = run_single(name).await;
2002 assert_eq!(msg.role, "tool");
2003 assert_eq!(msg.tool_name.as_deref(), Some(name));
2004 assert_eq!(msg.id.as_deref(), Some("c1"));
2005 assert_eq!(msg.is_error, None);
2006
2007 let MessageContent::Blocks(blocks) = &msg.content else {
2008 panic!("expected block content, got {:?}", msg.content);
2009 };
2010 assert_eq!(blocks.len(), 1);
2011 let ContentBlock::Image { data, mime_type } = &blocks[0] else {
2012 panic!("expected image block, got {:?}", blocks[0]);
2013 };
2014 assert_eq!(mime_type, "image/png");
2015 assert!(!data.starts_with("data:"));
2016 assert!(!data.is_empty());
2017 }
2018
2019 #[cfg(feature = "image")]
2020 #[tokio::test]
2021 async fn mixed_array_result_becomes_ordered_blocks() {
2022 let name = "result_content_mixed_array";
2023 register_fixed(
2024 name,
2025 AgentValue::array(vector![
2026 AgentValue::image_default(),
2027 AgentValue::string("caption"),
2028 ]),
2029 );
2030
2031 let msg = run_single(name).await;
2032 let MessageContent::Blocks(blocks) = &msg.content else {
2033 panic!("expected block content, got {:?}", msg.content);
2034 };
2035 assert_eq!(blocks.len(), 2);
2036 assert!(matches!(&blocks[0], ContentBlock::Image { .. }));
2037 let ContentBlock::Text { text } = &blocks[1] else {
2038 panic!("expected text block, got {:?}", blocks[1]);
2039 };
2040 assert_eq!(text, "\"caption\"");
2042 }
2043
2044 #[tokio::test]
2045 async fn non_image_results_keep_legacy_text_form() {
2046 let object = AgentValue::from_json(serde_json::json!({"a": 1, "b": "x"})).unwrap();
2047 let imageless_array =
2048 AgentValue::from_json(serde_json::json!([1, "two", null])).unwrap();
2049 let cases = [
2050 ("result_content_object", object),
2051 ("result_content_string", AgentValue::string("plain")),
2052 ("result_content_array", imageless_array),
2053 ];
2054 for (name, value) in cases {
2055 let expected = value.to_json().to_string();
2056 register_fixed(name, value);
2057 let msg = run_single(name).await;
2058 assert_eq!(msg.content, MessageContent::Text(expected));
2059 }
2060 }
2061
2062 #[tokio::test]
2063 async fn error_result_stays_text() {
2064 let calls = vector![call("result_content_no_such_tool", "c1")];
2066 let msgs = call_tools(&AgentContext::new(), &calls, 8).await.unwrap();
2067 assert_eq!(msgs.len(), 1);
2068 assert_eq!(msgs[0].is_error, Some(true));
2069 assert!(matches!(msgs[0].content, MessageContent::Text(_)));
2070 assert!(msgs[0].text().contains("not found"));
2071 }
2072 }
2073
2074 #[cfg(feature = "test-utils")]
2075 mod loop_control {
2076 use super::*;
2077 use crate::test_utils::{ProbeReceiver, probe_receiver};
2078 use crate::{AgentContext, ConnectionSpec, SharedAgent};
2079
2080 const LOOP_DEF: &str = "modular_agent_core::tool::LoopControlAgent";
2081 const PROBE_DEF: &str = "modular_agent_core::test_utils::TestProbeAgent";
2082 const PROBE_PORT: &str = "value";
2083
2084 struct Fixture {
2085 ma: ModularAgent,
2086 loop_agent: SharedAgent,
2087 forwarded: ProbeReceiver,
2088 limit: ProbeReceiver,
2089 }
2090
2091 async fn setup(max_iterations: i64) -> Fixture {
2094 let ma = ModularAgent::init().unwrap();
2095 ma.ready().await.unwrap();
2096 let preset_id = ma.new_preset().unwrap();
2097
2098 let loop_def = ma.get_agent_definition(LOOP_DEF).unwrap();
2099 let loop_id = ma
2100 .add_agent(preset_id.clone(), loop_def.to_spec())
2101 .await
2102 .unwrap();
2103
2104 let probe_def = ma.get_agent_definition(PROBE_DEF).unwrap();
2105 let fwd_id = ma
2106 .add_agent(preset_id.clone(), probe_def.to_spec())
2107 .await
2108 .unwrap();
2109 let lim_id = ma
2110 .add_agent(preset_id.clone(), probe_def.to_spec())
2111 .await
2112 .unwrap();
2113
2114 for (source_handle, target) in [
2115 (PORT_MESSAGE, fwd_id.clone()),
2116 (PORT_LIMIT_EXCEEDED, lim_id.clone()),
2117 ] {
2118 ma.add_connection(
2119 &preset_id,
2120 ConnectionSpec {
2121 source: loop_id.clone(),
2122 source_handle: source_handle.to_string(),
2123 target,
2124 target_handle: PROBE_PORT.to_string(),
2125 },
2126 )
2127 .await
2128 .unwrap();
2129 }
2130
2131 let loop_agent = ma.get_agent(&loop_id).unwrap();
2132 loop_agent
2133 .lock()
2134 .await
2135 .set_config(
2136 CONFIG_MAX_ITERATIONS.into(),
2137 AgentValue::integer(max_iterations),
2138 )
2139 .unwrap();
2140
2141 ma.start_preset(&preset_id).await.unwrap();
2142
2143 let forwarded = probe_receiver(&ma, &fwd_id).await.unwrap();
2144 let limit = probe_receiver(&ma, &lim_id).await.unwrap();
2145
2146 Fixture {
2147 ma,
2148 loop_agent,
2149 forwarded,
2150 limit,
2151 }
2152 }
2153
2154 fn assistant_tool_call_msg(id: Option<&str>, streaming: bool) -> AgentValue {
2155 let mut msg = Message::assistant("use tools".to_string());
2156 msg.id = id.map(str::to_string);
2157 msg.streaming = streaming;
2158 msg.tool_calls = Some(vector![ToolCall {
2159 function: ToolCallFunction {
2160 name: "my_tool".to_string(),
2161 parameters: serde_json::json!({}),
2162 id: Some("call1".to_string()),
2163 parse_error: None,
2164 },
2165 }]);
2166 AgentValue::message(msg)
2167 }
2168
2169 async fn send(fixture: &Fixture, ctx: &AgentContext, value: AgentValue) {
2170 fixture
2171 .loop_agent
2172 .lock()
2173 .await
2174 .process(ctx.clone(), PORT_MESSAGE.to_string(), value)
2175 .await
2176 .unwrap();
2177 }
2178
2179 async fn recv(rx: &ProbeReceiver) -> AgentValue {
2180 let (_ctx, value) = rx.recv().await.unwrap();
2181 value
2182 }
2183
2184 async fn expect_no_event(rx: &ProbeReceiver) {
2185 assert!(
2186 rx.recv_with_timeout(Duration::from_millis(200))
2187 .await
2188 .is_err()
2189 );
2190 }
2191
2192 async fn count_for(fixture: &Fixture, ctx_key: &str) -> Option<(u32, Option<String>)> {
2193 let guard = fixture.loop_agent.lock().await;
2194 let agent = guard.as_agent::<LoopControlAgent>().unwrap();
2195 agent.counts.get(ctx_key).cloned()
2196 }
2197
2198 #[tokio::test]
2199 async fn streaming_partials_are_not_double_counted() {
2200 let fixture = setup(25).await;
2201 let ctx = AgentContext::new();
2202 let ctx_key = ctx.ctx_key().unwrap();
2203
2204 send(&fixture, &ctx, assistant_tool_call_msg(Some("m1"), true)).await;
2207 send(&fixture, &ctx, assistant_tool_call_msg(Some("m1"), true)).await;
2208 send(&fixture, &ctx, assistant_tool_call_msg(Some("m1"), false)).await;
2209
2210 for _ in 0..3 {
2212 let value = recv(&fixture.forwarded).await;
2213 assert_eq!(value.as_message().unwrap().id.as_deref(), Some("m1"));
2214 }
2215 assert_eq!(
2217 count_for(&fixture, &ctx_key).await,
2218 Some((1, Some("m1".to_string())))
2219 );
2220 expect_no_event(&fixture.limit).await;
2221
2222 fixture.ma.quit();
2223 }
2224
2225 #[tokio::test]
2226 async fn same_id_final_redelivered_counts_once() {
2227 let fixture = setup(25).await;
2228 let ctx = AgentContext::new();
2229 let ctx_key = ctx.ctx_key().unwrap();
2230
2231 send(&fixture, &ctx, assistant_tool_call_msg(Some("m1"), false)).await;
2232 send(&fixture, &ctx, assistant_tool_call_msg(Some("m1"), false)).await;
2233
2234 for _ in 0..2 {
2236 let value = recv(&fixture.forwarded).await;
2237 assert_eq!(value.as_message().unwrap().id.as_deref(), Some("m1"));
2238 }
2239 assert_eq!(
2240 count_for(&fixture, &ctx_key).await,
2241 Some((1, Some("m1".to_string())))
2242 );
2243 expect_no_event(&fixture.limit).await;
2244
2245 fixture.ma.quit();
2246 }
2247
2248 #[tokio::test]
2249 async fn blocks_and_emits_limit_exceeded_after_max_iterations() {
2250 let fixture = setup(2).await;
2251 let ctx = AgentContext::new();
2252
2253 send(&fixture, &ctx, assistant_tool_call_msg(Some("m1"), false)).await;
2254 send(&fixture, &ctx, assistant_tool_call_msg(Some("m2"), false)).await;
2255 for expected in ["m1", "m2"] {
2256 let value = recv(&fixture.forwarded).await;
2257 assert_eq!(value.as_message().unwrap().id.as_deref(), Some(expected));
2258 }
2259
2260 send(&fixture, &ctx, assistant_tool_call_msg(Some("m3"), false)).await;
2263 let notice = recv(&fixture.limit).await;
2264 let notice = notice.as_message().unwrap();
2265 assert_eq!(notice.role, "assistant");
2266 assert!(!notice.streaming);
2267 assert!(notice.tool_calls.is_none());
2268 assert!(notice.text().contains("max_iterations of 2"));
2269 expect_no_event(&fixture.forwarded).await;
2270
2271 send(&fixture, &ctx, assistant_tool_call_msg(Some("m3"), false)).await;
2274 expect_no_event(&fixture.forwarded).await;
2275 expect_no_event(&fixture.limit).await;
2276
2277 fixture.ma.quit();
2278 }
2279
2280 #[tokio::test]
2281 async fn non_countable_values_pass_through_even_over_limit() {
2282 let fixture = setup(1).await;
2283 let ctx = AgentContext::new();
2284
2285 send(&fixture, &ctx, assistant_tool_call_msg(Some("m1"), false)).await;
2286 let _ = recv(&fixture.forwarded).await;
2287 send(&fixture, &ctx, assistant_tool_call_msg(Some("m2"), false)).await;
2289 let _ = recv(&fixture.limit).await;
2290
2291 let passthrough = [
2293 AgentValue::message(Message::user("hi".to_string())),
2294 AgentValue::message(Message::tool("my_tool".to_string(), "ok".to_string())),
2295 AgentValue::message(Message::assistant("no tools".to_string())),
2296 assistant_tool_call_msg(Some("m4"), true),
2297 AgentValue::string("not a message"),
2298 ];
2299 for value in passthrough {
2300 send(&fixture, &ctx, value.clone()).await;
2301 let received = recv(&fixture.forwarded).await;
2302 assert_eq!(received, value);
2303 }
2304 expect_no_event(&fixture.limit).await;
2305
2306 fixture.ma.quit();
2307 }
2308
2309 #[tokio::test]
2310 async fn separate_ctx_keys_count_independently() {
2311 let fixture = setup(1).await;
2312 let ctx_a = AgentContext::new();
2313 let ctx_b = AgentContext::new();
2314
2315 send(&fixture, &ctx_a, assistant_tool_call_msg(Some("a1"), false)).await;
2316 let value = recv(&fixture.forwarded).await;
2317 assert_eq!(value.as_message().unwrap().id.as_deref(), Some("a1"));
2318
2319 send(&fixture, &ctx_a, assistant_tool_call_msg(Some("a2"), false)).await;
2321 let _ = recv(&fixture.limit).await;
2322 expect_no_event(&fixture.forwarded).await;
2323
2324 send(&fixture, &ctx_b, assistant_tool_call_msg(Some("b1"), false)).await;
2326 let value = recv(&fixture.forwarded).await;
2327 assert_eq!(value.as_message().unwrap().id.as_deref(), Some("b1"));
2328 expect_no_event(&fixture.limit).await;
2329
2330 fixture.ma.quit();
2331 }
2332
2333 #[tokio::test]
2334 async fn stop_clears_counters() {
2335 let fixture = setup(1).await;
2336 let ctx = AgentContext::new();
2337
2338 send(&fixture, &ctx, assistant_tool_call_msg(Some("m1"), false)).await;
2339 let _ = recv(&fixture.forwarded).await;
2340 send(&fixture, &ctx, assistant_tool_call_msg(Some("m2"), false)).await;
2341 let _ = recv(&fixture.limit).await;
2342
2343 {
2344 let mut guard = fixture.loop_agent.lock().await;
2345 guard.stop().await.unwrap();
2346 guard.start().await.unwrap();
2347 }
2348
2349 send(&fixture, &ctx, assistant_tool_call_msg(Some("m3"), false)).await;
2351 let value = recv(&fixture.forwarded).await;
2352 assert_eq!(value.as_message().unwrap().id.as_deref(), Some("m3"));
2353 expect_no_event(&fixture.limit).await;
2354
2355 fixture.ma.quit();
2356 }
2357 }
2358
2359 mod cancellation {
2360 use super::*;
2361 use std::time::Duration;
2362
2363 struct SlowTool {
2365 info: ToolInfo,
2366 }
2367
2368 #[async_trait]
2369 impl Tool for SlowTool {
2370 fn info(&self) -> &ToolInfo {
2371 &self.info
2372 }
2373
2374 async fn call(
2375 &self,
2376 _ctx: AgentContext,
2377 _args: AgentValue,
2378 ) -> Result<AgentValue, AgentError> {
2379 tokio::time::sleep(Duration::from_secs(30)).await;
2380 Ok(AgentValue::string("done"))
2381 }
2382 }
2383
2384 struct FastTool {
2386 info: ToolInfo,
2387 }
2388
2389 #[async_trait]
2390 impl Tool for FastTool {
2391 fn info(&self) -> &ToolInfo {
2392 &self.info
2393 }
2394
2395 async fn call(
2396 &self,
2397 _ctx: AgentContext,
2398 _args: AgentValue,
2399 ) -> Result<AgentValue, AgentError> {
2400 Ok(AgentValue::string("fast done"))
2401 }
2402 }
2403
2404 fn slow_call(name: &str, id: &str) -> ToolCall {
2405 ToolCall {
2406 function: ToolCallFunction {
2407 name: name.to_string(),
2408 parameters: serde_json::json!({}),
2409 id: Some(id.to_string()),
2410 parse_error: None,
2411 },
2412 }
2413 }
2414
2415 #[tokio::test]
2416 async fn cancelled_call_tools_synthesizes_aborted_results() {
2417 let tool_name = "cancel_test_slow_tool";
2420 register_tool(SlowTool {
2421 info: ToolInfo::new(tool_name, "sleeps forever for cancellation tests", None),
2422 });
2423
2424 let token = CancellationToken::new();
2425 let ctx = AgentContext::new().with_cancel_token(token.clone());
2426 let calls: Vector<ToolCall> =
2427 vector![slow_call(tool_name, "c1"), slow_call(tool_name, "c2")];
2428
2429 tokio::spawn(async move {
2430 tokio::time::sleep(Duration::from_millis(50)).await;
2431 token.cancel();
2432 });
2433
2434 let msgs = tokio::time::timeout(Duration::from_secs(5), call_tools(&ctx, &calls, 8))
2435 .await
2436 .expect("cancelled call_tools must return promptly")
2437 .unwrap();
2438
2439 assert_eq!(msgs.len(), 2);
2441 for (msg, id) in msgs.iter().zip(["c1", "c2"]) {
2442 assert_eq!(msg.role, "tool");
2443 assert_eq!(msg.id.as_deref(), Some(id));
2444 assert_eq!(msg.is_error, Some(true));
2445 assert_eq!(msg.text(), ABORTED_TOOL_RESULT);
2446 }
2447
2448 unregister_tool(tool_name);
2449 }
2450
2451 #[tokio::test]
2452 async fn cancellation_keeps_results_of_completed_parallel_calls() {
2453 let slow_name = "cancel_test_slow_parallel_tool";
2456 let fast_name = "cancel_test_fast_parallel_tool";
2457 register_tool(SlowTool {
2458 info: ToolInfo::new(slow_name, "sleeps forever for cancellation tests", None)
2459 .with_execution_mode(ExecutionMode::Parallel),
2460 });
2461 register_tool(FastTool {
2462 info: ToolInfo::new(
2463 fast_name,
2464 "completes immediately for cancellation tests",
2465 None,
2466 )
2467 .with_execution_mode(ExecutionMode::Parallel),
2468 });
2469
2470 let token = CancellationToken::new();
2471 let ctx = AgentContext::new().with_cancel_token(token.clone());
2472 let calls: Vector<ToolCall> =
2475 vector![slow_call(slow_name, "c1"), slow_call(fast_name, "c2")];
2476
2477 tokio::spawn(async move {
2478 tokio::time::sleep(Duration::from_millis(50)).await;
2479 token.cancel();
2480 });
2481
2482 let msgs = tokio::time::timeout(Duration::from_secs(5), call_tools(&ctx, &calls, 8))
2483 .await
2484 .expect("cancelled call_tools must return promptly")
2485 .unwrap();
2486
2487 assert_eq!(msgs.len(), 2);
2488 assert_eq!(msgs[0].id.as_deref(), Some("c1"));
2489 assert_eq!(msgs[0].is_error, Some(true));
2490 assert_eq!(msgs[0].text(), ABORTED_TOOL_RESULT);
2491 assert_eq!(msgs[1].id.as_deref(), Some("c2"));
2494 assert_eq!(msgs[1].is_error, None);
2495 assert!(msgs[1].text().contains("fast done"));
2496
2497 unregister_tool(slow_name);
2498 unregister_tool(fast_name);
2499 }
2500 }
2501}