1use schemars::JsonSchema;
2use serde::de::DeserializeOwned;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum ImageFormat {
10 Png,
11 Jpeg,
12 Gif,
13 Webp,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum DocumentFormat {
20 Pdf,
21 Csv,
22 Doc,
23 Docx,
24 Html,
25 Md,
26 Txt,
27 Xls,
28 Xlsx,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum ToolResult {
38 Text(String),
40
41 Json(Value),
43
44 Image {
46 format: ImageFormat,
47 data: Vec<u8>,
49 },
50
51 Document {
53 format: DocumentFormat,
54 data: Vec<u8>,
56 name: Option<String>,
58 },
59}
60
61impl ToolResult {
62 pub fn json<T: Serialize>(value: T) -> Result<Self, serde_json::Error> {
64 Ok(Self::Json(serde_json::to_value(value)?))
65 }
66
67 pub fn text(s: impl Into<String>) -> Self {
69 Self::Text(s.into())
70 }
71
72 pub fn image(format: ImageFormat, data: Vec<u8>) -> Self {
74 Self::Image { format, data }
75 }
76
77 pub fn document(format: DocumentFormat, data: Vec<u8>) -> Self {
79 Self::Document {
80 format,
81 data,
82 name: None,
83 }
84 }
85
86 pub fn document_with_name(
88 format: DocumentFormat,
89 data: Vec<u8>,
90 name: impl Into<String>,
91 ) -> Self {
92 Self::Document {
93 format,
94 data,
95 name: Some(name.into()),
96 }
97 }
98
99 pub fn as_text(&self) -> String {
101 match self {
102 ToolResult::Text(s) => s.clone(),
103 ToolResult::Json(v) => v.to_string(),
104 ToolResult::Image { format, data } => {
105 format!("[Image: {:?}, {} bytes]", format, data.len())
106 }
107 ToolResult::Document { format, data, name } => {
108 let name_str = name.as_deref().unwrap_or("unnamed");
109 format!(
110 "[Document: {:?}, {}, {} bytes]",
111 format,
112 name_str,
113 data.len()
114 )
115 }
116 }
117 }
118
119 pub fn as_str(&self) -> Option<&str> {
121 match self {
122 ToolResult::Text(s) => Some(s),
123 _ => None,
124 }
125 }
126}
127
128impl From<String> for ToolResult {
130 fn from(s: String) -> Self {
131 Self::Text(s)
132 }
133}
134
135impl From<&str> for ToolResult {
136 fn from(s: &str) -> Self {
137 Self::Text(s.to_string())
138 }
139}
140
141#[derive(Debug, thiserror::Error)]
143pub enum ToolError {
144 #[error("IO error: {0}")]
145 Io(#[from] std::io::Error),
146
147 #[error("Serialization error: {0}")]
148 Serialization(#[from] serde_json::Error),
149
150 #[error("Path validation failed: {0}")]
151 PathValidation(String),
152
153 #[error("{0}")]
154 Custom(String),
155}
156
157impl From<String> for ToolError {
158 fn from(s: String) -> Self {
159 Self::Custom(s)
160 }
161}
162
163impl From<&str> for ToolError {
164 fn from(s: &str) -> Self {
165 Self::Custom(s.to_string())
166 }
167}
168
169pub trait Tool: Send + Sync {
247 type Input: DeserializeOwned + JsonSchema;
249
250 fn name(&self) -> &str;
252
253 fn description(&self) -> &str;
255
256 fn execute(
258 &self,
259 input: Self::Input,
260 ) -> impl std::future::Future<Output = Result<ToolResult, ToolError>> + Send;
261
262 fn input_schema(&self) -> Value {
267 let schema = schemars::schema_for!(Self::Input);
268 serde_json::to_value(schema).expect("Failed to serialize schema")
269 }
270
271 fn format_input_plain(&self, params: &Value) -> String {
279 format_params_plain(self.name(), params)
280 }
281
282 fn format_input_ansi(&self, params: &Value) -> String {
286 format_params_ansi(self.name(), params)
287 }
288
289 fn format_input_markdown(&self, params: &Value) -> String {
293 format_params_markdown(self.name(), params)
294 }
295
296 fn format_output_plain(&self, result: &ToolResult) -> String {
300 format_result_plain(result)
301 }
302
303 fn format_output_ansi(&self, result: &ToolResult) -> String {
307 format_result_ansi(result)
308 }
309
310 fn format_output_markdown(&self, result: &ToolResult) -> String {
314 format_result_markdown(result)
315 }
316}
317
318pub trait DynTool: Send + Sync {
322 fn name(&self) -> &str;
323 fn description(&self) -> &str;
324 fn input_schema(&self) -> Value;
325 fn execute_raw(
326 &self,
327 input: Value,
328 ) -> std::pin::Pin<
329 Box<dyn std::future::Future<Output = Result<ToolResult, ToolError>> + Send + '_>,
330 >;
331
332 fn format_input_plain(&self, params: &Value) -> String;
334 fn format_input_ansi(&self, params: &Value) -> String;
335 fn format_input_markdown(&self, params: &Value) -> String;
336 fn format_output_plain(&self, result: &ToolResult) -> String;
337 fn format_output_ansi(&self, result: &ToolResult) -> String;
338 fn format_output_markdown(&self, result: &ToolResult) -> String;
339}
340
341pub fn box_tool<T: Tool + 'static>(tool: T) -> Box<dyn DynTool> {
343 Box::new(ToolWrapper(tool))
344}
345
346#[macro_export]
371macro_rules! box_tools {
372 ($($tool:expr),* $(,)?) => {
373 vec![$($crate::tool::box_tool($tool)),*]
374 };
375}
376
377struct ToolWrapper<T>(T);
379
380impl<T: Tool + 'static> DynTool for ToolWrapper<T> {
381 fn name(&self) -> &str {
382 self.0.name()
383 }
384
385 fn description(&self) -> &str {
386 self.0.description()
387 }
388
389 fn input_schema(&self) -> Value {
390 self.0.input_schema()
391 }
392
393 fn execute_raw(
394 &self,
395 input: Value,
396 ) -> std::pin::Pin<
397 Box<dyn std::future::Future<Output = Result<ToolResult, ToolError>> + Send + '_>,
398 > {
399 Box::pin(async move {
400 let typed_input: T::Input = serde_json::from_value(input)
401 .map_err(|e| ToolError::Custom(format!("Failed to deserialize input: {}", e)))?;
402
403 self.0.execute(typed_input).await
404 })
405 }
406
407 fn format_input_plain(&self, params: &Value) -> String {
408 self.0.format_input_plain(params)
409 }
410
411 fn format_input_ansi(&self, params: &Value) -> String {
412 self.0.format_input_ansi(params)
413 }
414
415 fn format_input_markdown(&self, params: &Value) -> String {
416 self.0.format_input_markdown(params)
417 }
418
419 fn format_output_plain(&self, result: &ToolResult) -> String {
420 self.0.format_output_plain(result)
421 }
422
423 fn format_output_ansi(&self, result: &ToolResult) -> String {
424 self.0.format_output_ansi(result)
425 }
426
427 fn format_output_markdown(&self, result: &ToolResult) -> String {
428 self.0.format_output_markdown(result)
429 }
430}
431
432const MAX_PARAMS: usize = 10;
437const MAX_VALUE_LEN: usize = 80;
438const MAX_OUTPUT_LINES: usize = 12;
439
440fn format_value_preview(value: &Value) -> String {
442 match value {
443 Value::String(s) => {
444 if s.len() > MAX_VALUE_LEN {
445 format!("\"{}…\"", &s[..MAX_VALUE_LEN])
446 } else {
447 format!("\"{}\"", s)
448 }
449 }
450 Value::Array(arr) => format!("[{} items]", arr.len()),
451 Value::Object(obj) => format!("{{{} keys}}", obj.len()),
452 Value::Null => "null".to_string(),
453 Value::Bool(b) => b.to_string(),
454 Value::Number(n) => n.to_string(),
455 }
456}
457
458pub fn format_params_plain(tool_name: &str, params: &Value) -> String {
460 let mut output = tool_name.to_string();
461
462 if let Some(obj) = params.as_object() {
463 for (key, value) in obj.iter().take(MAX_PARAMS) {
464 output.push_str(&format!("\n {}: {}", key, format_value_preview(value)));
465 }
466 if obj.len() > MAX_PARAMS {
467 output.push_str(&format!("\n … +{} more", obj.len() - MAX_PARAMS));
468 }
469 }
470
471 output
472}
473
474pub fn format_params_ansi(tool_name: &str, params: &Value) -> String {
476 let mut output = format!("\x1b[1m{}\x1b[0m", tool_name);
478
479 if let Some(obj) = params.as_object() {
480 for (key, value) in obj.iter().take(MAX_PARAMS) {
481 output.push_str(&format!(
483 "\n \x1b[2m{}:\x1b[0m {}",
484 key,
485 format_value_preview(value)
486 ));
487 }
488 if obj.len() > MAX_PARAMS {
489 output.push_str(&format!(
490 "\n \x1b[2m… +{} more\x1b[0m",
491 obj.len() - MAX_PARAMS
492 ));
493 }
494 }
495
496 output
497}
498
499pub fn format_params_markdown(tool_name: &str, params: &Value) -> String {
501 let mut output = format!("**{}**", tool_name);
502
503 if let Some(obj) = params.as_object() {
504 for (key, value) in obj.iter().take(MAX_PARAMS) {
505 output.push_str(&format!("\n- `{}`: {}", key, format_value_preview(value)));
506 }
507 if obj.len() > MAX_PARAMS {
508 output.push_str(&format!("\n- *… +{} more*", obj.len() - MAX_PARAMS));
509 }
510 }
511
512 output
513}
514
515fn result_to_text(result: &ToolResult) -> String {
517 match result {
518 ToolResult::Text(s) => s.clone(),
519 ToolResult::Json(v) => serde_json::to_string_pretty(v).unwrap_or_else(|_| v.to_string()),
520 ToolResult::Image { format, data } => {
521 format!("[Image: {:?}, {} bytes]", format, data.len())
522 }
523 ToolResult::Document { format, data, name } => {
524 let name_str = name.as_deref().unwrap_or("unnamed");
525 format!(
526 "[Document: {:?}, {}, {} bytes]",
527 format,
528 name_str,
529 data.len()
530 )
531 }
532 }
533}
534
535fn truncate_lines(text: &str, max_lines: usize) -> (String, usize) {
537 let lines: Vec<&str> = text.lines().collect();
538 if lines.len() <= max_lines {
539 (text.to_string(), 0)
540 } else {
541 let truncated = lines[..max_lines].join("\n");
542 (truncated, lines.len() - max_lines)
543 }
544}
545
546pub fn format_result_plain(result: &ToolResult) -> String {
548 let text = result_to_text(result);
549 let (truncated, remaining) = truncate_lines(&text, MAX_OUTPUT_LINES);
550
551 if remaining > 0 {
552 format!("{}\n… +{} more lines", truncated, remaining)
553 } else {
554 truncated
555 }
556}
557
558pub fn format_result_ansi(result: &ToolResult) -> String {
560 let text = result_to_text(result);
561 let (truncated, remaining) = truncate_lines(&text, MAX_OUTPUT_LINES);
562
563 if remaining > 0 {
564 format!(
565 "\x1b[32m✓\x1b[0m\n{}\n\x1b[2m… +{} more lines\x1b[0m",
566 truncated, remaining
567 )
568 } else {
569 format!("\x1b[32m✓\x1b[0m\n{}", truncated)
570 }
571}
572
573pub fn format_result_markdown(result: &ToolResult) -> String {
575 let text = result_to_text(result);
576 let (truncated, remaining) = truncate_lines(&text, MAX_OUTPUT_LINES);
577
578 let mut output = String::from("```\n");
579 output.push_str(&truncated);
580 output.push_str("\n```");
581
582 if remaining > 0 {
583 output.push_str(&format!("\n*… +{} more lines*", remaining));
584 }
585
586 output
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
596 fn test_format_value_preview_string_short() {
597 let value = serde_json::json!("hello");
598 assert_eq!(format_value_preview(&value), "\"hello\"");
599 }
600
601 #[test]
602 fn test_format_value_preview_string_long() {
603 let long_string = "x".repeat(100);
604 let value = serde_json::json!(long_string);
605 let preview = format_value_preview(&value);
606
607 assert!(preview.len() < 100);
609 assert!(preview.ends_with("…\""));
610 }
611
612 #[test]
613 fn test_format_value_preview_array() {
614 let value = serde_json::json!([1, 2, 3, 4, 5]);
615 assert_eq!(format_value_preview(&value), "[5 items]");
616 }
617
618 #[test]
619 fn test_format_value_preview_object() {
620 let value = serde_json::json!({"a": 1, "b": 2});
621 assert_eq!(format_value_preview(&value), "{2 keys}");
622 }
623
624 #[test]
625 fn test_format_value_preview_null() {
626 let value = serde_json::json!(null);
627 assert_eq!(format_value_preview(&value), "null");
628 }
629
630 #[test]
631 fn test_format_value_preview_bool() {
632 assert_eq!(format_value_preview(&serde_json::json!(true)), "true");
633 assert_eq!(format_value_preview(&serde_json::json!(false)), "false");
634 }
635
636 #[test]
637 fn test_format_value_preview_number() {
638 assert_eq!(format_value_preview(&serde_json::json!(42)), "42");
639 assert_eq!(format_value_preview(&serde_json::json!(1.5)), "1.5");
640 }
641
642 #[test]
645 fn test_truncate_lines_no_truncation() {
646 let text = "line1\nline2\nline3";
647 let (result, remaining) = truncate_lines(text, 5);
648 assert_eq!(result, text);
649 assert_eq!(remaining, 0);
650 }
651
652 #[test]
653 fn test_truncate_lines_with_truncation() {
654 let text = "line1\nline2\nline3\nline4\nline5";
655 let (result, remaining) = truncate_lines(text, 3);
656 assert_eq!(result, "line1\nline2\nline3");
657 assert_eq!(remaining, 2);
658 }
659
660 #[test]
661 fn test_truncate_lines_exact_limit() {
662 let text = "line1\nline2\nline3";
663 let (result, remaining) = truncate_lines(text, 3);
664 assert_eq!(result, text);
665 assert_eq!(remaining, 0);
666 }
667
668 #[test]
671 fn test_format_params_plain_simple() {
672 let params = serde_json::json!({"path": "/tmp/test.txt"});
673 let output = format_params_plain("read_file", ¶ms);
674
675 assert!(output.starts_with("read_file"));
676 assert!(output.contains("path:"));
677 assert!(output.contains("/tmp/test.txt"));
678 }
679
680 #[test]
681 fn test_format_params_plain_many_params() {
682 let mut obj = serde_json::Map::new();
684 for i in 0..15 {
685 obj.insert(format!("key{}", i), serde_json::json!(i));
686 }
687 let params = serde_json::Value::Object(obj);
688 let output = format_params_plain("test_tool", ¶ms);
689
690 assert!(output.contains("… +"));
691 assert!(output.contains("more"));
692 }
693
694 #[test]
695 fn test_format_params_ansi_has_codes() {
696 let params = serde_json::json!({"name": "test"});
697 let output = format_params_ansi("my_tool", ¶ms);
698
699 assert!(output.contains("\x1b["));
701 assert!(output.contains("my_tool"));
703 }
704
705 #[test]
706 fn test_format_params_markdown_format() {
707 let params = serde_json::json!({"file": "test.rs"});
708 let output = format_params_markdown("edit", ¶ms);
709
710 assert!(output.starts_with("**edit**"));
712 assert!(output.contains("- `file`:"));
714 }
715
716 #[test]
719 fn test_format_result_plain_short() {
720 let result = ToolResult::Text("Success!".to_string());
721 let output = format_result_plain(&result);
722 assert_eq!(output, "Success!");
723 }
724
725 #[test]
726 fn test_format_result_plain_truncated() {
727 let long_text = (0..20)
729 .map(|i| format!("line {}", i))
730 .collect::<Vec<_>>()
731 .join("\n");
732 let result = ToolResult::Text(long_text);
733 let output = format_result_plain(&result);
734
735 assert!(output.contains("… +"));
736 assert!(output.contains("more lines"));
737 }
738
739 #[test]
740 fn test_format_result_ansi_success_marker() {
741 let result = ToolResult::Text("Done".to_string());
742 let output = format_result_ansi(&result);
743
744 assert!(output.contains("\x1b[32m✓\x1b[0m"));
746 }
747
748 #[test]
749 fn test_format_result_markdown_code_block() {
750 let result = ToolResult::Text("code here".to_string());
751 let output = format_result_markdown(&result);
752
753 assert!(output.starts_with("```\n"));
754 assert!(output.contains("code here"));
755 assert!(output.contains("\n```"));
756 }
757
758 #[test]
759 fn test_format_result_json() {
760 let result = ToolResult::Json(serde_json::json!({"status": "ok"}));
761 let output = format_result_plain(&result);
762
763 assert!(output.contains("status"));
765 assert!(output.contains("ok"));
766 }
767
768 #[test]
769 fn test_format_result_image() {
770 let result = ToolResult::Image {
771 format: ImageFormat::Png,
772 data: vec![0u8; 1000],
773 };
774 let output = format_result_plain(&result);
775
776 assert!(output.contains("Image"));
777 assert!(output.contains("Png"));
778 assert!(output.contains("1000 bytes"));
779 }
780
781 #[test]
782 fn test_format_result_document() {
783 let result = ToolResult::Document {
784 format: DocumentFormat::Pdf,
785 data: vec![0u8; 500],
786 name: Some("report.pdf".to_string()),
787 };
788 let output = format_result_plain(&result);
789
790 assert!(output.contains("Document"));
791 assert!(output.contains("Pdf"));
792 assert!(output.contains("report.pdf"));
793 assert!(output.contains("500 bytes"));
794 }
795
796 #[test]
797 fn test_format_result_document_unnamed() {
798 let result = ToolResult::Document {
799 format: DocumentFormat::Txt,
800 data: vec![0u8; 100],
801 name: None,
802 };
803 let output = format_result_plain(&result);
804
805 assert!(output.contains("unnamed"));
806 }
807
808 #[test]
811 fn test_tool_result_image_factory() {
812 let result = ToolResult::image(ImageFormat::Jpeg, vec![1, 2, 3]);
813
814 if let ToolResult::Image { format, data } = result {
815 assert_eq!(format, ImageFormat::Jpeg);
816 assert_eq!(data, vec![1, 2, 3]);
817 } else {
818 panic!("Expected Image variant");
819 }
820 }
821
822 #[test]
823 fn test_tool_result_document_factory() {
824 let result = ToolResult::document(DocumentFormat::Csv, vec![4, 5, 6]);
825
826 if let ToolResult::Document { format, data, name } = result {
827 assert_eq!(format, DocumentFormat::Csv);
828 assert_eq!(data, vec![4, 5, 6]);
829 assert!(name.is_none());
830 } else {
831 panic!("Expected Document variant");
832 }
833 }
834
835 #[test]
836 fn test_tool_result_document_with_name_factory() {
837 let result = ToolResult::document_with_name(DocumentFormat::Html, vec![7, 8], "page.html");
838
839 if let ToolResult::Document { format, data, name } = result {
840 assert_eq!(format, DocumentFormat::Html);
841 assert_eq!(data, vec![7, 8]);
842 assert_eq!(name, Some("page.html".to_string()));
843 } else {
844 panic!("Expected Document variant");
845 }
846 }
847
848 #[test]
851 fn test_tool_result_as_text_image() {
852 let result = ToolResult::Image {
853 format: ImageFormat::Gif,
854 data: vec![0u8; 2000],
855 };
856 let text = result.as_text();
857
858 assert!(text.contains("Image"));
859 assert!(text.contains("Gif"));
860 assert!(text.contains("2000 bytes"));
861 }
862
863 #[test]
864 fn test_tool_result_as_text_document() {
865 let result = ToolResult::Document {
866 format: DocumentFormat::Xlsx,
867 data: vec![0u8; 3000],
868 name: Some("data.xlsx".to_string()),
869 };
870 let text = result.as_text();
871
872 assert!(text.contains("Document"));
873 assert!(text.contains("Xlsx"));
874 assert!(text.contains("data.xlsx"));
875 assert!(text.contains("3000 bytes"));
876 }
877
878 #[test]
879 fn test_tool_result_as_str_binary_types() {
880 let image = ToolResult::Image {
881 format: ImageFormat::Webp,
882 data: vec![],
883 };
884 assert!(image.as_str().is_none());
885
886 let doc = ToolResult::Document {
887 format: DocumentFormat::Doc,
888 data: vec![],
889 name: None,
890 };
891 assert!(doc.as_str().is_none());
892 }
893}