construct/tools/
traits.rs1use crate::tools::progress::ProgressSink;
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ToolResult {
8 pub success: bool,
9 pub output: String,
10 pub error: Option<String>,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ToolSpec {
16 pub name: String,
17 pub description: String,
18 pub parameters: serde_json::Value,
19}
20
21#[async_trait]
23pub trait Tool: Send + Sync {
24 fn name(&self) -> &str;
26
27 fn description(&self) -> &str;
29
30 fn parameters_schema(&self) -> serde_json::Value;
32
33 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult>;
35
36 async fn execute_with_progress(
49 &self,
50 args: serde_json::Value,
51 _sink: &dyn ProgressSink,
52 ) -> anyhow::Result<ToolResult> {
53 self.execute(args).await
54 }
55
56 fn spec(&self) -> ToolSpec {
58 ToolSpec {
59 name: self.name().to_string(),
60 description: self.description().to_string(),
61 parameters: self.parameters_schema(),
62 }
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 struct DummyTool;
71
72 #[async_trait]
73 impl Tool for DummyTool {
74 fn name(&self) -> &str {
75 "dummy_tool"
76 }
77
78 fn description(&self) -> &str {
79 "A deterministic test tool"
80 }
81
82 fn parameters_schema(&self) -> serde_json::Value {
83 serde_json::json!({
84 "type": "object",
85 "properties": {
86 "value": { "type": "string" }
87 }
88 })
89 }
90
91 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
92 Ok(ToolResult {
93 success: true,
94 output: args
95 .get("value")
96 .and_then(serde_json::Value::as_str)
97 .unwrap_or_default()
98 .to_string(),
99 error: None,
100 })
101 }
102 }
103
104 #[test]
105 fn spec_uses_tool_metadata_and_schema() {
106 let tool = DummyTool;
107 let spec = tool.spec();
108
109 assert_eq!(spec.name, "dummy_tool");
110 assert_eq!(spec.description, "A deterministic test tool");
111 assert_eq!(spec.parameters["type"], "object");
112 assert_eq!(spec.parameters["properties"]["value"]["type"], "string");
113 }
114
115 #[tokio::test]
116 async fn execute_returns_expected_output() {
117 let tool = DummyTool;
118 let result = tool
119 .execute(serde_json::json!({ "value": "hello-tool" }))
120 .await
121 .unwrap();
122
123 assert!(result.success);
124 assert_eq!(result.output, "hello-tool");
125 assert!(result.error.is_none());
126 }
127
128 #[test]
129 fn tool_result_serialization_roundtrip() {
130 let result = ToolResult {
131 success: false,
132 output: String::new(),
133 error: Some("boom".into()),
134 };
135
136 let json = serde_json::to_string(&result).unwrap();
137 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
138
139 assert!(!parsed.success);
140 assert_eq!(parsed.error.as_deref(), Some("boom"));
141 }
142}