1use async_trait::async_trait;
14use serde_json::Value;
15use std::collections::HashMap;
16use std::sync::Arc;
17use tracing::error;
18
19use crate::background::BackgroundExecutableTool;
20use crate::tool_types::{
21 BuiltinTool, DeferrablePolicy, ToolCall, ToolDefinition, ToolHints, ToolPolicy, ToolResult,
22 ToolResultImage,
23};
24use crate::{
25 tool_context::ToolContext, tool_context::ToolContextService, tool_context::ToolContextServices,
26};
27
28use crate::error::Result;
29use crate::tool_execution::ToolExecutor;
30#[derive(Debug)]
56pub enum ToolExecutionResult {
57 Success(Value),
59
60 SuccessWithImages {
64 result: Value,
65 images: Vec<ToolResultImage>,
66 },
67
68 ToolError(String),
73
74 InternalError(ToolInternalError),
80
81 ConnectionRequired {
88 provider: String,
90 },
91}
92
93impl ToolExecutionResult {
94 pub fn success(value: impl Into<Value>) -> Self {
96 ToolExecutionResult::Success(value.into())
97 }
98
99 pub fn success_with_raw_output(value: impl Into<Value>, raw_output: String) -> Self {
102 let mut value = value.into();
103 match value.as_object_mut() {
107 Some(obj) => {
108 obj.insert("_raw_output".to_string(), Value::String(raw_output));
109 }
110 None => {
111 value = serde_json::json!({
112 "_raw_output_scalar": value,
113 "_raw_output": raw_output,
114 });
115 }
116 }
117 ToolExecutionResult::Success(value)
118 }
119
120 pub fn success_with_images(value: impl Into<Value>, images: Vec<ToolResultImage>) -> Self {
122 ToolExecutionResult::SuccessWithImages {
123 result: value.into(),
124 images,
125 }
126 }
127
128 pub fn tool_error(message: impl Into<String>) -> Self {
130 ToolExecutionResult::ToolError(message.into())
131 }
132
133 pub fn internal_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
135 ToolExecutionResult::InternalError(ToolInternalError::new(error))
136 }
137
138 pub fn internal_error_msg(message: impl Into<String>) -> Self {
140 ToolExecutionResult::InternalError(ToolInternalError::from_message(message))
141 }
142
143 pub fn connection_required(provider: impl Into<String>) -> Self {
145 ToolExecutionResult::ConnectionRequired {
146 provider: provider.into(),
147 }
148 }
149
150 pub fn is_success(&self) -> bool {
152 matches!(
153 self,
154 ToolExecutionResult::Success(_) | ToolExecutionResult::SuccessWithImages { .. }
155 )
156 }
157
158 pub fn is_error(&self) -> bool {
160 matches!(
161 self,
162 ToolExecutionResult::ToolError(_) | ToolExecutionResult::InternalError(_)
163 )
164 }
165
166 pub fn is_connection_required(&self) -> bool {
168 matches!(self, ToolExecutionResult::ConnectionRequired { .. })
169 }
170
171 pub fn into_tool_result(self, tool_call_id: &str, tool_name: &str) -> ToolResult {
179 match self {
180 ToolExecutionResult::Success(mut value) => {
181 let raw_output = value
183 .as_object_mut()
184 .and_then(|obj| obj.remove("_raw_output"))
185 .and_then(|v| v.as_str().map(|s| s.to_string()));
186 let result_value = if let Some(obj) = value.as_object_mut() {
189 let is_scalar_carrier = raw_output.is_some()
190 && obj.len() == 1
191 && obj.contains_key("_raw_output_scalar");
192 if is_scalar_carrier {
193 obj.remove("_raw_output_scalar").unwrap_or(Value::Null)
194 } else {
195 value
196 }
197 } else {
198 value
199 };
200 ToolResult {
201 tool_call_id: tool_call_id.to_string(),
202 result: Some(result_value),
203 images: None,
204 error: None,
205 connection_required: None,
206 raw_output,
207 }
208 }
209 ToolExecutionResult::SuccessWithImages { result, images } => ToolResult {
210 tool_call_id: tool_call_id.to_string(),
211 result: Some(result),
212 images: if images.is_empty() {
213 None
214 } else {
215 Some(images)
216 },
217 error: None,
218 connection_required: None,
219 raw_output: None,
220 },
221 ToolExecutionResult::ToolError(message) => ToolResult {
222 tool_call_id: tool_call_id.to_string(),
223 result: Some(serde_json::json!({ "error": &message })),
224 images: None,
225 error: Some(message),
226 connection_required: None,
227 raw_output: None,
228 },
229 ToolExecutionResult::InternalError(err) => {
230 error!(
232 tool_name = %tool_name,
233 tool_call_id = %tool_call_id,
234 error = %err.message,
235 error_chain = %err.chain_string(),
236 "Tool internal error (details hidden from LLM)"
237 );
238
239 let generic_msg = "An internal error occurred while executing the tool";
241 ToolResult {
242 tool_call_id: tool_call_id.to_string(),
243 result: Some(serde_json::json!({
244 "error": generic_msg
245 })),
246 images: None,
247 error: Some(generic_msg.to_string()),
248 connection_required: None,
249 raw_output: None,
250 }
251 }
252 ToolExecutionResult::ConnectionRequired { ref provider } => ToolResult {
253 tool_call_id: tool_call_id.to_string(),
254 result: Some(serde_json::json!({
255 "connection_required": provider,
256 })),
257 images: None,
258 error: None,
259 connection_required: Some(provider.clone()),
260 raw_output: None,
261 },
262 }
263 }
264}
265
266#[derive(Debug)]
268pub struct ToolInternalError {
269 pub message: String,
271 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
273}
274
275impl ToolInternalError {
276 pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
278 Self {
279 message: error.to_string(),
280 source: Some(Box::new(error)),
281 }
282 }
283
284 pub fn from_message(message: impl Into<String>) -> Self {
286 Self {
287 message: message.into(),
288 source: None,
289 }
290 }
291
292 pub fn chain_string(&self) -> String {
293 let mut parts = vec![self.message.clone()];
294 let mut current = <Self as std::error::Error>::source(self);
295 while let Some(source) = current {
296 let message = source.to_string();
297 if parts.last() != Some(&message) {
298 parts.push(message);
299 }
300 current = source.source();
301 }
302 parts.join(": ")
303 }
304}
305
306impl std::fmt::Display for ToolInternalError {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 write!(f, "{}", self.message)
309 }
310}
311
312impl std::error::Error for ToolInternalError {
313 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
314 self.source
315 .as_ref()
316 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
317 }
318}
319
320#[async_trait]
369pub trait Tool: Send + Sync {
370 fn name(&self) -> &str;
375
376 fn display_name(&self) -> Option<&str> {
383 None
384 }
385
386 fn description(&self) -> &str;
391
392 fn parameters_schema(&self) -> Value;
398
399 async fn execute(&self, arguments: Value) -> ToolExecutionResult;
410
411 async fn execute_with_context(
428 &self,
429 arguments: Value,
430 _context: &ToolContext,
431 ) -> ToolExecutionResult {
432 self.execute(arguments).await
434 }
435
436 fn requires_context(&self) -> bool {
441 false
442 }
443
444 fn required_context_services(&self) -> &'static [ToolContextService] {
449 &[]
450 }
451
452 fn policy(&self) -> ToolPolicy {
457 ToolPolicy::Auto
458 }
459
460 fn hints(&self) -> ToolHints {
465 ToolHints::default()
466 }
467
468 fn narrate(
478 &self,
479 _tool_call: &crate::tool_types::ToolCall,
480 _phase: crate::tool_narration::ToolNarrationPhase,
481 _locale: Option<&str>,
482 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
483 ) -> Option<String> {
484 None
485 }
486
487 fn as_background_executable(&self) -> Option<&dyn BackgroundExecutableTool> {
490 None
491 }
492
493 fn deferrable_policy(&self) -> DeferrablePolicy {
498 DeferrablePolicy::default()
499 }
500
501 fn to_definition(&self) -> ToolDefinition {
506 ToolDefinition::Builtin(BuiltinTool {
507 name: self.name().to_string(),
508 display_name: self.display_name().map(|s| s.to_string()),
509 description: self.description().to_string(),
510 parameters: self.parameters_schema(),
511 policy: self.policy(),
512 category: None,
513 deferrable: self.deferrable_policy(),
514 hints: self.hints(),
515 full_parameters: None,
516 })
517 }
518}
519
520#[derive(Default, Clone)]
547pub struct ToolRegistry {
548 tools: HashMap<String, Arc<dyn Tool>>,
549}
550
551impl ToolRegistry {
552 pub fn new() -> Self {
554 Self {
555 tools: HashMap::new(),
556 }
557 }
558
559 pub fn with_defaults() -> Self {
569 use crate::progress_reporting::ReportProgressTool;
570
571 let builder = ToolRegistry::builder()
572 .tool(ReportProgressTool);
582
583 builder.build()
584 }
585
586 pub fn with_monitor_probe_defaults() -> Self {
594 Self::new()
595 }
596
597 pub fn register(&mut self, tool: impl Tool + 'static) {
601 self.tools.insert(tool.name().to_string(), Arc::new(tool));
602 }
603
604 pub fn register_boxed(&mut self, tool: Box<dyn Tool>) {
606 self.tools.insert(tool.name().to_string(), Arc::from(tool));
607 }
608
609 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) {
611 self.tools.insert(tool.name().to_string(), tool);
612 }
613
614 pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
616 self.tools.get(name)
617 }
618
619 pub fn has(&self, name: &str) -> bool {
621 self.tools.contains_key(name)
622 }
623
624 pub fn len(&self) -> usize {
626 self.tools.len()
627 }
628
629 pub fn is_empty(&self) -> bool {
631 self.tools.is_empty()
632 }
633
634 pub fn tool_names(&self) -> Vec<&str> {
636 self.tools.keys().map(|s| s.as_str()).collect()
637 }
638
639 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
644 self.tools.values().map(|t| t.to_definition()).collect()
645 }
646
647 pub fn validate_context_services(&self, services: &ToolContextServices) -> Result<()> {
650 let mut tools: Vec<_> = self.tools.values().collect();
651 tools.sort_by_key(|tool| tool.name());
652 for tool in tools {
653 for service in tool.required_context_services() {
654 if !services.provides(*service) {
655 return Err(crate::error::AgentLoopError::config(format!(
656 "tool \"{}\" requires unavailable ToolContext service {}",
657 tool.name(),
658 service.name(),
659 )));
660 }
661 }
662 }
663 Ok(())
664 }
665
666 pub fn unregister(&mut self, name: &str) -> Option<Arc<dyn Tool>> {
668 self.tools.remove(name)
669 }
670
671 pub fn clear(&mut self) {
673 self.tools.clear();
674 }
675
676 pub fn builder() -> ToolRegistryBuilder {
678 ToolRegistryBuilder::new()
679 }
680}
681
682impl std::fmt::Debug for ToolRegistry {
683 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684 f.debug_struct("ToolRegistry")
685 .field("tools", &self.tool_names())
686 .finish()
687 }
688}
689
690#[async_trait]
691impl ToolExecutor for ToolRegistry {
692 async fn execute(
693 &self,
694 tool_call: &ToolCall,
695 _tool_def: &ToolDefinition,
696 ) -> Result<ToolResult> {
697 let tool = self.tools.get(&tool_call.name).ok_or_else(|| {
698 crate::error::AgentLoopError::tool(format!("Tool not found: {}", tool_call.name))
699 })?;
700
701 let result = tool.execute(tool_call.arguments.clone()).await;
702 Ok(result.into_tool_result(&tool_call.id, &tool_call.name))
703 }
704
705 async fn execute_with_context(
706 &self,
707 tool_call: &ToolCall,
708 _tool_def: &ToolDefinition,
709 context: &ToolContext,
710 ) -> Result<ToolResult> {
711 let tool = self.tools.get(&tool_call.name).ok_or_else(|| {
712 crate::error::AgentLoopError::tool(format!("Tool not found: {}", tool_call.name))
713 })?;
714
715 let result = tool
718 .execute_with_context(tool_call.arguments.clone(), context)
719 .await;
720 Ok(result.into_tool_result(&tool_call.id, &tool_call.name))
721 }
722}
723
724pub struct ToolRegistryBuilder {
739 registry: ToolRegistry,
740}
741
742impl ToolRegistryBuilder {
743 pub fn new() -> Self {
745 Self {
746 registry: ToolRegistry::new(),
747 }
748 }
749
750 pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
752 self.registry.register(tool);
753 self
754 }
755
756 pub fn tool_boxed(mut self, tool: Box<dyn Tool>) -> Self {
758 self.registry.register_boxed(tool);
759 self
760 }
761
762 pub fn tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
764 self.registry.register_arc(tool);
765 self
766 }
767
768 pub fn build(self) -> ToolRegistry {
770 self.registry
771 }
772}
773
774impl Default for ToolRegistryBuilder {
775 fn default() -> Self {
776 Self::new()
777 }
778}
779
780#[cfg(test)]
786pub struct EchoTool;
787
788#[cfg(test)]
789#[async_trait]
790impl Tool for EchoTool {
791 fn name(&self) -> &str {
792 "echo"
793 }
794
795 fn display_name(&self) -> Option<&str> {
796 Some("Echo")
797 }
798
799 fn description(&self) -> &str {
800 "Echo back the provided message. Useful for testing tool execution."
801 }
802
803 fn parameters_schema(&self) -> Value {
804 serde_json::json!({
805 "type": "object",
806 "properties": {
807 "message": {
808 "type": "string",
809 "description": "The message to echo back"
810 }
811 },
812 "required": ["message"],
813 "additionalProperties": false
814 })
815 }
816
817 fn hints(&self) -> ToolHints {
818 ToolHints::default()
819 .with_readonly(true)
820 .with_idempotent(true)
821 }
822
823 async fn execute(&self, arguments: Value) -> ToolExecutionResult {
824 let message = arguments
825 .get("message")
826 .and_then(|v| v.as_str())
827 .unwrap_or("");
828
829 ToolExecutionResult::success(serde_json::json!({
830 "echoed": message,
831 "length": message.len()
832 }))
833 }
834}
835
836#[cfg(test)]
838pub struct FailingTool {
839 error_message: String,
840 use_internal_error: bool,
841}
842
843#[cfg(test)]
844impl FailingTool {
845 pub fn with_tool_error(message: impl Into<String>) -> Self {
847 Self {
848 error_message: message.into(),
849 use_internal_error: false,
850 }
851 }
852
853 pub fn with_internal_error(message: impl Into<String>) -> Self {
855 Self {
856 error_message: message.into(),
857 use_internal_error: true,
858 }
859 }
860}
861
862#[cfg(test)]
863impl Default for FailingTool {
864 fn default() -> Self {
865 Self::with_tool_error("Tool execution failed")
866 }
867}
868
869#[cfg(test)]
870#[async_trait]
871impl Tool for FailingTool {
872 fn name(&self) -> &str {
873 "failing_tool"
874 }
875
876 fn display_name(&self) -> Option<&str> {
877 Some("Failing Tool")
878 }
879
880 fn description(&self) -> &str {
881 "A tool that always fails (for testing error handling)"
882 }
883
884 fn parameters_schema(&self) -> Value {
885 serde_json::json!({
886 "type": "object",
887 "properties": {},
888 "additionalProperties": false
889 })
890 }
891
892 fn hints(&self) -> ToolHints {
893 ToolHints::default()
894 .with_readonly(true)
895 .with_idempotent(true)
896 }
897
898 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
899 if self.use_internal_error {
900 ToolExecutionResult::internal_error_msg(&self.error_message)
901 } else {
902 ToolExecutionResult::tool_error(&self.error_message)
903 }
904 }
905}
906
907#[cfg(test)]
912mod tests {
913 use super::*;
914
915 struct RequiresOrgId;
916
917 #[async_trait]
918 impl Tool for RequiresOrgId {
919 fn name(&self) -> &str {
920 "requires_org_id"
921 }
922
923 fn description(&self) -> &str {
924 "Exercises required ToolContext service validation"
925 }
926
927 fn parameters_schema(&self) -> Value {
928 serde_json::json!({"type": "object", "additionalProperties": false})
929 }
930
931 fn required_context_services(&self) -> &'static [ToolContextService] {
932 &[ToolContextService::OrgId]
933 }
934
935 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
936 ToolExecutionResult::success(Value::Null)
937 }
938 }
939
940 #[test]
941 fn required_context_service_validation_is_structured() {
942 let mut registry = ToolRegistry::new();
943 registry.register(RequiresOrgId);
944
945 let error = registry
946 .validate_context_services(&ToolContextServices::default())
947 .expect_err("missing required service must fail before tool exposure");
948
949 assert!(matches!(
950 error,
951 crate::AgentLoopError::Configuration(message)
952 if message.contains("requires_org_id") && message.contains("OrgId")
953 ));
954 }
955
956 #[test]
957 fn required_context_service_validation_accepts_supplied_service() {
958 let mut registry = ToolRegistry::new();
959 registry.register(RequiresOrgId);
960 let services = ToolContextServices {
961 org_id: Some(crate::typed_id::OrgId::from_seed(1)),
962 ..ToolContextServices::default()
963 };
964
965 registry
966 .validate_context_services(&services)
967 .expect("advertised required service should validate");
968 }
969
970 #[tokio::test]
971 async fn test_echo_tool() {
972 let tool = EchoTool;
973
974 let result = tool
975 .execute(serde_json::json!({"message": "Hello, world!"}))
976 .await;
977
978 if let ToolExecutionResult::Success(value) = result {
979 assert_eq!(
980 value.get("echoed").unwrap().as_str().unwrap(),
981 "Hello, world!"
982 );
983 assert_eq!(value.get("length").unwrap().as_u64().unwrap(), 13);
984 } else {
985 panic!("Expected success");
986 }
987 }
988
989 #[tokio::test]
990 async fn test_failing_tool_with_tool_error() {
991 let tool = FailingTool::with_tool_error("Something went wrong");
992
993 let result = tool.execute(serde_json::json!({})).await;
994
995 if let ToolExecutionResult::ToolError(msg) = result {
996 assert_eq!(msg, "Something went wrong");
997 } else {
998 panic!("Expected tool error");
999 }
1000 }
1001
1002 #[tokio::test]
1003 async fn test_failing_tool_with_internal_error() {
1004 let tool = FailingTool::with_internal_error("Database connection failed");
1005
1006 let result = tool.execute(serde_json::json!({})).await;
1007
1008 if let ToolExecutionResult::InternalError(err) = result {
1009 assert_eq!(err.message, "Database connection failed");
1010 } else {
1011 panic!("Expected internal error");
1012 }
1013 }
1014
1015 #[tokio::test]
1016 async fn test_tool_result_conversion() {
1017 let result = ToolExecutionResult::success(serde_json::json!({"value": 42}));
1019 let tool_result = result.into_tool_result("call_1", "test_tool");
1020 assert!(tool_result.error.is_none());
1021 assert_eq!(tool_result.result.unwrap()["value"], 42);
1022
1023 let result = ToolExecutionResult::tool_error("Invalid input");
1025 let tool_result = result.into_tool_result("call_2", "test_tool");
1026 assert_eq!(tool_result.error.as_deref(), Some("Invalid input"));
1027 assert_eq!(
1028 tool_result.result.unwrap(),
1029 serde_json::json!({"error": "Invalid input"})
1030 );
1031
1032 let result = ToolExecutionResult::internal_error_msg("Secret database error");
1034 let tool_result = result.into_tool_result("call_3", "test_tool");
1035 assert_eq!(
1036 tool_result.error.as_deref(),
1037 Some("An internal error occurred while executing the tool")
1038 );
1039 assert_eq!(
1040 tool_result.result.unwrap(),
1041 serde_json::json!({"error": "An internal error occurred while executing the tool"})
1042 );
1043 }
1044
1045 #[tokio::test]
1046 async fn test_tool_registry() {
1047 let mut registry = ToolRegistry::new();
1048 registry.register(EchoTool);
1049
1050 assert_eq!(registry.len(), 1);
1051 assert!(registry.has("echo"));
1052 assert!(!registry.has("nonexistent"));
1053
1054 let definitions = registry.tool_definitions();
1055 assert_eq!(definitions.len(), 1);
1056 }
1057
1058 #[tokio::test]
1059 async fn test_tool_registry_builder() {
1060 let registry = ToolRegistry::builder().tool(EchoTool).build();
1061
1062 assert_eq!(registry.len(), 1);
1063 }
1064
1065 #[test]
1066 fn test_tool_display_name_in_definition() {
1067 let tool = EchoTool;
1068 assert_eq!(tool.display_name(), Some("Echo"));
1069
1070 let def = tool.to_definition();
1071 assert_eq!(def.display_name(), Some("Echo"));
1072 }
1073
1074 #[test]
1075 fn test_success_with_raw_output_object_preserves_shape() {
1076 let res = ToolExecutionResult::success_with_raw_output(
1077 serde_json::json!({"stdout": "hello"}),
1078 "raw stdout bytes".to_string(),
1079 );
1080 let tr = res.into_tool_result("call_1", "demo");
1081 assert_eq!(tr.result.as_ref().unwrap()["stdout"], "hello");
1082 assert!(
1083 tr.result
1084 .as_ref()
1085 .unwrap()
1086 .as_object()
1087 .unwrap()
1088 .get("_raw_output")
1089 .is_none(),
1090 "sidecar key must not leak to the LLM-visible result"
1091 );
1092 assert_eq!(tr.raw_output.as_deref(), Some("raw stdout bytes"));
1093 }
1094
1095 #[test]
1096 fn test_success_with_raw_output_scalar_unwraps_to_string() {
1097 let res = ToolExecutionResult::success_with_raw_output(
1098 "compact summary".to_string(),
1099 "full output bytes".to_string(),
1100 );
1101 let tr = res.into_tool_result("call_1", "demo");
1102 assert_eq!(
1103 tr.result,
1104 Some(serde_json::Value::String("compact summary".into()))
1105 );
1106 assert_eq!(tr.raw_output.as_deref(), Some("full output bytes"));
1107 }
1108
1109 #[test]
1110 fn test_success_result_with_raw_output_scalar_key_is_not_unwrapped() {
1111 let res = ToolExecutionResult::success(
1112 serde_json::json!({"_raw_output_scalar": "user_value", "kept": true}),
1113 );
1114 let tr = res.into_tool_result("call_1", "demo");
1115 assert_eq!(
1116 tr.result,
1117 Some(serde_json::json!({"_raw_output_scalar": "user_value", "kept": true}))
1118 );
1119 assert_eq!(tr.raw_output, None);
1120 }
1121
1122 #[test]
1123 fn test_success_result_with_only_raw_output_scalar_key_is_not_unwrapped() {
1124 let res = ToolExecutionResult::success(serde_json::json!({"_raw_output_scalar": "v"}));
1127 let tr = res.into_tool_result("call_1", "demo");
1128 assert_eq!(
1129 tr.result,
1130 Some(serde_json::json!({"_raw_output_scalar": "v"}))
1131 );
1132 assert_eq!(tr.raw_output, None);
1133 }
1134
1135 #[test]
1136 fn test_echo_tool_display_name() {
1137 let tool = EchoTool;
1138 assert_eq!(tool.display_name(), Some("Echo"));
1139
1140 let def = tool.to_definition();
1141 assert_eq!(def.display_name(), Some("Echo"));
1142 }
1143
1144 #[test]
1145 fn test_all_default_tools_have_display_names() {
1146 let registry = ToolRegistry::with_defaults();
1147 let definitions = registry.tool_definitions();
1148
1149 for def in &definitions {
1150 assert!(
1151 def.display_name().is_some(),
1152 "Tool '{}' should have a display_name",
1153 def.name()
1154 );
1155 }
1156 }
1157
1158 #[tokio::test]
1159 async fn test_tool_registry_as_executor() {
1160 let mut registry = ToolRegistry::new();
1161 registry.register(EchoTool);
1162
1163 let tool_call = ToolCall {
1164 id: "call_1".to_string(),
1165 name: "echo".to_string(),
1166 arguments: serde_json::json!({"message": "test"}),
1167 };
1168
1169 let tool_def = registry.get("echo").unwrap().to_definition();
1170 let result = registry.execute(&tool_call, &tool_def).await.unwrap();
1171
1172 assert!(result.error.is_none());
1173 assert_eq!(result.result.unwrap()["echoed"], "test");
1174 }
1175
1176 #[test]
1177 fn test_tool_to_definition() {
1178 let tool = EchoTool;
1179 let def = tool.to_definition();
1180
1181 let ToolDefinition::Builtin(builtin) = def else {
1182 panic!("expected Builtin variant");
1183 };
1184 assert_eq!(builtin.name, "echo");
1185 assert_eq!(builtin.policy, ToolPolicy::Auto);
1186 }
1187
1188 #[test]
1189 fn test_with_defaults_has_expected_tools() {
1190 let registry = ToolRegistry::with_defaults();
1191
1192 assert!(
1196 !registry.has("spawn_background"),
1197 "spawn_background must NOT be in defaults — it comes from the \
1198 background_execution capability"
1199 );
1200 assert!(
1201 registry.has("report_progress"),
1202 "should have report_progress"
1203 );
1204
1205 assert!(!registry.has("add"), "add must NOT be in defaults");
1208 assert!(
1209 !registry.has("get_weather"),
1210 "get_weather must NOT be in defaults"
1211 );
1212
1213 for tool in ["read_file", "write_file", "bash", "web_fetch"] {
1215 assert!(!registry.has(tool), "`{tool}` must not be a core default");
1216 }
1217
1218 assert_eq!(registry.len(), 1, "should have one core default tool");
1219 }
1220
1221 #[tokio::test]
1222 async fn test_with_defaults_tools_are_executable() {
1223 let registry = ToolRegistry::with_defaults();
1224
1225 let tool_call = ToolCall {
1227 id: "call_1".to_string(),
1228 name: "report_progress".to_string(),
1229 arguments: serde_json::json!({
1230 "status": "completed",
1231 "summary": "Boundary audit complete"
1232 }),
1233 };
1234
1235 let tool_def = registry.get("report_progress").unwrap().to_definition();
1236 let result = registry.execute(&tool_call, &tool_def).await.unwrap();
1237
1238 assert!(result.error.is_none());
1239 assert_eq!(result.result.unwrap()["summary"], "Boundary audit complete");
1240 }
1241
1242 #[test]
1246 fn test_with_defaults_excludes_capability_only_tools() {
1247 let registry = ToolRegistry::with_defaults();
1248
1249 assert!(
1251 !registry.has("bash"),
1252 "bash must not be in defaults — it comes from bashkit_shell capability"
1253 );
1254 assert!(
1256 !registry.has("kv_store"),
1257 "kv_store must not be in defaults — it comes from session_storage capability"
1258 );
1259 assert!(
1263 !registry.has("spawn_background"),
1264 "spawn_background must not be in defaults — it comes from the \
1265 background_execution capability (auto-activated by tool hints)"
1266 );
1267 }
1268
1269 }