Skip to main content

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