Skip to main content

lenso_plugin_sdk/
lib.rs

1//! Target-independent Rust authoring for portable Lenso Plugins.
2//!
3//! Plugin business code implements a product-facing trait once. The export
4//! macro lowers that implementation to the selected execution target at
5//! compile time, keeping WIT and process framing out of the Plugin project.
6
7use schemars::JsonSchema;
8use serde::{Serialize, de::DeserializeOwned};
9use serde_json::{Value, json};
10
11#[cfg(target_arch = "wasm32")]
12extern crate self as lenso_plugin_sdk;
13
14#[cfg(target_arch = "wasm32")]
15#[doc(hidden)]
16pub mod __wasm {
17    wit_bindgen::generate!({
18        inline: r#"
19            package lenso:runtime@1.0.0;
20
21            world plugin {
22              export describe: func() -> string;
23              export invoke: func(capability: string, operation: string, request-json: string) -> result<string, string>;
24            }
25        "#,
26        world: "plugin",
27        export_macro_name: "export_lenso_plugin",
28        pub_export_macro: true,
29        default_bindings_module: "::lenso_plugin_sdk::__wasm",
30    });
31}
32
33/// One target-independent request result.
34#[derive(Clone, Debug)]
35pub enum RequestOutcome {
36    /// A successful capability response.
37    Success(Value),
38    /// A domain error defined by the provided capability.
39    DomainError(Value),
40    /// An adapter-safe implementation failure.
41    Failure(String),
42}
43
44/// Business implementation for one request-based Lenso Plugin.
45pub trait RequestPlugin: Default {
46    /// Handles one validated capability request.
47    fn invoke(&self, capability: &str, operation: &str, request: Value) -> RequestOutcome;
48}
49
50/// A typed Agent Tool exposed through `lenso.agent.tool-provider@2`.
51pub trait AgentTool: Default {
52    /// JSON arguments accepted by the Tool.
53    type Arguments: DeserializeOwned + JsonSchema;
54    /// Domain error returned by the Tool.
55    type Error: Serialize;
56
57    /// Stable Tool name presented to the model.
58    const NAME: &'static str;
59    /// Human-readable Tool description presented to the model.
60    const DESCRIPTION: &'static str;
61
62    /// Executes the Tool business operation.
63    fn execute(&self, arguments: Self::Arguments) -> Result<String, Self::Error>;
64}
65
66#[doc(hidden)]
67pub struct AgentToolAdapter<T>(T);
68
69impl<T> std::fmt::Debug for AgentToolAdapter<T> {
70    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        formatter.write_str("AgentToolAdapter")
72    }
73}
74
75impl<T: AgentTool> Default for AgentToolAdapter<T> {
76    fn default() -> Self {
77        Self(T::default())
78    }
79}
80
81impl<T: AgentTool> RequestPlugin for AgentToolAdapter<T> {
82    fn invoke(&self, capability: &str, operation: &str, request: Value) -> RequestOutcome {
83        if capability != "lenso.agent.tool-provider@2" {
84            return RequestOutcome::DomainError(json!("not_found"));
85        }
86        match operation {
87            "catalog" => catalog::<T>(),
88            "execute" => execute(&self.0, &request),
89            _ => RequestOutcome::DomainError(json!("not_found")),
90        }
91    }
92}
93
94fn catalog<T: AgentTool>() -> RequestOutcome {
95    let input_schema_json = match serde_json::to_string(&schemars::schema_for!(T::Arguments)) {
96        Ok(schema) => schema,
97        Err(error) => return RequestOutcome::Failure(error.to_string()),
98    };
99    RequestOutcome::Success(json!({
100        "tools": [{
101            "name": T::NAME,
102            "description": T::DESCRIPTION,
103            "input_schema_json": input_schema_json,
104            "execution": "parallel_safe",
105        }],
106    }))
107}
108
109fn execute<T: AgentTool>(tool: &T, request: &Value) -> RequestOutcome {
110    let Some(name) = request.get("name").and_then(Value::as_str) else {
111        return RequestOutcome::DomainError(json!("invalid_arguments"));
112    };
113    let Some(arguments_json) = request.get("arguments_json").and_then(Value::as_str) else {
114        return RequestOutcome::DomainError(json!("invalid_arguments"));
115    };
116    if name != T::NAME {
117        return RequestOutcome::DomainError(json!("not_found"));
118    }
119    let Ok(arguments) = serde_json::from_str::<T::Arguments>(arguments_json) else {
120        return RequestOutcome::DomainError(json!("invalid_arguments"));
121    };
122    match tool.execute(arguments) {
123        Ok(content) => RequestOutcome::Success(json!({
124            "content_type": "text",
125            "content": content,
126            "metadata_json": r#"{"provider":"lenso-portable"}"#,
127        })),
128        Err(error) => match serde_json::to_value(error) {
129            Ok(error) => RequestOutcome::DomainError(error),
130            Err(error) => RequestOutcome::Failure(error.to_string()),
131        },
132    }
133}
134
135/// Exports one request Plugin as Wasm Component or Process according to the
136/// active Cargo target.
137#[macro_export]
138macro_rules! export_request_plugin {
139    (
140        $plugin:ty {
141            capability_id: $capability_id:literal,
142            descriptor_version: $descriptor_version:literal,
143            requests: [$first_request:literal $(, $request:literal)* $(,)?] $(,)?
144        }
145    ) => {
146        type __LensoExportedPlugin = $plugin;
147
148        #[cfg(target_arch = "wasm32")]
149        mod __lenso_wasm_export {
150            struct Component;
151
152            const DESCRIPTOR: &str = $crate::__private::lenso_guest_sdk::__request_plugin_descriptor!(
153                $capability_id,
154                $descriptor_version,
155                $first_request $(, $request)*
156            );
157
158            #[used]
159            #[unsafe(link_section = "lenso.plugin-descriptor.v1")]
160            static DESCRIPTOR_SECTION: [u8; DESCRIPTOR.len()] =
161                $crate::__private::lenso_guest_sdk::__descriptor_bytes(DESCRIPTOR);
162
163            impl $crate::__private::wasm::Guest for Component {
164                fn describe() -> ::std::string::String {
165                    DESCRIPTOR.to_owned()
166                }
167
168                fn invoke(
169                    capability: ::std::string::String,
170                    operation: ::std::string::String,
171                    request_json: ::std::string::String,
172                ) -> ::std::result::Result<::std::string::String, ::std::string::String> {
173                    let request = $crate::__private::serde_json::from_str(&request_json)
174                        .map_err(|_| "\"invalid_arguments\"".to_owned())?;
175                    match <super::__LensoExportedPlugin as $crate::RequestPlugin>::invoke(
176                        &<super::__LensoExportedPlugin as ::std::default::Default>::default(),
177                        &capability,
178                        &operation,
179                        request,
180                    ) {
181                        $crate::RequestOutcome::Success(value) =>
182                            $crate::__private::serde_json::to_string(&value)
183                                .map_err(|error| error.to_string()),
184                        $crate::RequestOutcome::DomainError(value) =>
185                            ::std::result::Result::Err(
186                                $crate::__private::serde_json::to_string(&value)
187                                    .unwrap_or_else(|_| "\"execution_failed\"".to_owned()),
188                            ),
189                        $crate::RequestOutcome::Failure(detail) =>
190                            ::std::result::Result::Err(
191                                $crate::__private::serde_json::to_string(&detail)
192                                    .unwrap_or_else(|_| "\"execution_failed\"".to_owned()),
193                            ),
194                    }
195                }
196            }
197
198            $crate::__private::wasm::export_lenso_plugin!(Component);
199        }
200
201        #[cfg(not(target_arch = "wasm32"))]
202        mod __lenso_process_export {
203            struct Component(super::__LensoExportedPlugin);
204
205            impl $crate::__private::lenso_process_sdk::ProcessPlugin for Component {
206                fn descriptor(&self) -> $crate::__private::serde_json::Value {
207                    $crate::__private::serde_json::json!({
208                        "abi": "lenso.json-request@1",
209                        "capabilities": [{
210                            "capability_id": $capability_id,
211                            "descriptor_version": $descriptor_version,
212                            "request_operations": [$first_request $(, $request)*],
213                        }],
214                    })
215                }
216
217                fn invoke(
218                    &self,
219                    capability: &str,
220                    operation: &str,
221                    request: $crate::__private::serde_json::Value,
222                ) -> $crate::__private::lenso_process_sdk::ProcessOutcome {
223                    match <super::__LensoExportedPlugin as $crate::RequestPlugin>::invoke(
224                        &self.0,
225                        capability,
226                        operation,
227                        request,
228                    ) {
229                        $crate::RequestOutcome::Success(value) =>
230                            $crate::__private::lenso_process_sdk::ProcessOutcome::Success(value),
231                        $crate::RequestOutcome::DomainError(value) =>
232                            $crate::__private::lenso_process_sdk::ProcessOutcome::DomainError(value),
233                        $crate::RequestOutcome::Failure(detail) =>
234                            $crate::__private::lenso_process_sdk::ProcessOutcome::Failure(detail),
235                    }
236                }
237            }
238
239            pub fn serve() {
240                $crate::__private::lenso_process_sdk::serve(&Component(
241                    <super::__LensoExportedPlugin as ::std::default::Default>::default(),
242                ))
243                .expect("serve Lenso Process Plugin");
244            }
245        }
246
247        #[cfg(not(target_arch = "wasm32"))]
248        fn main() {
249            __lenso_process_export::serve();
250        }
251    };
252}
253
254/// Exports one typed Agent Tool from the same source as Wasm or Process.
255#[macro_export]
256macro_rules! export_agent_tool {
257    ($plugin:ty $(,)?) => {
258        $crate::export_request_plugin! {
259            $crate::AgentToolAdapter<$plugin> {
260                capability_id: "lenso.agent.tool-provider@2",
261                descriptor_version: "2.0.0",
262                requests: ["catalog", "execute"],
263            }
264        }
265    };
266}
267
268#[doc(hidden)]
269pub mod __private {
270    pub use serde_json;
271
272    #[cfg(target_arch = "wasm32")]
273    pub use crate::__wasm as wasm;
274    #[cfg(target_arch = "wasm32")]
275    pub use lenso_guest_sdk;
276    #[cfg(not(target_arch = "wasm32"))]
277    pub use lenso_process_sdk;
278}