spec_ai_core/tools/
plugin_adapter.rs

1//! Adapter for wrapping plugin tools as async Tool implementations
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde_json::Value;
6use spec_ai_plugin::{PluginToolRef, PluginToolResult as PluginResult};
7
8use super::{Tool, ToolResult};
9
10/// Wraps an ABI-stable plugin tool and implements the async Tool trait
11pub struct PluginToolAdapter {
12    tool_ref: PluginToolRef,
13    /// Cached tool name
14    name: String,
15    /// Cached tool description
16    description: String,
17    /// Cached parameters schema
18    parameters: Value,
19    /// Name of the plugin this tool came from
20    plugin_name: String,
21}
22
23impl PluginToolAdapter {
24    /// Create a new adapter for a plugin tool
25    ///
26    /// # Arguments
27    /// * `tool_ref` - Reference to the plugin tool
28    /// * `plugin_name` - Name of the plugin for identification
29    ///
30    /// # Returns
31    /// The adapter, or an error if the tool's parameters JSON is invalid
32    pub fn new(tool_ref: PluginToolRef, plugin_name: impl Into<String>) -> Result<Self> {
33        let info = (tool_ref.info)();
34
35        let parameters: Value = serde_json::from_str(info.parameters_json.as_str())
36            .map_err(|e| anyhow::anyhow!("Invalid parameters JSON from plugin: {}", e))?;
37
38        Ok(Self {
39            tool_ref,
40            name: info.name.to_string(),
41            description: info.description.to_string(),
42            parameters,
43            plugin_name: plugin_name.into(),
44        })
45    }
46
47    /// Get the name of the plugin this tool came from
48    pub fn plugin_name(&self) -> &str {
49        &self.plugin_name
50    }
51}
52
53#[async_trait]
54impl Tool for PluginToolAdapter {
55    fn name(&self) -> &str {
56        &self.name
57    }
58
59    fn description(&self) -> &str {
60        &self.description
61    }
62
63    fn parameters(&self) -> Value {
64        self.parameters.clone()
65    }
66
67    async fn execute(&self, args: Value) -> Result<ToolResult> {
68        // Serialize arguments to JSON
69        let args_json = serde_json::to_string(&args)?;
70
71        // Call the plugin's execute function
72        let result: PluginResult = (self.tool_ref.execute)(args_json.as_str().into());
73
74        // Convert to our ToolResult
75        Ok(ToolResult {
76            success: result.success,
77            output: result.output.to_string(),
78            error: result.error.map(|e| e.to_string()).into_option(),
79        })
80    }
81}
82
83impl std::fmt::Debug for PluginToolAdapter {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.debug_struct("PluginToolAdapter")
86            .field("name", &self.name)
87            .field("plugin_name", &self.plugin_name)
88            .field("description", &self.description)
89            .finish()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    // Note: Testing actual plugin loading requires a compiled plugin library.
98    // These tests verify the adapter logic with mock data.
99
100    #[test]
101    fn test_tool_result_conversion() {
102        // Test success conversion
103        let plugin_result = PluginResult::success("test output");
104        let result = ToolResult {
105            success: plugin_result.success,
106            output: plugin_result.output.to_string(),
107            error: plugin_result.error.map(|e| e.to_string()).into_option(),
108        };
109        assert!(result.success);
110        assert_eq!(result.output, "test output");
111        assert!(result.error.is_none());
112
113        // Test failure conversion
114        let plugin_result = PluginResult::failure("test error");
115        let result = ToolResult {
116            success: plugin_result.success,
117            output: plugin_result.output.to_string(),
118            error: plugin_result.error.map(|e| e.to_string()).into_option(),
119        };
120        assert!(!result.success);
121        assert_eq!(result.error, Some("test error".to_string()));
122    }
123}