Skip to main content

construct/tools/
traits.rs

1use crate::tools::progress::ProgressSink;
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4
5/// Result of a tool execution
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ToolResult {
8    pub success: bool,
9    pub output: String,
10    pub error: Option<String>,
11}
12
13/// Description of a tool for the LLM
14#[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/// Core tool trait — implement for any capability
22#[async_trait]
23pub trait Tool: Send + Sync {
24    /// Tool name (used in LLM function calling)
25    fn name(&self) -> &str;
26
27    /// Human-readable description
28    fn description(&self) -> &str;
29
30    /// JSON schema for parameters
31    fn parameters_schema(&self) -> serde_json::Value;
32
33    /// Execute the tool with given arguments
34    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult>;
35
36    /// Execute the tool with a progress-notification sink.
37    ///
38    /// This is an **additive**, opt-in entry point for long-running tools
39    /// that want to emit progress updates (modeled on MCP
40    /// `notifications/progress`). The default implementation simply forwards
41    /// to [`Tool::execute`] and ignores `sink`, so every existing tool keeps
42    /// compiling and behaving identically.
43    ///
44    /// Callers that do not care about progress should keep calling
45    /// [`Tool::execute`] directly; callers that do (e.g. the forthcoming
46    /// in-process MCP server) call `execute_with_progress` with a real sink
47    /// and forward emissions as MCP notifications.
48    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    /// Get the full spec for LLM registration
57    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}