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::{AgentLoopError, 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 if !obj.contains_key("_raw_output") && !obj.contains_key("_raw_output_scalar") =>
109 {
110 obj.insert("_raw_output".to_string(), Value::String(raw_output));
111 }
112 _ => {
113 value = serde_json::json!({
116 "_raw_output_scalar": value,
117 "_raw_output": raw_output,
118 });
119 }
120 }
121 ToolExecutionResult::Success(value)
122 }
123
124 pub fn success_with_images(value: impl Into<Value>, images: Vec<ToolResultImage>) -> Self {
126 ToolExecutionResult::SuccessWithImages {
127 result: value.into(),
128 images,
129 }
130 }
131
132 pub fn tool_error(message: impl Into<String>) -> Self {
134 ToolExecutionResult::ToolError(message.into())
135 }
136
137 pub fn internal_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
139 ToolExecutionResult::InternalError(ToolInternalError::new(error))
140 }
141
142 pub fn internal_error_msg(message: impl Into<String>) -> Self {
144 ToolExecutionResult::InternalError(ToolInternalError::from_message(message))
145 }
146
147 pub fn connection_required(provider: impl Into<String>) -> Self {
149 ToolExecutionResult::ConnectionRequired {
150 provider: provider.into(),
151 }
152 }
153
154 pub fn is_success(&self) -> bool {
156 matches!(
157 self,
158 ToolExecutionResult::Success(_) | ToolExecutionResult::SuccessWithImages { .. }
159 )
160 }
161
162 pub fn is_error(&self) -> bool {
164 matches!(
165 self,
166 ToolExecutionResult::ToolError(_) | ToolExecutionResult::InternalError(_)
167 )
168 }
169
170 pub fn is_connection_required(&self) -> bool {
172 matches!(self, ToolExecutionResult::ConnectionRequired { .. })
173 }
174
175 pub fn into_tool_result(self, tool_call_id: &str, tool_name: &str) -> ToolResult {
183 match self {
184 ToolExecutionResult::Success(mut value) => {
185 let raw_output = value
187 .as_object_mut()
188 .and_then(|obj| obj.remove("_raw_output"))
189 .and_then(|v| v.as_str().map(|s| s.to_string()));
190 let result_value = if let Some(obj) = value.as_object_mut() {
193 let is_scalar_carrier = raw_output.is_some()
194 && obj.len() == 1
195 && obj.contains_key("_raw_output_scalar");
196 if is_scalar_carrier {
197 obj.remove("_raw_output_scalar").unwrap_or(Value::Null)
198 } else {
199 value
200 }
201 } else {
202 value
203 };
204 ToolResult {
205 tool_call_id: tool_call_id.to_string(),
206 result: Some(result_value),
207 images: None,
208 error: None,
209 connection_required: None,
210 raw_output,
211 }
212 }
213 ToolExecutionResult::SuccessWithImages { result, images } => ToolResult {
214 tool_call_id: tool_call_id.to_string(),
215 result: Some(result),
216 images: if images.is_empty() {
217 None
218 } else {
219 Some(images)
220 },
221 error: None,
222 connection_required: None,
223 raw_output: None,
224 },
225 ToolExecutionResult::ToolError(message) => ToolResult {
226 tool_call_id: tool_call_id.to_string(),
227 result: Some(serde_json::json!({ "error": &message })),
228 images: None,
229 error: Some(message),
230 connection_required: None,
231 raw_output: None,
232 },
233 ToolExecutionResult::InternalError(err) => {
234 error!(
236 tool_name = %tool_name,
237 tool_call_id = %tool_call_id,
238 error = %err.message,
239 error_chain = %err.chain_string(),
240 "Tool internal error (details hidden from LLM)"
241 );
242
243 let generic_msg = "An internal error occurred while executing the tool";
245 ToolResult {
246 tool_call_id: tool_call_id.to_string(),
247 result: Some(serde_json::json!({
248 "error": generic_msg
249 })),
250 images: None,
251 error: Some(generic_msg.to_string()),
252 connection_required: None,
253 raw_output: None,
254 }
255 }
256 ToolExecutionResult::ConnectionRequired { ref provider } => ToolResult {
257 tool_call_id: tool_call_id.to_string(),
258 result: Some(serde_json::json!({
259 "connection_required": provider,
260 })),
261 images: None,
262 error: None,
263 connection_required: Some(provider.clone()),
264 raw_output: None,
265 },
266 }
267 }
268}
269
270#[derive(Debug)]
272pub struct ToolInternalError {
273 pub message: String,
275 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
277}
278
279impl ToolInternalError {
280 pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
282 Self {
283 message: error.to_string(),
284 source: Some(Box::new(error)),
285 }
286 }
287
288 pub fn from_message(message: impl Into<String>) -> Self {
290 Self {
291 message: message.into(),
292 source: None,
293 }
294 }
295
296 pub fn chain_string(&self) -> String {
297 let mut parts = vec![self.message.clone()];
298 let mut current = <Self as std::error::Error>::source(self);
299 while let Some(source) = current {
300 let message = source.to_string();
301 if parts.last() != Some(&message) {
302 parts.push(message);
303 }
304 current = source.source();
305 }
306 parts.join(": ")
307 }
308}
309
310impl std::fmt::Display for ToolInternalError {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 write!(f, "{}", self.message)
313 }
314}
315
316impl std::error::Error for ToolInternalError {
317 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
318 self.source
319 .as_ref()
320 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
321 }
322}
323
324#[async_trait]
373pub trait Tool: Send + Sync {
374 fn name(&self) -> &str;
379
380 fn display_name(&self) -> Option<&str> {
387 None
388 }
389
390 fn description(&self) -> &str;
395
396 fn parameters_schema(&self) -> Value;
402
403 async fn execute(&self, arguments: Value) -> ToolExecutionResult;
414
415 async fn execute_with_context(
432 &self,
433 arguments: Value,
434 _context: &ToolContext,
435 ) -> ToolExecutionResult {
436 self.execute(arguments).await
438 }
439
440 fn requires_context(&self) -> bool {
445 false
446 }
447
448 fn required_context_services(&self) -> &'static [ToolContextService] {
453 &[]
454 }
455
456 fn policy(&self) -> ToolPolicy {
461 ToolPolicy::Auto
462 }
463
464 fn hints(&self) -> ToolHints {
469 ToolHints::default()
470 }
471
472 fn narrate(
482 &self,
483 _tool_call: &crate::tool_types::ToolCall,
484 _phase: crate::tool_narration::ToolNarrationPhase,
485 _locale: Option<&str>,
486 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
487 ) -> Option<String> {
488 None
489 }
490
491 fn as_background_executable(&self) -> Option<&dyn BackgroundExecutableTool> {
494 None
495 }
496
497 fn deferrable_policy(&self) -> DeferrablePolicy {
502 DeferrablePolicy::default()
503 }
504
505 fn to_definition(&self) -> ToolDefinition {
510 ToolDefinition::Builtin(BuiltinTool {
511 name: self.name().to_string(),
512 display_name: self.display_name().map(|s| s.to_string()),
513 description: self.description().to_string(),
514 parameters: self.parameters_schema(),
515 policy: self.policy(),
516 category: None,
517 deferrable: self.deferrable_policy(),
518 hints: self.hints(),
519 full_parameters: None,
520 })
521 }
522}
523
524#[derive(Default, Clone)]
551pub struct ToolRegistry {
552 tools: HashMap<String, Arc<dyn Tool>>,
553}
554
555impl ToolRegistry {
556 pub fn new() -> Self {
558 Self {
559 tools: HashMap::new(),
560 }
561 }
562
563 pub fn with_defaults() -> Self {
573 use crate::progress_reporting::ReportProgressTool;
574
575 let builder = ToolRegistry::builder()
576 .tool(ReportProgressTool);
586
587 builder.build()
588 }
589
590 pub fn with_monitor_probe_defaults() -> Self {
598 Self::new()
599 }
600
601 pub fn register(&mut self, tool: impl Tool + 'static) {
605 self.tools.insert(tool.name().to_string(), Arc::new(tool));
606 }
607
608 pub fn register_boxed(&mut self, tool: Box<dyn Tool>) {
610 self.tools.insert(tool.name().to_string(), Arc::from(tool));
611 }
612
613 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) {
615 self.tools.insert(tool.name().to_string(), tool);
616 }
617
618 pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
620 self.tools.get(name)
621 }
622
623 pub fn has(&self, name: &str) -> bool {
625 self.tools.contains_key(name)
626 }
627
628 pub fn len(&self) -> usize {
630 self.tools.len()
631 }
632
633 pub fn is_empty(&self) -> bool {
635 self.tools.is_empty()
636 }
637
638 pub fn tool_names(&self) -> Vec<&str> {
640 self.tools.keys().map(|s| s.as_str()).collect()
641 }
642
643 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
648 self.tools.values().map(|t| t.to_definition()).collect()
649 }
650
651 pub fn validate_context_services(&self, services: &ToolContextServices) -> Result<()> {
654 let mut tools: Vec<_> = self.tools.values().collect();
655 tools.sort_by_key(|tool| tool.name());
656 for tool in tools {
657 for service in tool.required_context_services() {
658 if !services.provides(*service) {
659 return Err(crate::error::AgentLoopError::config(format!(
660 "tool \"{}\" requires unavailable ToolContext service {}",
661 tool.name(),
662 service.name(),
663 )));
664 }
665 }
666 }
667 Ok(())
668 }
669
670 pub fn unregister(&mut self, name: &str) -> Option<Arc<dyn Tool>> {
672 self.tools.remove(name)
673 }
674
675 pub fn clear(&mut self) {
677 self.tools.clear();
678 }
679
680 pub fn builder() -> ToolRegistryBuilder {
682 ToolRegistryBuilder::new()
683 }
684}
685
686impl std::fmt::Debug for ToolRegistry {
687 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
688 f.debug_struct("ToolRegistry")
689 .field("tools", &self.tool_names())
690 .finish()
691 }
692}
693
694fn validate_tool_arguments(tool: &dyn Tool, tool_call: &ToolCall) -> Result<Option<String>> {
695 let arguments = tool_call.execution_arguments();
696 let definition = tool.to_definition();
697 let validator = jsonschema::validator_for(definition.parameters()).map_err(|error| {
698 AgentLoopError::config(format!(
699 "Tool '{}' has an invalid parameters schema: {error}",
700 tool_call.name
701 ))
702 })?;
703 let issues: Vec<_> = validator
704 .iter_errors(&arguments)
705 .map(|error| {
706 serde_json::json!({
707 "instance_path": error.instance_path().to_string(),
708 "message": error.to_string(),
709 "schema_path": error.schema_path().to_string(),
710 })
711 })
712 .collect();
713 if issues.is_empty() {
714 return Ok(None);
715 }
716
717 Ok(Some(
718 serde_json::json!({
719 "code": "invalid_tool_arguments",
720 "tool": tool_call.name,
721 "issues": issues,
722 })
723 .to_string(),
724 ))
725}
726
727#[async_trait]
728impl ToolExecutor for ToolRegistry {
729 async fn execute(
730 &self,
731 tool_call: &ToolCall,
732 _tool_def: &ToolDefinition,
733 ) -> Result<ToolResult> {
734 let tool = self.tools.get(&tool_call.name).ok_or_else(|| {
735 crate::error::AgentLoopError::tool(format!("Tool not found: {}", tool_call.name))
736 })?;
737
738 if let Some(error) = validate_tool_arguments(tool.as_ref(), tool_call)? {
739 return Ok(ToolExecutionResult::tool_error(error)
740 .into_tool_result(&tool_call.id, &tool_call.name));
741 }
742
743 let result = tool.execute(tool_call.execution_arguments()).await;
744 Ok(result.into_tool_result(&tool_call.id, &tool_call.name))
745 }
746
747 async fn execute_with_context(
748 &self,
749 tool_call: &ToolCall,
750 _tool_def: &ToolDefinition,
751 context: &ToolContext,
752 ) -> Result<ToolResult> {
753 let tool = self.tools.get(&tool_call.name).ok_or_else(|| {
754 crate::error::AgentLoopError::tool(format!("Tool not found: {}", tool_call.name))
755 })?;
756
757 if let Some(error) = validate_tool_arguments(tool.as_ref(), tool_call)? {
758 return Ok(ToolExecutionResult::tool_error(error)
759 .into_tool_result(&tool_call.id, &tool_call.name));
760 }
761
762 let result = tool
764 .execute_with_context(tool_call.execution_arguments(), context)
765 .await;
766 Ok(result.into_tool_result(&tool_call.id, &tool_call.name))
767 }
768}
769
770pub struct ToolRegistryBuilder {
785 registry: ToolRegistry,
786}
787
788impl ToolRegistryBuilder {
789 pub fn new() -> Self {
791 Self {
792 registry: ToolRegistry::new(),
793 }
794 }
795
796 pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
798 self.registry.register(tool);
799 self
800 }
801
802 pub fn tool_boxed(mut self, tool: Box<dyn Tool>) -> Self {
804 self.registry.register_boxed(tool);
805 self
806 }
807
808 pub fn tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
810 self.registry.register_arc(tool);
811 self
812 }
813
814 pub fn build(self) -> ToolRegistry {
816 self.registry
817 }
818}
819
820impl Default for ToolRegistryBuilder {
821 fn default() -> Self {
822 Self::new()
823 }
824}
825
826#[cfg(test)]
832pub struct EchoTool;
833
834#[cfg(test)]
835#[async_trait]
836impl Tool for EchoTool {
837 fn name(&self) -> &str {
838 "echo"
839 }
840
841 fn display_name(&self) -> Option<&str> {
842 Some("Echo")
843 }
844
845 fn description(&self) -> &str {
846 "Echo back the provided message. Useful for testing tool execution."
847 }
848
849 fn parameters_schema(&self) -> Value {
850 serde_json::json!({
851 "type": "object",
852 "properties": {
853 "message": {
854 "type": "string",
855 "description": "The message to echo back"
856 }
857 },
858 "required": ["message"],
859 "additionalProperties": false
860 })
861 }
862
863 fn hints(&self) -> ToolHints {
864 ToolHints::default()
865 .with_readonly(true)
866 .with_idempotent(true)
867 }
868
869 async fn execute(&self, arguments: Value) -> ToolExecutionResult {
870 let message = arguments
871 .get("message")
872 .and_then(|v| v.as_str())
873 .unwrap_or("");
874
875 ToolExecutionResult::success(serde_json::json!({
876 "echoed": message,
877 "length": message.len()
878 }))
879 }
880}
881
882#[cfg(test)]
884pub struct FailingTool {
885 error_message: String,
886 use_internal_error: bool,
887}
888
889#[cfg(test)]
890impl FailingTool {
891 pub fn with_tool_error(message: impl Into<String>) -> Self {
893 Self {
894 error_message: message.into(),
895 use_internal_error: false,
896 }
897 }
898
899 pub fn with_internal_error(message: impl Into<String>) -> Self {
901 Self {
902 error_message: message.into(),
903 use_internal_error: true,
904 }
905 }
906}
907
908#[cfg(test)]
909impl Default for FailingTool {
910 fn default() -> Self {
911 Self::with_tool_error("Tool execution failed")
912 }
913}
914
915#[cfg(test)]
916#[async_trait]
917impl Tool for FailingTool {
918 fn name(&self) -> &str {
919 "failing_tool"
920 }
921
922 fn display_name(&self) -> Option<&str> {
923 Some("Failing Tool")
924 }
925
926 fn description(&self) -> &str {
927 "A tool that always fails (for testing error handling)"
928 }
929
930 fn parameters_schema(&self) -> Value {
931 serde_json::json!({
932 "type": "object",
933 "properties": {},
934 "additionalProperties": false
935 })
936 }
937
938 fn hints(&self) -> ToolHints {
939 ToolHints::default()
940 .with_readonly(true)
941 .with_idempotent(true)
942 }
943
944 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
945 if self.use_internal_error {
946 ToolExecutionResult::internal_error_msg(&self.error_message)
947 } else {
948 ToolExecutionResult::tool_error(&self.error_message)
949 }
950 }
951}
952
953#[cfg(test)]
958mod tests {
959 use super::*;
960
961 struct CountingTool {
962 calls: Arc<std::sync::atomic::AtomicUsize>,
963 label: &'static str,
964 }
965
966 #[async_trait]
967 impl Tool for CountingTool {
968 fn name(&self) -> &str {
969 "counting"
970 }
971 fn display_name(&self) -> Option<&str> {
972 Some(self.label)
973 }
974 fn description(&self) -> &str {
975 "Count validated dispatches"
976 }
977 fn parameters_schema(&self) -> Value {
978 serde_json::json!({"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false})
979 }
980 fn policy(&self) -> ToolPolicy {
981 ToolPolicy::RequiresApproval
982 }
983 fn deferrable_policy(&self) -> DeferrablePolicy {
984 DeferrablePolicy::Never
985 }
986 fn hints(&self) -> ToolHints {
987 ToolHints::default()
988 .with_readonly(true)
989 .with_idempotent(true)
990 }
991 async fn execute(&self, arguments: Value) -> ToolExecutionResult {
992 self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
993 ToolExecutionResult::success(
994 serde_json::json!({"label":self.label,"arguments":arguments}),
995 )
996 }
997 }
998
999 #[tokio::test]
1000 async fn registry_registration_paths_replace_and_dispatch_complete_definitions() {
1001 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1002 let tool = |label| CountingTool {
1003 calls: calls.clone(),
1004 label,
1005 };
1006 let mut registry = ToolRegistry::builder()
1007 .tool(tool("first"))
1008 .tool_boxed(Box::new(tool("boxed")))
1009 .tool_arc(Arc::new(tool("last")))
1010 .build();
1011 assert_eq!(registry.tool_names(), ["counting"]);
1012 let definitions = registry.tool_definitions();
1013 assert_eq!(definitions.len(), 1);
1014 let ToolDefinition::Builtin(definition) = &definitions[0] else {
1015 panic!("builtin expected")
1016 };
1017 assert_eq!(definition.name, "counting");
1018 assert_eq!(definition.display_name.as_deref(), Some("last"));
1019 assert_eq!(definition.description, "Count validated dispatches");
1020 assert_eq!(
1021 definition.parameters,
1022 serde_json::json!({"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false})
1023 );
1024 assert_eq!(definition.policy, ToolPolicy::RequiresApproval);
1025 assert_eq!(definition.deferrable, DeferrablePolicy::Never);
1026 assert_eq!(
1027 definition.hints,
1028 ToolHints::default()
1029 .with_readonly(true)
1030 .with_idempotent(true)
1031 );
1032 assert!(definition.category.is_none());
1033 assert!(definition.full_parameters.is_none());
1034 let call = ToolCall {
1035 id: "dispatch-id".into(),
1036 name: "counting".into(),
1037 arguments: serde_json::json!({"message":"payload"}),
1038 };
1039 let result = registry.execute(&call, &definitions[0]).await.unwrap();
1040 assert_eq!(result.tool_call_id, "dispatch-id");
1041 assert_eq!(
1042 result.result,
1043 Some(serde_json::json!({"label":"last","arguments":{"message":"payload"}}))
1044 );
1045 assert!(result.error.is_none());
1046 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
1047 assert_eq!(
1048 registry.unregister("counting").unwrap().display_name(),
1049 Some("last")
1050 );
1051 assert!(registry.is_empty());
1052 assert!(registry.unregister("counting").is_none());
1053 registry.register(tool("again"));
1054 registry.clear();
1055 assert!(registry.tool_definitions().is_empty());
1056 }
1057
1058 #[tokio::test]
1059 async fn registry_errors_preserve_public_failures_and_hide_internal_details() {
1060 for (tool, expected) in [
1061 (FailingTool::with_tool_error("Invalid city"), "Invalid city"),
1062 (
1063 FailingTool::with_internal_error("PRIVATE-DATABASE-TOKEN"),
1064 "An internal error occurred while executing the tool",
1065 ),
1066 ] {
1067 let registry = ToolRegistry::builder().tool(tool).build();
1068 let call = ToolCall {
1069 id: "failure-id".into(),
1070 name: "failing_tool".into(),
1071 arguments: serde_json::json!({}),
1072 };
1073 let result = registry
1074 .execute(&call, ®istry.tool_definitions()[0])
1075 .await
1076 .unwrap();
1077 assert_eq!(result.tool_call_id, "failure-id");
1078 assert_eq!(result.error.as_deref(), Some(expected));
1079 assert_eq!(result.result, Some(serde_json::json!({"error":expected})));
1080 assert!(
1081 !serde_json::to_string(&result)
1082 .unwrap()
1083 .contains("PRIVATE-DATABASE-TOKEN")
1084 );
1085 }
1086 }
1087
1088 struct RequiresOrgId;
1089
1090 #[async_trait]
1091 impl Tool for RequiresOrgId {
1092 fn name(&self) -> &str {
1093 "requires_org_id"
1094 }
1095
1096 fn description(&self) -> &str {
1097 "Exercises required ToolContext service validation"
1098 }
1099
1100 fn parameters_schema(&self) -> Value {
1101 serde_json::json!({"type": "object", "additionalProperties": false})
1102 }
1103
1104 fn required_context_services(&self) -> &'static [ToolContextService] {
1105 &[ToolContextService::OrgId]
1106 }
1107
1108 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1109 ToolExecutionResult::success(Value::Null)
1110 }
1111 }
1112
1113 #[test]
1114 fn required_context_service_validation_is_structured() {
1115 let mut registry = ToolRegistry::new();
1116 registry.register(RequiresOrgId);
1117
1118 let error = registry
1119 .validate_context_services(&ToolContextServices::default())
1120 .expect_err("missing required service must fail before tool exposure");
1121
1122 assert!(matches!(
1123 error,
1124 crate::AgentLoopError::Configuration(message)
1125 if message.contains("requires_org_id") && message.contains("OrgId")
1126 ));
1127 }
1128
1129 #[test]
1130 fn required_context_service_validation_accepts_supplied_service() {
1131 let mut registry = ToolRegistry::new();
1132 registry.register(RequiresOrgId);
1133 let services = ToolContextServices {
1134 org_id: Some(crate::typed_id::OrgId::from_seed(1)),
1135 ..ToolContextServices::default()
1136 };
1137
1138 registry
1139 .validate_context_services(&services)
1140 .expect("advertised required service should validate");
1141 }
1142
1143 #[test]
1144 fn test_tool_result_conversion() {
1145 let result = ToolExecutionResult::success(serde_json::json!({"value": 42}));
1147 let tool_result = result.into_tool_result("call_1", "test_tool");
1148 assert_eq!(tool_result.tool_call_id, "call_1");
1149 assert!(tool_result.error.is_none());
1150 assert!(tool_result.images.is_none());
1151 assert!(tool_result.connection_required.is_none());
1152 assert!(tool_result.raw_output.is_none());
1153 assert_eq!(tool_result.result, Some(serde_json::json!({"value": 42})));
1154
1155 let result = ToolExecutionResult::tool_error("Invalid input");
1157 let tool_result = result.into_tool_result("call_2", "test_tool");
1158 assert_eq!(tool_result.error.as_deref(), Some("Invalid input"));
1159 assert_eq!(
1160 tool_result.result.unwrap(),
1161 serde_json::json!({"error": "Invalid input"})
1162 );
1163
1164 let result = ToolExecutionResult::internal_error_msg("Secret database error");
1166 let tool_result = result.into_tool_result("call_3", "test_tool");
1167 assert_eq!(
1168 tool_result.error.as_deref(),
1169 Some("An internal error occurred while executing the tool")
1170 );
1171 assert_eq!(
1172 tool_result.result.unwrap(),
1173 serde_json::json!({"error": "An internal error occurred while executing the tool"})
1174 );
1175 }
1176
1177 #[test]
1178 fn test_success_with_raw_output_object_preserves_shape() {
1179 let res = ToolExecutionResult::success_with_raw_output(
1180 serde_json::json!({"stdout": "hello"}),
1181 "raw stdout bytes".to_string(),
1182 );
1183 let tr = res.into_tool_result("call_1", "demo");
1184 assert_eq!(tr.result.as_ref().unwrap()["stdout"], "hello");
1185 assert!(
1186 tr.result
1187 .as_ref()
1188 .unwrap()
1189 .as_object()
1190 .unwrap()
1191 .get("_raw_output")
1192 .is_none(),
1193 "sidecar key must not leak to the LLM-visible result"
1194 );
1195 assert_eq!(tr.raw_output.as_deref(), Some("raw stdout bytes"));
1196 }
1197
1198 #[test]
1199 fn raw_output_round_trips_all_nonobject_shapes_without_serializing_sidecar() {
1200 for value in [
1201 serde_json::json!("compact summary"),
1202 Value::Null,
1203 serde_json::json!(false),
1204 serde_json::json!(42),
1205 serde_json::json!(["a", 2]),
1206 ] {
1207 let result =
1208 ToolExecutionResult::success_with_raw_output(value.clone(), "PRIVATE-RAW".into())
1209 .into_tool_result("raw-id", "demo");
1210 assert_eq!(result.result, Some(value));
1211 assert_eq!(result.raw_output.as_deref(), Some("PRIVATE-RAW"));
1212 assert!(
1213 !serde_json::to_string(&result)
1214 .unwrap()
1215 .contains("PRIVATE-RAW")
1216 );
1217 }
1218 }
1219
1220 #[test]
1221 fn test_success_result_with_raw_output_scalar_key_is_not_unwrapped() {
1222 let res = ToolExecutionResult::success(
1223 serde_json::json!({"_raw_output_scalar": "user_value", "kept": true}),
1224 );
1225 let tr = res.into_tool_result("call_1", "demo");
1226 assert_eq!(
1227 tr.result,
1228 Some(serde_json::json!({"_raw_output_scalar": "user_value", "kept": true}))
1229 );
1230 assert_eq!(tr.raw_output, None);
1231 }
1232
1233 #[test]
1234 fn test_success_result_with_only_raw_output_scalar_key_is_not_unwrapped() {
1235 let res = ToolExecutionResult::success(serde_json::json!({"_raw_output_scalar": "v"}));
1238 let tr = res.into_tool_result("call_1", "demo");
1239 assert_eq!(
1240 tr.result,
1241 Some(serde_json::json!({"_raw_output_scalar": "v"}))
1242 );
1243 assert_eq!(tr.raw_output, None);
1244 }
1245
1246 #[tokio::test]
1247 async fn invalid_arguments_never_dispatch_through_either_executor_path() {
1248 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1249 let registry = ToolRegistry::builder()
1250 .tool(CountingTool {
1251 calls: calls.clone(),
1252 label: "validated",
1253 })
1254 .build();
1255 let definition = registry.tool_definitions().remove(0);
1256 let context = ToolContext::new(crate::typed_id::SessionId::new());
1257 for (arguments, instance, keyword) in [
1258 (serde_json::json!({}), "", "required"),
1259 (serde_json::json!({"message":42}), "/message", "type"),
1260 (
1261 serde_json::json!({"message":"ok","unexpected":true}),
1262 "",
1263 "additionalProperties",
1264 ),
1265 ] {
1266 let call = ToolCall {
1267 id: "invalid-id".into(),
1268 name: "counting".into(),
1269 arguments,
1270 };
1271 for with_context in [false, true] {
1272 let result = if with_context {
1273 registry
1274 .execute_with_context(&call, &definition, &context)
1275 .await
1276 .unwrap()
1277 } else {
1278 registry.execute(&call, &definition).await.unwrap()
1279 };
1280 assert_eq!(result.tool_call_id, "invalid-id");
1281 let message = result.error.unwrap();
1282 assert_eq!(result.result, Some(serde_json::json!({"error":message})));
1283 let error: Value = serde_json::from_str(&message).unwrap();
1284 assert_eq!(error["code"], "invalid_tool_arguments");
1285 assert_eq!(error["tool"], "counting");
1286 let issues = error["issues"].as_array().unwrap();
1287 assert_eq!(issues.len(), 1);
1288 assert_eq!(issues[0]["instance_path"], instance);
1289 assert!(issues[0]["schema_path"].as_str().unwrap().contains(keyword));
1290 assert!(!issues[0]["message"].as_str().unwrap().is_empty());
1291 }
1292 }
1293 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 0);
1294 let valid = ToolCall {
1295 id: "valid-id".into(),
1296 name: "counting".into(),
1297 arguments: serde_json::json!({"message":"accepted"}),
1298 };
1299 let result = registry
1300 .execute_with_context(&valid, &definition, &context)
1301 .await
1302 .unwrap();
1303 assert_eq!(
1304 result.result,
1305 Some(serde_json::json!({"label":"validated","arguments":{"message":"accepted"}}))
1306 );
1307 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
1308 }
1309
1310 #[test]
1311 fn result_variants_keep_images_connections_and_classification_distinct() {
1312 use serde_json::json;
1313 for (result, classification, expected) in [
1314 (
1315 ToolExecutionResult::success_with_images(
1316 json!({"page":2}),
1317 vec![ToolResultImage {
1318 base64: "aW1hZ2U=".into(),
1319 media_type: "image/jpeg".into(),
1320 }],
1321 ),
1322 (true, false, false),
1323 json!({"tool_call_id":"variant-id","result":{"page":2},"error":null,"images":[{"base64":"aW1hZ2U=","media_type":"image/jpeg"}]}),
1324 ),
1325 (
1326 ToolExecutionResult::success_with_images(Value::Null, vec![]),
1327 (true, false, false),
1328 json!({"tool_call_id":"variant-id","result":null,"error":null}),
1329 ),
1330 (
1331 ToolExecutionResult::connection_required("daytona"),
1332 (false, false, true),
1333 json!({"tool_call_id":"variant-id","result":{"connection_required":"daytona"},"error":null,"connection_required":"daytona"}),
1334 ),
1335 (
1336 ToolExecutionResult::tool_error("visible"),
1337 (false, true, false),
1338 json!({"tool_call_id":"variant-id","result":{"error":"visible"},"error":"visible"}),
1339 ),
1340 (
1341 ToolExecutionResult::internal_error(std::io::Error::other("PRIVATE-SOURCE")),
1342 (false, true, false),
1343 json!({"tool_call_id":"variant-id","result":{"error":"An internal error occurred while executing the tool"},"error":"An internal error occurred while executing the tool"}),
1344 ),
1345 ] {
1346 assert_eq!(
1347 (
1348 result.is_success(),
1349 result.is_error(),
1350 result.is_connection_required()
1351 ),
1352 classification
1353 );
1354 let result = result.into_tool_result("variant-id", "tool");
1355 assert!(result.raw_output.is_none());
1356 assert_eq!(serde_json::to_value(result).unwrap(), expected);
1357 }
1358 }
1359
1360 #[tokio::test]
1361 async fn test_tool_registry_as_executor() {
1362 let mut registry = ToolRegistry::new();
1363 registry.register(EchoTool);
1364
1365 let tool_call = ToolCall {
1366 id: "call_1".to_string(),
1367 name: "echo".to_string(),
1368 arguments: serde_json::json!({"message": "test"}),
1369 };
1370
1371 let tool_def = registry.get("echo").unwrap().to_definition();
1372 let result = registry.execute(&tool_call, &tool_def).await.unwrap();
1373
1374 assert!(result.error.is_none());
1375 assert_eq!(result.result.unwrap()["echoed"], "test");
1376 }
1377
1378 #[test]
1379 fn test_with_defaults_has_expected_tools() {
1380 let registry = ToolRegistry::with_defaults();
1381 assert_eq!(registry.tool_names(), ["report_progress"]);
1384 assert!(registry.tool_definitions()[0].display_name().is_some());
1385 }
1386
1387 #[tokio::test]
1388 async fn test_with_defaults_tools_are_executable() {
1389 let registry = ToolRegistry::with_defaults();
1390
1391 let tool_call = ToolCall {
1393 id: "call_1".to_string(),
1394 name: "report_progress".to_string(),
1395 arguments: serde_json::json!({
1396 "status": "completed",
1397 "summary": "Boundary audit complete"
1398 }),
1399 };
1400
1401 let tool_def = registry.get("report_progress").unwrap().to_definition();
1402 let result = registry.execute(&tool_call, &tool_def).await.unwrap();
1403
1404 assert!(result.error.is_none());
1405 assert_eq!(result.result.unwrap()["summary"], "Boundary audit complete");
1406 }
1407
1408 #[test]
1413 fn raw_output_preserves_object_keys_that_resemble_carriers() {
1414 for value in [
1415 serde_json::json!({"_raw_output_scalar": "user-value"}),
1416 serde_json::json!({"_raw_output": "user-value", "kept": true}),
1417 ] {
1418 let result = ToolExecutionResult::success_with_raw_output(
1419 value.clone(),
1420 "actual raw output".into(),
1421 )
1422 .into_tool_result("call", "tool");
1423 assert_eq!(result.result, Some(value));
1424 assert_eq!(result.raw_output.as_deref(), Some("actual raw output"));
1425 }
1426 }
1427 #[tokio::test]
1428 async fn monitor_probe_registry_rejects_unregistered_tools() {
1429 let registry = ToolRegistry::with_monitor_probe_defaults();
1430 let call = ToolCall {
1431 id: "missing-id".into(),
1432 name: "echo".into(),
1433 arguments: serde_json::json!({"message":"x"}),
1434 };
1435 let definition = EchoTool.to_definition();
1436 let context = ToolContext::new(crate::typed_id::SessionId::new());
1437 for with_context in [false, true] {
1438 let error = if with_context {
1439 registry
1440 .execute_with_context(&call, &definition, &context)
1441 .await
1442 .unwrap_err()
1443 } else {
1444 registry.execute(&call, &definition).await.unwrap_err()
1445 };
1446 assert!(
1447 matches!(error, AgentLoopError::ToolExecution(message) if message.contains("echo"))
1448 );
1449 }
1450 }
1451 #[tokio::test]
1452 async fn invalid_registered_schema_fails_configuration_before_dispatch() {
1453 struct InvalidSchema;
1454 #[async_trait]
1455 impl Tool for InvalidSchema {
1456 fn name(&self) -> &str {
1457 "invalid_schema"
1458 }
1459 fn description(&self) -> &str {
1460 "Invalid schema fixture"
1461 }
1462 fn parameters_schema(&self) -> Value {
1463 serde_json::json!({"type":42})
1464 }
1465 async fn execute(&self, _: Value) -> ToolExecutionResult {
1466 panic!("invalid schema must never dispatch")
1467 }
1468 }
1469 let registry = ToolRegistry::builder().tool(InvalidSchema).build();
1470 let call = ToolCall {
1471 id: "schema-id".into(),
1472 name: "invalid_schema".into(),
1473 arguments: serde_json::json!({}),
1474 };
1475 let context = ToolContext::new(crate::typed_id::SessionId::new());
1476 let supplied = EchoTool.to_definition();
1478 for with_context in [false, true] {
1479 let error = if with_context {
1480 registry
1481 .execute_with_context(&call, &supplied, &context)
1482 .await
1483 .unwrap_err()
1484 } else {
1485 registry.execute(&call, &supplied).await.unwrap_err()
1486 };
1487 assert!(
1488 matches!(error,AgentLoopError::Configuration(message) if message.contains("invalid_schema") && message.contains("invalid parameters schema"))
1489 );
1490 }
1491 }
1492}