use serde::{Deserialize, Serialize};
pub use serde_json;
use std::ffi::c_void;
pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const NATIVE_ABI_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub media_type: String,
pub mime_type: String,
pub url: String,
pub caption: Option<String>,
pub size: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionScope {
#[default]
Foreground,
Background,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InvocationContext {
pub tool_name: String,
pub session_id: Option<String>,
pub actor: Option<String>,
pub source: Option<String>,
pub execution_scope: ExecutionScope,
}
impl InvocationContext {
#[must_use]
pub fn is_background(&self) -> bool {
self.execution_scope == ExecutionScope::Background
}
#[must_use]
pub fn is_foreground(&self) -> bool {
self.execution_scope == ExecutionScope::Foreground
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ToolCapabilities {
pub emits_progress: bool,
pub emits_observer_text: bool,
pub background_safe: bool,
}
pub trait ToolRuntime: Send + Sync {
fn invocation(&self) -> &InvocationContext;
fn emit_progress(&self, message: &str);
fn emit_observer(&self, source: Option<&str>, content: &str);
}
pub trait Tool: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn input_schema(&self) -> serde_json::Value;
fn execute(&self, input: serde_json::Value) -> Result<ToolResult, ToolError>;
fn execute_with_runtime(
&self,
input: serde_json::Value,
runtime: &dyn ToolRuntime,
) -> Result<ToolResult, ToolError> {
let _ = runtime;
self.execute(input)
}
fn timeout_secs(&self) -> Option<u64> {
None
}
fn capabilities(&self) -> ToolCapabilities {
ToolCapabilities::default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub output: String,
pub media: Vec<Attachment>,
pub is_error: bool,
}
impl ToolResult {
#[must_use]
pub fn success(output: impl Into<String>) -> Self {
Self {
output: output.into(),
media: Vec::new(),
is_error: false,
}
}
#[must_use]
pub fn error(output: impl Into<String>) -> Self {
Self {
output: output.into(),
media: Vec::new(),
is_error: true,
}
}
#[must_use]
pub fn with_media(mut self, attachment: Attachment) -> Self {
self.media.push(attachment);
self
}
#[must_use]
pub fn with_media_many(mut self, media: impl IntoIterator<Item = Attachment>) -> Self {
self.media.extend(media);
self
}
}
#[derive(Debug)]
pub enum ToolError {
InvalidInput(String),
ExecutionFailed(String),
}
impl std::fmt::Display for ToolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidInput(e) => write!(f, "invalid input: {e}"),
Self::ExecutionFailed(e) => write!(f, "execution failed: {e}"),
}
}
}
impl std::error::Error for ToolError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub description: String,
}
pub trait MultiToolPlugin: Send + Sync {
fn plugin_info(&self) -> PluginInfo;
fn create_tools(&self) -> Vec<Box<dyn Tool>>;
}
#[repr(C)]
pub struct CortexBuffer {
pub ptr: *mut u8,
pub len: usize,
pub cap: usize,
}
impl CortexBuffer {
#[must_use]
pub const fn empty() -> Self {
Self {
ptr: std::ptr::null_mut(),
len: 0,
cap: 0,
}
}
}
impl From<String> for CortexBuffer {
fn from(value: String) -> Self {
let mut bytes = value.into_bytes();
let buffer = Self {
ptr: bytes.as_mut_ptr(),
len: bytes.len(),
cap: bytes.capacity(),
};
std::mem::forget(bytes);
buffer
}
}
impl CortexBuffer {
pub const unsafe fn as_str(&self) -> Result<&str, std::str::Utf8Error> {
if self.ptr.is_null() || self.len == 0 {
return Ok("");
}
let bytes = unsafe { std::slice::from_raw_parts(self.ptr.cast_const(), self.len) };
std::str::from_utf8(bytes)
}
}
pub unsafe extern "C" fn cortex_buffer_free(buffer: CortexBuffer) {
if buffer.ptr.is_null() {
return;
}
unsafe {
drop(Vec::from_raw_parts(buffer.ptr, buffer.len, buffer.cap));
}
}
#[repr(C)]
pub struct CortexHostApi {
pub abi_version: u32,
}
#[repr(C)]
pub struct CortexPluginApi {
pub abi_version: u32,
pub plugin: *mut c_void,
pub plugin_info: Option<unsafe extern "C" fn(*mut c_void) -> CortexBuffer>,
pub tool_count: Option<unsafe extern "C" fn(*mut c_void) -> usize>,
pub tool_descriptor: Option<unsafe extern "C" fn(*mut c_void, usize) -> CortexBuffer>,
pub tool_execute: Option<
unsafe extern "C" fn(*mut c_void, CortexBuffer, CortexBuffer, CortexBuffer) -> CortexBuffer,
>,
pub plugin_drop: Option<unsafe extern "C" fn(*mut c_void)>,
pub buffer_free: Option<unsafe extern "C" fn(CortexBuffer)>,
}
impl CortexPluginApi {
#[must_use]
pub const fn empty() -> Self {
Self {
abi_version: 0,
plugin: std::ptr::null_mut(),
plugin_info: None,
tool_count: None,
tool_descriptor: None,
tool_execute: None,
plugin_drop: None,
buffer_free: None,
}
}
}
#[derive(Serialize)]
struct ToolDescriptor<'a> {
name: &'a str,
description: &'a str,
input_schema: serde_json::Value,
timeout_secs: Option<u64>,
capabilities: ToolCapabilities,
}
struct NoopToolRuntime {
invocation: InvocationContext,
}
impl ToolRuntime for NoopToolRuntime {
fn invocation(&self) -> &InvocationContext {
&self.invocation
}
fn emit_progress(&self, _message: &str) {}
fn emit_observer(&self, _source: Option<&str>, _content: &str) {}
}
#[doc(hidden)]
pub struct NativePluginState {
plugin: Box<dyn MultiToolPlugin>,
tools: Vec<Box<dyn Tool>>,
}
impl NativePluginState {
#[must_use]
pub fn new(plugin: Box<dyn MultiToolPlugin>) -> Self {
let tools = plugin.create_tools();
Self { plugin, tools }
}
}
fn json_buffer<T: Serialize>(value: &T) -> CortexBuffer {
match serde_json::to_string(value) {
Ok(json) => CortexBuffer::from(json),
Err(err) => CortexBuffer::from(
serde_json::json!({
"output": format!("native ABI serialization error: {err}"),
"media": [],
"is_error": true
})
.to_string(),
),
}
}
#[doc(hidden)]
pub unsafe extern "C" fn native_plugin_info(state: *mut c_void) -> CortexBuffer {
if state.is_null() {
return CortexBuffer::empty();
}
let state = unsafe { &*state.cast::<NativePluginState>() };
json_buffer(&state.plugin.plugin_info())
}
#[doc(hidden)]
pub unsafe extern "C" fn native_tool_count(state: *mut c_void) -> usize {
if state.is_null() {
return 0;
}
let state = unsafe { &*state.cast::<NativePluginState>() };
state.tools.len()
}
#[doc(hidden)]
pub unsafe extern "C" fn native_tool_descriptor(state: *mut c_void, index: usize) -> CortexBuffer {
if state.is_null() {
return CortexBuffer::empty();
}
let state = unsafe { &*state.cast::<NativePluginState>() };
let Some(tool) = state.tools.get(index) else {
return CortexBuffer::empty();
};
let descriptor = ToolDescriptor {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
timeout_secs: tool.timeout_secs(),
capabilities: tool.capabilities(),
};
json_buffer(&descriptor)
}
#[doc(hidden)]
pub unsafe extern "C" fn native_tool_execute(
state: *mut c_void,
tool_name: CortexBuffer,
input_json: CortexBuffer,
invocation_json: CortexBuffer,
) -> CortexBuffer {
if state.is_null() {
return json_buffer(&ToolResult::error("native plugin state is null"));
}
let tool_name = match unsafe { tool_name.as_str() } {
Ok(value) => value,
Err(err) => return json_buffer(&ToolResult::error(format!("invalid tool name: {err}"))),
};
let input_json = match unsafe { input_json.as_str() } {
Ok(value) => value,
Err(err) => return json_buffer(&ToolResult::error(format!("invalid input JSON: {err}"))),
};
let invocation_json = match unsafe { invocation_json.as_str() } {
Ok(value) => value,
Err(err) => {
return json_buffer(&ToolResult::error(format!(
"invalid invocation JSON: {err}"
)));
}
};
let input = match serde_json::from_str(input_json) {
Ok(value) => value,
Err(err) => return json_buffer(&ToolResult::error(format!("invalid input JSON: {err}"))),
};
let invocation = match serde_json::from_str(invocation_json) {
Ok(value) => value,
Err(err) => {
return json_buffer(&ToolResult::error(format!(
"invalid invocation JSON: {err}"
)));
}
};
let state = unsafe { &*state.cast::<NativePluginState>() };
let Some(tool) = state.tools.iter().find(|tool| tool.name() == tool_name) else {
return json_buffer(&ToolResult::error(format!(
"native plugin does not expose tool '{tool_name}'"
)));
};
let runtime = NoopToolRuntime { invocation };
match tool.execute_with_runtime(input, &runtime) {
Ok(result) => json_buffer(&result),
Err(err) => json_buffer(&ToolResult::error(format!("tool error: {err}"))),
}
}
#[doc(hidden)]
pub unsafe extern "C" fn native_plugin_drop(state: *mut c_void) {
if state.is_null() {
return;
}
unsafe {
drop(Box::from_raw(state.cast::<NativePluginState>()));
}
}
#[macro_export]
macro_rules! export_plugin {
($plugin_type:ty) => {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cortex_plugin_init(
host: *const $crate::CortexHostApi,
out_plugin: *mut $crate::CortexPluginApi,
) -> i32 {
if host.is_null() || out_plugin.is_null() {
return -1;
}
let host = unsafe { &*host };
if host.abi_version != $crate::NATIVE_ABI_VERSION {
return -2;
}
let plugin: Box<dyn $crate::MultiToolPlugin> = Box::new(<$plugin_type>::default());
let state = Box::new($crate::NativePluginState::new(plugin));
unsafe {
*out_plugin = $crate::CortexPluginApi {
abi_version: $crate::NATIVE_ABI_VERSION,
plugin: Box::into_raw(state).cast(),
plugin_info: Some($crate::native_plugin_info),
tool_count: Some($crate::native_tool_count),
tool_descriptor: Some($crate::native_tool_descriptor),
tool_execute: Some($crate::native_tool_execute),
plugin_drop: Some($crate::native_plugin_drop),
buffer_free: Some($crate::cortex_buffer_free),
};
}
0
}
};
}
pub mod prelude {
pub use super::{
Attachment, CortexBuffer, CortexHostApi, CortexPluginApi, ExecutionScope,
InvocationContext, MultiToolPlugin, NATIVE_ABI_VERSION, PluginInfo, SDK_VERSION, Tool,
ToolCapabilities, ToolError, ToolResult, ToolRuntime,
};
pub use serde_json;
}