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, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolEffectKind {
ReadFile,
ReadSecret,
WriteFile,
DeleteFile,
RunProcess,
NetworkRequest,
SendMessage,
SpendMoney,
Deploy,
ModifyCredential,
PersistMemory,
PublishContent,
ScheduleTask,
GenerateMedia,
IntrospectRuntime,
DelegateWork,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EffectReversibility {
Reversible,
PartiallyReversible,
Irreversible,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EffectConfirmation {
Never,
OnRisk,
Always,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DryRunSupport {
NotSupported,
Supported,
RequiredBeforeExecute,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolEffect {
pub kind: ToolEffectKind,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub target: String,
pub reversibility: EffectReversibility,
pub confirmation: EffectConfirmation,
pub dry_run: DryRunSupport,
}
impl ToolEffect {
#[must_use]
pub const fn new(kind: ToolEffectKind) -> Self {
Self {
kind,
target: String::new(),
reversibility: kind.default_reversibility(),
confirmation: kind.default_confirmation(),
dry_run: DryRunSupport::NotSupported,
}
}
#[must_use]
pub fn with_target(mut self, target: impl Into<String>) -> Self {
self.target = target.into();
self
}
#[must_use]
pub const fn with_reversibility(mut self, reversibility: EffectReversibility) -> Self {
self.reversibility = reversibility;
self
}
#[must_use]
pub const fn with_confirmation(mut self, confirmation: EffectConfirmation) -> Self {
self.confirmation = confirmation;
self
}
#[must_use]
pub const fn with_dry_run(mut self, dry_run: DryRunSupport) -> Self {
self.dry_run = dry_run;
self
}
#[must_use]
pub const fn is_mutating(&self) -> bool {
self.kind.is_mutating()
}
#[must_use]
pub fn label(&self) -> String {
if self.target.is_empty() {
format!("{:?}", self.kind)
} else {
format!("{:?}:{}", self.kind, self.target)
}
}
}
impl ToolEffectKind {
#[must_use]
pub const fn is_mutating(self) -> bool {
!matches!(
self,
Self::ReadFile | Self::ReadSecret | Self::NetworkRequest | Self::IntrospectRuntime
)
}
const fn default_reversibility(self) -> EffectReversibility {
match self {
Self::ReadFile | Self::NetworkRequest | Self::IntrospectRuntime => {
EffectReversibility::Reversible
}
Self::WriteFile
| Self::RunProcess
| Self::PersistMemory
| Self::ScheduleTask
| Self::GenerateMedia
| Self::DelegateWork => EffectReversibility::PartiallyReversible,
Self::ReadSecret
| Self::DeleteFile
| Self::SendMessage
| Self::SpendMoney
| Self::Deploy
| Self::ModifyCredential
| Self::PublishContent => EffectReversibility::Irreversible,
}
}
const fn default_confirmation(self) -> EffectConfirmation {
match self {
Self::ReadFile | Self::NetworkRequest | Self::IntrospectRuntime => {
EffectConfirmation::OnRisk
}
_ => EffectConfirmation::Always,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ToolCapabilities {
pub emits_progress: bool,
pub emits_observer_text: bool,
pub background_safe: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effects: Vec<ToolEffect>,
}
impl ToolCapabilities {
#[must_use]
pub fn with_effect(mut self, effect: ToolEffect) -> Self {
self.effects.push(effect);
self
}
#[must_use]
pub fn with_effects(mut self, effects: impl IntoIterator<Item = ToolEffect>) -> Self {
self.effects.extend(effects);
self
}
}
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, DryRunSupport,
EffectConfirmation, EffectReversibility, ExecutionScope, InvocationContext,
MultiToolPlugin, NATIVE_ABI_VERSION, PluginInfo, SDK_VERSION, Tool, ToolCapabilities,
ToolEffect, ToolEffectKind, ToolError, ToolResult, ToolRuntime,
};
pub use serde_json;
}