1use std::collections::{BTreeMap, HashMap};
4use std::sync::Arc;
5use std::time::Duration;
6
7use tokio_util::sync::CancellationToken;
8
9use gemini_genai_rs::prelude::{FunctionCall, FunctionDeclaration, FunctionResponse, Tool};
10
11use crate::error::ToolError;
12
13use super::{ActiveStreamingTool, DEFAULT_TOOL_TIMEOUT, ToolClass, ToolFunction, ToolKind};
14
15pub struct ToolDispatcher {
17 tools: BTreeMap<String, ToolKind>,
19 active: Arc<tokio::sync::Mutex<HashMap<String, ActiveStreamingTool>>>,
20 default_timeout: Duration,
21 cached_declarations: std::sync::OnceLock<Vec<Tool>>,
24 confirmation_provider: Option<Arc<dyn crate::confirmation::ConfirmationProvider>>,
26}
27
28impl ToolDispatcher {
29 pub fn new() -> Self {
44 Self {
45 tools: BTreeMap::new(),
46 active: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
47 default_timeout: DEFAULT_TOOL_TIMEOUT,
48 cached_declarations: std::sync::OnceLock::new(),
49 confirmation_provider: None,
50 }
51 }
52
53 pub fn with_timeout(mut self, timeout: Duration) -> Self {
55 self.default_timeout = timeout;
56 self
57 }
58
59 pub fn with_confirmation_provider(
68 mut self,
69 provider: Arc<dyn crate::confirmation::ConfirmationProvider>,
70 ) -> Self {
71 self.confirmation_provider = Some(provider);
72 self
73 }
74
75 pub fn set_confirmation_provider(
78 &mut self,
79 provider: Arc<dyn crate::confirmation::ConfirmationProvider>,
80 ) {
81 self.confirmation_provider = Some(provider);
82 }
83
84 pub fn has_confirmation_provider(&self) -> bool {
86 self.confirmation_provider.is_some()
87 }
88
89 async fn ensure_confirmed(
93 &self,
94 func: &Arc<dyn ToolFunction>,
95 args: &serde_json::Value,
96 ) -> Result<(), ToolError> {
97 if !func.requires_confirmation() {
98 return Ok(());
99 }
100 let Some(provider) = &self.confirmation_provider else {
101 return Ok(());
102 };
103 let request = crate::confirmation::ConfirmationRequest {
104 tool_name: func.name().to_string(),
105 args: args.clone(),
106 message: func.confirmation_message().map(str::to_string),
107 };
108 let decision = provider.confirm(request).await;
109 if decision.confirmed {
110 Ok(())
111 } else {
112 Err(ToolError::Declined(
113 decision.hint.unwrap_or_else(|| "no reason given".into()),
114 ))
115 }
116 }
117
118 pub fn default_timeout(&self) -> Duration {
120 self.default_timeout
121 }
122
123 pub fn register(&mut self, tool: impl ToolFunction) {
125 self.insert(ToolKind::Function(Arc::new(tool)));
126 }
127
128 pub fn register_function(&mut self, tool: Arc<dyn ToolFunction>) {
130 self.insert(ToolKind::Function(tool));
131 }
132
133 pub fn register_streaming(&mut self, tool: Arc<dyn super::StreamingTool>) {
135 self.insert(ToolKind::Streaming(tool));
136 }
137
138 pub fn register_input_streaming(&mut self, tool: Arc<dyn super::InputStreamingTool>) {
140 self.insert(ToolKind::InputStream(tool));
141 }
142
143 fn insert(&mut self, tool: ToolKind) {
145 let name = match &tool {
146 ToolKind::Function(f) => f.name(),
147 ToolKind::Streaming(s) => s.name(),
148 ToolKind::InputStream(i) => i.name(),
149 }
150 .to_string();
151 self.tools.insert(name, tool);
152 self.cached_declarations.take();
153 }
154
155 pub fn merge(&mut self, other: ToolDispatcher) {
159 for (name, tool) in other.tools {
160 self.tools.entry(name).or_insert(tool);
161 }
162 self.cached_declarations.take();
163 }
164
165 pub fn gated_tools(&self) -> impl Iterator<Item = &str> {
168 self.tools.iter().filter_map(|(name, tool)| match tool {
169 ToolKind::Function(f) if f.requires_confirmation() => Some(name.as_str()),
170 _ => None,
171 })
172 }
173
174 pub fn names(&self) -> impl Iterator<Item = &str> {
176 self.tools.keys().map(String::as_str)
177 }
178
179 pub fn get_tool(&self, name: &str) -> Option<&ToolKind> {
181 self.tools.get(name)
182 }
183
184 pub fn classify(&self, name: &str) -> Option<ToolClass> {
186 self.tools.get(name).map(|t| match t {
187 ToolKind::Function(_) => ToolClass::Regular,
188 ToolKind::Streaming(_) => ToolClass::Streaming,
189 ToolKind::InputStream(_) => ToolClass::InputStream,
190 })
191 }
192
193 pub async fn call_function(
199 &self,
200 name: &str,
201 args: serde_json::Value,
202 ) -> Result<serde_json::Value, ToolError> {
203 self.call_function_with_timeout(name, args, self.default_timeout)
204 .await
205 }
206
207 pub async fn call_function_in(
212 &self,
213 name: &str,
214 args: serde_json::Value,
215 ctx: super::ToolContext,
216 ) -> Result<serde_json::Value, ToolError> {
217 let func = self.function(name)?;
218 self.ensure_confirmed(&func, &args).await?;
219 let timeout = self.default_timeout;
220 let cancel = ctx.cancel.clone();
221 tokio::select! {
222 biased;
223 () = cancel.cancelled() => Err(ToolError::Cancelled),
224 result = tokio::time::timeout(timeout, func.call_with_context(args, ctx)) => {
225 result.unwrap_or(Err(ToolError::Timeout(timeout)))
226 }
227 }
228 }
229
230 fn function(&self, name: &str) -> Result<Arc<dyn super::ToolFunction>, ToolError> {
232 match self.tools.get(name) {
233 Some(ToolKind::Function(f)) => Ok(f.clone()),
234 Some(_) => Err(ToolError::Other(format!(
235 "{name} is not a regular function tool"
236 ))),
237 None => Err(ToolError::NotFound(name.to_string())),
238 }
239 }
240
241 pub async fn call_function_with_timeout(
246 &self,
247 name: &str,
248 args: serde_json::Value,
249 timeout: Duration,
250 ) -> Result<serde_json::Value, ToolError> {
251 let func = match self.tools.get(name) {
252 Some(ToolKind::Function(f)) => f.clone(),
253 Some(_) => {
254 return Err(ToolError::Other(format!(
255 "{name} is not a regular function tool"
256 )));
257 }
258 None => return Err(ToolError::NotFound(name.to_string())),
259 };
260
261 self.ensure_confirmed(&func, &args).await?;
262
263 match tokio::time::timeout(
264 timeout,
265 func.call_with_context(args, super::ToolContext::detached()),
266 )
267 .await
268 {
269 Ok(result) => result,
270 Err(_elapsed) => Err(ToolError::Timeout(timeout)),
271 }
272 }
273
274 pub async fn call_function_with_cancel(
279 &self,
280 name: &str,
281 args: serde_json::Value,
282 cancel: CancellationToken,
283 ) -> Result<serde_json::Value, ToolError> {
284 let func = match self.tools.get(name) {
285 Some(ToolKind::Function(f)) => f.clone(),
286 Some(_) => {
287 return Err(ToolError::Other(format!(
288 "{name} is not a regular function tool"
289 )));
290 }
291 None => return Err(ToolError::NotFound(name.to_string())),
292 };
293
294 self.ensure_confirmed(&func, &args).await?;
295
296 tokio::select! {
297 result = func.call_with_context(
298 args,
299 super::ToolContext::detached().with_cancel(cancel.clone()),
300 ) => result,
301 _ = cancel.cancelled() => Err(ToolError::Cancelled),
302 }
303 }
304
305 pub fn build_response(
307 call: &FunctionCall,
308 result: Result<serde_json::Value, ToolError>,
309 ) -> FunctionResponse {
310 match result {
311 Ok(value) => FunctionResponse {
312 name: call.name.clone(),
313 response: value,
314 id: call.id.clone(),
315 scheduling: None,
316 },
317 Err(e) => FunctionResponse {
318 name: call.name.clone(),
319 response: serde_json::json!({"error": e.to_string()}),
320 id: call.id.clone(),
321 scheduling: None,
322 },
323 }
324 }
325
326 pub async fn cancel_streaming(&self, name: &str) {
328 let mut active = self.active.lock().await;
329 if let Some(tool) = active.remove(name) {
330 tool.cancel.cancel();
331 tool.task.abort();
332 }
333 }
334
335 pub(crate) async fn store_active(&self, id: String, tool: ActiveStreamingTool) {
337 self.active.lock().await.insert(id, tool);
338 }
339
340 pub async fn cancel_by_ids(&self, ids: &[String]) {
342 let mut active = self.active.lock().await;
343 for id in ids {
344 if let Some(tool) = active.remove(id.as_str()) {
345 tool.cancel.cancel();
346 tool.task.abort();
347 }
348 }
349 }
350
351 pub fn to_tool_declarations(&self) -> Vec<Tool> {
356 self.cached_declarations
357 .get_or_init(|| {
358 let declarations: Vec<FunctionDeclaration> = self
359 .tools
360 .values()
361 .map(|t| {
362 let (name, desc, params) = match t {
363 ToolKind::Function(f) => (f.name(), f.description(), f.parameters()),
364 ToolKind::Streaming(s) => (s.name(), s.description(), s.parameters()),
365 ToolKind::InputStream(i) => (i.name(), i.description(), i.parameters()),
366 };
367 FunctionDeclaration {
368 name: name.to_string(),
369 description: desc.to_string(),
370 parameters: params,
371 behavior: None,
372 }
373 })
374 .collect();
375
376 if declarations.is_empty() {
377 vec![]
378 } else {
379 vec![Tool::functions(declarations)]
380 }
381 })
382 .clone()
383 }
384
385 pub fn len(&self) -> usize {
387 self.tools.len()
388 }
389
390 pub fn is_empty(&self) -> bool {
392 self.tools.is_empty()
393 }
394}
395
396impl Default for ToolDispatcher {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402impl gemini_genai_rs::prelude::ToolProvider for ToolDispatcher {
403 fn declarations(&self) -> Vec<gemini_genai_rs::prelude::Tool> {
404 self.to_tool_declarations()
405 }
406}
407
408#[cfg(test)]
409mod confirmation_tests {
410 use super::*;
411 use crate::confirmation::StaticConfirmation;
412 use crate::tool::{PolicyTool, SimpleTool, policy::ToolPolicy};
413 use serde_json::json;
414 use std::sync::atomic::{AtomicUsize, Ordering};
415
416 fn confirm_tool(runs: Arc<AtomicUsize>) -> Arc<dyn ToolFunction> {
418 let inner: Arc<dyn ToolFunction> = Arc::new(SimpleTool::new(
419 "danger",
420 "does something sensitive",
421 None,
422 move |_| {
423 let runs = runs.clone();
424 async move {
425 runs.fetch_add(1, Ordering::SeqCst);
426 Ok(json!({ "ok": true }))
427 }
428 },
429 ));
430 Arc::new(PolicyTool::new(
431 inner,
432 ToolPolicy::new().with_confirm(Some("delete production data?".into())),
433 ))
434 }
435
436 #[tokio::test]
437 async fn denied_confirmation_blocks_execution() {
438 let runs = Arc::new(AtomicUsize::new(0));
439 let mut d = ToolDispatcher::new();
440 d.register_function(confirm_tool(runs.clone()));
441 d.set_confirmation_provider(StaticConfirmation::deny_all("blocked by policy"));
442
443 let result = d.call_function("danger", json!({})).await;
444 assert!(
445 matches!(&result, Err(ToolError::Declined(reason)) if reason == "blocked by policy"),
446 "the model must learn why: {result:?}"
447 );
448 assert_eq!(
449 runs.load(Ordering::SeqCst),
450 0,
451 "tool must not run when denied"
452 );
453 }
454
455 #[tokio::test]
456 async fn approved_confirmation_runs() {
457 let runs = Arc::new(AtomicUsize::new(0));
458 let mut d = ToolDispatcher::new();
459 d.register_function(confirm_tool(runs.clone()));
460 d.set_confirmation_provider(StaticConfirmation::allow_all());
461
462 let out = d.call_function("danger", json!({})).await.unwrap();
463 assert_eq!(out["ok"], true);
464 assert_eq!(runs.load(Ordering::SeqCst), 1);
465 }
466
467 #[tokio::test]
468 async fn no_provider_runs_optin() {
469 let runs = Arc::new(AtomicUsize::new(0));
471 let mut d = ToolDispatcher::new();
472 d.register_function(confirm_tool(runs.clone()));
473
474 let out = d.call_function("danger", json!({})).await.unwrap();
475 assert_eq!(out["ok"], true);
476 assert_eq!(runs.load(Ordering::SeqCst), 1);
477 }
478
479 #[tokio::test]
480 async fn provider_sees_request_and_ignores_non_gated_tools() {
481 let mut d = ToolDispatcher::new();
483 d.register(SimpleTool::new(
484 "plain",
485 "no confirmation",
486 None,
487 |_| async move { Ok(json!({ "ran": true })) },
488 ));
489 d.set_confirmation_provider(StaticConfirmation::deny_all("should not be consulted"));
490
491 let out = d.call_function("plain", json!({})).await.unwrap();
492 assert_eq!(out["ran"], true);
493 }
494
495 #[tokio::test]
496 async fn nested_policy_wrapper_does_not_bypass_confirmation() {
497 let runs = Arc::new(AtomicUsize::new(0));
500 let inner_confirm = confirm_tool(runs.clone()); let outer_cached: Arc<dyn ToolFunction> = Arc::new(PolicyTool::new(
502 inner_confirm,
503 ToolPolicy::new().with_cache(),
504 ));
505 assert!(
506 outer_cached.requires_confirmation(),
507 "must propagate through nesting"
508 );
509
510 let mut d = ToolDispatcher::new();
511 d.register_function(outer_cached);
512 d.set_confirmation_provider(StaticConfirmation::deny_all("blocked"));
513
514 let result = d.call_function("danger", json!({})).await;
515 assert!(matches!(result, Err(ToolError::Declined(_))));
516 assert_eq!(
517 runs.load(Ordering::SeqCst),
518 0,
519 "nested confirm must not run when denied"
520 );
521 }
522
523 #[tokio::test]
524 async fn closure_provider_can_gate_by_name() {
525 let runs = Arc::new(AtomicUsize::new(0));
526 let mut d = ToolDispatcher::new();
527 d.register_function(confirm_tool(runs.clone()));
528 d.set_confirmation_provider(Arc::new(
529 |req: crate::confirmation::ConfirmationRequest| async move {
530 if req.tool_name == "danger" {
531 crate::confirmation::ToolConfirmation::denied("name-gated")
532 } else {
533 crate::confirmation::ToolConfirmation::confirmed()
534 }
535 },
536 ));
537
538 assert!(d.call_function("danger", json!({})).await.is_err());
539 assert_eq!(runs.load(Ordering::SeqCst), 0);
540 }
541}
542
543#[cfg(test)]
544mod declaration_tests {
545 use super::*;
546 use crate::tool::SimpleTool;
547
548 fn named(name: &'static str) -> Arc<dyn ToolFunction> {
549 Arc::new(SimpleTool::new(name, name, None, |_| async {
550 Ok(serde_json::json!({}))
551 }))
552 }
553
554 fn declared(d: &ToolDispatcher) -> Vec<String> {
555 d.to_tool_declarations()
556 .iter()
557 .flat_map(|t| t.function_declarations.iter().flatten())
558 .map(|f| f.name.clone())
559 .collect()
560 }
561
562 #[test]
565 fn registering_a_tool_refreshes_the_declarations() {
566 let mut d = ToolDispatcher::new();
567 d.register_function(named("a"));
568 assert_eq!(declared(&d), ["a"]);
569 d.register_function(named("b"));
570 assert_eq!(declared(&d), ["a", "b"]);
571 }
572
573 #[test]
576 fn declarations_are_deterministic() {
577 let mut d = ToolDispatcher::new();
578 for name in ["zeta", "alpha", "mid"] {
579 d.register_function(named(name));
580 }
581 assert_eq!(declared(&d), ["alpha", "mid", "zeta"]);
582 assert_eq!(d.names().collect::<Vec<_>>(), ["alpha", "mid", "zeta"]);
583 }
584
585 #[test]
586 fn merge_keeps_both_and_this_dispatcher_wins_a_clash() {
587 let mut mine = ToolDispatcher::new().with_timeout(Duration::from_secs(3));
588 mine.register_function(Arc::new(SimpleTool::new(
589 "shared",
590 "mine",
591 None,
592 |_| async { Ok(serde_json::json!({})) },
593 )));
594 let mut theirs = ToolDispatcher::new();
595 theirs.register_function(named("shared"));
596 theirs.register_function(named("extra"));
597 mine.merge(theirs);
598 assert_eq!(declared(&mine), ["extra", "shared"]);
599 match mine.get_tool("shared") {
600 Some(ToolKind::Function(f)) => assert_eq!(f.description(), "mine"),
601 _ => panic!("shared must stay a function tool"),
602 }
603 assert_eq!(mine.default_timeout(), Duration::from_secs(3));
604 }
605}