1pub mod builtin;
24mod extensions;
25mod result;
26pub mod server;
27
28pub use extensions::{MissingExtension, ToolCallExtensions, ToolResultExtensions};
29pub use result::{
30 ToolExecutionResult, ToolFailure, ToolFailureKind, ToolOutcome, ToolReturn, ToolReturnOutcome,
31};
32use std::collections::HashMap;
33use std::fmt;
34use std::sync::Arc;
35
36use futures::Future;
37use indexmap::IndexMap;
38use serde::{Deserialize, Serialize};
39
40use crate::{
41 completion::{self, ToolDefinition},
42 embeddings::{embed::EmbedError, tool::ToolSchema},
43 wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync},
44};
45
46#[derive(Debug, thiserror::Error)]
47pub enum ToolError {
48 #[cfg(not(target_family = "wasm"))]
49 ToolCallError(#[from] Box<dyn std::error::Error + Send + Sync>),
51
52 #[cfg(target_family = "wasm")]
53 ToolCallError(#[from] Box<dyn std::error::Error>),
55 JsonError(#[from] serde_json::Error),
57}
58
59impl fmt::Display for ToolError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 ToolError::ToolCallError(e) => {
63 let error_str = e.to_string();
64 if error_str.starts_with("ToolCallError: ") {
67 write!(f, "{}", error_str)
68 } else {
69 write!(f, "ToolCallError: {}", error_str)
70 }
71 }
72 ToolError::JsonError(e) => write!(f, "JsonError: {e}"),
73 }
74 }
75}
76
77pub trait Tool: Sized + WasmCompatSend + WasmCompatSync {
134 const NAME: &'static str;
137
138 type Error: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
140 type Args: for<'a> Deserialize<'a> + WasmCompatSend + WasmCompatSync;
142 type Output: Serialize;
144
145 fn name(&self) -> String {
147 Self::NAME.to_string()
148 }
149
150 fn description(&self) -> String;
152
153 fn parameters(&self) -> serde_json::Value;
155
156 fn call(
160 &self,
161 args: Self::Args,
162 ) -> impl Future<Output = Result<Self::Output, Self::Error>> + WasmCompatSend;
163
164 fn call_with_extensions(
182 &self,
183 args: Self::Args,
184 _extensions: &ToolCallExtensions,
185 ) -> impl Future<Output = Result<Self::Output, Self::Error>> + WasmCompatSend {
186 self.call(args)
187 }
188
189 fn classify_error(&self, error: &Self::Error) -> ToolFailure {
211 ToolFailure::other(error.to_string())
212 }
213
214 fn call_structured(
237 &self,
238 args: Self::Args,
239 extensions: &ToolCallExtensions,
240 ) -> impl Future<Output = Result<ToolReturn<Self::Output>, Self::Error>> + WasmCompatSend {
241 async move {
242 self.call_with_extensions(args, extensions)
243 .await
244 .map(ToolReturn::success)
245 }
246 }
247}
248
249pub trait ToolEmbedding: Tool {
251 type InitError: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
253
254 type Context: for<'a> Deserialize<'a> + Serialize;
259
260 type State: WasmCompatSend;
264
265 fn embedding_docs(&self) -> Vec<String>;
269
270 fn context(&self) -> Self::Context;
272
273 fn init(state: Self::State, context: Self::Context) -> Result<Self, Self::InitError>;
275}
276
277pub trait ToolDyn: WasmCompatSend + WasmCompatSync {
282 fn name(&self) -> String;
284
285 fn description(&self) -> String;
287
288 fn parameters(&self) -> serde_json::Value;
290
291 fn call<'a>(&'a self, args: String) -> WasmBoxedFuture<'a, Result<String, ToolError>>;
293
294 fn call_with_extensions<'a>(
300 &'a self,
301 args: String,
302 _extensions: &'a ToolCallExtensions,
303 ) -> WasmBoxedFuture<'a, Result<String, ToolError>> {
304 self.call(args)
305 }
306
307 fn call_structured<'a>(
326 &'a self,
327 args: String,
328 extensions: &'a ToolCallExtensions,
329 ) -> WasmBoxedFuture<'a, ToolExecutionResult> {
330 Box::pin(async move {
331 match self.call_with_extensions(args, extensions).await {
332 Ok(model_output) => ToolExecutionResult::success(model_output),
333 Err(err) => tool_error_to_execution_result(err),
334 }
335 })
336 }
337}
338
339fn serialize_tool_output(output: impl Serialize) -> serde_json::Result<String> {
340 match serde_json::to_value(output)? {
341 serde_json::Value::String(text) => Ok(text),
342 value => Ok(value.to_string()),
343 }
344}
345
346fn parse_tool_args<A>(args: &str) -> serde_json::Result<A>
355where
356 A: for<'de> Deserialize<'de>,
357{
358 match serde_json::from_str(args) {
359 Ok(parsed) => Ok(parsed),
360 Err(err) if args.trim() == "null" => serde_json::from_str("{}").map_err(|_| err),
361 Err(err) => Err(err),
362 }
363}
364
365fn tool_error_to_execution_result(err: ToolError) -> ToolExecutionResult {
370 let message = err.to_string();
371 let failure = match err {
372 ToolError::JsonError(_) => ToolFailure::invalid_args(message.clone()),
373 ToolError::ToolCallError(_) => ToolFailure::other(message.clone()),
374 };
375 ToolExecutionResult::failed(message, failure)
376}
377
378pub fn tool_definition(tool: &dyn ToolDyn) -> ToolDefinition {
384 tool_definition_with_name(tool.name(), tool)
385}
386
387pub(crate) fn tool_definition_with_name(
388 name: impl Into<String>,
389 tool: &dyn ToolDyn,
390) -> ToolDefinition {
391 ToolDefinition {
392 name: name.into(),
393 description: tool.description(),
394 parameters: tool.parameters(),
395 }
396}
397
398impl<T: Tool> ToolDyn for T {
399 fn name(&self) -> String {
400 <Self as Tool>::name(self)
401 }
402
403 fn description(&self) -> String {
404 <Self as Tool>::description(self)
405 }
406
407 fn parameters(&self) -> serde_json::Value {
408 <Self as Tool>::parameters(self)
409 }
410
411 fn call<'a>(&'a self, args: String) -> WasmBoxedFuture<'a, Result<String, ToolError>> {
412 ToolDyn::call_with_extensions(self, args, &ToolCallExtensions::EMPTY)
413 }
414
415 fn call_with_extensions<'a>(
416 &'a self,
417 args: String,
418 extensions: &'a ToolCallExtensions,
419 ) -> WasmBoxedFuture<'a, Result<String, ToolError>> {
420 Box::pin(async move {
421 match parse_tool_args::<T::Args>(&args) {
422 Ok(args) => <Self as Tool>::call_with_extensions(self, args, extensions)
423 .await
424 .map_err(|e| ToolError::ToolCallError(Box::new(e)))
425 .and_then(|output| serialize_tool_output(output).map_err(ToolError::JsonError)),
426 Err(e) => Err(ToolError::JsonError(e)),
427 }
428 })
429 }
430
431 fn call_structured<'a>(
438 &'a self,
439 args: String,
440 extensions: &'a ToolCallExtensions,
441 ) -> WasmBoxedFuture<'a, ToolExecutionResult> {
442 Box::pin(async move {
443 let parsed = match parse_tool_args::<T::Args>(&args) {
444 Ok(parsed) => parsed,
445 Err(err) => {
446 return ToolExecutionResult::failed(
447 format!("failed to parse tool arguments: {err}"),
448 ToolFailure::invalid_args(err.to_string()),
449 );
450 }
451 };
452 match <Self as Tool>::call_structured(self, parsed, extensions).await {
453 Ok(tool_return) => tool_return.into_execution_result(),
454 Err(err) => {
455 let failure = self.classify_error(&err);
456 ToolExecutionResult::failed(err.to_string(), failure)
457 }
458 }
459 })
460 }
461}
462
463#[cfg(feature = "rmcp")]
464#[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
465pub mod rmcp;
466
467pub trait ToolEmbeddingDyn: ToolDyn {
469 fn context(&self) -> serde_json::Result<serde_json::Value>;
471
472 fn embedding_docs(&self) -> Vec<String>;
474}
475
476impl<T> ToolEmbeddingDyn for T
477where
478 T: ToolEmbedding + 'static,
479{
480 fn context(&self) -> serde_json::Result<serde_json::Value> {
481 serde_json::to_value(self.context())
482 }
483
484 fn embedding_docs(&self) -> Vec<String> {
485 self.embedding_docs()
486 }
487}
488
489#[derive(Clone)]
490pub(crate) enum ToolType {
491 Simple(Arc<dyn ToolDyn>),
492 Embedding(Arc<dyn ToolEmbeddingDyn>),
493}
494
495impl ToolType {
496 pub fn name(&self) -> String {
497 match self {
498 ToolType::Simple(tool) => tool.name(),
499 ToolType::Embedding(tool) => tool.name(),
500 }
501 }
502
503 pub fn definition_with_name(&self, name: impl Into<String>) -> ToolDefinition {
504 match self {
505 ToolType::Simple(tool) => tool_definition_with_name(name, &**tool),
506 ToolType::Embedding(tool) => tool_definition_with_name(name, &**tool),
507 }
508 }
509
510 pub async fn call_with_extensions(
511 &self,
512 args: String,
513 extensions: &ToolCallExtensions,
514 ) -> Result<String, ToolError> {
515 match self {
516 ToolType::Simple(tool) => tool.call_with_extensions(args, extensions).await,
517 ToolType::Embedding(tool) => tool.call_with_extensions(args, extensions).await,
518 }
519 }
520
521 pub async fn call_structured(
523 &self,
524 args: String,
525 extensions: &ToolCallExtensions,
526 ) -> ToolExecutionResult {
527 match self {
528 ToolType::Simple(tool) => tool.call_structured(args, extensions).await,
529 ToolType::Embedding(tool) => tool.call_structured(args, extensions).await,
530 }
531 }
532}
533
534#[derive(Debug, thiserror::Error)]
535pub enum ToolSetError {
536 #[error("ToolCallError: {0}")]
538 ToolCallError(#[from] ToolError),
539
540 #[error("ToolNotFoundError: {0}")]
542 ToolNotFoundError(String),
543
544 #[error("JsonError: {0}")]
546 JsonError(#[from] serde_json::Error),
547
548 #[error("Tool call interrupted")]
550 Interrupted,
551}
552
553#[derive(Default)]
560pub struct ToolSet {
561 pub(crate) tools: IndexMap<String, ToolType>,
562}
563
564impl ToolSet {
565 pub fn from_tools(tools: Vec<impl ToolDyn + 'static>) -> Self {
567 let mut toolset = Self::default();
568 tools.into_iter().for_each(|tool| {
569 toolset.add_tool(tool);
570 });
571 toolset
572 }
573
574 pub fn from_tools_boxed(tools: Vec<Box<dyn ToolDyn + 'static>>) -> Self {
576 let mut toolset = Self::default();
577 tools.into_iter().for_each(|tool| {
578 toolset.add_tool_boxed(tool);
579 });
580 toolset
581 }
582
583 pub fn builder() -> ToolSetBuilder {
585 ToolSetBuilder::default()
586 }
587
588 pub fn contains(&self, toolname: &str) -> bool {
590 self.tools.contains_key(toolname)
591 }
592
593 pub fn add_tool(&mut self, tool: impl ToolDyn + 'static) -> String {
595 self.insert(ToolType::Simple(Arc::new(tool)))
596 }
597
598 pub fn add_tool_boxed(&mut self, tool: Box<dyn ToolDyn>) -> String {
601 self.insert(ToolType::Simple(Arc::from(tool)))
602 }
603
604 pub(crate) fn insert(&mut self, tool: ToolType) -> String {
605 let name = tool.name();
606 self.insert_with_name(name, tool)
607 }
608
609 fn insert_with_name(&mut self, name: String, tool: ToolType) -> String {
610 if self.tools.insert(name.clone(), tool).is_some() {
614 tracing::warn!(
615 tool_name = %name,
616 "a tool named {name:?} was already registered; replacing it with the new registration"
617 );
618 }
619 name
620 }
621
622 pub fn delete_tool(&mut self, tool_name: &str) {
624 self.tools.shift_remove(tool_name);
627 }
628
629 pub fn add_tools(&mut self, toolset: ToolSet) {
632 for (name, tool) in toolset.tools {
633 self.insert_with_name(name, tool);
634 }
635 }
636
637 pub(crate) fn get(&self, toolname: &str) -> Option<&ToolType> {
638 self.tools.get(toolname)
639 }
640
641 pub(crate) fn ordered_names(&self) -> impl Iterator<Item = &String> {
643 self.tools.keys()
644 }
645
646 fn ordered_entries(&self) -> impl Iterator<Item = (&String, &ToolType)> {
648 self.tools.iter()
649 }
650
651 pub fn get_tool_definitions(&self) -> Result<Vec<ToolDefinition>, ToolSetError> {
654 Ok(self
655 .ordered_entries()
656 .map(|(name, tool)| tool.definition_with_name(name.clone()))
657 .collect::<Vec<_>>())
658 }
659
660 pub async fn call(&self, toolname: &str, args: String) -> Result<String, ToolSetError> {
662 self.call_with_extensions(toolname, args, &ToolCallExtensions::EMPTY)
663 .await
664 }
665
666 pub async fn call_with_extensions(
672 &self,
673 toolname: &str,
674 args: String,
675 extensions: &ToolCallExtensions,
676 ) -> Result<String, ToolSetError> {
677 if let Some(tool) = self.tools.get(toolname) {
678 tracing::debug!(target: "rig",
679 "Calling tool {toolname} with args:\n{}",
680 args
681 );
682 Ok(tool.call_with_extensions(args, extensions).await?)
683 } else {
684 Err(ToolSetError::ToolNotFoundError(toolname.to_string()))
685 }
686 }
687
688 pub async fn call_structured(
696 &self,
697 toolname: &str,
698 args: String,
699 extensions: &ToolCallExtensions,
700 ) -> ToolExecutionResult {
701 match self.tools.get(toolname) {
702 Some(tool) => {
703 tracing::debug!(target: "rig", "Calling tool {toolname} with args:\n{args}");
704 tool.call_structured(args, extensions).await
705 }
706 None => ToolExecutionResult::failed(
707 format!("tool `{toolname}` not found"),
708 ToolFailure::not_found(format!("no tool named `{toolname}` is registered")),
709 ),
710 }
711 }
712
713 pub async fn documents(&self) -> Result<Vec<completion::Document>, ToolSetError> {
715 let mut docs = Vec::new();
716 for (name, tool) in self.ordered_entries() {
717 let definition = tool.definition_with_name(name.clone());
718 docs.push(completion::Document {
719 id: name.clone(),
720 text: format!(
721 "\
722 Tool: {}\n\
723 Definition: \n\
724 {}\
725 ",
726 name,
727 serde_json::to_string_pretty(&definition)?
728 ),
729 additional_props: HashMap::new(),
730 });
731 }
732 Ok(docs)
733 }
734
735 pub fn schemas(&self) -> Result<Vec<ToolSchema>, EmbedError> {
739 self.ordered_entries()
740 .filter_map(|(name, tool_type)| {
741 if let ToolType::Embedding(tool) = tool_type {
742 Some(ToolSchema::from_tool(name.clone(), &**tool))
743 } else {
744 None
745 }
746 })
747 .collect::<Result<Vec<_>, _>>()
748 }
749}
750
751#[derive(Default)]
752pub struct ToolSetBuilder {
754 tools: Vec<ToolType>,
755}
756
757impl ToolSetBuilder {
758 pub fn static_tool(mut self, tool: impl ToolDyn + 'static) -> Self {
760 self.tools.push(ToolType::Simple(Arc::new(tool)));
761 self
762 }
763
764 pub fn dynamic_tool(mut self, tool: impl ToolEmbeddingDyn + 'static) -> Self {
766 self.tools.push(ToolType::Embedding(Arc::new(tool)));
767 self
768 }
769
770 pub fn build(self) -> ToolSet {
772 let mut toolset = ToolSet::default();
773 for tool in self.tools {
774 toolset.insert(tool);
775 }
776 toolset
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use crate::message::{DocumentSourceKind, ToolResultContent};
783 use crate::test_utils::{
784 MockExampleTool, MockImageOutputTool, MockObjectOutputTool, MockStringOutputTool,
785 MockToolError, mock_math_toolset,
786 };
787 use serde_json::json;
788
789 use super::*;
790
791 fn get_test_toolset() -> ToolSet {
792 mock_math_toolset()
793 }
794
795 #[test]
796 fn test_get_tool_definitions() {
797 let toolset = get_test_toolset();
798 let tools = toolset.get_tool_definitions().unwrap();
799 assert_eq!(tools.len(), 2);
800 assert_eq!(
801 tools
802 .iter()
803 .map(|tool| tool.name.as_str())
804 .collect::<Vec<_>>(),
805 vec!["add", "subtract"],
806 "provider definitions must use registered tool names in order"
807 );
808 assert!(tools.iter().all(|tool| !tool.description.is_empty()));
809 assert!(tools.iter().all(|tool| tool.parameters.is_object()));
810 }
811
812 #[test]
813 fn test_tool_deletion() {
814 let mut toolset = get_test_toolset();
815 assert_eq!(toolset.tools.len(), 2);
816 toolset.delete_tool("add");
817 assert!(!toolset.contains("add"));
818 assert_eq!(toolset.tools.len(), 1);
819 assert_eq!(
820 toolset.ordered_names().cloned().collect::<Vec<_>>(),
821 vec!["subtract".to_string()]
822 );
823 }
824
825 #[test]
826 fn deleting_a_middle_tool_preserves_order_of_survivors() {
827 let mut toolset = ToolSet::default();
832 for name in ["alpha", "beta", "gamma", "delta"] {
833 toolset.add_tool(named_tool(name, "test tool"));
834 }
835
836 toolset.delete_tool("beta");
837
838 assert_eq!(
839 toolset.ordered_names().cloned().collect::<Vec<_>>(),
840 vec![
841 "alpha".to_string(),
842 "gamma".to_string(),
843 "delta".to_string()
844 ],
845 "survivors must keep their registration order after a middle deletion"
846 );
847 }
848
849 struct NamedTool {
852 name: String,
853 description: String,
854 }
855
856 impl ToolDyn for NamedTool {
857 fn name(&self) -> String {
858 self.name.clone()
859 }
860
861 fn description(&self) -> String {
862 self.description.clone()
863 }
864
865 fn parameters(&self) -> serde_json::Value {
866 json!({ "type": "object", "properties": {} })
867 }
868
869 fn call(&self, _args: String) -> WasmBoxedFuture<'_, Result<String, ToolError>> {
870 let output = format!("called {}", self.description);
871 Box::pin(async move { Ok(output) })
872 }
873 }
874
875 fn named_tool(name: &str, description: &str) -> NamedTool {
876 NamedTool {
877 name: name.to_string(),
878 description: description.to_string(),
879 }
880 }
881
882 #[test]
883 fn tool_definition_uses_flattened_dyn_metadata() {
884 let tool = named_tool("alpha", "runtime description");
885 let definition = tool_definition(&tool);
886
887 assert_eq!(definition.name, "alpha");
888 assert_eq!(definition.description, "runtime description");
889 assert_eq!(definition.parameters["type"], "object");
890 }
891
892 #[tokio::test]
893 async fn tool_definitions_follow_registration_order() {
894 let names: Vec<String> = (0..32).map(|i| format!("tool_{i:02}")).collect();
898 let mut toolset = ToolSet::default();
899 for name in &names {
900 toolset.add_tool(named_tool(name, "test tool"));
901 }
902
903 let defs = toolset.get_tool_definitions().unwrap();
904 let def_names: Vec<String> = defs.into_iter().map(|def| def.name).collect();
905 assert_eq!(def_names, names);
906
907 let docs = toolset.documents().await.unwrap();
908 let doc_ids: Vec<String> = docs.into_iter().map(|doc| doc.id).collect();
909 assert_eq!(doc_ids, names);
910 }
911
912 #[tokio::test]
913 async fn registered_name_is_definition_source_of_truth() {
914 use std::sync::atomic::{AtomicUsize, Ordering};
915
916 struct ChangingNameTool {
917 calls: AtomicUsize,
918 }
919
920 impl ToolDyn for ChangingNameTool {
921 fn name(&self) -> String {
922 match self.calls.fetch_add(1, Ordering::SeqCst) {
923 0 => "registered".to_string(),
924 _ => "changed".to_string(),
925 }
926 }
927
928 fn description(&self) -> String {
929 "changes name after registration".to_string()
930 }
931
932 fn parameters(&self) -> serde_json::Value {
933 json!({ "type": "object", "properties": {} })
934 }
935
936 fn call(&self, _args: String) -> WasmBoxedFuture<'_, Result<String, ToolError>> {
937 Box::pin(async { Ok("ok".to_string()) })
938 }
939 }
940
941 let mut toolset = ToolSet::default();
942 toolset.add_tool(ChangingNameTool {
943 calls: AtomicUsize::new(0),
944 });
945
946 let defs = toolset.get_tool_definitions().unwrap();
947 assert_eq!(defs[0].name, "registered");
948
949 let docs = toolset.documents().await.unwrap();
950 assert_eq!(docs[0].id, "registered");
951 assert!(docs[0].text.contains("registered"));
952 assert!(!docs[0].text.contains("changed"));
953 }
954
955 #[test]
956 fn dynamic_tool_schemas_use_registered_name() {
957 use std::sync::atomic::{AtomicUsize, Ordering};
958
959 #[derive(Debug, thiserror::Error)]
960 #[error("init error")]
961 struct InitError;
962
963 struct ChangingDynamicTool {
964 calls: AtomicUsize,
965 }
966
967 impl Tool for ChangingDynamicTool {
968 const NAME: &'static str = "unused";
969 type Error = MockToolError;
970 type Args = serde_json::Value;
971 type Output = String;
972
973 fn name(&self) -> String {
974 match self.calls.fetch_add(1, Ordering::SeqCst) {
975 0 => "registered_dynamic".to_string(),
976 _ => "changed_dynamic".to_string(),
977 }
978 }
979
980 fn description(&self) -> String {
981 "dynamic tool".to_string()
982 }
983
984 fn parameters(&self) -> serde_json::Value {
985 json!({ "type": "object", "properties": {} })
986 }
987
988 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
989 Ok("ok".to_string())
990 }
991 }
992
993 impl ToolEmbedding for ChangingDynamicTool {
994 type InitError = InitError;
995 type Context = ();
996 type State = ();
997
998 fn embedding_docs(&self) -> Vec<String> {
999 vec!["dynamic tool docs".to_string()]
1000 }
1001
1002 fn context(&self) -> Self::Context {}
1003
1004 fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> {
1005 Ok(Self {
1006 calls: AtomicUsize::new(0),
1007 })
1008 }
1009 }
1010
1011 let toolset = ToolSet::builder()
1012 .dynamic_tool(ChangingDynamicTool {
1013 calls: AtomicUsize::new(0),
1014 })
1015 .build();
1016
1017 let schemas = toolset.schemas().unwrap();
1018 assert_eq!(schemas.len(), 1);
1019 assert_eq!(schemas[0].name, "registered_dynamic");
1020 assert_eq!(schemas[0].embedding_docs, vec!["dynamic tool docs"]);
1021 }
1022
1023 #[tokio::test]
1024 async fn duplicate_registration_replaces_in_place() {
1025 let mut toolset = ToolSet::default();
1026 toolset.add_tool(named_tool("alpha", "first alpha"));
1027 toolset.add_tool(named_tool("beta", "beta"));
1028 toolset.add_tool(named_tool("alpha", "second alpha"));
1029
1030 let defs = toolset.get_tool_definitions().unwrap();
1031 assert_eq!(
1032 defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>(),
1033 vec!["alpha", "beta"],
1034 "the duplicate should be deduped and keep its original position"
1035 );
1036 assert_eq!(
1037 defs[0].description, "second alpha",
1038 "the last registration should win"
1039 );
1040
1041 let output = toolset.call("alpha", "{}".to_string()).await.unwrap();
1042 assert_eq!(output, "called second alpha");
1043 }
1044
1045 #[tokio::test]
1046 async fn add_tools_merges_in_order_and_replaces_existing() {
1047 let mut base = ToolSet::default();
1048 base.add_tool(named_tool("alpha", "base alpha"));
1049 base.add_tool(named_tool("beta", "base beta"));
1050
1051 let mut incoming = ToolSet::default();
1052 incoming.add_tool(named_tool("gamma", "incoming gamma"));
1053 incoming.add_tool(named_tool("alpha", "incoming alpha"));
1054
1055 base.add_tools(incoming);
1056
1057 let defs = base.get_tool_definitions().unwrap();
1058 assert_eq!(
1059 defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>(),
1060 vec!["alpha", "beta", "gamma"],
1061 "merged tools should follow registration order with replaced names keeping position"
1062 );
1063 assert_eq!(defs[0].description, "incoming alpha");
1064 }
1065
1066 #[tokio::test]
1067 async fn string_tool_outputs_are_preserved_verbatim() {
1068 let mut toolset = ToolSet::default();
1069 toolset.add_tool(MockStringOutputTool);
1070
1071 let output = toolset
1072 .call("string_output", "{}".to_string())
1073 .await
1074 .expect("tool should succeed");
1075
1076 assert_eq!(output, "Hello\nWorld");
1077 }
1078
1079 #[tokio::test]
1080 async fn structured_string_tool_outputs_remain_parseable() {
1081 let mut toolset = ToolSet::default();
1082 toolset.add_tool(MockImageOutputTool);
1083
1084 let output = toolset
1085 .call("image_output", "{}".to_string())
1086 .await
1087 .expect("tool should succeed");
1088 let content = ToolResultContent::from_tool_output(output);
1089
1090 assert_eq!(content.len(), 1);
1091 match content.first() {
1092 ToolResultContent::Image(image) => {
1093 assert!(matches!(image.data, DocumentSourceKind::Base64(_)));
1094 assert_eq!(image.media_type, Some(crate::message::ImageMediaType::PNG));
1095 }
1096 other => panic!("expected image tool result content, got {other:?}"),
1097 }
1098 }
1099
1100 #[tokio::test]
1101 async fn object_tool_outputs_still_serialize_as_json() {
1102 let mut toolset = ToolSet::default();
1103 toolset.add_tool(MockObjectOutputTool);
1104
1105 let output = toolset
1106 .call("object_output", "{}".to_string())
1107 .await
1108 .expect("tool should succeed");
1109
1110 assert!(output.starts_with('{'));
1111 assert_eq!(
1112 serde_json::from_str::<serde_json::Value>(&output).unwrap(),
1113 json!({
1114 "status": "ok",
1115 "count": 42
1116 })
1117 );
1118 }
1119
1120 #[tokio::test]
1121 async fn null_args_are_preserved_for_unit_args() {
1122 let mut toolset = ToolSet::default();
1123 toolset.add_tool(MockExampleTool);
1124
1125 let output = toolset
1126 .call("example_tool", "null".to_string())
1127 .await
1128 .expect("unit args should accept null without object fallback");
1129
1130 assert_eq!(output, "Example answer");
1131 }
1132
1133 #[tokio::test]
1138 async fn null_args_are_normalized_to_empty_object() {
1139 use crate::test_utils::MockToolError;
1140
1141 #[derive(serde::Deserialize, serde::Serialize)]
1142 struct NoRequiredArgs {
1143 label: Option<String>,
1144 }
1145
1146 struct NoArgTool;
1147
1148 impl Tool for NoArgTool {
1149 const NAME: &'static str = "no_arg_tool";
1150 type Error = MockToolError;
1151 type Args = NoRequiredArgs;
1152 type Output = String;
1153
1154 fn description(&self) -> String {
1155 "Tool with no required arguments".to_string()
1156 }
1157
1158 fn parameters(&self) -> serde_json::Value {
1159 json!({"type": "object", "properties": {}})
1160 }
1161
1162 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
1163 Ok(args.label.unwrap_or_else(|| "default".to_string()))
1164 }
1165 }
1166
1167 let mut toolset = ToolSet::default();
1168 toolset.add_tool(NoArgTool);
1169
1170 let output = toolset
1173 .call("no_arg_tool", "null".to_string())
1174 .await
1175 .expect("null args should succeed after normalisation");
1176
1177 assert_eq!(output, "default");
1178 }
1179}