use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use tokio::sync::Mutex;
use crate::language_models::llm::LLM;
use crate::schemas::memory::BaseMemory;
use crate::tools::Tool;
#[derive(Error, Debug)]
#[error("Plugin error: {0}")]
pub struct PluginError(pub String);
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct PluginCompatibility {
#[serde(default)]
pub plugin_api_version: String,
#[serde(default)]
pub kernel_compat: String,
#[serde(default)]
pub schema_hash: Option<String>,
}
pub fn validate_plugin_compatibility(
compat: &PluginCompatibility,
kernel_version: &str,
) -> Result<(), PluginError> {
if kernel_version.is_empty() {
return Ok(());
}
if compat.kernel_compat.is_empty() {
return Ok(());
}
let req = compat.kernel_compat.trim();
if req.starts_with(">=") {
let min = req.trim_start_matches(">=").trim();
if kernel_version < min {
return Err(PluginError(format!(
"kernel version {} does not meet plugin requirement {}",
kernel_version, compat.kernel_compat
)));
}
} else if req != kernel_version {
return Err(PluginError(format!(
"kernel version {} does not match plugin kernel_compat {}",
kernel_version, compat.kernel_compat
)));
}
Ok(())
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct PluginMetadata {
#[serde(default)]
pub deterministic: bool,
#[serde(default = "default_true")]
pub side_effects: bool,
#[serde(default)]
pub replay_safe: bool,
}
fn default_true() -> bool {
true
}
impl PluginMetadata {
pub fn conservative() -> Self {
Self {
deterministic: false,
side_effects: true,
replay_safe: false,
}
}
pub fn pure() -> Self {
Self {
deterministic: true,
side_effects: false,
replay_safe: true,
}
}
}
pub trait HasPluginMetadata: Send + Sync {
fn plugin_metadata(&self) -> PluginMetadata {
PluginMetadata::conservative()
}
}
#[inline]
pub fn allow_in_replay(meta: &PluginMetadata) -> bool {
meta.replay_safe
}
#[inline]
pub fn requires_sandbox(meta: &PluginMetadata) -> bool {
meta.side_effects
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum PluginExecutionMode {
InProcess,
IsolatedProcess,
Remote,
}
impl PluginExecutionMode {
pub fn as_str(self) -> &'static str {
match self {
PluginExecutionMode::InProcess => "in_process",
PluginExecutionMode::IsolatedProcess => "isolated_process",
PluginExecutionMode::Remote => "remote",
}
}
}
#[inline]
pub fn route_to_execution_mode(meta: &PluginMetadata) -> PluginExecutionMode {
if requires_sandbox(meta) {
PluginExecutionMode::IsolatedProcess
} else {
PluginExecutionMode::InProcess
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum PluginCategory {
Node,
Tool,
Memory,
LLMAdapter,
Scheduler,
}
impl PluginCategory {
pub fn as_str(self) -> &'static str {
match self {
PluginCategory::Node => "node",
PluginCategory::Tool => "tool",
PluginCategory::Memory => "memory",
PluginCategory::LLMAdapter => "llm_adapter",
PluginCategory::Scheduler => "scheduler",
}
}
}
pub trait ToolPlugin: HasPluginMetadata {
fn plugin_type(&self) -> &str;
fn create_tool(&self, config: &Value) -> Result<Arc<dyn Tool>, PluginError>;
}
pub trait MemoryPlugin: HasPluginMetadata {
fn plugin_type(&self) -> &str;
fn create_memory(&self, config: &Value) -> Result<Arc<Mutex<dyn BaseMemory>>, PluginError>;
}
pub trait LLMAdapter: HasPluginMetadata {
fn plugin_type(&self) -> &str;
fn create_llm(&self, config: &Value) -> Result<Arc<dyn LLM>, PluginError>;
}
pub trait SchedulerPlugin: HasPluginMetadata {
fn plugin_type(&self) -> &str;
fn description(&self) -> &str {
""
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plugin_category_as_str() {
assert_eq!(PluginCategory::Node.as_str(), "node");
assert_eq!(PluginCategory::Tool.as_str(), "tool");
assert_eq!(PluginCategory::Memory.as_str(), "memory");
assert_eq!(PluginCategory::LLMAdapter.as_str(), "llm_adapter");
assert_eq!(PluginCategory::Scheduler.as_str(), "scheduler");
}
#[test]
fn plugin_category_equality() {
assert_eq!(PluginCategory::Node, PluginCategory::Node);
assert_ne!(PluginCategory::Tool, PluginCategory::Memory);
}
#[test]
fn plugin_metadata_conservative() {
let m = PluginMetadata::conservative();
assert!(!m.deterministic);
assert!(m.side_effects);
assert!(!m.replay_safe);
}
#[test]
fn plugin_metadata_pure() {
let m = PluginMetadata::pure();
assert!(m.deterministic);
assert!(!m.side_effects);
assert!(m.replay_safe);
}
#[test]
fn allow_in_replay_uses_replay_safe() {
assert!(allow_in_replay(&PluginMetadata::pure()));
assert!(!allow_in_replay(&PluginMetadata::conservative()));
}
#[test]
fn requires_sandbox_uses_side_effects() {
assert!(requires_sandbox(&PluginMetadata::conservative()));
assert!(!requires_sandbox(&PluginMetadata::pure()));
}
#[test]
fn plugin_execution_mode_as_str() {
assert_eq!(PluginExecutionMode::InProcess.as_str(), "in_process");
assert_eq!(
PluginExecutionMode::IsolatedProcess.as_str(),
"isolated_process"
);
assert_eq!(PluginExecutionMode::Remote.as_str(), "remote");
}
#[test]
fn route_to_execution_mode_pure_in_process() {
assert_eq!(
route_to_execution_mode(&PluginMetadata::pure()),
PluginExecutionMode::InProcess
);
}
#[test]
fn route_to_execution_mode_side_effects_isolated() {
assert_eq!(
route_to_execution_mode(&PluginMetadata::conservative()),
PluginExecutionMode::IsolatedProcess
);
}
#[test]
fn validate_plugin_compatibility_empty_ok() {
let c = PluginCompatibility::default();
assert!(validate_plugin_compatibility(&c, "0.2.7").is_ok());
assert!(validate_plugin_compatibility(&c, "").is_ok());
}
#[test]
fn validate_plugin_compatibility_exact_match() {
let c = PluginCompatibility {
plugin_api_version: "0.2".into(),
kernel_compat: "0.2.7".into(),
schema_hash: None,
};
assert!(validate_plugin_compatibility(&c, "0.2.7").is_ok());
assert!(validate_plugin_compatibility(&c, "0.2.8").is_err());
}
#[test]
fn validate_plugin_compatibility_gte() {
let c = PluginCompatibility {
plugin_api_version: "0.2".into(),
kernel_compat: ">=0.2.0".into(),
schema_hash: None,
};
assert!(validate_plugin_compatibility(&c, "0.2.7").is_ok());
assert!(validate_plugin_compatibility(&c, "0.2.0").is_ok());
assert!(validate_plugin_compatibility(&c, "0.1.9").is_err());
}
}