1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17#[non_exhaustive]
18pub enum ToolCaller {
19 Direct,
21 Tool(ToolName),
23}
24
25pub type ToolCallers = HashMap<ToolName, Vec<ToolCaller>>;
28
29pub type LocalBindFn = Arc<dyn Fn(ToolSet) -> Tool + Send + Sync>;
31pub type PrepareProviderOptionsFn =
34 Arc<dyn Fn(Option<ProviderOptions>) -> ProviderOptions + Send + Sync>;
35
36#[derive(Clone)]
38#[non_exhaustive]
39pub enum ToolCallerDefinition {
40 Local(LocalBindFn),
42 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 pub fn local(bind: impl Fn(ToolSet) -> Tool + Send + Sync + 'static) -> Self {
59 Self::Local(Arc::new(bind))
60 }
61
62 pub fn provider(
64 prepare: impl Fn(Option<ProviderOptions>) -> ProviderOptions + Send + Sync + 'static,
65 ) -> Self {
66 Self::Provider(Arc::new(prepare))
67 }
68}
69
70pub 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#[derive(Debug, Clone)]
105pub struct PreparedToolCallers {
106 pub execution_tools: ToolSet,
108 pub model_tools: ToolSet,
111}
112
113#[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}