spec_ai_plugin/
abi.rs

1//! ABI-stable types for the plugin interface
2//!
3//! This module defines the stable interface between the host application and plugins.
4//! All types here use `abi_stable` to ensure binary compatibility across different
5//! compiler versions.
6
7use abi_stable::{
8    declare_root_module_statics,
9    library::RootModule,
10    package_version_strings,
11    sabi_types::VersionStrings,
12    std_types::{ROption, RStr, RString, RVec},
13    StableAbi,
14};
15
16/// Version of the plugin API.
17/// Bump this when making breaking changes to the plugin interface.
18pub const PLUGIN_API_VERSION: u32 = 1;
19
20/// ABI-stable result type for plugin tool execution
21#[repr(C)]
22#[derive(StableAbi, Debug, Clone)]
23pub struct PluginToolResult {
24    /// Whether execution succeeded
25    pub success: bool,
26    /// Output from the tool (empty on failure)
27    pub output: RString,
28    /// Error message if execution failed
29    pub error: ROption<RString>,
30}
31
32impl PluginToolResult {
33    /// Create a successful result
34    pub fn success(output: impl Into<String>) -> Self {
35        Self {
36            success: true,
37            output: RString::from(output.into()),
38            error: ROption::RNone,
39        }
40    }
41
42    /// Create a failure result
43    pub fn failure(error: impl Into<String>) -> Self {
44        Self {
45            success: false,
46            output: RString::new(),
47            error: ROption::RSome(RString::from(error.into())),
48        }
49    }
50}
51
52/// ABI-stable tool metadata
53#[repr(C)]
54#[derive(StableAbi, Debug, Clone)]
55pub struct PluginToolInfo {
56    /// Unique name of the tool
57    pub name: RString,
58    /// Human-readable description of what the tool does
59    pub description: RString,
60    /// JSON Schema describing the tool's parameters (as JSON string)
61    pub parameters_json: RString,
62}
63
64impl PluginToolInfo {
65    /// Create new tool info
66    pub fn new(
67        name: impl Into<String>,
68        description: impl Into<String>,
69        parameters_json: impl Into<String>,
70    ) -> Self {
71        Self {
72            name: RString::from(name.into()),
73            description: RString::from(description.into()),
74            parameters_json: RString::from(parameters_json.into()),
75        }
76    }
77}
78
79/// ABI-stable tool interface that plugins implement.
80///
81/// This struct contains function pointers for the tool's operations.
82/// Plugins create static instances of this struct for each tool they provide.
83#[repr(C)]
84#[derive(StableAbi)]
85pub struct PluginTool {
86    /// Get tool metadata (name, description, parameters schema)
87    pub info: extern "C" fn() -> PluginToolInfo,
88
89    /// Execute the tool with JSON-encoded arguments
90    ///
91    /// # Arguments
92    /// * `args_json` - JSON string containing the tool arguments
93    ///
94    /// # Returns
95    /// Result containing output or error message
96    pub execute: extern "C" fn(args_json: RStr<'_>) -> PluginToolResult,
97
98    /// Optional: Initialize plugin with host context
99    ///
100    /// Called once when the plugin is loaded. Can be used to set up
101    /// resources or validate the environment.
102    ///
103    /// # Arguments
104    /// * `context_json` - JSON string with context from the host (currently empty object)
105    ///
106    /// # Returns
107    /// `true` if initialization succeeded, `false` to abort loading
108    pub initialize: Option<extern "C" fn(context_json: RStr<'_>) -> bool>,
109}
110
111/// Reference to a PluginTool for use in collections
112pub type PluginToolRef = &'static PluginTool;
113
114/// Root module that plugins export.
115///
116/// This is the entry point for the plugin. The host loads this module
117/// and uses it to discover and access the plugin's tools.
118#[repr(C)]
119#[derive(StableAbi)]
120#[sabi(kind(Prefix(prefix_ref = PluginModuleRef)))]
121pub struct PluginModule {
122    /// Get the plugin API version
123    ///
124    /// Must return `PLUGIN_API_VERSION` for compatibility
125    pub api_version: extern "C" fn() -> u32,
126
127    /// Get all tools provided by this plugin
128    pub get_tools: extern "C" fn() -> RVec<PluginToolRef>,
129
130    /// Get the plugin name for identification
131    pub plugin_name: extern "C" fn() -> RString,
132
133    /// Optional cleanup function called when the plugin is unloaded
134    #[sabi(last_prefix_field)]
135    pub shutdown: Option<extern "C" fn()>,
136}
137
138impl RootModule for PluginModuleRef {
139    declare_root_module_statics! {PluginModuleRef}
140
141    const BASE_NAME: &'static str = "spec_ai_plugin";
142    const NAME: &'static str = "spec_ai_plugin";
143    const VERSION_STRINGS: VersionStrings = package_version_strings!();
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_plugin_tool_result_success() {
152        let result = PluginToolResult::success("test output");
153        assert!(result.success);
154        assert_eq!(result.output.as_str(), "test output");
155        assert!(result.error.is_none());
156    }
157
158    #[test]
159    fn test_plugin_tool_result_failure() {
160        let result = PluginToolResult::failure("test error");
161        assert!(!result.success);
162        assert!(result.output.is_empty());
163        match &result.error {
164            ROption::RSome(s) => assert_eq!(s.as_str(), "test error"),
165            ROption::RNone => panic!("Expected error message"),
166        }
167    }
168
169    #[test]
170    fn test_plugin_tool_info() {
171        let info = PluginToolInfo::new("test", "A test tool", r#"{"type": "object"}"#);
172        assert_eq!(info.name.as_str(), "test");
173        assert_eq!(info.description.as_str(), "A test tool");
174        assert_eq!(info.parameters_json.as_str(), r#"{"type": "object"}"#);
175    }
176}