1use std::collections::HashMap;
8use std::collections::HashSet;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use serde_json::Value;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
17pub enum ToolError {
18 #[error("tool not found: {0}")]
20 ToolNotFound(String),
21
22 #[error("invalid input for tool: {0}")]
24 InvalidInput(String),
25
26 #[error("tool execution error: {0}")]
28 ExecutionError(String),
29}
30
31use schemars::JsonSchema;
32use serde::{Deserialize, Serialize};
33
34#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
36#[serde(tag = "type", rename_all = "snake_case")]
37pub enum ToolProvenance {
38 #[default]
40 Native,
41 McpRemote { server: String },
43}
44
45#[derive(Debug, Clone)]
47pub struct ToolResult {
48 pub content: String,
50 pub is_error: bool,
52}
53
54impl ToolResult {
55 pub fn success(content: impl Into<String>) -> Self {
57 Self {
58 content: content.into(),
59 is_error: false,
60 }
61 }
62
63 pub fn error(content: impl Into<String>) -> Self {
65 Self {
66 content: content.into(),
67 is_error: true,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
74pub enum ToolNature {
75 #[default]
77 Read,
78 Write,
80 Execute,
82 Network,
84}
85
86#[derive(
92 Debug,
93 Clone,
94 Copy,
95 Default,
96 PartialEq,
97 Eq,
98 Hash,
99 PartialOrd,
100 Ord,
101 Serialize,
102 Deserialize,
103 JsonSchema,
104)]
105#[serde(rename_all = "snake_case")]
106pub enum ToolFamily {
107 #[default]
109 File,
110 Search,
112 CodeIntelligence,
114 Git,
116 Network,
118 Shell,
120 Extension,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
126pub struct ToolPresentationPolicy {
127 pub include_all: bool,
129 pub include_always_on: bool,
131 #[serde(default)]
133 pub families: Vec<ToolFamily>,
134}
135
136impl ToolPresentationPolicy {
137 #[must_use]
139 pub fn full() -> Self {
140 Self {
141 include_all: true,
142 include_always_on: true,
143 families: Vec::new(),
144 }
145 }
146
147 #[must_use]
149 pub fn always_on() -> Self {
150 Self {
151 include_all: false,
152 include_always_on: true,
153 families: Vec::new(),
154 }
155 }
156
157 #[must_use]
159 pub fn with_families(families: impl IntoIterator<Item = ToolFamily>) -> Self {
160 Self {
161 include_all: false,
162 include_always_on: true,
163 families: families.into_iter().collect(),
164 }
165 }
166
167 #[must_use]
169 pub fn allows_tool(&self, tool: &dyn AgentTool) -> bool {
170 self.include_all
171 || (self.include_always_on && tool.is_always_on())
172 || self.families.contains(&tool.family())
173 }
174
175 #[must_use]
177 pub fn family_set(&self) -> HashSet<ToolFamily> {
178 self.families.iter().copied().collect()
179 }
180}
181
182impl Default for ToolPresentationPolicy {
183 fn default() -> Self {
184 Self::full()
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
190#[serde(rename_all = "lowercase")]
191pub enum ToolResourceKind {
192 Path,
194 Domain,
196 Command,
198 Remote,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204pub struct ToolPermissionFacet {
205 pub nature: ToolNature,
207 #[serde(default)]
209 pub resource: Option<String>,
210 #[serde(default)]
212 pub resource_kind: Option<ToolResourceKind>,
213 #[serde(default)]
215 pub description: Option<String>,
216}
217
218impl ToolPermissionFacet {
219 pub fn new(nature: ToolNature) -> Self {
221 Self {
222 nature,
223 resource: None,
224 resource_kind: None,
225 description: None,
226 }
227 }
228
229 pub fn with_resource(
231 nature: ToolNature,
232 resource: impl Into<String>,
233 resource_kind: ToolResourceKind,
234 ) -> Self {
235 Self {
236 nature,
237 resource: Some(resource.into()),
238 resource_kind: Some(resource_kind),
239 description: None,
240 }
241 }
242
243 pub fn with_description(mut self, description: impl Into<String>) -> Self {
245 self.description = Some(description.into());
246 self
247 }
248}
249
250#[async_trait]
256pub trait AgentTool: Send + Sync {
257 fn name(&self) -> &str;
259
260 fn description(&self) -> &str;
262
263 fn parameters(&self) -> Value;
269
270 async fn execute(&self, input: Value) -> ToolResult;
275
276 fn is_read_only(&self) -> bool {
281 false
282 }
283
284 fn nature(&self) -> ToolNature {
285 if self.is_read_only() {
286 ToolNature::Read
287 } else {
288 ToolNature::Write
289 }
290 }
291
292 fn family(&self) -> ToolFamily {
294 ToolFamily::Extension
295 }
296
297 fn is_always_on(&self) -> bool {
299 false
300 }
301
302 fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
308 vec![ToolPermissionFacet::new(self.nature())]
309 }
310
311 fn summary_fields(&self) -> &'static [&'static str] {
312 &[]
313 }
314
315 fn provenance(&self) -> ToolProvenance {
322 ToolProvenance::Native
323 }
324}
325
326#[derive(Default)]
331pub struct ToolRegistry {
332 tools: HashMap<String, Arc<dyn AgentTool>>,
333}
334
335impl ToolRegistry {
336 pub fn new() -> Self {
338 Self::default()
339 }
340
341 pub fn register(&mut self, tool: Arc<dyn AgentTool>) {
344 self.tools.insert(tool.name().to_owned(), tool);
345 }
346
347 pub fn get(&self, name: &str) -> Option<&dyn AgentTool> {
349 self.tools.get(name).map(|t| t.as_ref())
350 }
351
352 pub fn list(&self) -> Vec<&dyn AgentTool> {
354 self.tools.values().map(|t| t.as_ref()).collect()
355 }
356
357 pub fn validate_input(&self, name: &str, input: &Value) -> Result<(), ToolError> {
365 let tool = self
366 .get(name)
367 .ok_or_else(|| ToolError::ToolNotFound(name.to_owned()))?;
368
369 let params = tool.parameters();
370
371 if !input.is_object() {
373 return Err(ToolError::InvalidInput(format!(
374 "expected object for tool '{name}', got {}",
375 input_type_name(input)
376 )));
377 }
378
379 if let Some(schema_obj) = params.as_object()
381 && let Some(Value::Array(required)) = schema_obj.get("required")
382 && let Some(input_obj) = input.as_object()
383 {
384 for req in required {
385 if let Some(req_key) = req.as_str()
386 && !input_obj.contains_key(req_key)
387 {
388 return Err(ToolError::InvalidInput(format!(
389 "missing required field '{req_key}' for tool '{name}'"
390 )));
391 }
392 }
393 }
394
395 Ok(())
396 }
397}
398
399fn input_type_name(value: &Value) -> &'static str {
401 match value {
402 Value::Null => "null",
403 Value::Bool(_) => "boolean",
404 Value::Number(_) => "number",
405 Value::String(_) => "string",
406 Value::Array(_) => "array",
407 Value::Object(_) => "object",
408 }
409}
410
411#[macro_export]
414macro_rules! tool_parameters {
415 ($type:ty) => {{
416 let schema = schemars::schema_for!($type);
417 serde_json::to_value(schema).unwrap_or(serde_json::Value::Object(Default::default()))
418 }};
419}
420
421#[cfg(test)]
422#[allow(warnings)]
423#[allow(warnings)]
424#[allow(warnings)]
425#[allow(warnings)]
426mod tests {
427 use super::*;
428 use schemars::JsonSchema;
429 use serde::Deserialize;
430
431 struct MockTool {
433 tool_name: String,
434 tool_description: String,
435 read_only: bool,
436 family: ToolFamily,
437 always_on: bool,
438 }
439
440 impl MockTool {
441 fn new(name: &str, description: &str) -> Self {
442 Self {
443 tool_name: name.to_owned(),
444 tool_description: description.to_owned(),
445 read_only: true,
446 family: ToolFamily::Extension,
447 always_on: false,
448 }
449 }
450
451 fn with_family(mut self, family: ToolFamily) -> Self {
452 self.family = family;
453 self
454 }
455
456 fn always_on(mut self) -> Self {
457 self.always_on = true;
458 self
459 }
460 }
461
462 #[async_trait]
463 impl AgentTool for MockTool {
464 fn name(&self) -> &str {
465 &self.tool_name
466 }
467
468 fn description(&self) -> &str {
469 &self.tool_description
470 }
471
472 fn parameters(&self) -> Value {
473 serde_json::json!({
474 "type": "object",
475 "properties": {
476 "message": {
477 "type": "string",
478 "description": "A message to echo"
479 }
480 },
481 "required": ["message"]
482 })
483 }
484
485 async fn execute(&self, input: Value) -> ToolResult {
486 if let Some(msg) = input.get("message").and_then(Value::as_str) {
487 ToolResult::success(format!("echo: {msg}"))
488 } else {
489 ToolResult::error("missing 'message' field".to_owned())
490 }
491 }
492
493 fn is_read_only(&self) -> bool {
494 self.read_only
495 }
496
497 fn family(&self) -> ToolFamily {
498 self.family
499 }
500
501 fn is_always_on(&self) -> bool {
502 self.always_on
503 }
504 }
505
506 #[derive(JsonSchema, Deserialize)]
508 #[allow(dead_code)]
509 struct GreetParams {
510 name: String,
512 #[serde(default)]
514 formal: bool,
515 }
516
517 #[allow(dead_code)]
518 struct TypedMockTool;
519
520 #[async_trait]
521 impl AgentTool for TypedMockTool {
522 fn name(&self) -> &str {
523 "greet"
524 }
525
526 fn description(&self) -> &str {
527 "Greet someone by name"
528 }
529
530 fn parameters(&self) -> Value {
531 tool_parameters!(GreetParams)
532 }
533
534 async fn execute(&self, input: Value) -> ToolResult {
535 let name = input.get("name").and_then(Value::as_str).unwrap_or("World");
536 ToolResult::success(format!("Hello, {name}!"))
537 }
538 }
539
540 #[test]
541 fn test_register_and_get_tool() {
542 let mut registry = ToolRegistry::new();
543 let tool = Arc::new(MockTool::new("echo", "Echoes a message"));
544 registry.register(tool);
545
546 let retrieved = registry.get("echo");
547 assert!(retrieved.is_some());
548 assert_eq!(retrieved.unwrap().name(), "echo");
549 }
550
551 #[test]
552 fn test_tool_not_found() {
553 let registry = ToolRegistry::new();
554 assert!(registry.get("nonexistent").is_none());
555
556 let result = registry.validate_input("nonexistent", &serde_json::json!({}));
557 assert!(matches!(result, Err(ToolError::ToolNotFound(_))));
558 }
559
560 #[test]
561 fn test_list_tools() {
562 let mut registry = ToolRegistry::new();
563 registry.register(Arc::new(MockTool::new("echo", "Echoes a message")));
564 registry.register(Arc::new(MockTool::new("reverse", "Reverses a string")));
565
566 let tools = registry.list();
567 assert_eq!(tools.len(), 2);
568 }
569
570 #[test]
571 fn test_validate_input_valid() {
572 let mut registry = ToolRegistry::new();
573 registry.register(Arc::new(MockTool::new("echo", "Echoes a message")));
574
575 let input = serde_json::json!({ "message": "hello" });
576 assert!(registry.validate_input("echo", &input).is_ok());
577 }
578
579 #[test]
580 fn test_validate_input_missing_required() {
581 let mut registry = ToolRegistry::new();
582 registry.register(Arc::new(MockTool::new("echo", "Echoes a message")));
583
584 let input = serde_json::json!({});
585 let result = registry.validate_input("echo", &input);
586 assert!(matches!(result, Err(ToolError::InvalidInput(_))));
587 assert!(
588 result
589 .unwrap_err()
590 .to_string()
591 .contains("missing required field 'message'")
592 );
593 }
594
595 #[test]
596 fn test_validate_input_not_object() {
597 let mut registry = ToolRegistry::new();
598 registry.register(Arc::new(MockTool::new("echo", "Echoes a message")));
599
600 let input = serde_json::json!("not an object");
601 let result = registry.validate_input("echo", &input);
602 assert!(matches!(result, Err(ToolError::InvalidInput(_))));
603 }
604
605 #[tokio::test]
606 async fn test_tool_execute() {
607 let mut registry = ToolRegistry::new();
608 registry.register(Arc::new(MockTool::new("echo", "Echoes a message")));
609
610 let tool = registry.get("echo").unwrap();
611 let result = tool
612 .execute(serde_json::json!({ "message": "hello" }))
613 .await;
614 assert!(!result.is_error);
615 assert_eq!(result.content, "echo: hello");
616 }
617
618 #[tokio::test]
619 async fn test_tool_execute_error() {
620 let mut registry = ToolRegistry::new();
621 registry.register(Arc::new(MockTool::new("echo", "Echoes a message")));
622
623 let tool = registry.get("echo").unwrap();
624 let result = tool.execute(serde_json::json!({})).await;
625 assert!(result.is_error);
626 }
627
628 #[test]
629 fn test_tool_is_read_only() {
630 let tool = MockTool::new("echo", "Echoes a message");
631 assert!(tool.is_read_only());
632 }
633
634 #[test]
635 fn test_tool_parameters_macro() {
636 let schema = tool_parameters!(GreetParams);
637 assert!(schema.is_object());
638 let obj = schema.as_object().unwrap();
639 assert!(obj.contains_key("properties"));
640 }
641
642 #[test]
643 fn test_register_replaces_existing() {
644 let mut registry = ToolRegistry::new();
645 registry.register(Arc::new(MockTool::new("echo", "Original")));
646 registry.register(Arc::new(MockTool::new("echo", "Replacement")));
647
648 let tool = registry.get("echo").unwrap();
649 assert_eq!(tool.description(), "Replacement");
650 }
651
652 #[test]
653 fn test_tool_result_helpers() {
654 let success = ToolResult::success("ok");
655 assert!(!success.is_error);
656 assert_eq!(success.content, "ok");
657
658 let error = ToolResult::error("failed");
659 assert!(error.is_error);
660 assert_eq!(error.content, "failed");
661 }
662
663 #[test]
664 fn test_tool_presentation_policy_selects_always_on_baseline() {
665 let baseline = MockTool::new("read", "Read file").always_on();
666 let shell = MockTool::new("bash", "Run command").with_family(ToolFamily::Shell);
667
668 let policy = ToolPresentationPolicy::always_on();
669
670 assert!(policy.allows_tool(&baseline));
671 assert!(!policy.allows_tool(&shell));
672 }
673
674 #[test]
675 fn test_tool_presentation_policy_selects_explicit_family() {
676 let git = MockTool::new("git_status", "Git status").with_family(ToolFamily::Git);
677 let network = MockTool::new("web_search", "Search web").with_family(ToolFamily::Network);
678
679 let policy = ToolPresentationPolicy::with_families([ToolFamily::Git]);
680
681 assert!(policy.allows_tool(&git));
682 assert!(!policy.allows_tool(&network));
683 assert!(policy.family_set().contains(&ToolFamily::Git));
684 }
685}
686
687#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
688#[serde(rename_all = "kebab-case")]
689pub enum ToolProtocol {
690 #[default]
691 Native,
692 TalosStrict,
693 Compat,
694}
695
696impl ToolProtocol {
697 pub fn parse(s: &str) -> Option<Self> {
698 match s {
699 "native" => Some(ToolProtocol::Native),
700 "talos-strict" | "talos_xml_json_strict" => Some(ToolProtocol::TalosStrict),
701 "compat" | "compatibility" => Some(ToolProtocol::Compat),
702 _ => None,
703 }
704 }
705}
706
707#[derive(Debug, Clone, Default)]
708pub struct ToolProtocolConfig {
709 pub protocol: ToolProtocol,
710 pub strict_prompt: bool,
711 pub stream_filter: bool,
712 pub schema_validate: bool,
713}
714
715impl ToolProtocolConfig {
716 pub fn for_protocol(protocol: ToolProtocol) -> Self {
717 match protocol {
718 ToolProtocol::Native => ToolProtocolConfig {
719 protocol,
720 strict_prompt: false,
721 stream_filter: false,
722 schema_validate: false,
723 },
724 ToolProtocol::TalosStrict => ToolProtocolConfig {
725 protocol,
726 strict_prompt: true,
727 stream_filter: true,
728 schema_validate: true,
729 },
730 ToolProtocol::Compat => ToolProtocolConfig {
731 protocol,
732 strict_prompt: false,
733 stream_filter: true,
734 schema_validate: false,
735 },
736 }
737 }
738}