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 self.call_function_with_context_timeout(name, args, ctx, self.default_timeout)
218 .await
219 }
220
221 fn function(&self, name: &str) -> Result<Arc<dyn super::ToolFunction>, ToolError> {
223 match self.tools.get(name) {
224 Some(ToolKind::Function(f)) => Ok(f.clone()),
225 Some(_) => Err(ToolError::Other(format!(
226 "{name} is not a regular function tool"
227 ))),
228 None => Err(ToolError::NotFound(name.to_string())),
229 }
230 }
231
232 pub async fn call_function_with_timeout(
237 &self,
238 name: &str,
239 args: serde_json::Value,
240 timeout: Duration,
241 ) -> Result<serde_json::Value, ToolError> {
242 self.call_function_with_context_timeout(name, args, super::ToolContext::detached(), timeout)
243 .await
244 }
245
246 pub async fn call_function_with_cancel(
252 &self,
253 name: &str,
254 args: serde_json::Value,
255 cancel: CancellationToken,
256 ) -> Result<serde_json::Value, ToolError> {
257 self.call_function_in(
258 name,
259 args,
260 super::ToolContext::detached().with_cancel(cancel),
261 )
262 .await
263 }
264
265 async fn call_function_with_context_timeout(
266 &self,
267 name: &str,
268 args: serde_json::Value,
269 ctx: super::ToolContext,
270 timeout: Duration,
271 ) -> Result<serde_json::Value, ToolError> {
272 let func = self.function(name)?;
273 let cancel = ctx.cancel.clone();
274 tokio::select! {
275 biased;
276 () = cancel.cancelled() => Err(ToolError::Cancelled),
277 result = tokio::time::timeout(timeout, async {
278 self.ensure_confirmed(&func, &args).await?;
279 if cancel.is_cancelled() {
280 return Err(ToolError::Cancelled);
281 }
282 func.call_with_context(args, ctx).await
283 }) => result.unwrap_or(Err(ToolError::Timeout(timeout))),
284 }
285 }
286
287 pub fn build_response(
289 call: &FunctionCall,
290 result: Result<serde_json::Value, ToolError>,
291 ) -> FunctionResponse {
292 match result {
293 Ok(value) => FunctionResponse {
294 name: call.name.clone(),
295 response: value,
296 id: call.id.clone(),
297 scheduling: None,
298 },
299 Err(e) => FunctionResponse {
300 name: call.name.clone(),
301 response: serde_json::json!({"error": e.to_string()}),
302 id: call.id.clone(),
303 scheduling: None,
304 },
305 }
306 }
307
308 pub async fn cancel_streaming(&self, name: &str) {
310 let mut active = self.active.lock().await;
311 if let Some(tool) = active.remove(name) {
312 tool.cancel.cancel();
313 tool.task.abort();
314 }
315 }
316
317 pub(crate) async fn store_active(&self, id: String, tool: ActiveStreamingTool) {
319 self.active.lock().await.insert(id, tool);
320 }
321
322 pub async fn cancel_by_ids(&self, ids: &[String]) {
324 let mut active = self.active.lock().await;
325 for id in ids {
326 if let Some(tool) = active.remove(id.as_str()) {
327 tool.cancel.cancel();
328 tool.task.abort();
329 }
330 }
331 }
332
333 pub fn to_tool_declarations(&self) -> Vec<Tool> {
338 self.cached_declarations
339 .get_or_init(|| {
340 let declarations: Vec<FunctionDeclaration> = self
341 .tools
342 .values()
343 .map(|t| {
344 let (name, desc, params) = match t {
345 ToolKind::Function(f) => (f.name(), f.description(), f.parameters()),
346 ToolKind::Streaming(s) => (s.name(), s.description(), s.parameters()),
347 ToolKind::InputStream(i) => (i.name(), i.description(), i.parameters()),
348 };
349 FunctionDeclaration {
350 name: name.to_string(),
351 description: desc.to_string(),
352 parameters: params,
353 behavior: None,
354 }
355 })
356 .collect();
357
358 if declarations.is_empty() {
359 vec![]
360 } else {
361 vec![Tool::functions(declarations)]
362 }
363 })
364 .clone()
365 }
366
367 pub fn len(&self) -> usize {
369 self.tools.len()
370 }
371
372 pub fn is_empty(&self) -> bool {
374 self.tools.is_empty()
375 }
376}
377
378impl Default for ToolDispatcher {
379 fn default() -> Self {
380 Self::new()
381 }
382}
383
384impl gemini_genai_rs::prelude::ToolProvider for ToolDispatcher {
385 fn declarations(&self) -> Vec<gemini_genai_rs::prelude::Tool> {
386 self.to_tool_declarations()
387 }
388}
389
390#[cfg(test)]
391mod confirmation_tests {
392 use super::*;
393 use crate::confirmation::StaticConfirmation;
394 use crate::tool::{PolicyTool, SimpleTool, policy::ToolPolicy};
395 use serde_json::json;
396 use std::sync::atomic::{AtomicUsize, Ordering};
397
398 fn confirm_tool(runs: Arc<AtomicUsize>) -> Arc<dyn ToolFunction> {
400 let inner: Arc<dyn ToolFunction> = Arc::new(SimpleTool::new(
401 "danger",
402 "does something sensitive",
403 None,
404 move |_| {
405 let runs = runs.clone();
406 async move {
407 runs.fetch_add(1, Ordering::SeqCst);
408 Ok(json!({ "ok": true }))
409 }
410 },
411 ));
412 Arc::new(PolicyTool::new(
413 inner,
414 ToolPolicy::new().with_confirm(Some("delete production data?".into())),
415 ))
416 }
417
418 #[tokio::test]
419 async fn denied_confirmation_blocks_execution() {
420 let runs = Arc::new(AtomicUsize::new(0));
421 let mut d = ToolDispatcher::new();
422 d.register_function(confirm_tool(runs.clone()));
423 d.set_confirmation_provider(StaticConfirmation::deny_all("blocked by policy"));
424
425 let result = d.call_function("danger", json!({})).await;
426 assert!(
427 matches!(&result, Err(ToolError::Declined(reason)) if reason == "blocked by policy"),
428 "the model must learn why: {result:?}"
429 );
430 assert_eq!(
431 runs.load(Ordering::SeqCst),
432 0,
433 "tool must not run when denied"
434 );
435 }
436
437 #[tokio::test]
438 async fn approved_confirmation_runs() {
439 let runs = Arc::new(AtomicUsize::new(0));
440 let mut d = ToolDispatcher::new();
441 d.register_function(confirm_tool(runs.clone()));
442 d.set_confirmation_provider(StaticConfirmation::allow_all());
443
444 let out = d.call_function("danger", json!({})).await.unwrap();
445 assert_eq!(out["ok"], true);
446 assert_eq!(runs.load(Ordering::SeqCst), 1);
447 }
448
449 fn awaiting_confirmation(timeout: Duration) -> (ToolDispatcher, Arc<AtomicUsize>) {
450 let runs = Arc::new(AtomicUsize::new(0));
451 let mut dispatcher = ToolDispatcher::new().with_timeout(timeout);
452 dispatcher.register_function(confirm_tool(runs.clone()));
453 dispatcher.set_confirmation_provider(Arc::new(
454 |_: crate::confirmation::ConfirmationRequest| {
455 std::future::pending::<crate::confirmation::ToolConfirmation>()
456 },
457 ));
458 (dispatcher, runs)
459 }
460
461 #[tokio::test]
462 async fn session_cancellation_stops_pending_confirmation() {
463 let (dispatcher, runs) = awaiting_confirmation(Duration::from_secs(30));
464 let cancel = CancellationToken::new();
465 let ctx = crate::tool::ToolContext::detached().with_cancel(cancel.clone());
466 let mut call =
467 tokio_test::task::spawn(dispatcher.call_function_in("danger", json!({}), ctx));
468 tokio_test::assert_pending!(call.poll());
469
470 cancel.cancel();
471
472 assert!(matches!(
473 tokio_test::assert_ready!(call.poll()),
474 Err(ToolError::Cancelled)
475 ));
476 assert_eq!(runs.load(Ordering::SeqCst), 0);
477 }
478
479 #[tokio::test]
480 async fn token_cancellation_stops_pending_confirmation() {
481 let (dispatcher, runs) = awaiting_confirmation(Duration::from_secs(30));
482 let cancel = CancellationToken::new();
483 let mut call = tokio_test::task::spawn(dispatcher.call_function_with_cancel(
484 "danger",
485 json!({}),
486 cancel.clone(),
487 ));
488 tokio_test::assert_pending!(call.poll());
489
490 cancel.cancel();
491
492 assert!(matches!(
493 tokio_test::assert_ready!(call.poll()),
494 Err(ToolError::Cancelled)
495 ));
496 assert_eq!(runs.load(Ordering::SeqCst), 0);
497 }
498
499 async fn assert_confirmation_timeout(
500 call: impl std::future::Future<Output = Result<serde_json::Value, ToolError>>,
501 timeout: Duration,
502 ) {
503 let mut call = tokio_test::task::spawn(call);
504 tokio_test::assert_pending!(call.poll());
505 tokio::time::advance(timeout).await;
506 assert!(matches!(
507 tokio_test::assert_ready!(call.poll()),
508 Err(ToolError::Timeout(elapsed)) if elapsed == timeout
509 ));
510 }
511
512 #[tokio::test(start_paused = true)]
513 async fn session_timeout_includes_pending_confirmation() {
514 let timeout = Duration::from_secs(5);
515 let (dispatcher, runs) = awaiting_confirmation(timeout);
516 assert_confirmation_timeout(
517 dispatcher.call_function_in("danger", json!({}), crate::tool::ToolContext::detached()),
518 timeout,
519 )
520 .await;
521 assert_eq!(runs.load(Ordering::SeqCst), 0);
522 }
523
524 #[tokio::test(start_paused = true)]
525 async fn cancellable_call_timeout_includes_pending_confirmation() {
526 let timeout = Duration::from_secs(5);
527 let (dispatcher, runs) = awaiting_confirmation(timeout);
528 assert_confirmation_timeout(
529 dispatcher.call_function_with_cancel("danger", json!({}), CancellationToken::new()),
530 timeout,
531 )
532 .await;
533 assert_eq!(runs.load(Ordering::SeqCst), 0);
534 }
535
536 #[tokio::test(start_paused = true)]
537 async fn explicit_timeout_includes_pending_confirmation() {
538 let timeout = Duration::from_secs(5);
539 let (dispatcher, runs) = awaiting_confirmation(Duration::from_secs(30));
540 assert_confirmation_timeout(
541 dispatcher.call_function_with_timeout("danger", json!({}), timeout),
542 timeout,
543 )
544 .await;
545 assert_eq!(runs.load(Ordering::SeqCst), 0);
546 }
547
548 #[tokio::test]
549 async fn cancellation_during_approval_prevents_execution() {
550 let runs = Arc::new(AtomicUsize::new(0));
551 let cancel = CancellationToken::new();
552 let approval_cancel = cancel.clone();
553 let mut dispatcher = ToolDispatcher::new();
554 dispatcher.register_function(confirm_tool(runs.clone()));
555 dispatcher.set_confirmation_provider(Arc::new(
556 move |_: crate::confirmation::ConfirmationRequest| {
557 let cancel = approval_cancel.clone();
558 async move {
559 cancel.cancel();
560 crate::confirmation::ToolConfirmation::confirmed()
561 }
562 },
563 ));
564
565 let result = dispatcher
566 .call_function_in(
567 "danger",
568 json!({}),
569 crate::tool::ToolContext::detached().with_cancel(cancel),
570 )
571 .await;
572 assert!(matches!(result, Err(ToolError::Cancelled)));
573 assert_eq!(runs.load(Ordering::SeqCst), 0);
574 }
575
576 #[tokio::test]
577 async fn pending_confirmation_can_approve_execution() {
578 let runs = Arc::new(AtomicUsize::new(0));
579 let approve = Arc::new(tokio::sync::Notify::new());
580 let approval = approve.clone();
581 let mut dispatcher = ToolDispatcher::new();
582 dispatcher.register_function(confirm_tool(runs.clone()));
583 dispatcher.set_confirmation_provider(Arc::new(
584 move |_: crate::confirmation::ConfirmationRequest| {
585 let approve = approval.clone();
586 async move {
587 approve.notified().await;
588 crate::confirmation::ToolConfirmation::confirmed()
589 }
590 },
591 ));
592 let mut call = tokio_test::task::spawn(dispatcher.call_function_with_cancel(
593 "danger",
594 json!({}),
595 CancellationToken::new(),
596 ));
597 tokio_test::assert_pending!(call.poll());
598 assert_eq!(runs.load(Ordering::SeqCst), 0);
599
600 approve.notify_one();
601
602 let result = tokio_test::assert_ready!(call.poll()).unwrap();
603 assert_eq!(result["ok"], true);
604 assert_eq!(runs.load(Ordering::SeqCst), 1);
605 }
606
607 #[tokio::test(start_paused = true)]
608 async fn approval_and_execution_share_one_timeout_budget() {
609 let mut dispatcher = ToolDispatcher::new();
610 dispatcher.register(PolicyTool::new(
611 Arc::new(SimpleTool::new(
612 "danger",
613 "slow operation",
614 None,
615 |_| async {
616 tokio::time::sleep(Duration::from_secs(2)).await;
617 Ok(json!({ "ok": true }))
618 },
619 )),
620 ToolPolicy::new().with_confirm(None),
621 ));
622 dispatcher.set_confirmation_provider(Arc::new(
623 |_: crate::confirmation::ConfirmationRequest| async {
624 tokio::time::sleep(Duration::from_secs(4)).await;
625 crate::confirmation::ToolConfirmation::confirmed()
626 },
627 ));
628
629 let timeout = Duration::from_secs(5);
630 let started = tokio::time::Instant::now();
631 let result = dispatcher
632 .call_function_with_timeout("danger", json!({}), timeout)
633 .await;
634 assert!(matches!(result, Err(ToolError::Timeout(elapsed)) if elapsed == timeout));
635 assert_eq!(started.elapsed(), timeout);
636 }
637
638 #[tokio::test]
639 async fn no_provider_runs_optin() {
640 let runs = Arc::new(AtomicUsize::new(0));
642 let mut d = ToolDispatcher::new();
643 d.register_function(confirm_tool(runs.clone()));
644
645 let out = d.call_function("danger", json!({})).await.unwrap();
646 assert_eq!(out["ok"], true);
647 assert_eq!(runs.load(Ordering::SeqCst), 1);
648 }
649
650 #[tokio::test]
651 async fn provider_sees_request_and_ignores_non_gated_tools() {
652 let mut d = ToolDispatcher::new();
654 d.register(SimpleTool::new(
655 "plain",
656 "no confirmation",
657 None,
658 |_| async move { Ok(json!({ "ran": true })) },
659 ));
660 d.set_confirmation_provider(StaticConfirmation::deny_all("should not be consulted"));
661
662 let out = d.call_function("plain", json!({})).await.unwrap();
663 assert_eq!(out["ran"], true);
664 }
665
666 #[tokio::test]
667 async fn nested_policy_wrapper_does_not_bypass_confirmation() {
668 let runs = Arc::new(AtomicUsize::new(0));
671 let inner_confirm = confirm_tool(runs.clone()); let outer_cached: Arc<dyn ToolFunction> = Arc::new(PolicyTool::new(
673 inner_confirm,
674 ToolPolicy::new().with_cache(),
675 ));
676 assert!(
677 outer_cached.requires_confirmation(),
678 "must propagate through nesting"
679 );
680
681 let mut d = ToolDispatcher::new();
682 d.register_function(outer_cached);
683 d.set_confirmation_provider(StaticConfirmation::deny_all("blocked"));
684
685 let result = d.call_function("danger", json!({})).await;
686 assert!(matches!(result, Err(ToolError::Declined(_))));
687 assert_eq!(
688 runs.load(Ordering::SeqCst),
689 0,
690 "nested confirm must not run when denied"
691 );
692 }
693
694 #[tokio::test]
695 async fn closure_provider_can_gate_by_name() {
696 let runs = Arc::new(AtomicUsize::new(0));
697 let mut d = ToolDispatcher::new();
698 d.register_function(confirm_tool(runs.clone()));
699 d.set_confirmation_provider(Arc::new(
700 |req: crate::confirmation::ConfirmationRequest| async move {
701 if req.tool_name == "danger" {
702 crate::confirmation::ToolConfirmation::denied("name-gated")
703 } else {
704 crate::confirmation::ToolConfirmation::confirmed()
705 }
706 },
707 ));
708
709 assert!(d.call_function("danger", json!({})).await.is_err());
710 assert_eq!(runs.load(Ordering::SeqCst), 0);
711 }
712}
713
714#[cfg(test)]
715mod declaration_tests {
716 use super::*;
717 use crate::tool::SimpleTool;
718
719 fn named(name: &'static str) -> Arc<dyn ToolFunction> {
720 Arc::new(SimpleTool::new(name, name, None, |_| async {
721 Ok(serde_json::json!({}))
722 }))
723 }
724
725 fn declared(d: &ToolDispatcher) -> Vec<String> {
726 d.to_tool_declarations()
727 .iter()
728 .flat_map(|t| t.function_declarations.iter().flatten())
729 .map(|f| f.name.clone())
730 .collect()
731 }
732
733 #[test]
736 fn registering_a_tool_refreshes_the_declarations() {
737 let mut d = ToolDispatcher::new();
738 d.register_function(named("a"));
739 assert_eq!(declared(&d), ["a"]);
740 d.register_function(named("b"));
741 assert_eq!(declared(&d), ["a", "b"]);
742 }
743
744 #[test]
747 fn declarations_are_deterministic() {
748 let mut d = ToolDispatcher::new();
749 for name in ["zeta", "alpha", "mid"] {
750 d.register_function(named(name));
751 }
752 assert_eq!(declared(&d), ["alpha", "mid", "zeta"]);
753 assert_eq!(d.names().collect::<Vec<_>>(), ["alpha", "mid", "zeta"]);
754 }
755
756 #[test]
757 fn merge_keeps_both_and_this_dispatcher_wins_a_clash() {
758 let mut mine = ToolDispatcher::new().with_timeout(Duration::from_secs(3));
759 mine.register_function(Arc::new(SimpleTool::new(
760 "shared",
761 "mine",
762 None,
763 |_| async { Ok(serde_json::json!({})) },
764 )));
765 let mut theirs = ToolDispatcher::new();
766 theirs.register_function(named("shared"));
767 theirs.register_function(named("extra"));
768 mine.merge(theirs);
769 assert_eq!(declared(&mine), ["extra", "shared"]);
770 match mine.get_tool("shared") {
771 Some(ToolKind::Function(f)) => assert_eq!(f.description(), "mine"),
772 _ => panic!("shared must stay a function tool"),
773 }
774 assert_eq!(mine.default_timeout(), Duration::from_secs(3));
775 }
776}