Skip to main content

zai_rs/toolkits/
executor.rs

1//! Tool registry and executor with caching, retries, and bounded concurrency.
2
3use std::{
4    panic::AssertUnwindSafe,
5    sync::Arc,
6    sync::atomic::{AtomicU64, Ordering},
7    time::{Duration, Instant},
8};
9
10use dashmap::{DashMap, mapref::entry::Entry};
11use futures_util::{FutureExt, StreamExt};
12use serde::Serialize;
13use tokio::time::timeout;
14use tracing::warn;
15
16use super::{
17    cache::{CacheKey, ToolCallCache},
18    core::{ToolHandler, validate_tool_name},
19};
20use crate::{
21    model::{
22        chat_base_response::ToolCallMessage,
23        chat_message_types::TextMessage,
24        tools::{Function, Tools},
25    },
26    toolkits::{
27        core::DynTool,
28        error::{ToolError, ToolResult, error_context},
29    },
30};
31
32/// Cap on how many tool calls run concurrently in
33/// [`execute_tool_calls_parallel`](ToolExecutor::execute_tool_calls_parallel)
34/// / [`execute_tool_calls_ordered`](ToolExecutor::execute_tool_calls_ordered).
35///
36/// Prevents a model that emits many tool calls in one turn from fanning them
37/// all out at once and overwhelming downstream services.
38const MAX_CONCURRENT_TOOL_CALLS: usize = 8;
39
40fn tool_message(payload: serde_json::Value, id: Option<&str>) -> TextMessage {
41    let content = payload.to_string();
42    match id {
43        Some(id) => TextMessage::tool_with_id(content, id),
44        None => TextMessage::tool(content),
45    }
46}
47
48fn tool_error_message(error_type: &str, message: &str, id: Option<&str>) -> TextMessage {
49    tool_message(
50        serde_json::json!({"error": {"type": error_type, "message": message}}),
51        id,
52    )
53}
54
55fn task_panic_tool_message(id: Option<&str>) -> TextMessage {
56    tool_error_message("task_panic", "a tool execution future panicked", id)
57}
58
59fn sort_exported_tools(tools: &mut [Tools]) {
60    fn function_name(tool: &Tools) -> Option<&str> {
61        match tool {
62            Tools::Function { function } => Some(&function.name),
63            _ => None,
64        }
65    }
66    tools.sort_unstable_by(|left, right| function_name(left).cmp(&function_name(right)));
67}
68
69fn export_enabled_tool(tool: &dyn DynTool) -> Option<Tools> {
70    let metadata = tool.metadata();
71    metadata.is_enabled().then(|| Tools::Function {
72        function: Function::new(metadata.name(), metadata.description(), tool.input_schema()),
73    })
74}
75
76/// Retry configuration with exponential backoff.
77#[derive(Debug, Clone)]
78pub struct RetryConfig {
79    /// Maximum number of retry attempts after the first try.
80    pub max_retries: u32,
81    /// Delay before the first retry.
82    pub initial_delay: Duration,
83    /// Upper bound on the per-attempt delay.
84    pub max_delay: Duration,
85    /// Multiplier applied to the delay between successive retries.
86    pub backoff_multiplier: f64,
87}
88
89impl Default for RetryConfig {
90    fn default() -> Self {
91        Self {
92            // Arbitrary tools may have side effects; retries require an
93            // explicit idempotency decision by the caller.
94            max_retries: 0,
95            initial_delay: Duration::from_millis(100),
96            max_delay: Duration::from_secs(30),
97            backoff_multiplier: 2.0,
98        }
99    }
100}
101
102impl RetryConfig {
103    /// Compute the delay to apply before the given (1-based) attempt.
104    pub fn calculate_delay(&self, attempt: u32) -> Duration {
105        if attempt == 0 {
106            return Duration::ZERO;
107        }
108
109        // Public fields allow callers to construct a malformed configuration.
110        // Falling back to a neutral multiplier keeps delay calculation total
111        // without turning a bad value into an unexpectedly long sleep.
112        let initial_ms = self.initial_delay.as_millis() as f64;
113        let max_ms = self.max_delay.as_millis() as f64;
114        let multiplier = if self.backoff_multiplier.is_finite() && self.backoff_multiplier >= 1.0 {
115            self.backoff_multiplier
116        } else {
117            1.0
118        };
119        let raw = initial_ms * multiplier.powf(f64::from(attempt - 1));
120        let capped = if raw.is_finite() { raw } else { max_ms };
121        let delay_ms = capped.clamp(0.0, max_ms) as u64;
122
123        Duration::from_millis(delay_ms)
124    }
125}
126
127/// Tool execution configuration.
128#[derive(Debug, Clone)]
129pub struct ExecutionConfig {
130    /// Per-call execution timeout (`None` = no timeout).
131    ///
132    /// When the deadline elapses the tool's execution future is dropped
133    /// (cancelled) and the call resolves to a [`ToolError::TimeoutError`]. The
134    /// timeout cancels only the *local* future — for a tool backed by a remote
135    /// service the request may already be on the wire (see
136    /// [`ToolError::TimeoutError`] for the full caveat).
137    pub timeout: Option<Duration>,
138    /// Retry/backoff configuration.
139    pub retry_config: RetryConfig,
140}
141
142impl Default for ExecutionConfig {
143    fn default() -> Self {
144        Self {
145            timeout: Some(Duration::from_secs(30)),
146            retry_config: RetryConfig::default(),
147        }
148    }
149}
150
151/// Detailed outcome of a tool execution.
152#[derive(Debug, Clone, Serialize)]
153pub struct ExecutionResult {
154    /// Name of the tool that was executed.
155    pub tool_name: String,
156    /// The tool's return value (JSON).
157    pub result: serde_json::Value,
158    /// Wall-clock duration of the (possibly retried) execution.
159    pub duration: Duration,
160    /// Whether the execution succeeded.
161    pub success: bool,
162    /// Error message, present only on failure.
163    pub error: Option<String>,
164    /// Number of retries performed.
165    pub retries: u32,
166    /// When the execution completed.
167    pub timestamp: std::time::SystemTime,
168    /// Whether the value came from the executor cache.
169    pub cache_hit: bool,
170}
171
172impl ExecutionResult {
173    fn success(
174        tool_name: String,
175        result: serde_json::Value,
176        duration: Duration,
177        retries: u32,
178    ) -> Self {
179        Self {
180            tool_name,
181            result,
182            duration,
183            success: true,
184            error: None,
185            retries,
186            timestamp: std::time::SystemTime::now(),
187            cache_hit: false,
188        }
189    }
190
191    fn failure(tool_name: String, error: String, duration: Duration, retries: u32) -> Self {
192        Self {
193            tool_name,
194            result: serde_json::Value::Null,
195            duration,
196            success: false,
197            error: Some(error),
198            retries,
199            timestamp: std::time::SystemTime::now(),
200            cache_hit: false,
201        }
202    }
203
204    fn with_cache_hit(mut self) -> Self {
205        self.cache_hit = true;
206        self
207    }
208}
209
210/// Tool registry and executor with optional result caching.
211#[derive(Clone)]
212pub struct ToolExecutor {
213    tools: Arc<DashMap<String, RegisteredTool>>,
214    next_generation: Arc<AtomicU64>,
215    config: ExecutionConfig,
216    cache: ToolCallCache,
217}
218
219struct RegisteredTool {
220    tool: Arc<dyn DynTool>,
221    generation: u64,
222}
223
224impl std::fmt::Debug for ToolExecutor {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        let tool_count = self.tools.len();
227        let cache_enabled = self.cache.enabled();
228        f.debug_struct("ToolExecutor")
229            .field("tool_count", &tool_count)
230            .field("config", &self.config)
231            .field("cache_enabled", &cache_enabled)
232            .finish()
233    }
234}
235
236impl Default for ToolExecutor {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242impl ToolExecutor {
243    /// Create a new executor with default config
244    pub fn new() -> Self {
245        Self {
246            tools: Arc::new(DashMap::new()),
247            next_generation: Arc::new(AtomicU64::new(0)),
248            config: ExecutionConfig::default(),
249            // Tool purity is unknown at registration time, so caching is an
250            // explicit opt-in.
251            cache: ToolCallCache::new().with_enabled(false),
252        }
253    }
254
255    /// Create an executor builder for fluent API
256    pub fn builder() -> ExecutorBuilder {
257        ExecutorBuilder::new()
258    }
259
260    /// Clear the cache
261    pub fn clear_cache(&self) {
262        self.cache.clear();
263    }
264
265    /// Invalidate cache for a specific tool
266    pub fn invalidate_cache_for_tool(&self, tool_name: &str) {
267        self.cache.invalidate_tool(tool_name);
268    }
269
270    /// Get cache statistics
271    pub fn cache_stats(&self) -> super::CacheStats {
272        self.cache.stats()
273    }
274
275    /// Chain-friendly: add a dynamic tool, returns error if already registered
276    pub fn add_dyn_tool(&self, tool: Box<dyn DynTool>) -> ToolResult<&Self> {
277        let name = tool.name().to_string();
278        validate_tool_name(&name)?;
279        match self.tools.entry(name.clone()) {
280            Entry::Occupied(_) => Err(ToolError::RegistrationError {
281                message: format!("Tool '{name}' is already registered").into(),
282            }),
283            Entry::Vacant(entry) => {
284                let generation = self.next_generation.fetch_add(1, Ordering::Relaxed);
285                entry.insert(RegisteredTool {
286                    tool: Arc::from(tool),
287                    generation,
288                });
289                // Remove entries from a prior registration with this name.
290                self.cache.invalidate_tool(&name);
291                Ok(self)
292            },
293        }
294    }
295
296    /// Unregister a tool
297    pub fn unregister(&self, name: &str) -> ToolResult<()> {
298        if self.tools.remove(name).is_none() {
299            return Err(error_context().with_tool(name).tool_not_found());
300        }
301        self.cache.invalidate_tool(name);
302        Ok(())
303    }
304
305    /// Get input schema for a tool
306    pub fn input_schema(&self, name: &str) -> Option<serde_json::Value> {
307        self.tools.get(name).map(|entry| entry.tool.input_schema())
308    }
309
310    /// Check if tool exists
311    pub fn has_tool(&self, name: &str) -> bool {
312        self.tools.contains_key(name)
313    }
314
315    /// List tool names in deterministic lexicographic order.
316    pub fn tool_names(&self) -> Vec<String> {
317        let mut names: Vec<_> = self.tools.iter().map(|entry| entry.key().clone()).collect();
318        names.sort_unstable();
319        names
320    }
321
322    fn get_tool(&self, name: &str) -> Option<(Arc<dyn DynTool>, u64)> {
323        self.tools
324            .get(name)
325            .map(|entry| (Arc::clone(&entry.tool), entry.generation))
326    }
327
328    fn cache_if_still_registered(
329        &self,
330        tool_name: &str,
331        generation: u64,
332        key: CacheKey,
333        result: serde_json::Value,
334    ) {
335        // Keep the map guard until after insertion. An unregister operation
336        // either happens first (and this write is skipped) or happens after
337        // the write and invalidates it, so an old in-flight call cannot leave
338        // an unreachable stale entry behind.
339        if let Some(registered) = self.tools.get(tool_name)
340            && registered.generation == generation
341        {
342            self.cache.insert(key, result, None);
343        }
344    }
345
346    /// Execute a tool with caching, timeout, and exponential backoff.
347    ///
348    /// Tool-level failures are returned as `Ok(ExecutionResult)` with its
349    /// `success` field set to `false`. Use [`ToolExecutor::execute_simple`]
350    /// when failures should be returned as [`ToolError`] values instead.
351    #[tracing::instrument(skip(self, input))]
352    pub async fn execute(
353        &self,
354        tool_name: &str,
355        input: serde_json::Value,
356    ) -> ToolResult<ExecutionResult> {
357        let start_time = Instant::now();
358        let mut retries = 0;
359        let retry_config = &self.config.retry_config;
360
361        let Some((tool, generation)) = self.get_tool(tool_name) else {
362            let error = error_context().with_tool(tool_name).tool_not_found();
363            return Ok(ExecutionResult::failure(
364                tool_name.to_string(),
365                error.to_string(),
366                start_time.elapsed(),
367                0,
368            ));
369        };
370        if !tool.metadata().is_enabled() {
371            let error = error_context()
372                .with_tool(tool_name)
373                .execution_failed(format!("tool '{tool_name}' is disabled"));
374            return Ok(ExecutionResult::failure(
375                tool_name.to_string(),
376                error.to_string(),
377                start_time.elapsed(),
378                0,
379            ));
380        }
381
382        // Registration generation prevents an in-flight call from repopulating
383        // a cache entry that a later tool with the same name could consume.
384        let cache_key = if self.cache.enabled() {
385            Some(CacheKey::for_generation(
386                tool_name.to_string(),
387                input.clone(),
388                generation,
389            ))
390        } else {
391            None
392        };
393        if let Some(ref key) = cache_key
394            && let Some(cached_result) = self.cache.get(key)
395        {
396            let duration = start_time.elapsed();
397            return Ok(ExecutionResult::success(
398                tool_name.to_string(),
399                cached_result,
400                duration,
401                retries,
402            )
403            .with_cache_hit());
404        }
405
406        loop {
407            match self.execute_once(&tool, tool_name, &input).await {
408                Ok(result) => {
409                    let duration = start_time.elapsed();
410                    if let Some(key) = cache_key {
411                        self.cache_if_still_registered(tool_name, generation, key, result.clone());
412                    }
413
414                    return Ok(ExecutionResult::success(
415                        tool_name.to_string(),
416                        result,
417                        duration,
418                        retries,
419                    ));
420                },
421                Err(error) => {
422                    // Only retry on retryable errors (timeout, transient failures)
423                    if !error.is_retryable() {
424                        let duration = start_time.elapsed();
425                        return Ok(ExecutionResult::failure(
426                            tool_name.to_string(),
427                            error.to_string(),
428                            duration,
429                            retries,
430                        ));
431                    }
432
433                    if retries >= retry_config.max_retries {
434                        let duration = start_time.elapsed();
435                        return Ok(ExecutionResult::failure(
436                            tool_name.to_string(),
437                            error.to_string(),
438                            duration,
439                            retries,
440                        ));
441                    }
442
443                    retries += 1;
444
445                    warn!(attempt = retries, "Tool execution failed, retrying");
446
447                    // Use exponential backoff
448                    let delay = retry_config.calculate_delay(retries);
449                    tokio::time::sleep(delay).await;
450                },
451            }
452        }
453    }
454
455    /// Execute a tool and return its JSON value, converting an unsuccessful
456    /// execution into a [`ToolError`].
457    pub async fn execute_simple(
458        &self,
459        tool_name: &str,
460        input: serde_json::Value,
461    ) -> ToolResult<serde_json::Value> {
462        let result = self.execute(tool_name, input).await?;
463        if result.success {
464            Ok(result.result)
465        } else {
466            Err(error_context()
467                .with_tool(tool_name)
468                .execution_failed(result.error.unwrap_or_else(|| "Unknown error".to_string())))
469        }
470    }
471
472    /// Register function specifications loaded from `.json` files in `dir`.
473    ///
474    /// Each file may contain a direct function object or an OpenAI-style
475    /// `{ "type": "function", "function": ... }` wrapper. `handlers` maps
476    /// function names to implementations. When `strict` is `false`, files
477    /// without a matching handler are skipped; otherwise they cause an error.
478    ///
479    /// Returns the list of function names successfully registered.
480    pub fn add_functions_from_dir_with_registry(
481        &self,
482        dir: impl AsRef<std::path::Path>,
483        handlers: &std::collections::HashMap<String, ToolHandler>,
484        strict: bool,
485    ) -> ToolResult<Vec<String>> {
486        use std::fs;
487
488        use serde_json::Value;
489        let dir = dir.as_ref();
490        let mut added = Vec::new();
491        let read_dir = fs::read_dir(dir).map_err(|e| {
492            error_context().invalid_parameters(format!(
493                "Failed to read dir {}: {}",
494                dir.display(),
495                e
496            ))
497        })?;
498        let mut paths = read_dir
499            .map(|entry| {
500                entry.map(|entry| entry.path()).map_err(|error| {
501                    error_context().invalid_parameters(format!("Dir entry error: {error}"))
502                })
503            })
504            .collect::<ToolResult<Vec<_>>>()?;
505        paths.retain(|path| {
506            path.is_file()
507                && path.extension().and_then(|extension| extension.to_str()) == Some("json")
508        });
509        paths.sort_unstable();
510
511        for path in paths {
512            let content = fs::read_to_string(&path).map_err(|e| {
513                error_context().invalid_parameters(format!(
514                    "Failed to read {}: {}",
515                    path.display(),
516                    e
517                ))
518            })?;
519            let spec: Value = serde_json::from_str(&content).map_err(|e| {
520                error_context().invalid_parameters(format!(
521                    "Invalid JSON in {}: {}",
522                    path.display(),
523                    e
524                ))
525            })?;
526
527            let (name, description, parameters) =
528                crate::toolkits::core::parse_function_spec_details(&spec).map_err(|e| {
529                    error_context().invalid_parameters(format!(
530                        "Failed to parse spec {}: {}",
531                        path.display(),
532                        e
533                    ))
534                })?;
535
536            let handler = match handlers.get(&name) {
537                Some(handler) => Arc::clone(handler),
538                None => {
539                    if strict {
540                        return Err(error_context().invalid_parameters(format!(
541                            "No handler registered for function '{}' (file {})",
542                            name,
543                            path.display()
544                        )));
545                    }
546                    continue;
547                },
548            };
549
550            let mut builder =
551                crate::toolkits::core::FunctionTool::builder(name.clone(), description);
552            if let Some(p) = parameters {
553                builder = builder.schema(p);
554            }
555            let tool = builder
556                .handler(move |args| {
557                    let handler = Arc::clone(&handler);
558                    handler(args)
559                })
560                .build()?;
561
562            self.add_dyn_tool(Box::new(tool))?;
563            added.push(name);
564        }
565        Ok(added)
566    }
567
568    async fn execute_single_tool_call(&self, tc: &ToolCallMessage) -> TextMessage {
569        let id = tc.id();
570        let Some(function) = tc.function() else {
571            return tool_error_message("missing_function", "tool_call.function is missing", id);
572        };
573        let name = function.name();
574        if name.trim().is_empty() {
575            return tool_error_message(
576                "missing_function_name",
577                "tool_call.function.name is blank",
578                id,
579            );
580        }
581        let arguments = match serde_json::from_str(function.arguments()) {
582            Ok(serde_json::Value::Object(arguments)) => serde_json::Value::Object(arguments),
583            Ok(_) => {
584                return tool_error_message(
585                    "invalid_arguments",
586                    "tool arguments must decode to a JSON object",
587                    id,
588                );
589            },
590            Err(error) => {
591                return tool_error_message(
592                    "invalid_arguments",
593                    &format!("tool arguments are not valid JSON: {error}"),
594                    id,
595                );
596            },
597        };
598
599        match self.execute_simple(name, arguments).await {
600            Ok(result) => tool_message(result, id),
601            Err(error) => tool_error_message("execution_failed", &error.to_string(), id),
602        }
603    }
604
605    async fn execute_tool_calls_bounded(
606        &self,
607        calls: &[ToolCallMessage],
608    ) -> Vec<(usize, TextMessage)> {
609        futures_util::stream::iter(calls.iter().enumerate())
610            .map(|(index, call)| async move {
611                let message = match AssertUnwindSafe(self.execute_single_tool_call(call))
612                    .catch_unwind()
613                    .await
614                {
615                    Ok(message) => message,
616                    Err(_) => {
617                        warn!(index, "tool execution future panicked");
618                        task_panic_tool_message(call.id())
619                    },
620                };
621                (index, message)
622            })
623            .buffer_unordered(MAX_CONCURRENT_TOOL_CALLS)
624            .collect()
625            .await
626    }
627
628    /// Execute tool calls concurrently, returning one tool message per call.
629    ///
630    /// Results are returned in completion order; use
631    /// [`Self::execute_tool_calls_ordered`] to restore input order.
632    pub async fn execute_tool_calls_parallel(&self, calls: &[ToolCallMessage]) -> Vec<TextMessage> {
633        self.execute_tool_calls_bounded(calls)
634            .await
635            .into_iter()
636            .map(|(_, message)| message)
637            .collect()
638    }
639
640    /// Execute tool calls concurrently and restore input order in the result.
641    ///
642    /// At most eight handlers run concurrently. Invalid arguments, execution
643    /// errors, and handler panics are isolated to their call and encoded as a
644    /// tool-error message, so one failure does not discard other results.
645    pub async fn execute_tool_calls_ordered(&self, calls: &[ToolCallMessage]) -> Vec<TextMessage> {
646        let mut results = self.execute_tool_calls_bounded(calls).await;
647        results.sort_unstable_by_key(|(index, _)| *index);
648        results.into_iter().map(|(_, message)| message).collect()
649    }
650
651    /// Export one registered tool as an LLM function definition.
652    pub fn export_tool_as_function(&self, name: &str) -> Option<Tools> {
653        let registered = self.tools.get(name)?;
654        export_enabled_tool(registered.tool.as_ref())
655    }
656
657    /// Export all registered tools as function definitions sorted by name.
658    pub fn export_all_tools_as_functions(&self) -> Vec<Tools> {
659        let mut tools: Vec<_> = self
660            .tools
661            .iter()
662            .filter_map(|entry| export_enabled_tool(entry.value().tool.as_ref()))
663            .collect();
664        sort_exported_tools(&mut tools);
665        tools
666    }
667    /// Export enabled tools selected by a metadata predicate.
668    pub fn export_tools_filtered<F>(&self, mut filter: F) -> Vec<Tools>
669    where
670        F: FnMut(&crate::toolkits::core::ToolMetadata) -> bool,
671    {
672        let mut tools: Vec<_> = self
673            .tools
674            .iter()
675            .filter(|entry| filter(entry.value().tool.metadata()))
676            .filter_map(|entry| export_enabled_tool(entry.value().tool.as_ref()))
677            .collect();
678        sort_exported_tools(&mut tools);
679        tools
680    }
681
682    #[tracing::instrument(skip(self, tool, input))]
683    async fn execute_once(
684        &self,
685        tool: &Arc<dyn DynTool>,
686        tool_name: &str,
687        input: &serde_json::Value,
688    ) -> ToolResult<serde_json::Value> {
689        let execution_future = AssertUnwindSafe(tool.execute_json(input.clone())).catch_unwind();
690
691        let outcome = match self.config.timeout {
692            Some(timeout_duration) => match timeout(timeout_duration, execution_future).await {
693                Ok(result) => result,
694                Err(_) => {
695                    return Err(error_context()
696                        .with_tool(tool_name)
697                        .timeout_error(timeout_duration));
698                },
699            },
700            None => execution_future.await,
701        };
702
703        match outcome {
704            Ok(result) => result,
705            Err(_) => {
706                warn!("tool execution handler panicked");
707                Err(ToolError::ExecutionPanicked {
708                    tool: tool_name.to_string().into(),
709                })
710            },
711        }
712    }
713
714    /// Get the executor's [`ExecutionConfig`].
715    pub fn config(&self) -> &ExecutionConfig {
716        &self.config
717    }
718}
719
720/// Builder for creating tool executors with fluent API
721pub struct ExecutorBuilder {
722    config: ExecutionConfig,
723    cache_enabled: bool,
724    cache_ttl: Duration,
725    cache_max_size: usize,
726}
727
728impl Default for ExecutorBuilder {
729    fn default() -> Self {
730        Self::new()
731    }
732}
733
734impl ExecutorBuilder {
735    /// Create a new executor builder
736    pub fn new() -> Self {
737        Self {
738            config: ExecutionConfig::default(),
739            cache_enabled: false,
740            cache_ttl: Duration::from_secs(300),
741            cache_max_size: 1000,
742        }
743    }
744
745    /// Set timeout for tool execution
746    pub fn timeout(mut self, timeout: Duration) -> Self {
747        self.config.timeout = Some(timeout);
748        self
749    }
750
751    /// Set maximum number of retries
752    pub fn retries(mut self, retries: u32) -> Self {
753        self.config.retry_config.max_retries = retries;
754        self
755    }
756
757    /// Enable tool call result caching
758    pub fn enable_cache(mut self) -> Self {
759        self.cache_enabled = true;
760        self
761    }
762
763    /// Configure cache TTL without implicitly enabling caching.
764    pub fn cache_ttl(mut self, ttl: Duration) -> Self {
765        self.cache_ttl = ttl;
766        self
767    }
768
769    /// Configure cache capacity without implicitly enabling caching.
770    pub fn cache_max_size(mut self, size: usize) -> Self {
771        self.cache_max_size = size;
772        self
773    }
774
775    /// Build the final executor
776    pub fn build(self) -> ToolExecutor {
777        let cache = ToolCallCache::new()
778            .with_enabled(self.cache_enabled)
779            .with_ttl(self.cache_ttl)
780            .with_max_size(self.cache_max_size);
781
782        ToolExecutor {
783            tools: Arc::new(DashMap::new()),
784            next_generation: Arc::new(AtomicU64::new(0)),
785            config: self.config,
786            cache,
787        }
788    }
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794    use crate::toolkits::core::FunctionTool;
795
796    #[test]
797    fn test_retry_config_default() {
798        let config = RetryConfig::default();
799        assert_eq!(config.max_retries, 0);
800        assert_eq!(config.initial_delay, Duration::from_millis(100));
801        assert_eq!(config.max_delay, Duration::from_secs(30));
802        assert_eq!(config.backoff_multiplier, 2.0);
803    }
804
805    #[test]
806    fn test_retry_config_calculate_delay() {
807        let config = RetryConfig::default();
808
809        // First attempt should have zero delay
810        assert_eq!(config.calculate_delay(0), Duration::ZERO);
811
812        // Second attempt should have initial delay
813        assert_eq!(config.calculate_delay(1), Duration::from_millis(100));
814
815        // Third attempt should double (100 * 2)
816        assert_eq!(config.calculate_delay(2), Duration::from_millis(200));
817
818        // Fourth attempt should quadruple (100 * 2^2)
819        assert_eq!(config.calculate_delay(3), Duration::from_millis(400));
820
821        // Test with exponential growth that exceeds max_delay
822        let config = RetryConfig {
823            max_retries: 10,
824            initial_delay: Duration::from_millis(500),
825            max_delay: Duration::from_secs(1),
826            backoff_multiplier: 3.0,
827        };
828        // 500ms, then 1500ms (capped at 1000ms)
829        assert_eq!(config.calculate_delay(1), Duration::from_millis(500));
830        assert_eq!(config.calculate_delay(2), Duration::from_secs(1));
831        assert_eq!(config.calculate_delay(3), Duration::from_secs(1));
832    }
833
834    #[test]
835    fn test_execution_config_default() {
836        let config = ExecutionConfig::default();
837        assert_eq!(config.timeout, Some(Duration::from_secs(30)));
838        assert_eq!(config.retry_config.max_retries, 0);
839    }
840
841    #[test]
842    fn test_execution_result_success() {
843        let result = ExecutionResult::success(
844            "test_tool".to_string(),
845            serde_json::json!({"value": 42}),
846            Duration::from_millis(100),
847            2,
848        );
849
850        assert_eq!(result.tool_name, "test_tool");
851        assert_eq!(result.result, serde_json::json!({"value": 42}));
852        assert_eq!(result.duration, Duration::from_millis(100));
853        assert!(result.success);
854        assert!(result.error.is_none());
855        assert_eq!(result.retries, 2);
856        assert!(!result.cache_hit);
857    }
858
859    #[test]
860    fn test_execution_result_failure() {
861        let result = ExecutionResult::failure(
862            "test_tool".to_string(),
863            "Something went wrong".to_string(),
864            Duration::from_millis(50),
865            1,
866        );
867
868        assert_eq!(result.tool_name, "test_tool");
869        assert_eq!(result.result, serde_json::Value::Null);
870        assert_eq!(result.duration, Duration::from_millis(50));
871        assert!(!result.success);
872        assert_eq!(result.error, Some("Something went wrong".to_string()));
873        assert_eq!(result.retries, 1);
874        assert!(!result.cache_hit);
875    }
876
877    #[test]
878    fn test_execution_result_serialization() {
879        let result = ExecutionResult::success(
880            "test_tool".to_string(),
881            serde_json::json!({"value": 42}),
882            Duration::from_millis(100),
883            0,
884        );
885
886        let json = serde_json::to_string(&result).unwrap();
887        assert!(json.contains("\"tool_name\":\"test_tool\""));
888        assert!(json.contains("\"success\":true"));
889        assert!(json.contains("\"value\":42"));
890    }
891
892    #[test]
893    fn test_tool_executor_default() {
894        let executor = ToolExecutor::new();
895        assert_eq!(executor.tool_names().len(), 0);
896        assert_eq!(executor.config.timeout, Some(Duration::from_secs(30)));
897    }
898
899    #[test]
900    fn test_tool_executor_register_and_unregister() {
901        let executor = ToolExecutor::new();
902
903        // Create a simple test tool
904        let tool = FunctionTool::builder("test_tool", "A test tool")
905            .handler(|_args| async move { Ok(serde_json::json!({"result": "success"})) })
906            .build()
907            .unwrap();
908
909        // Register the tool
910        executor.add_dyn_tool(Box::new(tool)).unwrap();
911        assert_eq!(executor.tool_names().len(), 1);
912        assert!(executor.has_tool("test_tool"));
913
914        // Unregister the tool
915        assert!(executor.unregister("test_tool").is_ok());
916        assert_eq!(executor.tool_names().len(), 0);
917        assert!(!executor.has_tool("test_tool"));
918    }
919
920    #[test]
921    fn test_tool_executor_duplicate_tool_returns_error() {
922        let executor = ToolExecutor::new();
923
924        let tool1 = FunctionTool::builder("duplicate_tool", "First tool")
925            .handler(|_args| async move { Ok(serde_json::json!({})) })
926            .build()
927            .unwrap();
928
929        let tool2 = FunctionTool::builder("duplicate_tool", "Second tool")
930            .handler(|_args| async move { Ok(serde_json::json!({})) })
931            .build()
932            .unwrap();
933
934        executor.add_dyn_tool(Box::new(tool1)).unwrap();
935
936        // Adding duplicate tool should return error
937        let result = executor.add_dyn_tool(Box::new(tool2));
938        assert!(result.is_err());
939    }
940
941    #[test]
942    fn test_tool_executor_unregister_nonexistent_tool() {
943        let executor = ToolExecutor::new();
944        let result = executor.unregister("nonexistent_tool");
945        assert!(result.is_err());
946    }
947
948    #[test]
949    fn test_tool_executor_input_schema() {
950        let executor = ToolExecutor::new();
951
952        let schema = serde_json::json!({
953            "type": "object",
954            "properties": {
955                "name": {"type": "string"}
956            }
957        });
958
959        let tool = FunctionTool::builder("test_tool", "A test tool")
960            .schema(schema)
961            .handler(|_args| async move { Ok(serde_json::json!({})) })
962            .build()
963            .unwrap();
964
965        executor.add_dyn_tool(Box::new(tool)).unwrap();
966
967        let retrieved_schema = executor.input_schema("test_tool");
968        assert!(retrieved_schema.is_some());
969        let retrieved = retrieved_schema.unwrap();
970
971        // Check that schema contains expected properties
972        assert_eq!(retrieved["type"], "object");
973        assert_eq!(retrieved["properties"]["name"]["type"], "string");
974        // additionalProperties is automatically set by FunctionToolBuilder
975        assert_eq!(retrieved["additionalProperties"], false);
976    }
977
978    #[test]
979    fn test_tool_executor_input_schema_nonexistent() {
980        let executor = ToolExecutor::new();
981        let schema = executor.input_schema("nonexistent");
982        assert!(schema.is_none());
983    }
984
985    #[test]
986    fn test_tool_executor_tool_names() {
987        let executor = ToolExecutor::new();
988
989        let tool1 = FunctionTool::builder("tool1", "First tool")
990            .handler(|_args| async move { Ok(serde_json::json!({})) })
991            .build()
992            .unwrap();
993
994        let tool2 = FunctionTool::builder("tool2", "Second tool")
995            .handler(|_args| async move { Ok(serde_json::json!({})) })
996            .build()
997            .unwrap();
998
999        let tool3 = FunctionTool::builder("tool3", "Third tool")
1000            .handler(|_args| async move { Ok(serde_json::json!({})) })
1001            .build()
1002            .unwrap();
1003
1004        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1005        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1006        executor.add_dyn_tool(Box::new(tool3)).unwrap();
1007
1008        let names = executor.tool_names();
1009        assert_eq!(names.len(), 3);
1010        assert!(names.contains(&"tool1".to_string()));
1011        assert!(names.contains(&"tool2".to_string()));
1012        assert!(names.contains(&"tool3".to_string()));
1013    }
1014
1015    #[tokio::test]
1016    async fn test_tool_executor_execute_success() {
1017        let executor = ToolExecutor::new();
1018
1019        let tool = FunctionTool::builder("add_tool", "Add two numbers")
1020            .property("a", serde_json::json!({"type": "number"}))
1021            .property("b", serde_json::json!({"type": "number"}))
1022            .handler(|args| async move {
1023                let a = args
1024                    .get("a")
1025                    .and_then(serde_json::Value::as_i64)
1026                    .unwrap_or(0);
1027                let b = args
1028                    .get("b")
1029                    .and_then(serde_json::Value::as_i64)
1030                    .unwrap_or(0);
1031                Ok(serde_json::json!({"result": a + b}))
1032            })
1033            .build()
1034            .unwrap();
1035
1036        executor.add_dyn_tool(Box::new(tool)).unwrap();
1037
1038        let input = serde_json::json!({"a": 5, "b": 3});
1039        let result = executor.execute("add_tool", input).await.unwrap();
1040
1041        assert!(result.success);
1042        assert_eq!(result.tool_name, "add_tool");
1043        assert_eq!(result.result, serde_json::json!({"result": 8}));
1044        assert_eq!(result.retries, 0);
1045    }
1046
1047    #[tokio::test]
1048    async fn test_tool_executor_execute_failure() {
1049        let executor = ToolExecutor::new();
1050
1051        let tool = FunctionTool::builder("failing_tool", "Always fails")
1052            .handler(|_args| async move {
1053                Err(error_context()
1054                    .with_tool("failing_tool")
1055                    .execution_failed("Intentional failure"))
1056            })
1057            .build()
1058            .unwrap();
1059
1060        executor.add_dyn_tool(Box::new(tool)).unwrap();
1061
1062        let input = serde_json::json!({});
1063        let result = executor.execute("failing_tool", input).await.unwrap();
1064
1065        assert!(!result.success);
1066        assert_eq!(result.tool_name, "failing_tool");
1067        assert!(result.error.is_some());
1068    }
1069
1070    #[tokio::test]
1071    async fn test_tool_executor_execute_nonexistent_tool() {
1072        let executor = ToolExecutor::new();
1073        let input = serde_json::json!({});
1074        let result = executor.execute("nonexistent_tool", input).await.unwrap();
1075
1076        assert!(!result.success);
1077        assert!(result.error.is_some());
1078    }
1079
1080    #[tokio::test]
1081    async fn test_tool_executor_execute_simple_success() {
1082        let executor = ToolExecutor::new();
1083
1084        let tool = FunctionTool::builder("echo_tool", "Echo input")
1085            .property("message", serde_json::json!({"type": "string"}))
1086            .handler(|args| async move { Ok(args) })
1087            .build()
1088            .unwrap();
1089
1090        executor.add_dyn_tool(Box::new(tool)).unwrap();
1091
1092        let input = serde_json::json!({"message": "hello"});
1093        let result = executor.execute_simple("echo_tool", input).await.unwrap();
1094
1095        assert_eq!(result, serde_json::json!({"message": "hello"}));
1096    }
1097
1098    #[tokio::test]
1099    async fn test_tool_executor_execute_simple_failure() {
1100        let executor = ToolExecutor::new();
1101
1102        let tool = FunctionTool::builder("failing_tool", "Always fails")
1103            .handler(|_args| async move {
1104                Err(error_context()
1105                    .with_tool("failing_tool")
1106                    .execution_failed("Intentional failure"))
1107            })
1108            .build()
1109            .unwrap();
1110
1111        executor.add_dyn_tool(Box::new(tool)).unwrap();
1112
1113        let input = serde_json::json!({});
1114        let result = executor.execute_simple("failing_tool", input).await;
1115
1116        assert!(result.is_err());
1117    }
1118
1119    #[tokio::test]
1120    async fn test_tool_executor_timeout() {
1121        let executor = ToolExecutor::builder()
1122            .timeout(Duration::from_millis(100))
1123            .build();
1124
1125        let tool = FunctionTool::builder("slow_tool", "Slow tool")
1126            .handler(|_args| async move {
1127                tokio::time::sleep(Duration::from_secs(1)).await;
1128                Ok(serde_json::json!({"done": true}))
1129            })
1130            .build()
1131            .unwrap();
1132
1133        executor.add_dyn_tool(Box::new(tool)).unwrap();
1134
1135        let input = serde_json::json!({});
1136        let result = executor.execute("slow_tool", input).await.unwrap();
1137
1138        assert!(!result.success);
1139        assert!(result.error.is_some());
1140        assert!(result.error.unwrap().contains("Timeout"));
1141    }
1142
1143    #[tokio::test]
1144    async fn test_tool_executor_retry() {
1145        let executor = ToolExecutor::builder()
1146            .retries(2)
1147            .timeout(Duration::from_secs(30))
1148            .build();
1149
1150        let attempt_counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1151        let counter_clone = attempt_counter.clone();
1152
1153        let tool = FunctionTool::builder("flaky_tool", "Flaky tool")
1154            .handler(move |_args| {
1155                let counter = counter_clone.clone();
1156                async move {
1157                    let attempts = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1158                    if attempts < 2 {
1159                        Err(error_context()
1160                            .with_tool("flaky_tool")
1161                            .execution_failed("Temporary failure"))
1162                    } else {
1163                        Ok(serde_json::json!({"attempts": attempts + 1}))
1164                    }
1165                }
1166            })
1167            .build()
1168            .unwrap();
1169
1170        executor.add_dyn_tool(Box::new(tool)).unwrap();
1171
1172        let input = serde_json::json!({});
1173        let result = executor.execute("flaky_tool", input).await.unwrap();
1174
1175        assert!(result.success);
1176        assert_eq!(result.retries, 2);
1177    }
1178
1179    #[test]
1180    fn test_executor_builder_default() {
1181        let builder = ExecutorBuilder::new();
1182        assert_eq!(builder.config.timeout, Some(Duration::from_secs(30)));
1183        assert_eq!(builder.config.retry_config.max_retries, 0);
1184    }
1185
1186    #[test]
1187    fn test_executor_builder_timeout() {
1188        let builder = ExecutorBuilder::new().timeout(Duration::from_secs(60));
1189        assert_eq!(builder.config.timeout, Some(Duration::from_secs(60)));
1190    }
1191
1192    #[test]
1193    fn test_executor_builder_retries() {
1194        let builder = ExecutorBuilder::new().retries(5);
1195        assert_eq!(builder.config.retry_config.max_retries, 5);
1196    }
1197
1198    #[test]
1199    fn test_executor_builder_build() {
1200        let executor = ExecutorBuilder::new()
1201            .timeout(Duration::from_secs(60))
1202            .retries(5)
1203            .build();
1204
1205        assert_eq!(executor.config.timeout, Some(Duration::from_secs(60)));
1206        assert_eq!(executor.config.retry_config.max_retries, 5);
1207    }
1208
1209    #[test]
1210    fn test_executor_builder_chainable() {
1211        let builder = ExecutorBuilder::new()
1212            .timeout(Duration::from_secs(45))
1213            .retries(3)
1214            .timeout(Duration::from_secs(50))
1215            .retries(4);
1216
1217        assert_eq!(builder.config.timeout, Some(Duration::from_secs(50)));
1218        assert_eq!(builder.config.retry_config.max_retries, 4);
1219    }
1220
1221    #[test]
1222    fn test_export_tool_as_function() {
1223        let executor = ToolExecutor::new();
1224
1225        let tool = FunctionTool::builder("greet_tool", "Greet someone")
1226            .handler(|_args| async move { Ok(serde_json::json!({"greeting": "hello"})) })
1227            .build()
1228            .unwrap();
1229
1230        executor.add_dyn_tool(Box::new(tool)).unwrap();
1231
1232        let exported = executor.export_tool_as_function("greet_tool");
1233        assert!(exported.is_some());
1234
1235        if let Some(Tools::Function { function }) = exported {
1236            assert_eq!(function.name, "greet_tool");
1237            assert_eq!(function.description, "Greet someone");
1238            // Schema is auto-generated with default values
1239            assert!(function.parameters.is_some());
1240        } else {
1241            panic!("Expected Tools::Function");
1242        }
1243    }
1244
1245    #[test]
1246    fn test_export_tool_as_function_nonexistent() {
1247        let executor = ToolExecutor::new();
1248        let exported = executor.export_tool_as_function("nonexistent");
1249        assert!(exported.is_none());
1250    }
1251
1252    #[test]
1253    fn test_export_all_tools_as_functions() {
1254        let executor = ToolExecutor::new();
1255
1256        let tool1 = FunctionTool::builder("tool1", "First tool")
1257            .handler(|_args| async move { Ok(serde_json::json!({})) })
1258            .build()
1259            .unwrap();
1260
1261        let tool2 = FunctionTool::builder("tool2", "Second tool")
1262            .handler(|_args| async move { Ok(serde_json::json!({})) })
1263            .build()
1264            .unwrap();
1265
1266        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1267        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1268
1269        let exported = executor.export_all_tools_as_functions();
1270        assert_eq!(exported.len(), 2);
1271
1272        let names: Vec<_> = exported
1273            .iter()
1274            .filter_map(|t| match t {
1275                Tools::Function { function } => Some(function.name.clone()),
1276                _ => None,
1277            })
1278            .collect();
1279
1280        assert!(names.contains(&"tool1".to_string()));
1281        assert!(names.contains(&"tool2".to_string()));
1282    }
1283
1284    #[test]
1285    fn test_export_tools_filtered() {
1286        let executor = ToolExecutor::new();
1287
1288        let tool1 = FunctionTool::builder("math_tool", "Math operations")
1289            .metadata(|m| m.with_version("1.0.0"))
1290            .handler(|_args| async move { Ok(serde_json::json!({})) })
1291            .build()
1292            .unwrap();
1293
1294        let tool2 = FunctionTool::builder("text_tool", "Text operations")
1295            .metadata(|m| m.with_version("2.0.0"))
1296            .handler(|_args| async move { Ok(serde_json::json!({})) })
1297            .build()
1298            .unwrap();
1299
1300        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1301        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1302
1303        let exported = executor.export_tools_filtered(|meta| meta.version() == "1.0.0");
1304        assert_eq!(exported.len(), 1);
1305
1306        if let Some(Tools::Function { function }) = exported.first() {
1307            assert_eq!(function.name, "math_tool");
1308        } else {
1309            panic!("Expected Tools::Function");
1310        }
1311    }
1312
1313    #[test]
1314    fn test_execution_result_timestamp() {
1315        let before = std::time::SystemTime::now();
1316        let result = ExecutionResult::success(
1317            "test_tool".to_string(),
1318            serde_json::json!({"value": 42}),
1319            Duration::from_millis(100),
1320            0,
1321        );
1322        let after = std::time::SystemTime::now();
1323
1324        assert!(result.timestamp >= before && result.timestamp <= after);
1325    }
1326
1327    #[tokio::test]
1328    async fn test_tool_executor_no_retry_for_non_retryable_error() {
1329        let executor = ToolExecutor::builder()
1330            .retries(3)
1331            .timeout(Duration::from_secs(30))
1332            .build();
1333
1334        let attempt_counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1335        let counter_clone = attempt_counter.clone();
1336
1337        // ToolNotFound is not retryable, so it should fail immediately without retries
1338        let tool = FunctionTool::builder("not_found_tool", "Not found tool")
1339            .handler(move |_args| {
1340                let counter = counter_clone.clone();
1341                async move {
1342                    counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1343                    Err(error_context()
1344                        .with_tool("not_found_tool")
1345                        .invalid_parameters("Invalid parameters"))
1346                }
1347            })
1348            .build()
1349            .unwrap();
1350
1351        executor.add_dyn_tool(Box::new(tool)).unwrap();
1352
1353        let input = serde_json::json!({});
1354        let result = executor.execute("not_found_tool", input).await.unwrap();
1355
1356        assert!(!result.success);
1357        assert_eq!(result.retries, 0); // Should not have retried
1358        // Should have been called exactly once
1359        assert_eq!(attempt_counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1360    }
1361
1362    #[tokio::test]
1363    async fn test_execute_tool_calls_ordered_preserves_order() {
1364        use crate::model::chat_base_response::{ToolCallMessage, ToolFunction};
1365
1366        let executor = ToolExecutor::new();
1367
1368        // Register two tools that return different results
1369        let tool1 = FunctionTool::builder("tool_a", "First tool")
1370            .property("n", serde_json::json!({"type": "number"}))
1371            .handler(|args| async move {
1372                let n = args
1373                    .get("n")
1374                    .and_then(serde_json::Value::as_i64)
1375                    .unwrap_or(0);
1376                Ok(serde_json::json!({"tool": "a", "n": n}))
1377            })
1378            .build()
1379            .unwrap();
1380
1381        let tool2 = FunctionTool::builder("tool_b", "Second tool")
1382            .property("n", serde_json::json!({"type": "number"}))
1383            .handler(|args| async move {
1384                let n = args
1385                    .get("n")
1386                    .and_then(serde_json::Value::as_i64)
1387                    .unwrap_or(0);
1388                Ok(serde_json::json!({"tool": "b", "n": n}))
1389            })
1390            .build()
1391            .unwrap();
1392
1393        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1394        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1395
1396        let calls = vec![
1397            ToolCallMessage {
1398                id: Some("call_1".to_string()),
1399                type_: Some("function".to_string()),
1400                function: Some(ToolFunction {
1401                    name: "tool_a".to_string(),
1402                    arguments: r#"{"n": 1}"#.to_string(),
1403                }),
1404                mcp: None,
1405            },
1406            ToolCallMessage {
1407                id: Some("call_2".to_string()),
1408                type_: Some("function".to_string()),
1409                function: Some(ToolFunction {
1410                    name: "tool_b".to_string(),
1411                    arguments: r#"{"n": 2}"#.to_string(),
1412                }),
1413                mcp: None,
1414            },
1415        ];
1416
1417        let results = executor.execute_tool_calls_ordered(&calls).await;
1418        assert_eq!(results.len(), 2);
1419        // Verify ordering: first result should be from tool_a, second from tool_b
1420        let first = &results[0];
1421        let first_content = match first {
1422            TextMessage::Tool { content, .. } => content.clone(),
1423            _ => panic!("Expected Tool message"),
1424        };
1425        let parsed1: serde_json::Value = serde_json::from_str(&first_content).unwrap();
1426        // Check both tools since order in the content identifies which tool ran
1427        assert!(parsed1.get("tool").is_some());
1428        assert!(parsed1["n"].as_i64() == Some(1));
1429
1430        let second = &results[1];
1431        let second_content = match second {
1432            TextMessage::Tool { content, .. } => content.clone(),
1433            _ => panic!("Expected Tool message"),
1434        };
1435        let parsed2: serde_json::Value = serde_json::from_str(&second_content).unwrap();
1436        assert!(parsed2.get("tool").is_some());
1437        assert!(parsed2["n"].as_i64() == Some(2));
1438    }
1439
1440    #[tokio::test]
1441    async fn test_execute_tool_calls_parallel_returns_all() {
1442        use crate::model::chat_base_response::{ToolCallMessage, ToolFunction};
1443
1444        let executor = ToolExecutor::new();
1445
1446        let tool1 = FunctionTool::builder("parallel_a", "First parallel tool")
1447            .property("n", serde_json::json!({"type": "number"}))
1448            .handler(|args| async move {
1449                let n = args
1450                    .get("n")
1451                    .and_then(serde_json::Value::as_i64)
1452                    .unwrap_or(0);
1453                Ok(serde_json::json!({"tool": "a", "n": n}))
1454            })
1455            .build()
1456            .unwrap();
1457
1458        let tool2 = FunctionTool::builder("parallel_b", "Second parallel tool")
1459            .property("n", serde_json::json!({"type": "number"}))
1460            .handler(|args| async move {
1461                let n = args
1462                    .get("n")
1463                    .and_then(serde_json::Value::as_i64)
1464                    .unwrap_or(0);
1465                Ok(serde_json::json!({"tool": "b", "n": n}))
1466            })
1467            .build()
1468            .unwrap();
1469
1470        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1471        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1472
1473        let calls = vec![
1474            ToolCallMessage {
1475                id: Some("call_1".to_string()),
1476                type_: Some("function".to_string()),
1477                function: Some(ToolFunction {
1478                    name: "parallel_a".to_string(),
1479                    arguments: r#"{"n": 1}"#.to_string(),
1480                }),
1481                mcp: None,
1482            },
1483            ToolCallMessage {
1484                id: Some("call_2".to_string()),
1485                type_: Some("function".to_string()),
1486                function: Some(ToolFunction {
1487                    name: "parallel_b".to_string(),
1488                    arguments: r#"{"n": 2}"#.to_string(),
1489                }),
1490                mcp: None,
1491            },
1492        ];
1493
1494        let results = executor.execute_tool_calls_parallel(&calls).await;
1495        assert_eq!(results.len(), 2);
1496    }
1497
1498    #[tokio::test]
1499    async fn test_execute_works_with_cache_disabled() {
1500        // Regression guard for the lazy cache-key path: with caching disabled,
1501        // execute() must not build a key / touch the cache, yet still succeed.
1502        let executor = ToolExecutor::new();
1503
1504        let tool = FunctionTool::builder("echo", "echo input")
1505            .property("x", serde_json::json!({"type": "number"}))
1506            .handler(|args| async move { Ok(args) })
1507            .build()
1508            .unwrap();
1509        executor.add_dyn_tool(Box::new(tool)).unwrap();
1510
1511        let result = executor
1512            .execute("echo", serde_json::json!({"x": 1}))
1513            .await
1514            .unwrap();
1515        assert!(result.success);
1516        assert_eq!(result.result, serde_json::json!({"x": 1}));
1517        // Cache stays empty (disabled).
1518        assert_eq!(executor.cache_stats().total_entries, 0);
1519    }
1520}