Skip to main content

ferrin_tool/
callers.rs

1//! Caller restrictions: which callers (the model directly, or other tools)
2//! may trigger a tool, and how caller tools receive their callees.
3
4use std::collections::HashMap;
5use std::fmt;
6use std::sync::Arc;
7
8use ferrin_spec::ProviderOptions;
9use ferrin_spec::ToolName;
10use ferrin_spec::error::InvalidArgumentError;
11
12use crate::set::ToolSet;
13use crate::tool::Tool;
14
15/// Something that may trigger a tool call.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17#[non_exhaustive]
18pub enum ToolCaller {
19    /// The model calls the tool directly.
20    Direct,
21    /// Another tool (one with a [`ToolCallerDefinition`]) calls it.
22    Tool(ToolName),
23}
24
25/// Allowed callers per tool. Tools not listed keep the default (direct
26/// calls only).
27pub type ToolCallers = HashMap<ToolName, Vec<ToolCaller>>;
28
29/// Binds callees to a caller tool.
30pub type LocalBindFn = Arc<dyn Fn(ToolSet) -> Tool + Send + Sync>;
31/// Adjusts a callee's provider options so the provider routes its calls
32/// through the caller tool.
33pub type PrepareProviderOptionsFn =
34    Arc<dyn Fn(Option<ProviderOptions>) -> ProviderOptions + Send + Sync>;
35
36/// How a caller tool reaches its callees.
37#[derive(Clone)]
38#[non_exhaustive]
39pub enum ToolCallerDefinition {
40    /// The caller runs locally and receives the callees as a [`ToolSet`].
41    Local(LocalBindFn),
42    /// The provider performs the calls; callees are marked through provider
43    /// options.
44    Provider(PrepareProviderOptionsFn),
45}
46
47impl fmt::Debug for ToolCallerDefinition {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::Local(_) => f.write_str("Local(..)"),
51            Self::Provider(_) => f.write_str("Provider(..)"),
52        }
53    }
54}
55
56impl ToolCallerDefinition {
57    /// A local caller.
58    pub fn local(bind: impl Fn(ToolSet) -> Tool + Send + Sync + 'static) -> Self {
59        Self::Local(Arc::new(bind))
60    }
61
62    /// A provider caller.
63    pub fn provider(
64        prepare: impl Fn(Option<ProviderOptions>) -> ProviderOptions + Send + Sync + 'static,
65    ) -> Self {
66        Self::Provider(Arc::new(prepare))
67    }
68}
69
70/// Checks a caller configuration against the tool set.
71///
72/// # Errors
73///
74/// Returns [`InvalidArgumentError`] (argument `tool_callers`) when a listed
75/// tool is unknown or a caller is not a tool with a caller definition.
76pub fn validate_tool_callers(
77    tools: &ToolSet,
78    callers: &ToolCallers,
79) -> Result<(), InvalidArgumentError> {
80    for (tool_name, list) in callers {
81        if !tools.contains(tool_name.as_str()) {
82            return Err(InvalidArgumentError::new(
83                "tool_callers",
84                format!("unknown tool \"{tool_name}\"."),
85            ));
86        }
87        for caller in list {
88            if let ToolCaller::Tool(caller_name) = caller
89                && tools
90                    .get(caller_name.as_str())
91                    .is_none_or(|tool| tool.caller_definition().is_none())
92            {
93                return Err(InvalidArgumentError::new(
94                    "tool_callers",
95                    format!("tool \"{tool_name}\" contains an invalid caller."),
96                ));
97            }
98        }
99    }
100    Ok(())
101}
102
103/// Tool sets derived from a caller configuration.
104#[derive(Debug, Clone)]
105pub struct PreparedToolCallers {
106    /// Tools the core may execute (all tools, with callers bound).
107    pub execution_tools: ToolSet,
108    /// Tools sent to the model (callees reachable only through local callers
109    /// are removed).
110    pub model_tools: ToolSet,
111}
112
113/// Applies a caller configuration: callees of provider callers get their
114/// provider options prepared, callees of local callers are bound into the
115/// caller and hidden from the model, and tools without a direct or provider
116/// caller are removed from the model tool set.
117#[must_use]
118pub fn prepare_tools_for_callers(tools: &ToolSet, callers: &ToolCallers) -> PreparedToolCallers {
119    let mut execution = tools.clone();
120    let mut model = tools.clone();
121    let mut local_by_caller: HashMap<ToolName, ToolSet> = HashMap::new();
122
123    for (tool_name, tool) in tools {
124        let Some(list) = callers.get(tool_name) else {
125            continue;
126        };
127        let mut direct = false;
128        let mut via_provider = false;
129        let mut prepared: Tool = (**tool).clone();
130        for caller in list {
131            match caller {
132                ToolCaller::Direct => direct = true,
133                ToolCaller::Tool(caller_name) => {
134                    let Some(definition) = execution
135                        .get(caller_name.as_str())
136                        .and_then(|caller_tool| caller_tool.caller_definition().cloned())
137                    else {
138                        continue;
139                    };
140                    match definition {
141                        ToolCallerDefinition::Provider(prepare) => {
142                            via_provider = true;
143                            let options = prepare(prepared.provider_options.take());
144                            prepared = prepared.with_provider_options(Some(options));
145                        }
146                        ToolCallerDefinition::Local(_) => {
147                            let entry = local_by_caller.entry(caller_name.clone()).or_default();
148                            entry.replace(tool_name.clone(), Arc::new(prepared.clone()));
149                        }
150                        #[allow(
151                            unreachable_patterns,
152                            reason = "ToolCallerDefinition is non-exhaustive"
153                        )]
154                        _ => {}
155                    }
156                }
157                #[allow(unreachable_patterns, reason = "ToolCaller is non-exhaustive")]
158                _ => {}
159            }
160        }
161        let prepared = Arc::new(prepared);
162        execution.replace(tool_name.clone(), Arc::clone(&prepared));
163        if direct || via_provider {
164            model.replace(tool_name.clone(), prepared);
165        } else {
166            model.remove(tool_name.as_str());
167        }
168    }
169
170    let snapshot: Vec<(ToolName, Arc<Tool>)> = execution
171        .iter()
172        .map(|(name, tool)| (name.clone(), Arc::clone(tool)))
173        .collect();
174    for (caller_name, caller_tool) in snapshot {
175        let Some(ToolCallerDefinition::Local(bind)) = caller_tool.caller_definition() else {
176            continue;
177        };
178        let callees = local_by_caller.remove(&caller_name).unwrap_or_default();
179        let bound = Arc::new(bind(callees));
180        execution.replace(caller_name.clone(), Arc::clone(&bound));
181        if model.contains(caller_name.as_str()) {
182            model.replace(caller_name, bound);
183        }
184    }
185
186    PreparedToolCallers {
187        execution_tools: execution,
188        model_tools: model,
189    }
190}