Skip to main content

anda_core/
tool.rs

1//! Tool traits and registries.
2//!
3//! Tools are reusable capabilities that agents can call through the runtime.
4//! This module provides:
5//! - [`Tool`] for strongly typed tool implementations.
6//! - [`DynTool`] for runtime dispatch through trait objects.
7//! - [`ToolSet`] for name-based registration and lookup.
8//!
9//! Tools define their own JSON function schema through [`FunctionDefinition`]
10//! and receive typed arguments after the runtime validates and deserializes a
11//! raw JSON call.
12
13use serde::{Deserialize, Serialize, de::DeserializeOwned};
14use std::{any::Any, collections::BTreeMap, future::Future, marker::PhantomData, sync::Arc};
15
16use crate::{
17    BoxError, BoxFut, BoxPinFut, Function, Json, Resource, ToolInput, ToolOutput,
18    context::BaseContext,
19    model::FunctionDefinition,
20    registry::{collect_groups, select_by_names},
21    select_resources, validate_function_name,
22};
23
24/// Strongly typed interface for an agent tool.
25///
26/// # Type Parameters
27/// - `C`: Runtime context implementing [`BaseContext`].
28pub trait Tool<C>: Send + Sync
29where
30    C: BaseContext + Send + Sync,
31{
32    /// The arguments type of the tool.
33    type Args: DeserializeOwned + Send;
34
35    /// The output type of the tool.
36    type Output: Serialize;
37
38    /// Returns the unique tool name.
39    ///
40    /// # Rules
41    /// - Must not be empty;
42    /// - Must not exceed 64 bytes;
43    /// - Must start with a lowercase letter;
44    /// - Can only contain: lowercase letters (a-z), digits (0-9), underscores (_), and hyphens (-);
45    /// - Unique within the engine.
46    fn name(&self) -> String;
47
48    /// Returns a concise description of the tool's capability.
49    fn description(&self) -> String;
50
51    /// Returns the function definition, including the JSON parameter schema.
52    ///
53    /// # Returns
54    /// - `FunctionDefinition`: The schema definition of the tool's parameters and metadata.
55    fn definition(&self) -> FunctionDefinition;
56
57    /// Returns the capability group this tool belongs to, if any.
58    ///
59    /// Tools that form a coherent bundle (for example the filesystem workspace
60    /// tools) return the same [`ToolGroupInfo`] so the registry can present them
61    /// as one group in discovery. The default implementation returns `None`.
62    fn group(&self) -> Option<ToolGroupInfo> {
63        None
64    }
65
66    /// Returns resource tags this tool can consume.
67    ///
68    /// The default implementation returns an empty list, meaning no resources
69    /// are selected for this tool. Return `vec!["*".into()]` to accept all
70    /// attached resources.
71    ///
72    /// # Returns
73    /// Resource tags supported by this tool.
74    fn supported_resource_tags(&self) -> Vec<String> {
75        Vec::new()
76    }
77
78    /// Removes and returns resources matching this tool's supported tags.
79    fn select_resources(&self, resources: &mut Vec<Resource>) -> Vec<Resource> {
80        let supported_tags = self.supported_resource_tags();
81        select_resources(resources, &supported_tags)
82    }
83
84    /// Initializes the tool with the given context.
85    ///
86    /// Runtimes call this once while building the engine.
87    fn init(&self, _ctx: C) -> impl Future<Output = Result<(), BoxError>> + Send {
88        std::future::ready(Ok(()))
89    }
90
91    /// Executes the tool with typed arguments and selected resources.
92    ///
93    /// # Arguments
94    /// - `ctx`: The execution context implementing [`BaseContext`].
95    /// - `args`: struct arguments for the tool.
96    /// - `resources`: Additional resources selected for this tool.
97    ///
98    /// # Returns
99    /// A future resolving to [`ToolOutput<Self::Output>`].
100    fn call(
101        &self,
102        ctx: C,
103        args: Self::Args,
104        resources: Vec<Resource>,
105    ) -> impl Future<Output = Result<ToolOutput<Self::Output>, BoxError>> + Send;
106
107    /// Executes the tool from raw JSON arguments and returns JSON output.
108    fn call_raw(
109        &self,
110        ctx: C,
111        args: Json,
112        resources: Vec<Resource>,
113    ) -> impl Future<Output = Result<ToolOutput<Json>, BoxError>> + Send {
114        async move {
115            let args: Self::Args = serde_json::from_value(args)
116                .map_err(|err| format!("tool {}, invalid args: {}", self.name(), err))?;
117            let mut result = self
118                .call(ctx, args, resources)
119                .await
120                .map_err(|err| format!("tool {}, call failed: {}", self.name(), err))?;
121            let output = serde_json::to_value(&result.output)?;
122            if result.usage.requests == 0 {
123                result.usage.requests = 1;
124            }
125
126            Ok(ToolOutput {
127                output,
128                is_error: result.is_error,
129                artifacts: result.artifacts,
130                usage: result.usage,
131                tools_usage: result.tools_usage,
132            })
133        }
134    }
135}
136
137/// Object-safe wrapper around [`Tool`] for runtime dispatch.
138///
139/// Runtime registries store tools through this trait so callers can select and
140/// execute tools by name without knowing their concrete Rust types.
141pub trait DynTool<C>: Send + Sync
142where
143    C: BaseContext + Send + Sync,
144{
145    /// Returns this tool as [`Any`] for type inspection.
146    fn as_any(&self) -> &(dyn Any + Send + Sync);
147
148    /// Converts the shared tool into [`Any`] for downcasting.
149    fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
150
151    /// Returns the unique tool name.
152    fn name(&self) -> String;
153
154    /// Returns the function definition exposed to model providers.
155    fn definition(&self) -> FunctionDefinition;
156
157    /// Returns the capability group this tool belongs to, if any.
158    fn group(&self) -> Option<ToolGroupInfo> {
159        None
160    }
161
162    /// Returns resource tags this tool can consume.
163    fn supported_resource_tags(&self) -> Vec<String>;
164
165    /// Initializes the tool through object-safe dispatch.
166    fn init(&self, ctx: C) -> BoxPinFut<Result<(), BoxError>>;
167
168    /// Executes the tool through object-safe dispatch with raw JSON arguments.
169    fn call(
170        &self,
171        ctx: C,
172        args: Json,
173        resources: Vec<Resource>,
174    ) -> BoxPinFut<Result<ToolOutput<Json>, BoxError>>;
175}
176
177/// Group membership a single [`Tool`] declares for itself.
178///
179/// A static tool uses this to say "I belong to bundle X" without knowing the
180/// other members. The registry ([`ToolSet`]) collects every tool that declares
181/// the same `id` and assembles the full [`ToolGroup`], so the member list always
182/// reflects the tools actually registered (no stale or missing entries).
183///
184/// Share one constructor across a bundle's tools to keep the metadata identical;
185/// when ids collide, the first-registered tool's metadata wins.
186#[derive(Debug, Clone, Default, Serialize, Deserialize)]
187pub struct ToolGroupInfo {
188    /// Stable group id, unique across the engine (for example `fs_workspace`).
189    pub id: String,
190    /// Human-facing group title.
191    pub title: String,
192    /// Concise summary of what this bundle of tools does.
193    pub description: String,
194    /// Optional usage instructions describing how the member tools work
195    /// together. Reference for the model, never a runtime directive.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub instructions: Option<String>,
198}
199
200/// A related set of callables surfaced together from one source.
201///
202/// A group tells the model that a bundle of tools share an origin (for example
203/// a single MCP server, or the built-in filesystem tools) and are meant to be
204/// combined to complete related work. Groups are a *discovery-layer* concept
205/// only: they are never sent to model providers as part of the function-calling
206/// schema. They are returned by the built-in discovery helpers (`tools_search` /
207/// `tools_select`) so the model can understand a bundle's purpose and pull in
208/// sibling tools as needed.
209///
210/// `instructions`, `title`, and `description` may originate from untrusted
211/// remote metadata. They are surfaced as plain data the model reads, never as
212/// system instructions, so they cannot escalate into runtime directives.
213#[derive(Debug, Clone, Default, Serialize, Deserialize)]
214pub struct ToolGroup {
215    /// Stable group id, unique across providers (for example `mcp:filesystem`).
216    pub id: String,
217    /// Human-facing group title.
218    pub title: String,
219    /// Concise summary of what this bundle of tools does.
220    pub description: String,
221    /// Optional usage instructions describing how the member tools work
222    /// together. Untrusted remote metadata; treat as reference, not directives.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub instructions: Option<String>,
225    /// Model-facing names of the tools that belong to this group.
226    pub members: Vec<String>,
227}
228
229impl ToolGroup {
230    /// Builds a group from a per-tool [`ToolGroupInfo`] and resolved members.
231    pub fn from_info(info: ToolGroupInfo, members: Vec<String>) -> Self {
232        Self {
233            id: info.id,
234            title: info.title,
235            description: info.description,
236            instructions: info.instructions,
237            members,
238        }
239    }
240}
241
242/// Dynamic source of callable tools.
243///
244/// Providers are useful for integrations whose tool set is discovered at
245/// runtime, such as remote MCP servers. A provider exposes a synchronous
246/// snapshot for model-facing discovery and async methods for refresh and call
247/// execution.
248pub trait ToolProvider<C>: Send + Sync
249where
250    C: BaseContext + Send + Sync,
251{
252    /// Returns the provider registry name.
253    ///
254    /// This name is for engine configuration and diagnostics, not a
255    /// model-facing tool name.
256    fn name(&self) -> String;
257
258    /// Returns the current function definitions from this provider.
259    ///
260    /// Definition names must satisfy [`validate_function_name`] (lowercase ASCII
261    /// letters, digits, `_`, and `-`, starting with a lowercase letter). The
262    /// registry lowercases names defensively, but providers should return
263    /// already-legal local names so dispatch and discovery stay consistent.
264    fn definitions(&self, names: Option<&[String]>) -> Vec<FunctionDefinition>;
265
266    /// Returns the capability groups exposed by this provider.
267    ///
268    /// Each group bundles related tools (for example all tools from one MCP
269    /// server) so the discovery layer can tell the model the tools are related
270    /// and how to combine them. The default implementation returns no groups.
271    fn groups(&self) -> Vec<ToolGroup> {
272        Vec::new()
273    }
274
275    /// Returns whether this provider can currently dispatch the lowercase name.
276    ///
277    /// The default implementation allocates and materializes a definition
278    /// snapshot on every call. Providers on hot dispatch paths should override it
279    /// with a direct lookup (as the MCP provider does).
280    fn contains_lowercase(&self, lowercase_name: &str) -> bool {
281        self.definitions(Some(&[lowercase_name.to_string()]))
282            .iter()
283            .any(|definition| definition.name.eq_ignore_ascii_case(lowercase_name))
284    }
285
286    /// Returns resource tags this provider's named tool can consume.
287    fn supported_resource_tags(&self, _name: &str) -> Vec<String> {
288        Vec::new()
289    }
290
291    /// Removes and returns resources matching the named tool.
292    fn select_resources(&self, name: &str, resources: &mut Vec<Resource>) -> Vec<Resource> {
293        let supported_tags = self.supported_resource_tags(name);
294        select_resources(resources, &supported_tags)
295    }
296
297    /// Initializes the provider and refreshes any runtime discovery cache.
298    fn init(&self, _ctx: C) -> BoxFut<'_, Result<(), BoxError>> {
299        Box::pin(async { Ok(()) })
300    }
301
302    /// Refreshes the provider's discovery cache.
303    fn refresh(&self) -> BoxFut<'_, Result<(), BoxError>> {
304        Box::pin(async { Ok(()) })
305    }
306
307    /// Executes a provider-backed tool by model-facing name.
308    fn call(
309        &self,
310        ctx: C,
311        input: ToolInput<Json>,
312    ) -> BoxFut<'_, Result<ToolOutput<Json>, BoxError>>;
313}
314
315impl<C> dyn DynTool<C>
316where
317    C: BaseContext + Send + Sync + 'static,
318{
319    /// Returns the inner concrete tool type when it matches `T`.
320    pub fn downcast_ref<T>(&self) -> Option<&T>
321    where
322        T: Tool<C> + 'static,
323    {
324        self.as_any().downcast_ref::<T>()
325    }
326
327    /// Returns the inner concrete tool when it matches `T`.
328    pub fn downcast<T>(self: Arc<Self>) -> Result<Arc<T>, Arc<Self>>
329    where
330        T: Tool<C> + 'static,
331    {
332        match self.clone().into_any().downcast::<T>() {
333            Ok(tool) => Ok(tool),
334            Err(_) => Err(self),
335        }
336    }
337}
338
339/// Adapter that exposes a concrete [`Tool`] through [`DynTool`].
340struct ToolWrapper<T, C>(Arc<T>, PhantomData<C>)
341where
342    T: Tool<C> + 'static,
343    C: BaseContext + Send + Sync + 'static;
344
345impl<T, C> DynTool<C> for ToolWrapper<T, C>
346where
347    T: Tool<C> + 'static,
348    C: BaseContext + Send + Sync + 'static,
349{
350    fn as_any(&self) -> &(dyn Any + Send + Sync) {
351        self.0.as_ref()
352    }
353
354    fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
355        self.0.clone()
356    }
357
358    fn name(&self) -> String {
359        self.0.name()
360    }
361
362    fn definition(&self) -> FunctionDefinition {
363        self.0.definition()
364    }
365
366    fn group(&self) -> Option<ToolGroupInfo> {
367        self.0.group()
368    }
369
370    fn supported_resource_tags(&self) -> Vec<String> {
371        self.0.supported_resource_tags()
372    }
373
374    fn init(&self, ctx: C) -> BoxPinFut<Result<(), BoxError>> {
375        let tool = self.0.clone();
376        Box::pin(async move { tool.init(ctx).await })
377    }
378
379    fn call(
380        &self,
381        ctx: C,
382        args: Json,
383        resources: Vec<Resource>,
384    ) -> BoxPinFut<Result<ToolOutput<Json>, BoxError>> {
385        let tool = self.0.clone();
386        Box::pin(async move { tool.call_raw(ctx, args, resources).await })
387    }
388}
389
390/// Name-based registry for tools.
391///
392/// # Type Parameters
393/// - `C`: The context type that implements [`BaseContext`].
394#[derive(Default)]
395pub struct ToolSet<C: BaseContext> {
396    /// Registered tools keyed by their lowercase function names.
397    ///
398    /// Keys are lowercase names satisfying [`validate_function_name`] and equal
399    /// each tool's own lowercased name; [`ToolSet::add_dyn`] is the only insert
400    /// path, so lookup and dispatch can assume lowercase keys.
401    set: BTreeMap<String, Arc<dyn DynTool<C>>>,
402}
403
404/// Registry for runtime-discovered tool providers.
405#[derive(Default)]
406pub struct ToolProviderSet<C: BaseContext> {
407    /// Registered providers keyed by their lowercase provider names.
408    ///
409    /// Keys are lowercase names satisfying [`validate_function_name`];
410    /// [`ToolProviderSet::add_dyn`] is the only insert path.
411    set: BTreeMap<String, Arc<dyn ToolProvider<C>>>,
412}
413
414impl<C> ToolProviderSet<C>
415where
416    C: BaseContext + Clone + Send + Sync + 'static,
417{
418    /// Creates an empty provider set.
419    pub fn new() -> Self {
420        Self {
421            set: BTreeMap::new(),
422        }
423    }
424
425    /// Returns whether a provider with the given name exists.
426    pub fn contains_provider(&self, name: &str) -> bool {
427        self.set.contains_key(&name.to_ascii_lowercase())
428    }
429
430    /// Registers a new dynamic tool provider.
431    pub fn add<T>(&mut self, provider: Arc<T>) -> Result<(), BoxError>
432    where
433        T: ToolProvider<C> + Send + Sync + 'static,
434    {
435        self.add_dyn(provider)
436    }
437
438    /// Registers a type-erased provider, e.g. one drained from another set.
439    ///
440    /// The registry key is the provider's lowercase name; it must satisfy
441    /// [`validate_function_name`] and be unique within the set.
442    pub fn add_dyn(&mut self, provider: Arc<dyn ToolProvider<C>>) -> Result<(), BoxError> {
443        let name = provider.name().to_ascii_lowercase();
444        validate_function_name(&name)?;
445        if self.set.contains_key(&name) {
446            return Err(format!("tool provider {} already exists", name).into());
447        }
448
449        self.set.insert(name, provider);
450        Ok(())
451    }
452
453    /// Iterates providers as `(lowercase_name, provider)` pairs in name order.
454    pub fn iter(&self) -> impl Iterator<Item = (&str, &Arc<dyn ToolProvider<C>>)> {
455        self.set
456            .iter()
457            .map(|(name, provider)| (name.as_str(), provider))
458    }
459
460    /// Returns whether any provider can currently dispatch the given name.
461    pub fn contains_lowercase(&self, lowercase_name: &str) -> bool {
462        self.set
463            .values()
464            .any(|provider| provider.contains_lowercase(lowercase_name))
465    }
466
467    /// Returns dynamic function definitions for all providers or selected names.
468    ///
469    /// Definition names are normalized to lowercase so downstream lookups
470    /// (dispatch, `supported_resource_tags`) stay consistent even if a provider
471    /// returns a mixed-case name, and so duplicate names across providers are
472    /// deduplicated by their canonical lowercase form.
473    pub fn definitions(&self, names: Option<&[String]>) -> Vec<FunctionDefinition> {
474        match names {
475            Some([]) => Vec::new(),
476            _ => {
477                let mut definitions = BTreeMap::new();
478                for provider in self.set.values() {
479                    for mut definition in provider.definitions(names) {
480                        definition.name.make_ascii_lowercase();
481                        definitions
482                            .entry(definition.name.clone())
483                            .or_insert(definition);
484                    }
485                }
486                definitions.into_values().collect()
487            }
488        }
489    }
490
491    /// Returns the capability groups exposed by every registered provider.
492    pub fn groups(&self) -> Vec<ToolGroup> {
493        self.set
494            .values()
495            .flat_map(|provider| provider.groups())
496            .collect()
497    }
498
499    /// Returns function metadata for all provider-backed tools or selected names.
500    pub fn functions(&self, names: Option<&[String]>) -> Vec<Function> {
501        self.definitions(names)
502            .into_iter()
503            .map(|definition| {
504                let supported_resource_tags = self
505                    .set
506                    .values()
507                    .find(|provider| provider.contains_lowercase(&definition.name))
508                    .map(|provider| provider.supported_resource_tags(&definition.name))
509                    .unwrap_or_default();
510                Function {
511                    definition,
512                    supported_resource_tags,
513                }
514            })
515            .collect()
516    }
517
518    /// Removes and returns resources supported by the named provider tool.
519    pub fn select_resources(&self, name: &str, resources: &mut Vec<Resource>) -> Vec<Resource> {
520        if resources.is_empty() {
521            return Vec::new();
522        }
523
524        let lowercase_name = name.to_ascii_lowercase();
525        self.set
526            .values()
527            .find(|provider| provider.contains_lowercase(&lowercase_name))
528            .map(|provider| provider.select_resources(&lowercase_name, resources))
529            .unwrap_or_default()
530    }
531
532    /// Initializes all providers.
533    pub async fn init_all(&self, ctx: C) -> Result<(), BoxError> {
534        for provider in self.set.values() {
535            provider.init(ctx.clone()).await?;
536        }
537        Ok(())
538    }
539
540    /// Refreshes all providers.
541    pub async fn refresh_all(&self) -> Result<(), BoxError> {
542        for provider in self.set.values() {
543            provider.refresh().await?;
544        }
545        Ok(())
546    }
547
548    /// Executes a dynamic provider-backed tool.
549    pub async fn call(
550        &self,
551        ctx: C,
552        mut input: ToolInput<Json>,
553    ) -> Result<ToolOutput<Json>, BoxError> {
554        input.name.make_ascii_lowercase();
555        let provider = self
556            .set
557            .values()
558            .find(|provider| provider.contains_lowercase(&input.name))
559            .ok_or_else(|| format!("tool {} not found", input.name))?;
560        provider.call(ctx, input).await
561    }
562}
563
564impl<C> IntoIterator for ToolProviderSet<C>
565where
566    C: BaseContext + Clone + Send + Sync + 'static,
567{
568    type Item = Arc<dyn ToolProvider<C>>;
569    type IntoIter = std::collections::btree_map::IntoValues<String, Arc<dyn ToolProvider<C>>>;
570
571    /// Consumes the set, yielding providers in lowercase-name order.
572    fn into_iter(self) -> Self::IntoIter {
573        self.set.into_values()
574    }
575}
576
577impl<C> ToolSet<C>
578where
579    C: BaseContext + Send + Sync + 'static,
580{
581    /// Creates an empty tool set.
582    pub fn new() -> Self {
583        Self {
584            set: BTreeMap::new(),
585        }
586    }
587
588    /// Returns whether a tool with the given name exists.
589    pub fn contains(&self, name: &str) -> bool {
590        self.set.contains_key(&name.to_ascii_lowercase())
591    }
592
593    /// Returns whether a tool with the given lowercase name exists.
594    pub fn contains_lowercase(&self, lowercase_name: &str) -> bool {
595        self.set.contains_key(lowercase_name)
596    }
597
598    /// Returns the names of all registered tools.
599    pub fn names(&self) -> Vec<String> {
600        self.set.keys().cloned().collect()
601    }
602
603    /// Returns the capability groups assembled from registered tools.
604    ///
605    /// Tools that declare the same [`ToolGroupInfo::id`] are collected into one
606    /// [`ToolGroup`] whose `members` are exactly the registered tool names in
607    /// that group, sorted for determinism. Group metadata is taken from the
608    /// first tool (by lowercase name order) that declares the id.
609    pub fn groups(&self) -> Vec<ToolGroup> {
610        collect_groups(self.set.iter().map(|(name, tool)| (name, tool.group())))
611    }
612
613    /// Returns the function definition for a specific tool.
614    pub fn definition(&self, name: &str) -> Option<FunctionDefinition> {
615        self.set
616            .get(&name.to_ascii_lowercase())
617            .map(|tool| tool.definition())
618    }
619
620    /// Returns function definitions for all tools or the selected names.
621    ///
622    /// Requested names are matched case-insensitively and deduplicated.
623    ///
624    /// # Arguments
625    /// - `names`: Optional slice of tool names to filter by.
626    ///
627    /// # Returns
628    /// A vector of tool definitions.
629    pub fn definitions(&self, names: Option<&[String]>) -> Vec<FunctionDefinition> {
630        select_by_names(&self.set, names, |tool| tool.definition())
631    }
632
633    /// Returns function metadata for all tools or the selected names.
634    ///
635    /// Requested names are matched case-insensitively and deduplicated.
636    ///
637    /// # Arguments
638    /// - `names`: Optional slice of tool names to filter by.
639    ///
640    /// # Returns
641    /// A vector of tool function metadata.
642    pub fn functions(&self, names: Option<&[String]>) -> Vec<Function> {
643        select_by_names(&self.set, names, |tool| Function {
644            definition: tool.definition(),
645            supported_resource_tags: tool.supported_resource_tags(),
646        })
647    }
648
649    /// Removes and returns resources supported by the named tool.
650    pub fn select_resources(&self, name: &str, resources: &mut Vec<Resource>) -> Vec<Resource> {
651        if resources.is_empty() {
652            return Vec::new();
653        }
654
655        self.set
656            .get(&name.to_ascii_lowercase())
657            .map(|tool| {
658                let supported_tags = tool.supported_resource_tags();
659                select_resources(resources, &supported_tags)
660            })
661            .unwrap_or_default()
662    }
663
664    /// Registers a new tool.
665    ///
666    /// # Arguments
667    /// - `tool`: The tool to register.
668    pub fn add<T>(&mut self, tool: Arc<T>) -> Result<(), BoxError>
669    where
670        T: Tool<C> + Send + Sync + 'static,
671    {
672        self.add_dyn(Arc::new(ToolWrapper(tool, PhantomData)))
673    }
674
675    /// Registers a type-erased tool, e.g. one drained from another set.
676    ///
677    /// The registry key is the tool's lowercase name; it must satisfy
678    /// [`validate_function_name`] and be unique within the set.
679    pub fn add_dyn(&mut self, tool: Arc<dyn DynTool<C>>) -> Result<(), BoxError> {
680        let name = tool.name().to_ascii_lowercase();
681        validate_function_name(&name)?;
682        if self.set.contains_key(&name) {
683            return Err(format!("tool {} already exists", name).into());
684        }
685
686        self.set.insert(name, tool);
687        Ok(())
688    }
689
690    /// Iterates registered tools as `(lowercase_name, tool)` pairs in name order.
691    pub fn iter(&self) -> impl Iterator<Item = (&str, &Arc<dyn DynTool<C>>)> {
692        self.set.iter().map(|(name, tool)| (name.as_str(), tool))
693    }
694
695    /// Returns a tool by name.
696    pub fn get(&self, name: &str) -> Option<Arc<dyn DynTool<C>>> {
697        self.set.get(&name.to_ascii_lowercase()).cloned()
698    }
699
700    /// Returns a tool by lowercase name.
701    pub fn get_lowercase(&self, lowercase_name: &str) -> Option<Arc<dyn DynTool<C>>> {
702        self.set.get(lowercase_name).cloned()
703    }
704}
705
706impl<C> IntoIterator for ToolSet<C>
707where
708    C: BaseContext + Send + Sync + 'static,
709{
710    type Item = Arc<dyn DynTool<C>>;
711    type IntoIter = std::collections::btree_map::IntoValues<String, Arc<dyn DynTool<C>>>;
712
713    /// Consumes the set, yielding tools in lowercase-name order.
714    fn into_iter(self) -> Self::IntoIter {
715        self.set.into_values()
716    }
717}
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722    use crate::test_support::{MockContext, resource};
723    use serde_json::json;
724
725    struct ExampleTool {
726        id: usize,
727    }
728
729    struct OtherTool;
730
731    #[derive(serde::Deserialize)]
732    struct EchoArgs {
733        value: String,
734        fail: bool,
735    }
736
737    struct TaggedTool;
738
739    struct InvalidTool;
740
741    impl Tool<MockContext> for ExampleTool {
742        type Args = ();
743        type Output = String;
744
745        fn name(&self) -> String {
746            "example_tool".to_string()
747        }
748
749        fn description(&self) -> String {
750            "Example tool used for downcast tests".to_string()
751        }
752
753        fn definition(&self) -> FunctionDefinition {
754            FunctionDefinition {
755                name: self.name(),
756                description: self.description(),
757                parameters: json!({
758                    "type": "object",
759                    "properties": {},
760                    "required": [],
761                    "additionalProperties": false
762                }),
763                strict: Some(true),
764            }
765        }
766
767        async fn call(
768            &self,
769            _ctx: MockContext,
770            _args: Self::Args,
771            _resources: Vec<Resource>,
772        ) -> Result<ToolOutput<Self::Output>, BoxError> {
773            Ok(ToolOutput::new(self.id.to_string()))
774        }
775    }
776
777    impl Tool<MockContext> for OtherTool {
778        type Args = ();
779        type Output = String;
780
781        fn name(&self) -> String {
782            "other_tool".to_string()
783        }
784
785        fn description(&self) -> String {
786            "Other tool used for downcast tests".to_string()
787        }
788
789        fn definition(&self) -> FunctionDefinition {
790            FunctionDefinition {
791                name: self.name(),
792                description: self.description(),
793                parameters: json!({
794                    "type": "object",
795                    "properties": {},
796                    "required": [],
797                    "additionalProperties": false
798                }),
799                strict: Some(true),
800            }
801        }
802
803        async fn call(
804            &self,
805            _ctx: MockContext,
806            _args: Self::Args,
807            _resources: Vec<Resource>,
808        ) -> Result<ToolOutput<Self::Output>, BoxError> {
809            Ok(ToolOutput::new("other".to_string()))
810        }
811    }
812
813    impl Tool<MockContext> for TaggedTool {
814        type Args = EchoArgs;
815        type Output = Json;
816
817        fn name(&self) -> String {
818            "tagged_tool".to_string()
819        }
820
821        fn description(&self) -> String {
822            "Tool that consumes text and code resources".to_string()
823        }
824
825        fn definition(&self) -> FunctionDefinition {
826            FunctionDefinition {
827                name: self.name(),
828                description: self.description(),
829                parameters: json!({
830                    "type": "object",
831                    "properties": {
832                        "value": {"type": "string"},
833                        "fail": {"type": "boolean"}
834                    },
835                    "required": ["value", "fail"],
836                    "additionalProperties": false
837                }),
838                strict: Some(true),
839            }
840        }
841
842        fn supported_resource_tags(&self) -> Vec<String> {
843            vec!["text".to_string(), "code".to_string()]
844        }
845
846        async fn call(
847            &self,
848            _ctx: MockContext,
849            args: Self::Args,
850            resources: Vec<Resource>,
851        ) -> Result<ToolOutput<Self::Output>, BoxError> {
852            if args.fail {
853                return Err("forced failure".into());
854            }
855
856            let mut output = ToolOutput::new(json!({
857                "value": args.value,
858                "resources": resources.len(),
859            }));
860            output.is_error = Some(false);
861            Ok(output)
862        }
863    }
864
865    impl Tool<MockContext> for InvalidTool {
866        type Args = ();
867        type Output = String;
868
869        fn name(&self) -> String {
870            "bad.tool".to_string()
871        }
872
873        fn description(&self) -> String {
874            "Invalid function name".to_string()
875        }
876
877        fn definition(&self) -> FunctionDefinition {
878            FunctionDefinition {
879                name: self.name(),
880                description: self.description(),
881                parameters: json!({"type": "object"}),
882                strict: Some(true),
883            }
884        }
885
886        async fn call(
887            &self,
888            _ctx: MockContext,
889            _args: Self::Args,
890            _resources: Vec<Resource>,
891        ) -> Result<ToolOutput<Self::Output>, BoxError> {
892            Ok(ToolOutput::new(String::new()))
893        }
894    }
895
896    #[test]
897    fn dyn_tool_downcast_ref_returns_inner_tool() {
898        let tool = Arc::new(ExampleTool { id: 7 });
899        let mut tool_set = ToolSet::<MockContext>::new();
900        tool_set.add(tool).unwrap();
901
902        let dyn_tool = tool_set.get("example_tool").unwrap();
903        let concrete = dyn_tool.downcast_ref::<ExampleTool>().unwrap();
904
905        assert_eq!(concrete.id, 7);
906        assert!(dyn_tool.downcast_ref::<OtherTool>().is_none());
907    }
908
909    #[test]
910    fn dyn_tool_downcast_returns_original_arc() {
911        let tool = Arc::new(ExampleTool { id: 9 });
912        let mut tool_set = ToolSet::<MockContext>::new();
913        tool_set.add(tool.clone()).unwrap();
914
915        let dyn_tool = tool_set.get("example_tool").unwrap();
916        let concrete = match dyn_tool.downcast::<ExampleTool>() {
917            Ok(tool) => tool,
918            Err(_) => panic!("expected downcast to ExampleTool to succeed"),
919        };
920
921        assert_eq!(concrete.id, 9);
922        assert!(Arc::ptr_eq(&concrete, &tool));
923    }
924
925    #[test]
926    fn dyn_tool_downcast_mismatch_returns_original_arc() {
927        let tool = Arc::new(ExampleTool { id: 11 });
928        let mut tool_set = ToolSet::<MockContext>::new();
929        tool_set.add(tool).unwrap();
930
931        let dyn_tool = tool_set.get("example_tool").unwrap();
932        let original = dyn_tool.clone();
933        let err = match dyn_tool.downcast::<OtherTool>() {
934            Ok(_) => panic!("expected downcast to OtherTool to fail"),
935            Err(err) => err,
936        };
937
938        assert!(Arc::ptr_eq(&err, &original));
939        assert_eq!(err.name(), "example_tool");
940    }
941
942    #[test]
943    fn fixture_tools_cover_direct_methods() {
944        futures::executor::block_on(async {
945            let other = OtherTool;
946            assert_eq!(other.name(), "other_tool");
947            assert_eq!(other.description(), "Other tool used for downcast tests");
948            let definition = other.definition();
949            assert_eq!(definition.name, "other_tool");
950            assert_eq!(definition.description, "Other tool used for downcast tests");
951            assert_eq!(definition.parameters["type"], "object");
952            let output = other
953                .call(MockContext::default(), (), Vec::new())
954                .await
955                .unwrap();
956            assert_eq!(output.output, "other");
957
958            let invalid = InvalidTool;
959            assert_eq!(invalid.name(), "bad.tool");
960            assert_eq!(invalid.description(), "Invalid function name");
961            let definition = invalid.definition();
962            assert_eq!(definition.name, "bad.tool");
963            assert_eq!(definition.description, "Invalid function name");
964            assert_eq!(definition.parameters["type"], "object");
965            let output = invalid
966                .call(MockContext::default(), (), Vec::new())
967                .await
968                .unwrap();
969            assert!(output.output.is_empty());
970        });
971    }
972
973    #[test]
974    fn tool_default_methods_call_raw_and_dyn_wrapper_forward_calls() {
975        futures::executor::block_on(async {
976            let tool = Arc::new(ExampleTool { id: 42 });
977            let mut resources = vec![resource(1, &["text"])];
978
979            assert!(tool.supported_resource_tags().is_empty());
980            assert!(tool.select_resources(&mut resources).is_empty());
981            assert_eq!(resources.len(), 1);
982            tool.init(MockContext::default()).await.unwrap();
983
984            let raw = tool
985                .call_raw(MockContext::default(), Json::Null, Vec::new())
986                .await
987                .unwrap();
988            assert_eq!(raw.output, json!("42"));
989            assert_eq!(raw.usage.requests, 1);
990
991            let invalid = tool
992                .call_raw(MockContext::default(), json!({"bad": true}), Vec::new())
993                .await
994                .unwrap_err();
995            assert!(invalid.to_string().contains("invalid args"));
996
997            let mut tool_set = ToolSet::<MockContext>::new();
998            tool_set.add(tool).unwrap();
999            let dyn_tool = tool_set.get("EXAMPLE_TOOL").unwrap();
1000
1001            assert_eq!(dyn_tool.name(), "example_tool");
1002            assert_eq!(dyn_tool.definition().name, "example_tool");
1003            assert!(dyn_tool.supported_resource_tags().is_empty());
1004            dyn_tool.init(MockContext::default()).await.unwrap();
1005
1006            let output = dyn_tool
1007                .call(MockContext::default(), Json::Null, Vec::new())
1008                .await
1009                .unwrap();
1010            assert_eq!(output.output, json!("42"));
1011            assert_eq!(output.usage.requests, 1);
1012        });
1013    }
1014
1015    #[test]
1016    fn tool_set_registry_filters_resources_and_reports_errors() {
1017        futures::executor::block_on(async {
1018            let mut tool_set = ToolSet::<MockContext>::new();
1019            tool_set.add(Arc::new(ExampleTool { id: 1 })).unwrap();
1020            tool_set.add(Arc::new(TaggedTool)).unwrap();
1021
1022            assert!(tool_set.contains("EXAMPLE_TOOL"));
1023            assert!(tool_set.contains_lowercase("tagged_tool"));
1024            assert!(!tool_set.contains("missing_tool"));
1025            assert_eq!(
1026                tool_set.names(),
1027                vec!["example_tool".to_string(), "tagged_tool".to_string()]
1028            );
1029
1030            let definition = tool_set.definition("TAGGED_TOOL").unwrap();
1031            assert_eq!(definition.name, "tagged_tool");
1032            assert!(tool_set.definition("missing_tool").is_none());
1033
1034            let selected_names = vec!["TAGGED_TOOL".to_string(), "missing_tool".to_string()];
1035            let selected_definitions = tool_set.definitions(Some(&selected_names));
1036            assert_eq!(selected_definitions.len(), 1);
1037            assert_eq!(selected_definitions[0].name, "tagged_tool");
1038            assert_eq!(tool_set.definitions(None).len(), 2);
1039
1040            // Repeated (case-insensitive) requested names are deduplicated.
1041            let duplicate_names = vec![
1042                "tagged_tool".to_string(),
1043                "TAGGED_TOOL".to_string(),
1044                "tagged_tool".to_string(),
1045            ];
1046            assert_eq!(tool_set.definitions(Some(&duplicate_names)).len(), 1);
1047            assert_eq!(tool_set.functions(Some(&duplicate_names)).len(), 1);
1048
1049            let selected_functions = tool_set.functions(Some(&selected_names));
1050            assert_eq!(selected_functions.len(), 1);
1051            assert_eq!(
1052                selected_functions[0].supported_resource_tags,
1053                vec!["text".to_string(), "code".to_string()]
1054            );
1055            assert_eq!(tool_set.functions(None).len(), 2);
1056
1057            let mut resources = vec![
1058                resource(1, &["image"]),
1059                resource(2, &["text"]),
1060                resource(3, &["code", "text"]),
1061                resource(4, &["audio"]),
1062            ];
1063            let selected = tool_set.select_resources("TAGGED_TOOL", &mut resources);
1064            assert_eq!(
1065                selected
1066                    .iter()
1067                    .map(|resource| resource._id)
1068                    .collect::<Vec<_>>(),
1069                vec![2, 3]
1070            );
1071            assert_eq!(
1072                resources
1073                    .iter()
1074                    .map(|resource| resource._id)
1075                    .collect::<Vec<_>>(),
1076                vec![1, 4]
1077            );
1078            assert!(
1079                tool_set
1080                    .select_resources("missing_tool", &mut resources)
1081                    .is_empty()
1082            );
1083
1084            let dyn_tool = tool_set.get_lowercase("tagged_tool").unwrap();
1085            let output = dyn_tool
1086                .call(
1087                    MockContext::default(),
1088                    json!({"value": "ok", "fail": false}),
1089                    vec![resource(9, &["text"])],
1090                )
1091                .await
1092                .unwrap();
1093            assert_eq!(output.output["value"], "ok");
1094            assert_eq!(output.output["resources"], 1);
1095            assert_eq!(output.is_error, Some(false));
1096            assert_eq!(output.usage.requests, 1);
1097            assert!(tool_set.get("missing_tool").is_none());
1098            assert!(tool_set.get_lowercase("missing_tool").is_none());
1099
1100            let failed = dyn_tool
1101                .call(
1102                    MockContext::default(),
1103                    json!({"value": "bad", "fail": true}),
1104                    Vec::new(),
1105                )
1106                .await
1107                .unwrap_err();
1108            assert!(failed.to_string().contains("call failed"));
1109
1110            let duplicate = tool_set.add(Arc::new(ExampleTool { id: 2 })).unwrap_err();
1111            assert!(duplicate.to_string().contains("already exists"));
1112
1113            let invalid = tool_set.add(Arc::new(InvalidTool)).unwrap_err();
1114            assert!(invalid.to_string().contains("invalid character"));
1115        });
1116    }
1117
1118    struct GroupedTool {
1119        name: &'static str,
1120        group: &'static str,
1121    }
1122
1123    impl Tool<MockContext> for GroupedTool {
1124        type Args = ();
1125        type Output = String;
1126
1127        fn name(&self) -> String {
1128            self.name.to_string()
1129        }
1130
1131        fn description(&self) -> String {
1132            "Grouped tool fixture".to_string()
1133        }
1134
1135        fn definition(&self) -> FunctionDefinition {
1136            FunctionDefinition {
1137                name: self.name(),
1138                description: self.description(),
1139                parameters: json!({
1140                    "type": "object",
1141                    "properties": {},
1142                    "required": [],
1143                    "additionalProperties": false
1144                }),
1145                strict: Some(true),
1146            }
1147        }
1148
1149        fn group(&self) -> Option<ToolGroupInfo> {
1150            Some(ToolGroupInfo {
1151                id: self.group.to_string(),
1152                title: format!("{} title", self.group),
1153                description: format!("{} description", self.group),
1154                instructions: Some(format!("{} instructions", self.group)),
1155            })
1156        }
1157
1158        async fn call(
1159            &self,
1160            _ctx: MockContext,
1161            _args: Self::Args,
1162            _resources: Vec<Resource>,
1163        ) -> Result<ToolOutput<Self::Output>, BoxError> {
1164            Ok(ToolOutput::new(String::new()))
1165        }
1166    }
1167
1168    #[test]
1169    fn tool_set_groups_aggregate_members_by_id() {
1170        let mut tool_set = ToolSet::<MockContext>::new();
1171        tool_set
1172            .add(Arc::new(GroupedTool {
1173                name: "fs_write",
1174                group: "fs",
1175            }))
1176            .unwrap();
1177        tool_set
1178            .add(Arc::new(GroupedTool {
1179                name: "fs_read",
1180                group: "fs",
1181            }))
1182            .unwrap();
1183        tool_set
1184            .add(Arc::new(GroupedTool {
1185                name: "mem_get",
1186                group: "memory",
1187            }))
1188            .unwrap();
1189        // A tool with no group declaration is excluded from every group.
1190        tool_set.add(Arc::new(ExampleTool { id: 1 })).unwrap();
1191
1192        let groups = tool_set.groups();
1193        assert_eq!(groups.len(), 2);
1194
1195        let fs = groups.iter().find(|group| group.id == "fs").unwrap();
1196        // Members reflect the registered tools, sorted for determinism.
1197        assert_eq!(
1198            fs.members,
1199            vec!["fs_read".to_string(), "fs_write".to_string()]
1200        );
1201        assert_eq!(fs.title, "fs title");
1202        assert_eq!(fs.instructions.as_deref(), Some("fs instructions"));
1203
1204        let memory = groups.iter().find(|group| group.id == "memory").unwrap();
1205        assert_eq!(memory.members, vec!["mem_get".to_string()]);
1206    }
1207}