1use futures::FutureExt;
11use indexmap::IndexMap;
12use std::any::Any;
13use std::collections::HashSet;
14use std::fmt;
15use std::panic::AssertUnwindSafe;
16use std::sync::Arc;
17
18use super::{
19 SharedState, Tool, ToolContext, ToolError, ToolNamespace, ToolResult, ToolSchema, ToolSource,
20};
21use crate::message::ToolCall;
22use crate::run::RunContext;
23
24fn panic_message(payload: &(dyn Any + Send)) -> String {
30 if let Some(s) = payload.downcast_ref::<&str>() {
31 return (*s).chars().take(500).collect();
32 }
33 if let Some(s) = payload.downcast_ref::<String>() {
34 return s.chars().take(500).collect();
35 }
36 "unknown panic".into()
37}
38
39#[derive(Clone, Default)]
106pub struct ToolRegistry {
107 tools: IndexMap<String, RegisteredTool>,
108}
109
110#[derive(Clone)]
111struct RegisteredTool {
112 tool: Arc<dyn Tool>,
113 source: Option<ToolSource>,
114}
115
116impl ToolRegistry {
117 pub fn new() -> Self {
119 Self {
120 tools: IndexMap::new(),
121 }
122 }
123
124 pub fn register(&mut self, tool: impl Tool + 'static) -> &mut Self {
130 self.tools.insert(
131 tool.schema().name,
132 RegisteredTool {
133 tool: Arc::new(tool),
134 source: None,
135 },
136 );
137 self
138 }
139
140 pub fn register_with_source(
154 &mut self,
155 tool: impl Tool + 'static,
156 source: ToolSource,
157 ) -> Result<&mut Self, RegistryError> {
158 let name = tool.schema().name;
159 if source.display_name != name {
160 return Err(RegistryError::SourceNameMismatch {
161 schema_name: name,
162 source_display_name: source.display_name,
163 });
164 }
165 if let Some(existing) = self.tools.get(&name) {
166 let existing_namespace = entry_namespace(existing);
167 if existing_namespace != source.namespace {
168 return Err(RegistryError::NameCollision {
169 name,
170 existing_namespace,
171 new_namespace: source.namespace,
172 });
173 }
174 }
175 self.tools.insert(
176 name,
177 RegisteredTool {
178 tool: Arc::new(tool),
179 source: Some(source),
180 },
181 );
182 Ok(self)
183 }
184
185 pub fn names(&self) -> Vec<String> {
188 self.tools.keys().cloned().collect()
189 }
190
191 pub fn remove(&mut self, name: &str) -> bool {
198 self.tools.shift_remove(name).is_some()
199 }
200
201 pub fn source(&self, display_name: &str) -> Option<&ToolSource> {
206 self.tools
207 .get(display_name)
208 .and_then(|entry| entry.source.as_ref())
209 }
210
211 pub fn names_in_namespace(&self, namespace: &ToolNamespace) -> Vec<String> {
216 self.tools
217 .iter()
218 .filter(|(_, entry)| entry_matches_namespace(entry, namespace))
219 .map(|(name, _)| name.clone())
220 .collect()
221 }
222
223 pub fn remove_namespace(&mut self, namespace: &ToolNamespace) -> Vec<String> {
229 let mut removed = Vec::new();
230 self.tools.retain(|name, entry| {
231 if entry_matches_namespace(entry, namespace) {
232 removed.push(name.clone());
233 false
234 } else {
235 true
236 }
237 });
238 removed
239 }
240
241 pub fn retain(&mut self, mut keep: impl FnMut(&str) -> bool) -> Vec<String> {
284 let mut removed = Vec::new();
285 self.tools.retain(|name, _| {
286 if keep(name) {
287 true
288 } else {
289 removed.push(name.clone());
290 false
291 }
292 });
293 removed
294 }
295
296 pub fn schemas(&self) -> Vec<ToolSchema> {
299 self.tools.values().map(schema_with_source).collect()
300 }
301
302 pub fn get(&self, name: &str) -> Option<&dyn Tool> {
306 self.tools.get(name).map(|entry| entry.tool.as_ref())
307 }
308
309 pub async fn call(
337 &self,
338 call: &ToolCall,
339 run: &RunContext,
340 state: &SharedState,
341 ) -> Result<ToolResult, RegistryError> {
342 let Some(entry) = self.tools.get(&call.name) else {
343 return Err(RegistryError::NotFound(call.name.clone()));
344 };
345 let args = match serde_json::from_str(&call.arguments) {
346 Ok(value) => value,
347 Err(e) => return Err(RegistryError::InvalidArguments(e.to_string())),
348 };
349 let context = ToolContext::new(run, state, &call.id, &call.name);
350 let result = AssertUnwindSafe(entry.tool.call(args, context))
354 .catch_unwind()
355 .await
356 .map_err(|payload| {
357 let message = panic_message(payload.as_ref());
361 RegistryError::Execution {
362 name: call.name.clone(),
363 source: ToolError::Execution(format!("panicked: {message}")),
364 }
365 })?;
366 result
374 .map_err(|e| match e {
375 ToolError::InvalidArguments(msg) => RegistryError::InvalidArguments(msg),
376 other => RegistryError::Execution {
377 name: call.name.clone(),
378 source: other,
379 },
380 })
381 .map(|result| match result {
382 ToolResult::Effect(request) => ToolResult::Effect(
383 request.with_source_if_missing(call.id.clone(), call.name.clone()),
384 ),
385 other => other,
386 })
387 }
388
389 pub async fn call_named(
395 &self,
396 name: impl Into<String>,
397 arguments: impl Into<String>,
398 run: &RunContext,
399 state: &SharedState,
400 ) -> Result<ToolResult, RegistryError> {
401 let name = name.into();
402 let call = ToolCall {
403 id: format!("call-{name}"),
404 name,
405 arguments: arguments.into(),
406 };
407 self.call(&call, run, state).await
408 }
409
410 pub fn subset(&self, names: &[&str]) -> Result<ToolRegistry, MissingTools> {
425 let wanted: HashSet<&str> = names.iter().copied().collect();
426 let mut tools = IndexMap::new();
427 let mut found = HashSet::new();
428 for (name, entry) in &self.tools {
429 if wanted.contains(name.as_str()) {
430 found.insert(name.clone());
431 tools.insert(name.clone(), entry.clone());
432 }
433 }
434 let missing: Vec<String> = names
435 .iter()
436 .filter(|n| !found.contains(**n))
437 .map(|n| (*n).to_string())
438 .collect();
439 if missing.is_empty() {
440 Ok(ToolRegistry { tools })
441 } else {
442 Err(MissingTools { names: missing })
443 }
444 }
445
446 pub fn subset_by_namespace(
457 &self,
458 namespace: &ToolNamespace,
459 ) -> Result<ToolRegistry, RegistryError> {
460 let mut tools = IndexMap::new();
461 for (name, entry) in &self.tools {
462 if entry_matches_namespace(entry, namespace) {
463 tools.insert(name.clone(), entry.clone());
464 }
465 }
466 Ok(ToolRegistry { tools })
467 }
468}
469
470impl fmt::Debug for ToolRegistry {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 f.debug_list().entries(self.names()).finish()
473 }
474}
475
476impl<T> Extend<T> for ToolRegistry
477where
478 T: Tool + 'static,
479{
480 fn extend<I>(&mut self, iter: I)
481 where
482 I: IntoIterator<Item = T>,
483 {
484 for tool in iter {
485 self.register(tool);
486 }
487 }
488}
489
490impl<T> FromIterator<T> for ToolRegistry
491where
492 T: Tool + 'static,
493{
494 fn from_iter<I>(iter: I) -> Self
495 where
496 I: IntoIterator<Item = T>,
497 {
498 let mut registry = Self::new();
499 registry.extend(iter);
500 registry
501 }
502}
503
504fn entry_namespace(entry: &RegisteredTool) -> ToolNamespace {
505 entry
506 .source
507 .as_ref()
508 .map(|source| source.namespace.clone())
509 .unwrap_or_else(ToolNamespace::local)
510}
511
512fn entry_matches_namespace(entry: &RegisteredTool, namespace: &ToolNamespace) -> bool {
513 entry_namespace(entry) == *namespace
514}
515
516fn schema_with_source(entry: &RegisteredTool) -> ToolSchema {
517 let mut schema = entry.tool.schema();
518 if let Some(source) = &entry.source
519 && let Ok(value) = serde_json::to_value(source)
520 {
521 schema.metadata.insert("tool_source".to_string(), value);
522 }
523 schema
524}
525
526#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
529#[error("tools not found in registry: {}", self.names.join(", "))]
530pub struct MissingTools {
531 names: Vec<String>,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
546#[non_exhaustive]
547pub enum RegistryError {
548 #[error("tool not found: {0}")]
550 NotFound(String),
551 #[error("invalid arguments: {0}")]
556 InvalidArguments(String),
557 #[error("tool error: {name} failed: {source}")]
566 Execution {
567 name: String,
569 #[source]
571 source: ToolError,
572 },
573 #[error(
576 "tool name collision: {name} already belongs to namespace {existing_namespace}, cannot register from namespace {new_namespace}"
577 )]
578 NameCollision {
579 name: String,
581 existing_namespace: ToolNamespace,
583 new_namespace: ToolNamespace,
585 },
586 #[error(
588 "tool source display name mismatch: schema name {schema_name}, source display name {source_display_name}"
589 )]
590 SourceNameMismatch {
591 schema_name: String,
593 source_display_name: String,
595 },
596}
597
598impl MissingTools {
599 pub fn names(&self) -> &[String] {
612 &self.names
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use crate::tool::{ToolError, ToolOutput, ToolSource, ToolTrustLevel};
620 use std::error::Error;
621 use std::sync::atomic::{AtomicUsize, Ordering};
622
623 struct FakeTool {
626 name: &'static str,
627 output: &'static str,
628 fail: bool,
629 }
630
631 #[async_trait::async_trait]
632 impl Tool for FakeTool {
633 fn schema(&self) -> ToolSchema {
634 ToolSchema::new(self.name, self.output, serde_json::json!({}))
635 }
636
637 async fn call(
638 &self,
639 _arguments: serde_json::Value,
640 _context: ToolContext<'_>,
641 ) -> Result<ToolResult, ToolError> {
642 if self.fail {
643 Err(ToolError::Execution("boom".into()))
644 } else {
645 Ok(ToolOutput::text(self.output).into())
646 }
647 }
648 }
649
650 struct CountingTool {
653 name: &'static str,
654 calls: Arc<AtomicUsize>,
655 }
656
657 #[async_trait::async_trait]
658 impl Tool for CountingTool {
659 fn schema(&self) -> ToolSchema {
660 ToolSchema::new(self.name, "counts calls", serde_json::json!({}))
661 }
662
663 async fn call(
664 &self,
665 _arguments: serde_json::Value,
666 _context: ToolContext<'_>,
667 ) -> Result<ToolResult, ToolError> {
668 self.calls.fetch_add(1, Ordering::Relaxed);
669 Ok(ToolOutput::text("ok").into())
670 }
671 }
672
673 fn echo(name: &'static str) -> FakeTool {
674 FakeTool {
675 name,
676 output: name,
677 fail: false,
678 }
679 }
680
681 fn registry() -> ToolRegistry {
684 let mut r = ToolRegistry::new();
685 r.register(echo("search"))
686 .register(echo("calculator"))
687 .register(echo("search"));
688 r
689 }
690
691 fn call(name: &str, arguments: &str) -> ToolCall {
692 ToolCall {
693 id: format!("call-{name}"),
694 name: name.into(),
695 arguments: arguments.into(),
696 }
697 }
698
699 async fn call_registry(
700 registry: &ToolRegistry,
701 name: &str,
702 arguments: &str,
703 state: &SharedState,
704 ) -> Result<ToolResult, RegistryError> {
705 registry
706 .call(&call(name, arguments), &RunContext::new("test-run"), state)
707 .await
708 }
709
710 #[test]
711 fn names_in_registration_order_dedup() {
712 assert_eq!(registry().names(), vec!["search", "calculator"]);
713 }
714
715 #[test]
716 fn schemas_in_registration_order() {
717 let schemas = registry().schemas();
718 assert_eq!(schemas.len(), 2);
719 assert_eq!(schemas[0].name, "search");
720 assert_eq!(schemas[1].name, "calculator");
721 }
722
723 #[test]
724 fn from_iter_and_extend_keep_registry_semantics() {
725 let mut registry: ToolRegistry = [echo("a"), echo("b"), echo("a")].into_iter().collect();
726 assert_eq!(registry.names(), vec!["a", "b"]);
727 assert_eq!(registry.schemas()[0].description, "a");
728
729 registry.extend([echo("c")]);
730 assert_eq!(registry.names(), vec!["a", "b", "c"]);
731 }
732
733 #[tokio::test]
734 async fn register_duplicate_replaces() {
735 let mut r = ToolRegistry::new();
736 r.register(FakeTool {
737 name: "a",
738 output: "first",
739 fail: false,
740 })
741 .register(FakeTool {
742 name: "a",
743 output: "second",
744 fail: false,
745 });
746 assert_eq!(r.names(), vec!["a"]);
747 assert_eq!(r.schemas()[0].description, "second");
748 assert_eq!(
749 call_registry(&r, "a", "{}", &SharedState::new())
750 .await
751 .unwrap(),
752 "second"
753 );
754 }
755
756 #[tokio::test]
757 async fn get_returns_tool_with_error_semantics() {
758 let mut r = ToolRegistry::new();
759 r.register(FakeTool {
760 name: "a",
761 output: "",
762 fail: true,
763 });
764 let tool = r.get("a").expect("registered");
767 let state = SharedState::new();
768 let run = RunContext::new("test-run");
769 let context = ToolContext {
770 run: &run,
771 state: &state,
772 tool_call_id: "call-a",
773 tool_name: "a",
774 };
775 let result = tool.call(serde_json::json!({}), context).await;
776 assert!(matches!(result, Err(ToolError::Execution(_))));
777 assert!(r.get("nope").is_none());
778 }
779
780 #[tokio::test]
781 async fn call_succeeds() {
782 assert_eq!(
783 registry()
784 .call(
785 &call("calculator", "{}"),
786 &RunContext::new("test-run"),
787 &SharedState::new()
788 )
789 .await
790 .unwrap(),
791 "calculator"
792 );
793 }
794
795 #[tokio::test]
796 async fn call_unknown_tool_returns_not_found() {
797 let err = registry()
800 .call(
801 &call("nope", "{}"),
802 &RunContext::new("test-run"),
803 &SharedState::new(),
804 )
805 .await
806 .unwrap_err();
807 assert!(matches!(&err, RegistryError::NotFound(name) if name == "nope"));
808 assert_eq!(err.to_string(), "tool not found: nope");
809 }
810
811 #[tokio::test]
812 async fn call_invalid_json_returns_invalid_arguments() {
813 let err = registry()
814 .call(
815 &call("calculator", "not-json"),
816 &RunContext::new("test-run"),
817 &SharedState::new(),
818 )
819 .await
820 .unwrap_err();
821 assert!(matches!(err, RegistryError::InvalidArguments(_)));
822 assert!(err.to_string().starts_with("invalid arguments:"));
823 }
824
825 #[tokio::test]
826 async fn call_structural_error_returns_invalid_arguments_not_execution() {
827 struct StrictTool;
832 #[async_trait::async_trait]
833 impl Tool for StrictTool {
834 fn schema(&self) -> ToolSchema {
835 ToolSchema::new(
836 "strict",
837 "requires a",
838 serde_json::json!({
839 "type": "object",
840 "properties": { "a": { "type": "integer" } },
841 "required": ["a"],
842 }),
843 )
844 }
845 async fn call(
846 &self,
847 arguments: serde_json::Value,
848 _context: ToolContext<'_>,
849 ) -> Result<ToolResult, ToolError> {
850 let a = arguments
851 .get("a")
852 .ok_or_else(|| ToolError::InvalidArguments("missing field `a`".into()))?;
853 Ok(ToolOutput::text(a.to_string()).into())
854 }
855 }
856
857 let mut r = ToolRegistry::new();
858 r.register(StrictTool);
859 let err = r
860 .call(
861 &call("strict", r#"{"b":1}"#),
862 &RunContext::new("test-run"),
863 &SharedState::new(),
864 )
865 .await
866 .unwrap_err();
867 assert!(matches!(&err, RegistryError::InvalidArguments(msg) if msg == "missing field `a`"));
868 assert_eq!(err.to_string(), "invalid arguments: missing field `a`");
869 }
870
871 #[tokio::test]
872 async fn call_execution_error_returns_execution_with_source() {
873 let mut r = ToolRegistry::new();
874 r.register(FakeTool {
875 name: "broken",
876 output: "",
877 fail: true,
878 });
879 let err = r
883 .call(
884 &call("broken", "{}"),
885 &RunContext::new("test-run"),
886 &SharedState::new(),
887 )
888 .await
889 .unwrap_err();
890 assert_eq!(
891 err.to_string(),
892 "tool error: broken failed: execution failed: boom"
893 );
894 assert!(matches!(err.source(), Some(e) if e.to_string() == "execution failed: boom"));
895 }
896
897 #[tokio::test]
900 async fn tool_panic_is_captured_as_execution_error() {
901 struct PanickingTool;
902 #[async_trait::async_trait]
903 impl Tool for PanickingTool {
904 fn schema(&self) -> ToolSchema {
905 ToolSchema::new("panic", "panics", serde_json::json!({}))
906 }
907 async fn call(
908 &self,
909 _arguments: serde_json::Value,
910 _context: ToolContext<'_>,
911 ) -> Result<ToolResult, ToolError> {
912 panic!("boom")
913 }
914 }
915
916 let mut r = ToolRegistry::new();
917 r.register(PanickingTool);
918 let err = r
919 .call(
920 &call("panic", "{}"),
921 &RunContext::new("test-run"),
922 &SharedState::new(),
923 )
924 .await
925 .unwrap_err();
926 assert!(
929 matches!(err, RegistryError::Execution { name, source: ToolError::Execution(msg) }
930 if name == "panic" && msg.contains("panicked") && msg.contains("boom"))
931 );
932 }
933
934 #[test]
935 fn retain_removes_by_prefix_in_registration_order() {
936 let mut r = ToolRegistry::new();
939 r.register(echo("fs__read"))
940 .register(echo("fs__write"))
941 .register(echo("calc"));
942 let removed = r.retain(|name| !name.starts_with("fs__"));
943 assert_eq!(removed, ["fs__read", "fs__write"]);
944 assert_eq!(r.names(), ["calc"]);
945 }
946
947 #[test]
948 fn retain_keep_all_returns_empty() {
949 let mut r = registry();
950 assert!(r.retain(|_| true).is_empty());
951 assert_eq!(r.names(), ["search", "calculator"]);
952 }
953
954 #[test]
955 fn register_with_source_tracks_namespace_and_metadata() {
956 let mut r = ToolRegistry::new();
957 let namespace = ToolNamespace::mcp_server("filesystem");
958 let source = ToolSource::new(namespace.clone(), "read_file", "filesystem__read_file")
959 .with_trust(ToolTrustLevel::External);
960 r.register_with_source(echo("filesystem__read_file"), source.clone())
961 .unwrap();
962
963 assert_eq!(r.source("filesystem__read_file"), Some(&source));
964 assert_eq!(
965 r.names_in_namespace(&namespace),
966 vec!["filesystem__read_file"]
967 );
968
969 let schema = r.schemas().remove(0);
970 assert_eq!(
971 schema.metadata["tool_source"]["namespace"]["id"],
972 serde_json::json!("filesystem")
973 );
974 assert_eq!(
975 schema.metadata["tool_source"]["raw_name"],
976 serde_json::json!("read_file")
977 );
978 }
979
980 #[test]
981 fn register_with_source_replaces_same_namespace_but_rejects_cross_namespace_collision() {
982 let mut r = ToolRegistry::new();
983 let first = ToolSource::new(ToolNamespace::mcp_server("one"), "search", "server__search");
984 let second_same =
985 ToolSource::new(ToolNamespace::mcp_server("one"), "search", "server__search");
986 let second_other =
987 ToolSource::new(ToolNamespace::mcp_server("two"), "search", "server__search");
988
989 r.register_with_source(echo("server__search"), first)
990 .unwrap();
991 r.register_with_source(
992 FakeTool {
993 name: "server__search",
994 output: "replacement",
995 fail: false,
996 },
997 second_same,
998 )
999 .unwrap();
1000
1001 let err = r
1002 .register_with_source(echo("server__search"), second_other)
1003 .unwrap_err();
1004 assert!(matches!(
1005 err,
1006 RegistryError::NameCollision {
1007 name,
1008 existing_namespace,
1009 new_namespace,
1010 } if name == "server__search"
1011 && existing_namespace == ToolNamespace::mcp_server("one")
1012 && new_namespace == ToolNamespace::mcp_server("two")
1013 ));
1014 }
1015
1016 #[test]
1017 fn source_display_name_must_match_schema_name() {
1018 let mut r = ToolRegistry::new();
1019 let err = r
1020 .register_with_source(
1021 echo("actual"),
1022 ToolSource::new(ToolNamespace::mcp_server("fs"), "raw", "different"),
1023 )
1024 .unwrap_err();
1025 assert!(matches!(
1026 err,
1027 RegistryError::SourceNameMismatch {
1028 schema_name,
1029 source_display_name,
1030 } if schema_name == "actual" && source_display_name == "different"
1031 ));
1032 }
1033
1034 #[test]
1035 fn remove_namespace_bulk_unloads_tools() {
1036 let mut r = ToolRegistry::new();
1037 let fs = ToolNamespace::mcp_server("fs");
1038 let db = ToolNamespace::mcp_server("db");
1039 r.register_with_source(
1040 echo("fs__read"),
1041 ToolSource::new(fs.clone(), "read", "fs__read"),
1042 )
1043 .unwrap()
1044 .register_with_source(
1045 echo("fs__write"),
1046 ToolSource::new(fs.clone(), "write", "fs__write"),
1047 )
1048 .unwrap()
1049 .register_with_source(
1050 echo("db__query"),
1051 ToolSource::new(db.clone(), "query", "db__query"),
1052 )
1053 .unwrap();
1054
1055 let sub = r.subset_by_namespace(&fs).unwrap();
1056 assert_eq!(sub.names(), ["fs__read", "fs__write"]);
1057
1058 let removed = r.remove_namespace(&fs);
1059 assert_eq!(removed, ["fs__read", "fs__write"]);
1060 assert_eq!(r.names(), ["db__query"]);
1061 assert_eq!(r.names_in_namespace(&db), ["db__query"]);
1062 }
1063
1064 #[test]
1065 fn subset_keeps_registration_order() {
1066 let sub = registry().subset(&["calculator", "search"]).unwrap();
1069 assert_eq!(sub.names(), vec!["search", "calculator"]);
1070 }
1071
1072 #[tokio::test]
1073 async fn subset_duplicate_name_takes_latest() {
1074 let mut r = ToolRegistry::new();
1075 r.register(FakeTool {
1076 name: "a",
1077 output: "first",
1078 fail: false,
1079 })
1080 .register(FakeTool {
1081 name: "a",
1082 output: "second",
1083 fail: false,
1084 });
1085 let sub = r.subset(&["a"]).unwrap();
1086 assert_eq!(sub.names(), vec!["a"]);
1087 assert_eq!(
1088 call_registry(&sub, "a", "{}", &SharedState::new())
1089 .await
1090 .unwrap(),
1091 "second"
1092 );
1093 }
1094
1095 #[test]
1096 fn subset_missing_names_error_with_list() {
1097 let err = registry()
1098 .subset(&["search", "nope", "calculator", "also-nope"])
1099 .unwrap_err();
1100 assert_eq!(err.names(), &["nope", "also-nope"]);
1101 }
1102
1103 #[tokio::test]
1104 async fn subset_shares_tool_instances() {
1105 let calls = Arc::new(AtomicUsize::new(0));
1106 let mut r = ToolRegistry::new();
1107 r.register(CountingTool {
1108 name: "counter",
1109 calls: calls.clone(),
1110 });
1111 let sub = r.subset(&["counter"]).unwrap();
1112 call_registry(&r, "counter", "{}", &SharedState::new())
1115 .await
1116 .unwrap();
1117 call_registry(&sub, "counter", "{}", &SharedState::new())
1118 .await
1119 .unwrap();
1120 assert_eq!(calls.load(Ordering::Relaxed), 2);
1121 }
1122
1123 #[tokio::test]
1126 async fn clone_shares_tool_instances() {
1127 let calls = Arc::new(AtomicUsize::new(0));
1128 let mut r = ToolRegistry::new();
1129 r.register(CountingTool {
1130 name: "counter",
1131 calls: calls.clone(),
1132 });
1133 let r2 = r.clone();
1134 call_registry(&r, "counter", "{}", &SharedState::new())
1135 .await
1136 .unwrap();
1137 call_registry(&r2, "counter", "{}", &SharedState::new())
1138 .await
1139 .unwrap();
1140 assert_eq!(calls.load(Ordering::Relaxed), 2);
1141 }
1142
1143 #[tokio::test]
1146 async fn call_passes_shared_state_to_tool() {
1147 struct StateTool;
1148 #[async_trait::async_trait]
1149 impl Tool for StateTool {
1150 fn schema(&self) -> ToolSchema {
1151 ToolSchema::new(
1152 "state_tool",
1153 "read and write shared state",
1154 serde_json::json!({}),
1155 )
1156 }
1157 async fn call(
1158 &self,
1159 _arguments: serde_json::Value,
1160 context: ToolContext<'_>,
1161 ) -> Result<ToolResult, ToolError> {
1162 let state = context.state;
1163 state.with_mut::<usize>(|n| *n += 1);
1164 Ok(ToolOutput::text(format!("count={}", state.get::<usize>().unwrap_or(0))).into())
1165 }
1166 }
1167
1168 let state = SharedState::new();
1169 state.insert(0usize);
1170 let mut r = ToolRegistry::new();
1171 r.register(StateTool);
1172
1173 assert_eq!(
1177 call_registry(&r, "state_tool", "{}", &state).await.unwrap(),
1178 "count=1"
1179 );
1180 assert_eq!(
1181 call_registry(&r, "state_tool", "{}", &state).await.unwrap(),
1182 "count=2"
1183 );
1184 }
1185}