cortex_sdk/lib.rs
1//! # Cortex SDK
2//!
3//! The official Rust SDK for Cortex's trusted native plugin boundary.
4//!
5//! This crate defines the public plugin surface with **zero dependency on
6//! Cortex internals**. The runtime loads trusted native plugins through a
7//! stable C-compatible ABI and bridges these traits to its own turn runtime,
8//! command surface, and transport layer.
9//!
10//! Process-isolated JSON plugins do **not** need this crate. They are defined
11//! through `manifest.toml` plus a child-process command. Use `cortex-sdk` when
12//! you are building a trusted in-process native plugin that exports
13//! `cortex_plugin_init`.
14//!
15//! ## Architecture
16//!
17//! ```text
18//! ┌──────────────┐ dlopen ┌──────────────────┐
19//! │ cortex-runtime│ ──────────────▶ │ your plugin.so │
20//! │ (daemon) │ │ cortex-sdk only │
21//! └──────┬───────┘ FFI call └────────┬─────────┘
22//! │ cortex_plugin_init() │
23//! ▼ ▼
24//! ToolRegistry ◀─── register ─── MultiToolPlugin
25//! ├─ plugin_info()
26//! └─ create_tools()
27//! ├─ Tool A
28//! └─ Tool B
29//! ```
30//!
31//! Plugins are compiled as `cdylib` shared libraries. The runtime calls
32//! `cortex_plugin_init`, receives a C-compatible function table, then asks that
33//! table for plugin metadata, tool descriptors, and tool execution results.
34//! Rust trait objects stay inside the plugin; they never cross the
35//! dynamic-library boundary.
36//!
37//! The SDK now exposes a runtime-aware execution surface as well:
38//!
39//! - [`InvocationContext`] gives tools stable metadata such as session id,
40//! canonical actor, transport/source, and foreground/background scope
41//! - [`ToolRuntime`] lets tools emit progress updates and observer text back
42//! to the parent turn
43//! - [`ToolCapabilities`] lets tools declare whether they emit runtime signals
44//! and whether they are background-safe
45//! - [`Attachment`] and [`ToolResult::with_media`] let tools return structured
46//! image, audio, video, or file outputs without depending on Cortex internals
47//! ## Quick Start
48//!
49//! **Cargo.toml:**
50//!
51//! ```toml
52//! [lib]
53//! crate-type = ["cdylib"]
54//!
55//! [dependencies]
56//! cortex-sdk = "1.2"
57//! serde_json = "1"
58//! ```
59//!
60//! **src/lib.rs:**
61//!
62//! ```rust,no_run
63//! use cortex_sdk::prelude::*;
64//!
65//! // 1. Define the plugin entry point.
66//! #[derive(Default)]
67//! struct MyPlugin;
68//!
69//! impl MultiToolPlugin for MyPlugin {
70//! fn plugin_info(&self) -> PluginInfo {
71//! PluginInfo {
72//! name: "my-plugin".into(),
73//! version: env!("CARGO_PKG_VERSION").into(),
74//! description: "My custom tools for Cortex".into(),
75//! }
76//! }
77//!
78//! fn create_tools(&self) -> Vec<Box<dyn Tool>> {
79//! vec![Box::new(WordCountTool)]
80//! }
81//! }
82//!
83//! // 2. Implement one or more tools.
84//! struct WordCountTool;
85//!
86//! impl Tool for WordCountTool {
87//! fn name(&self) -> &'static str { "word_count" }
88//!
89//! fn description(&self) -> &'static str {
90//! "Count words in a text string. Use when the user asks for word \
91//! counts, statistics, or text length metrics."
92//! }
93//!
94//! fn input_schema(&self) -> serde_json::Value {
95//! serde_json::json!({
96//! "type": "object",
97//! "properties": {
98//! "text": {
99//! "type": "string",
100//! "description": "The text to count words in"
101//! }
102//! },
103//! "required": ["text"]
104//! })
105//! }
106//!
107//! fn execute(&self, input: serde_json::Value) -> Result<ToolResult, ToolError> {
108//! let text = input["text"]
109//! .as_str()
110//! .ok_or_else(|| ToolError::InvalidInput("missing 'text' field".into()))?;
111//! let count = text.split_whitespace().count();
112//! Ok(ToolResult::success(format!("{count} words")))
113//! }
114//! }
115//!
116//! // 3. Export the FFI entry point.
117//! cortex_sdk::export_plugin!(MyPlugin);
118//! ```
119//!
120//! Tools that need runtime context can override
121//! [`Tool::execute_with_runtime`] instead of only [`Tool::execute`].
122//!
123//! ## Build & Install
124//!
125//! ```bash
126//! cargo build --release
127//! cortex plugin install ./my-plugin/
128//! ```
129//!
130//! If `my-plugin/manifest.toml` declares `[native].library = "lib/libmy_plugin.so"`
131//! (or `.dylib` on macOS), Cortex copies the built library from
132//! `target/release/` into the installed plugin's `lib/` directory
133//! automatically when you install from a local folder.
134//!
135//! For versioned distribution:
136//!
137//! ```bash
138//! cargo build --release
139//! cortex plugin pack ./my-plugin
140//! cortex plugin install ./my-plugin-v0.1.0-linux-amd64.cpx
141//! ```
142//!
143//! Installing or replacing a trusted native shared library still requires a
144//! daemon restart so the new code is loaded. Process-isolated plugin manifest
145//! changes hot-apply without that restart.
146//!
147//! ## Plugin Lifecycle
148//!
149//! 1. **Load** — `dlopen` at daemon startup
150//! 2. **Create** — runtime calls [`export_plugin!`]-generated stable ABI init
151//! 3. **Register** — [`MultiToolPlugin::create_tools`] is called once; each
152//! [`Tool`] is registered in the global tool registry
153//! 4. **Execute** — the LLM invokes tools by name during turns; the runtime
154//! calls [`Tool::execute`] with JSON parameters
155//! 5. **Retain** — the library handle is held for the daemon's lifetime;
156//! `Drop` runs only at shutdown
157//!
158//! ## Tool Design Guidelines
159//!
160//! - **`name`**: lowercase with underscores (`word_count`, not `WordCount`).
161//! Must be unique across all tools in the registry.
162//! - **`description`**: written for the LLM — explain what the tool does,
163//! when to use it, and when *not* to use it. The LLM reads this to decide
164//! whether to call the tool.
165//! - **`input_schema`**: a [JSON Schema](https://json-schema.org/) object
166//! describing the parameters. The LLM generates JSON matching this schema.
167//! - **`execute`**: receives the LLM-generated JSON. Return
168//! [`ToolResult::success`] for normal output or [`ToolResult::error`] for
169//! recoverable errors the LLM should see. Return [`ToolError`] only for
170//! unrecoverable failures (invalid input, missing deps).
171//! - **Media output**: attach files with [`ToolResult::with_media`]. Cortex
172//! delivers attachments through the active transport; plugins should not call
173//! channel-specific APIs directly.
174//! - **`execute_with_runtime`**: use this when the tool needs invocation
175//! metadata or wants to emit progress / observer updates during execution.
176//! - **`timeout_secs`**: optional per-tool timeout override. If `None`, the
177//! global `[turn].tool_timeout_secs` applies.
178
179use serde::{Deserialize, Serialize};
180pub use serde_json;
181use std::ffi::c_void;
182
183/// Version of the SDK crate used by native plugin builds.
184pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
185
186/// Stable native ABI version for trusted in-process plugins.
187///
188/// The runtime never exchanges Rust trait objects across the dynamic-library
189/// boundary. It loads a C-compatible function table through `cortex_plugin_init`
190/// and moves structured values as UTF-8 JSON buffers.
191pub const NATIVE_ABI_VERSION: u32 = 1;
192
193/// Stable multimedia attachment DTO exposed to plugins.
194///
195/// This type intentionally lives in `cortex-sdk` instead of depending on
196/// Cortex internal crates, so plugin authors only need the SDK.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct Attachment {
199 /// High-level type: `"image"`, `"audio"`, `"video"`, `"file"`.
200 pub media_type: String,
201 /// MIME type, for example `"image/png"` or `"audio/mpeg"`.
202 pub mime_type: String,
203 /// Local file path or remote URL readable by the runtime transport.
204 pub url: String,
205 /// Optional caption or description.
206 pub caption: Option<String>,
207 /// File size in bytes, if known.
208 pub size: Option<u64>,
209}
210
211/// Whether a tool invocation belongs to a user-visible foreground turn or a
212/// background maintenance execution.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
214#[serde(rename_all = "snake_case")]
215pub enum ExecutionScope {
216 #[default]
217 Foreground,
218 Background,
219}
220
221/// Stable runtime metadata exposed to plugin tools during execution.
222///
223/// This intentionally exposes the execution surface, not Cortex internals.
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct InvocationContext {
226 /// Tool name being invoked.
227 pub tool_name: String,
228 /// Active session id when available.
229 pub session_id: Option<String>,
230 /// Canonical actor identity when available.
231 pub actor: Option<String>,
232 /// Transport or invocation source (`http`, `rpc`, `telegram`, `heartbeat`, ...).
233 pub source: Option<String>,
234 /// Whether this invocation belongs to a foreground or background execution.
235 pub execution_scope: ExecutionScope,
236}
237
238impl InvocationContext {
239 #[must_use]
240 pub fn is_background(&self) -> bool {
241 self.execution_scope == ExecutionScope::Background
242 }
243
244 #[must_use]
245 pub fn is_foreground(&self) -> bool {
246 self.execution_scope == ExecutionScope::Foreground
247 }
248}
249
250/// Declarative hints about how a tool participates in the runtime.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
252pub struct ToolCapabilities {
253 /// Tool emits intermediate progress updates.
254 pub emits_progress: bool,
255 /// Tool emits observer-lane notes for the parent turn.
256 pub emits_observer_text: bool,
257 /// Tool is safe to run in background maintenance contexts.
258 pub background_safe: bool,
259}
260
261/// Runtime bridge presented to tools during execution.
262///
263/// This allows plugins to consume stable runtime context and emit bounded
264/// execution signals without depending on Cortex internals.
265pub trait ToolRuntime: Send + Sync {
266 /// Stable invocation metadata.
267 fn invocation(&self) -> &InvocationContext;
268
269 /// Emit an intermediate progress update for the current tool.
270 fn emit_progress(&self, message: &str);
271
272 /// Emit observer text for the parent turn. This never speaks directly to
273 /// the user-facing channel.
274 fn emit_observer(&self, source: Option<&str>, content: &str);
275}
276
277// ── Tool Interface ──────────────────────────────────────────
278
279/// A tool that the LLM can invoke during conversation.
280///
281/// Tools are the primary extension point for Cortex plugins. Each tool
282/// has a name, description, JSON Schema for input parameters, and an
283/// execute function. The runtime presents the tool definition to the LLM
284/// and routes invocations to [`Tool::execute`].
285///
286/// # Thread Safety
287///
288/// Tools must be `Send + Sync` because a single tool instance is shared
289/// across all turns in the daemon process. Use interior mutability
290/// (`Mutex`, `RwLock`, `AtomicXxx`) if you need mutable state.
291pub trait Tool: Send + Sync {
292 /// Unique tool name (lowercase, underscores, e.g. `"web_search"`).
293 ///
294 /// Must be unique across all registered tools. If two tools share a
295 /// name, the later registration wins.
296 fn name(&self) -> &'static str;
297
298 /// Human-readable description shown to the LLM.
299 ///
300 /// Write this for the LLM, not for humans. Include:
301 /// - What the tool does
302 /// - When to use it
303 /// - When *not* to use it
304 /// - Any constraints or limitations
305 fn description(&self) -> &'static str;
306
307 /// JSON Schema describing the tool's input parameters.
308 ///
309 /// The LLM generates a JSON object matching this schema. Example:
310 ///
311 /// ```json
312 /// {
313 /// "type": "object",
314 /// "properties": {
315 /// "query": { "type": "string", "description": "Search query" }
316 /// },
317 /// "required": ["query"]
318 /// }
319 /// ```
320 fn input_schema(&self) -> serde_json::Value;
321
322 /// Execute the tool with the given input.
323 ///
324 /// `input` is a JSON object matching [`Self::input_schema`]. The
325 /// runtime validates the schema before calling this method, but
326 /// individual field types should still be checked defensively.
327 ///
328 /// # Return Values
329 ///
330 /// - [`ToolResult::success`] — normal output returned to the LLM
331 /// - [`ToolResult::error`] — the tool ran but produced an error the
332 /// LLM should see and potentially recover from
333 ///
334 /// # Errors
335 ///
336 /// Return [`ToolError::InvalidInput`] for malformed parameters or
337 /// [`ToolError::ExecutionFailed`] for unrecoverable failures. These
338 /// are surfaced as error events in the turn journal.
339 fn execute(&self, input: serde_json::Value) -> Result<ToolResult, ToolError>;
340
341 /// Execute the tool with runtime context and host callbacks.
342 ///
343 /// Plugins can override this to read session/actor/source metadata and
344 /// emit progress or observer updates through the provided runtime bridge.
345 ///
346 /// The default implementation preserves the classic SDK contract and calls
347 /// [`Self::execute`].
348 ///
349 /// # Errors
350 ///
351 /// Returns the same `ToolError` variants that [`Self::execute`] would
352 /// return for invalid input or unrecoverable execution failure.
353 fn execute_with_runtime(
354 &self,
355 input: serde_json::Value,
356 runtime: &dyn ToolRuntime,
357 ) -> Result<ToolResult, ToolError> {
358 let _ = runtime;
359 self.execute(input)
360 }
361
362 /// Optional per-tool execution timeout in seconds.
363 ///
364 /// If `None` (the default), the global `[turn].tool_timeout_secs`
365 /// from the instance configuration applies.
366 fn timeout_secs(&self) -> Option<u64> {
367 None
368 }
369
370 /// Stable capability hints consumed by the runtime and observability
371 /// layers.
372 fn capabilities(&self) -> ToolCapabilities {
373 ToolCapabilities::default()
374 }
375}
376
377/// Result of a tool execution returned to the LLM.
378///
379/// Use [`ToolResult::success`] for normal output and [`ToolResult::error`]
380/// for recoverable errors the LLM should see.
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct ToolResult {
383 /// Output text returned to the LLM.
384 pub output: String,
385 /// Structured media attachments produced by this tool.
386 ///
387 /// Attachments are delivered by Cortex transports independently from the
388 /// text the model sees, so tools do not need transport-specific protocols.
389 pub media: Vec<Attachment>,
390 /// Whether this result represents an error condition.
391 ///
392 /// When `true`, the LLM sees this as a failed tool call and may retry
393 /// with different parameters or switch strategy.
394 pub is_error: bool,
395}
396
397impl ToolResult {
398 /// Create a successful result.
399 #[must_use]
400 pub fn success(output: impl Into<String>) -> Self {
401 Self {
402 output: output.into(),
403 media: Vec::new(),
404 is_error: false,
405 }
406 }
407
408 /// Create an error result (tool ran but failed).
409 ///
410 /// Use this for recoverable errors — the LLM sees the output and can
411 /// decide how to proceed. For example: "file not found", "permission
412 /// denied", "rate limit exceeded".
413 #[must_use]
414 pub fn error(output: impl Into<String>) -> Self {
415 Self {
416 output: output.into(),
417 media: Vec::new(),
418 is_error: true,
419 }
420 }
421
422 /// Attach one media item to the result.
423 #[must_use]
424 pub fn with_media(mut self, attachment: Attachment) -> Self {
425 self.media.push(attachment);
426 self
427 }
428
429 /// Attach multiple media items to the result.
430 #[must_use]
431 pub fn with_media_many(mut self, media: impl IntoIterator<Item = Attachment>) -> Self {
432 self.media.extend(media);
433 self
434 }
435}
436
437/// Error from tool execution.
438///
439/// Unlike [`ToolResult::error`] (which is a "soft" error the LLM sees),
440/// `ToolError` represents a hard failure that is logged in the turn
441/// journal as a tool invocation error.
442#[derive(Debug)]
443pub enum ToolError {
444 /// Input parameters are invalid or missing required fields.
445 InvalidInput(String),
446 /// Execution failed due to an external or internal error.
447 ExecutionFailed(String),
448}
449
450impl std::fmt::Display for ToolError {
451 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452 match self {
453 Self::InvalidInput(e) => write!(f, "invalid input: {e}"),
454 Self::ExecutionFailed(e) => write!(f, "execution failed: {e}"),
455 }
456 }
457}
458
459impl std::error::Error for ToolError {}
460
461// ── Plugin Interface ────────────────────────────────────────
462
463/// Plugin metadata returned to the runtime at load time.
464///
465/// The `name` field must match the plugin's directory name and the
466/// `name` field in `manifest.toml`.
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct PluginInfo {
469 /// Unique plugin identifier (e.g. `"my-plugin"`).
470 pub name: String,
471 /// Semantic version string (e.g. `"1.3.0"`).
472 pub version: String,
473 /// Human-readable one-line description.
474 pub description: String,
475}
476
477/// A plugin that provides multiple tools from a single shared library.
478///
479/// This is the primary interface between a plugin and the Cortex runtime.
480/// Implement this trait and use [`export_plugin!`] to generate the FFI
481/// entry point.
482///
483/// # Requirements
484///
485/// - The implementing type must also implement `Default` (required by
486/// [`export_plugin!`] for construction via FFI).
487/// - The type must be `Send + Sync` because the runtime may access it
488/// from multiple threads.
489///
490/// # Example
491///
492/// ```rust,no_run
493/// use cortex_sdk::prelude::*;
494///
495/// #[derive(Default)]
496/// struct MyPlugin;
497///
498/// impl MultiToolPlugin for MyPlugin {
499/// fn plugin_info(&self) -> PluginInfo {
500/// PluginInfo {
501/// name: "my-plugin".into(),
502/// version: "0.1.0".into(),
503/// description: "Example plugin".into(),
504/// }
505/// }
506///
507/// fn create_tools(&self) -> Vec<Box<dyn Tool>> {
508/// vec![]
509/// }
510/// }
511///
512/// cortex_sdk::export_plugin!(MyPlugin);
513/// ```
514pub trait MultiToolPlugin: Send + Sync {
515 /// Return plugin metadata.
516 fn plugin_info(&self) -> PluginInfo;
517
518 /// Create all tools this plugin provides.
519 ///
520 /// Called once at daemon startup. Returned tools live for the
521 /// daemon's lifetime. Each tool is registered by name into the
522 /// global tool registry.
523 fn create_tools(&self) -> Vec<Box<dyn Tool>>;
524}
525
526/// Native ABI-owned byte buffer.
527///
528/// All strings and JSON values that cross the stable native ABI boundary use
529/// this representation. Buffers returned by the plugin must be released by
530/// calling the table's `buffer_free` function.
531#[repr(C)]
532pub struct CortexBuffer {
533 /// Pointer to UTF-8 bytes.
534 pub ptr: *mut u8,
535 /// Number of initialized bytes at `ptr`.
536 pub len: usize,
537 /// Allocation capacity needed to reconstruct and free the buffer.
538 pub cap: usize,
539}
540
541impl CortexBuffer {
542 #[must_use]
543 pub const fn empty() -> Self {
544 Self {
545 ptr: std::ptr::null_mut(),
546 len: 0,
547 cap: 0,
548 }
549 }
550}
551
552impl From<String> for CortexBuffer {
553 fn from(value: String) -> Self {
554 let mut bytes = value.into_bytes();
555 let buffer = Self {
556 ptr: bytes.as_mut_ptr(),
557 len: bytes.len(),
558 cap: bytes.capacity(),
559 };
560 std::mem::forget(bytes);
561 buffer
562 }
563}
564
565impl CortexBuffer {
566 /// Read this buffer as UTF-8.
567 ///
568 /// # Errors
569 /// Returns a UTF-8 error when the buffer contains invalid UTF-8 bytes.
570 ///
571 /// # Safety
572 /// The caller must ensure `ptr` is valid for `len` bytes and remains alive
573 /// for the duration of this call.
574 pub const unsafe fn as_str(&self) -> Result<&str, std::str::Utf8Error> {
575 if self.ptr.is_null() || self.len == 0 {
576 return Ok("");
577 }
578 // SAFETY: upheld by the caller.
579 let bytes = unsafe { std::slice::from_raw_parts(self.ptr.cast_const(), self.len) };
580 std::str::from_utf8(bytes)
581 }
582}
583
584/// Free a buffer allocated by this SDK.
585///
586/// # Safety
587/// The buffer must have been returned by this SDK's ABI helpers and must not be
588/// freed more than once.
589pub unsafe extern "C" fn cortex_buffer_free(buffer: CortexBuffer) {
590 if buffer.ptr.is_null() {
591 return;
592 }
593 // SAFETY: the caller guarantees this buffer came from `CortexBuffer::from_string`.
594 unsafe {
595 drop(Vec::from_raw_parts(buffer.ptr, buffer.len, buffer.cap));
596 }
597}
598
599/// Host table supplied to a native plugin during initialization.
600#[repr(C)]
601pub struct CortexHostApi {
602 /// Runtime-supported native ABI version.
603 pub abi_version: u32,
604}
605
606/// Function table exported by a native plugin.
607#[repr(C)]
608pub struct CortexPluginApi {
609 /// Plugin-supported native ABI version.
610 pub abi_version: u32,
611 /// Opaque plugin state owned by the plugin.
612 pub plugin: *mut c_void,
613 /// Return [`PluginInfo`] encoded as JSON.
614 pub plugin_info: Option<unsafe extern "C" fn(*mut c_void) -> CortexBuffer>,
615 /// Return the number of tools exposed by the plugin.
616 pub tool_count: Option<unsafe extern "C" fn(*mut c_void) -> usize>,
617 /// Return one tool descriptor encoded as JSON.
618 pub tool_descriptor: Option<unsafe extern "C" fn(*mut c_void, usize) -> CortexBuffer>,
619 /// Execute a tool. The name, input, and invocation context are UTF-8 JSON
620 /// buffers except `tool_name`, which is a UTF-8 string.
621 pub tool_execute: Option<
622 unsafe extern "C" fn(*mut c_void, CortexBuffer, CortexBuffer, CortexBuffer) -> CortexBuffer,
623 >,
624 /// Drop plugin-owned state.
625 pub plugin_drop: Option<unsafe extern "C" fn(*mut c_void)>,
626 /// Free buffers returned by plugin functions.
627 pub buffer_free: Option<unsafe extern "C" fn(CortexBuffer)>,
628}
629
630impl CortexPluginApi {
631 #[must_use]
632 pub const fn empty() -> Self {
633 Self {
634 abi_version: 0,
635 plugin: std::ptr::null_mut(),
636 plugin_info: None,
637 tool_count: None,
638 tool_descriptor: None,
639 tool_execute: None,
640 plugin_drop: None,
641 buffer_free: None,
642 }
643 }
644}
645
646#[derive(Serialize)]
647struct ToolDescriptor<'a> {
648 name: &'a str,
649 description: &'a str,
650 input_schema: serde_json::Value,
651 timeout_secs: Option<u64>,
652 capabilities: ToolCapabilities,
653}
654
655struct NoopToolRuntime {
656 invocation: InvocationContext,
657}
658
659impl ToolRuntime for NoopToolRuntime {
660 fn invocation(&self) -> &InvocationContext {
661 &self.invocation
662 }
663
664 fn emit_progress(&self, _message: &str) {}
665
666 fn emit_observer(&self, _source: Option<&str>, _content: &str) {}
667}
668
669#[doc(hidden)]
670pub struct NativePluginState {
671 plugin: Box<dyn MultiToolPlugin>,
672 tools: Vec<Box<dyn Tool>>,
673}
674
675impl NativePluginState {
676 #[must_use]
677 pub fn new(plugin: Box<dyn MultiToolPlugin>) -> Self {
678 let tools = plugin.create_tools();
679 Self { plugin, tools }
680 }
681}
682
683fn json_buffer<T: Serialize>(value: &T) -> CortexBuffer {
684 match serde_json::to_string(value) {
685 Ok(json) => CortexBuffer::from(json),
686 Err(err) => CortexBuffer::from(
687 serde_json::json!({
688 "output": format!("native ABI serialization error: {err}"),
689 "media": [],
690 "is_error": true
691 })
692 .to_string(),
693 ),
694 }
695}
696
697#[doc(hidden)]
698pub unsafe extern "C" fn native_plugin_info(state: *mut c_void) -> CortexBuffer {
699 if state.is_null() {
700 return CortexBuffer::empty();
701 }
702 // SAFETY: the pointer is created by `export_plugin!` and remains owned by
703 // the plugin until `native_plugin_drop`.
704 let state = unsafe { &*state.cast::<NativePluginState>() };
705 json_buffer(&state.plugin.plugin_info())
706}
707
708#[doc(hidden)]
709pub unsafe extern "C" fn native_tool_count(state: *mut c_void) -> usize {
710 if state.is_null() {
711 return 0;
712 }
713 // SAFETY: see `native_plugin_info`.
714 let state = unsafe { &*state.cast::<NativePluginState>() };
715 state.tools.len()
716}
717
718#[doc(hidden)]
719pub unsafe extern "C" fn native_tool_descriptor(state: *mut c_void, index: usize) -> CortexBuffer {
720 if state.is_null() {
721 return CortexBuffer::empty();
722 }
723 // SAFETY: see `native_plugin_info`.
724 let state = unsafe { &*state.cast::<NativePluginState>() };
725 let Some(tool) = state.tools.get(index) else {
726 return CortexBuffer::empty();
727 };
728 let descriptor = ToolDescriptor {
729 name: tool.name(),
730 description: tool.description(),
731 input_schema: tool.input_schema(),
732 timeout_secs: tool.timeout_secs(),
733 capabilities: tool.capabilities(),
734 };
735 json_buffer(&descriptor)
736}
737
738#[doc(hidden)]
739pub unsafe extern "C" fn native_tool_execute(
740 state: *mut c_void,
741 tool_name: CortexBuffer,
742 input_json: CortexBuffer,
743 invocation_json: CortexBuffer,
744) -> CortexBuffer {
745 if state.is_null() {
746 return json_buffer(&ToolResult::error("native plugin state is null"));
747 }
748 // SAFETY: inbound buffers are supplied by the runtime and valid for this call.
749 let tool_name = match unsafe { tool_name.as_str() } {
750 Ok(value) => value,
751 Err(err) => return json_buffer(&ToolResult::error(format!("invalid tool name: {err}"))),
752 };
753 // SAFETY: inbound buffers are supplied by the runtime and valid for this call.
754 let input_json = match unsafe { input_json.as_str() } {
755 Ok(value) => value,
756 Err(err) => return json_buffer(&ToolResult::error(format!("invalid input JSON: {err}"))),
757 };
758 // SAFETY: inbound buffers are supplied by the runtime and valid for this call.
759 let invocation_json = match unsafe { invocation_json.as_str() } {
760 Ok(value) => value,
761 Err(err) => {
762 return json_buffer(&ToolResult::error(format!(
763 "invalid invocation JSON: {err}"
764 )));
765 }
766 };
767 let input = match serde_json::from_str(input_json) {
768 Ok(value) => value,
769 Err(err) => return json_buffer(&ToolResult::error(format!("invalid input JSON: {err}"))),
770 };
771 let invocation = match serde_json::from_str(invocation_json) {
772 Ok(value) => value,
773 Err(err) => {
774 return json_buffer(&ToolResult::error(format!(
775 "invalid invocation JSON: {err}"
776 )));
777 }
778 };
779 // SAFETY: see `native_plugin_info`.
780 let state = unsafe { &*state.cast::<NativePluginState>() };
781 let Some(tool) = state.tools.iter().find(|tool| tool.name() == tool_name) else {
782 return json_buffer(&ToolResult::error(format!(
783 "native plugin does not expose tool '{tool_name}'"
784 )));
785 };
786 let runtime = NoopToolRuntime { invocation };
787 match tool.execute_with_runtime(input, &runtime) {
788 Ok(result) => json_buffer(&result),
789 Err(err) => json_buffer(&ToolResult::error(format!("tool error: {err}"))),
790 }
791}
792
793#[doc(hidden)]
794pub unsafe extern "C" fn native_plugin_drop(state: *mut c_void) {
795 if state.is_null() {
796 return;
797 }
798 // SAFETY: pointer ownership is transferred from `export_plugin!` to this
799 // function exactly once by the runtime.
800 unsafe {
801 drop(Box::from_raw(state.cast::<NativePluginState>()));
802 }
803}
804
805// ── Export Macro ────────────────────────────────────────────
806
807/// Generate the stable native ABI entry point for a [`MultiToolPlugin`].
808///
809/// This macro expands to an `extern "C"` function named `cortex_plugin_init`
810/// that fills a C-compatible function table. The plugin type must implement
811/// [`Default`].
812///
813/// # Usage
814///
815/// `cortex_sdk::export_plugin!(MyPlugin);`
816///
817/// # Expansion
818///
819/// The macro constructs the Rust plugin internally and exposes it through the
820/// stable native ABI table. Rust trait objects never cross the dynamic-library
821/// boundary.
822#[macro_export]
823macro_rules! export_plugin {
824 ($plugin_type:ty) => {
825 #[unsafe(no_mangle)]
826 pub unsafe extern "C" fn cortex_plugin_init(
827 host: *const $crate::CortexHostApi,
828 out_plugin: *mut $crate::CortexPluginApi,
829 ) -> i32 {
830 if host.is_null() || out_plugin.is_null() {
831 return -1;
832 }
833 let host = unsafe { &*host };
834 if host.abi_version != $crate::NATIVE_ABI_VERSION {
835 return -2;
836 }
837 let plugin: Box<dyn $crate::MultiToolPlugin> = Box::new(<$plugin_type>::default());
838 let state = Box::new($crate::NativePluginState::new(plugin));
839 unsafe {
840 *out_plugin = $crate::CortexPluginApi {
841 abi_version: $crate::NATIVE_ABI_VERSION,
842 plugin: Box::into_raw(state).cast(),
843 plugin_info: Some($crate::native_plugin_info),
844 tool_count: Some($crate::native_tool_count),
845 tool_descriptor: Some($crate::native_tool_descriptor),
846 tool_execute: Some($crate::native_tool_execute),
847 plugin_drop: Some($crate::native_plugin_drop),
848 buffer_free: Some($crate::cortex_buffer_free),
849 };
850 }
851 0
852 }
853 };
854}
855
856// ── Prelude ─────────────────────────────────────────────────
857
858/// Convenience re-exports for plugin development.
859///
860/// ```rust,no_run
861/// use cortex_sdk::prelude::*;
862/// ```
863///
864/// This imports [`MultiToolPlugin`], [`PluginInfo`], [`Tool`],
865/// [`ToolError`], [`ToolResult`], and [`serde_json`].
866pub mod prelude {
867 pub use super::{
868 Attachment, CortexBuffer, CortexHostApi, CortexPluginApi, ExecutionScope,
869 InvocationContext, MultiToolPlugin, NATIVE_ABI_VERSION, PluginInfo, SDK_VERSION, Tool,
870 ToolCapabilities, ToolError, ToolResult, ToolRuntime,
871 };
872 pub use serde_json;
873}