#![cfg_attr(not(feature = "serde"), no_std)]
#![deny(missing_docs)]
#![allow(dead_code)]
#[cfg(feature = "serde")]
extern crate serde;
#[cfg(feature = "serde")]
extern crate alloc;
#[cfg(feature = "serde")]
pub use serde_types::*;
#[cfg(feature = "serde")]
pub use config::{ToolConfig, ToolConfigRegistry, GLOBAL_CONFIG_REGISTRY};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ToolDefinition {
#[cfg(feature = "serde")]
pub name: alloc::string::String,
#[cfg(not(feature = "serde"))]
pub name: &'static str,
#[cfg(feature = "serde")]
pub description: alloc::string::String,
#[cfg(not(feature = "serde"))]
pub description: &'static str,
#[cfg(feature = "serde")]
pub input_schema: alloc::string::String,
#[cfg(not(feature = "serde"))]
pub input_schema: &'static str,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "serde")]
pub version: Option<alloc::string::String>,
#[cfg(not(feature = "serde"))]
pub version: Option<&'static str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "serde")]
pub deprecated_since: Option<alloc::string::String>,
#[cfg(not(feature = "serde"))]
pub deprecated_since: Option<&'static str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "serde")]
pub remove_in: Option<alloc::string::String>,
#[cfg(not(feature = "serde"))]
pub remove_in: Option<&'static str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "serde")]
pub replaced_by: Option<alloc::string::String>,
#[cfg(not(feature = "serde"))]
pub replaced_by: Option<&'static str>,
}
#[doc(hidden)]
pub struct ToolDefinitionConst {
pub name: &'static str,
pub description: &'static str,
pub input_schema: &'static str,
}
impl ToolDefinition {
#[inline(always)]
pub fn from_const(data: ToolDefinitionConst) -> Self {
Self {
#[cfg(feature = "serde")]
name: data.name.into(),
#[cfg(not(feature = "serde"))]
name: data.name,
#[cfg(feature = "serde")]
description: data.description.into(),
#[cfg(not(feature = "serde"))]
description: data.description,
#[cfg(feature = "serde")]
input_schema: data.input_schema.into(),
#[cfg(not(feature = "serde"))]
input_schema: data.input_schema,
version: None,
deprecated_since: None,
remove_in: None,
replaced_by: None,
}
}
#[cfg(feature = "serde")]
pub fn new(
name: impl Into<alloc::string::String>,
description: impl Into<alloc::string::String>,
input_schema: impl Into<alloc::string::String>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
input_schema: input_schema.into(),
version: None,
deprecated_since: None,
remove_in: None,
replaced_by: None,
}
}
#[cfg(not(feature = "serde"))]
pub fn new(name: &'static str, description: &'static str, input_schema: &'static str) -> Self {
Self {
name,
description,
input_schema,
version: None,
deprecated_since: None,
remove_in: None,
replaced_by: None,
}
}
#[cfg(feature = "serde")]
pub fn with_version(mut self, version: impl Into<alloc::string::String>) -> Self {
self.version = Some(version.into());
self
}
#[cfg(not(feature = "serde"))]
pub fn with_version(mut self, version: &'static str) -> Self {
self.version = Some(version);
self
}
#[cfg(feature = "serde")]
pub fn with_deprecated(
mut self,
deprecated_since: impl Into<alloc::string::String>,
remove_in: impl Into<alloc::string::String>,
replaced_by: impl Into<alloc::string::String>,
) -> Self {
self.deprecated_since = Some(deprecated_since.into());
self.remove_in = Some(remove_in.into());
self.replaced_by = Some(replaced_by.into());
self
}
#[cfg(not(feature = "serde"))]
pub fn with_deprecated(
mut self,
deprecated_since: &'static str,
remove_in: &'static str,
replaced_by: &'static str,
) -> Self {
self.deprecated_since = Some(deprecated_since);
self.remove_in = Some(remove_in);
self.replaced_by = Some(replaced_by);
self
}
#[cfg(feature = "serde")]
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
#[cfg(feature = "serde")]
pub fn to_value(&self) -> Result<serde_json::Value, serde_json::Error> {
serde_json::to_value(self)
}
#[cfg(feature = "serde")]
pub fn input_schema_pretty(&self) -> Result<String, serde_json::Error> {
let value: serde_json::Value = serde_json::from_str(&self.input_schema)?;
serde_json::to_string_pretty(&value)
}
#[cfg(feature = "serde")]
pub fn input_schema_value(&self) -> Result<serde_json::Value, serde_json::Error> {
serde_json::from_str(&self.input_schema)
}
#[cfg(feature = "serde")]
pub fn apply_configs(&mut self, configs: &[ToolConfig]) {
for config in configs {
match config {
ToolConfig::Desc(desc) => {
self.description = desc.clone();
}
ToolConfig::Tags(tags) => {
if let Ok(mut schema) =
serde_json::from_str::<serde_json::Value>(&self.input_schema)
{
if let Some(obj) = schema.as_object_mut() {
obj.insert("tags".to_string(), serde_json::json!(tags));
}
self.input_schema = schema.to_string();
}
}
ToolConfig::ParamDesc { name, desc } => {
self.apply_param_desc(name, desc);
}
ToolConfig::ParamExample { name, example } => {
self.apply_param_example(name, example);
}
ToolConfig::ParamDefault { name, default } => {
self.apply_param_default(name, default);
}
ToolConfig::ParamRequired { name, required } => {
self.apply_param_required(name, *required);
}
ToolConfig::ParamMin { name, min } => {
self.apply_param_constraint(name, "minimum", serde_json::json!(min));
}
ToolConfig::ParamMax { name, max } => {
self.apply_param_constraint(name, "maximum", serde_json::json!(max));
}
ToolConfig::ParamMinLength { name, min_length } => {
self.apply_param_constraint(name, "minLength", serde_json::json!(min_length));
}
ToolConfig::ParamMaxLength { name, max_length } => {
self.apply_param_constraint(name, "maxLength", serde_json::json!(max_length));
}
ToolConfig::ParamPattern { name, pattern } => {
self.apply_param_constraint(name, "pattern", serde_json::json!(pattern));
}
ToolConfig::ParamMinItems { name, min_items } => {
self.apply_param_constraint(name, "minItems", serde_json::json!(min_items));
}
ToolConfig::ParamMaxItems { name, max_items } => {
self.apply_param_constraint(name, "maxItems", serde_json::json!(max_items));
}
ToolConfig::ParamMultipleOf { name, multiple_of } => {
self.apply_param_constraint(name, "multipleOf", serde_json::json!(multiple_of));
}
}
}
}
#[cfg(feature = "serde")]
fn apply_param_desc(&mut self, name: &str, desc: &str) {
if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
if let Some(obj) = schema.as_object_mut() {
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
param.insert("description".to_string(), serde_json::json!(desc));
}
}
}
self.input_schema = schema.to_string();
}
}
#[cfg(feature = "serde")]
fn apply_param_example(&mut self, name: &str, example: &serde_json::Value) {
if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
if let Some(obj) = schema.as_object_mut() {
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
param.insert("example".to_string(), example.clone());
}
}
}
self.input_schema = schema.to_string();
}
}
#[cfg(feature = "serde")]
fn apply_param_default(&mut self, name: &str, default: &serde_json::Value) {
if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
if let Some(obj) = schema.as_object_mut() {
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
param.insert("default".to_string(), default.clone());
}
}
}
self.input_schema = schema.to_string();
}
}
#[cfg(feature = "serde")]
fn apply_param_required(&mut self, name: &str, required: bool) {
if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
if let Some(obj) = schema.as_object_mut() {
let required_arr = obj
.entry("required".to_string())
.or_insert_with(|| serde_json::json!([]))
.as_array_mut();
if let Some(req_arr) = required_arr {
let name_json = serde_json::json!(name);
if required && !req_arr.contains(&name_json) {
req_arr.push(name_json);
} else if !required {
req_arr.retain(|v| v != &name_json);
}
}
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
param.insert("required".to_string(), serde_json::json!(required));
}
}
}
self.input_schema = schema.to_string();
}
}
#[cfg(feature = "serde")]
fn apply_param_constraint(
&mut self,
name: &str,
constraint_key: &str,
value: serde_json::Value,
) {
if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
if let Some(obj) = schema.as_object_mut() {
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
param.insert(constraint_key.to_string(), value);
}
}
}
self.input_schema = schema.to_string();
}
}
#[cfg(feature = "serde")]
pub fn to_openai_function(&self) -> serde_json::Value {
let parameters = self.parse_input_schema_or_empty();
serde_json::json!({
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": parameters,
}
})
}
#[cfg(feature = "serde")]
pub fn to_anthropic_tool(&self) -> serde_json::Value {
let input_schema = self.parse_input_schema_or_empty();
serde_json::json!({
"name": self.name,
"description": self.description,
"input_schema": input_schema,
})
}
#[cfg(feature = "serde")]
pub fn to_mcp_tool(&self) -> serde_json::Value {
let input_schema = self.parse_input_schema_or_empty();
serde_json::json!({
"name": self.name,
"description": self.description,
"inputSchema": input_schema,
})
}
#[cfg(feature = "serde")]
fn parse_input_schema_or_empty(&self) -> serde_json::Value {
serde_json::from_str::<serde_json::Value>(&self.input_schema)
.unwrap_or_else(|_| serde_json::json!({}))
}
}
impl core::fmt::Display for ToolDefinition {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}: {}", self.name, self.description)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum ParamType {
String = 0,
Integer = 1,
Number = 2,
Boolean = 3,
Array = 4,
Object = 5,
}
impl ParamType {
pub fn as_str(&self) -> &'static str {
match self {
ParamType::String => "string",
ParamType::Integer => "integer",
ParamType::Number => "number",
ParamType::Boolean => "boolean",
ParamType::Array => "array",
ParamType::Object => "object",
}
}
pub fn from_rust_type(type_name: &str) -> Option<Self> {
match type_name {
"String" | "str" => Some(ParamType::String),
"i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128"
| "usize" | "isize" => Some(ParamType::Integer),
"f32" | "f64" => Some(ParamType::Number),
"bool" => Some(ParamType::Boolean),
_ => {
if type_name.starts_with("Vec<") {
Some(ParamType::Array)
} else if type_name.starts_with("Option<") {
None
} else {
Some(ParamType::Object)
}
}
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ToolParameter {
pub name: &'static str,
#[cfg_attr(feature = "serde", serde(rename = "type"))]
pub param_type: ParamType,
pub description: &'static str,
pub required: bool,
}
impl ToolParameter {
pub fn new(
name: &'static str,
param_type: ParamType,
description: &'static str,
required: bool,
) -> Self {
Self {
name,
param_type,
description,
required,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ToolError {
pub kind: ToolErrorKind,
#[cfg(feature = "serde")]
pub message: crate::serde_types::String,
#[cfg(not(feature = "serde"))]
pub message: &'static str,
}
#[cfg(feature = "serde")]
impl std::error::Error for ToolError {}
#[cfg(feature = "serde")]
impl std::fmt::Display for ToolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ToolError: {:?} - {}", self.kind, self.message)
}
}
#[cfg(not(feature = "serde"))]
impl ToolError {
pub fn new(kind: ToolErrorKind, message: &'static str) -> Self {
Self { kind, message }
}
pub fn validation_error(message: &'static str) -> Self {
Self {
kind: ToolErrorKind::ValidationError,
message,
}
}
pub fn not_found(message: &'static str) -> Self {
Self {
kind: ToolErrorKind::NotFound,
message,
}
}
pub fn internal_error(message: &'static str) -> Self {
Self {
kind: ToolErrorKind::InternalError,
message,
}
}
}
#[cfg(feature = "serde")]
impl ToolError {
pub fn new(kind: ToolErrorKind, message: impl Into<crate::serde_types::String>) -> Self {
Self {
kind,
message: message.into(),
}
}
pub fn validation_error(message: impl Into<crate::serde_types::String>) -> Self {
Self {
kind: ToolErrorKind::ValidationError,
message: message.into(),
}
}
pub fn not_found(message: impl Into<crate::serde_types::String>) -> Self {
Self {
kind: ToolErrorKind::NotFound,
message: message.into(),
}
}
pub fn internal_error(message: impl Into<crate::serde_types::String>) -> Self {
Self {
kind: ToolErrorKind::InternalError,
message: message.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum ToolErrorKind {
ValidationError = 0,
NotFound = 1,
InternalError = 2,
TypeError = 3,
}
pub trait ToolProvider {
fn tool_definitions() -> &'static [ToolDefinition];
fn tool_count() -> usize {
Self::tool_definitions().len()
}
fn find_tool(name: &str) -> Option<&'static ToolDefinition> {
Self::tool_definitions().iter().find(|t| t.name == name)
}
}
#[cfg(feature = "serde")]
pub trait ToolCaller {
fn call_tool(
&self,
name: &str,
args: &crate::serde_types::Value,
) -> Result<crate::serde_types::Value, ToolError>;
}
#[cfg(feature = "serde")]
pub trait FromJsonValue: Sized {
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError>;
fn from_json_value_opt(args: &crate::serde_types::Value, key: &str) -> Option<Self> {
Self::from_json_value(args, key).ok()
}
}
#[cfg(feature = "serde")]
impl FromJsonValue for i64 {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_i64()
.ok_or_else(|| {
ToolError::validation_error(format!(
"Parameter '{}' has wrong type, expected integer",
key
))
})
}
}
#[cfg(feature = "serde")]
impl FromJsonValue for i32 {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_i64()
.map(|v| v as i32)
.ok_or_else(|| {
ToolError::validation_error(format!(
"Parameter '{}' has wrong type, expected integer",
key
))
})
}
}
#[cfg(feature = "serde")]
impl FromJsonValue for u64 {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_u64()
.ok_or_else(|| {
ToolError::validation_error(format!(
"Parameter '{}' has wrong type, expected unsigned integer",
key
))
})
}
}
#[cfg(feature = "serde")]
impl FromJsonValue for u32 {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_u64()
.map(|v| v as u32)
.ok_or_else(|| {
ToolError::validation_error(format!(
"Parameter '{}' has wrong type, expected unsigned integer",
key
))
})
}
}
#[cfg(feature = "serde")]
impl FromJsonValue for f64 {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_f64()
.ok_or_else(|| {
ToolError::validation_error(format!(
"Parameter '{}' has wrong type, expected number",
key
))
})
}
}
#[cfg(feature = "serde")]
impl FromJsonValue for f32 {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_f64()
.map(|v| v as f32)
.ok_or_else(|| {
ToolError::validation_error(format!(
"Parameter '{}' has wrong type, expected number",
key
))
})
}
}
#[cfg(feature = "serde")]
impl FromJsonValue for bool {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_bool()
.ok_or_else(|| {
ToolError::validation_error(format!(
"Parameter '{}' has wrong type, expected boolean",
key
))
})
}
}
#[cfg(feature = "serde")]
impl FromJsonValue for String {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| {
ToolError::validation_error(format!(
"Parameter '{}' has wrong type, expected string",
key
))
})
}
}
#[cfg(feature = "serde")]
#[inline(always)]
pub fn from_json_value_str<'a>(
args: &'a crate::serde_types::Value,
key: &str,
) -> Result<&'a str, ToolError> {
args.get(key)
.ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?
.as_str()
.ok_or_else(|| {
ToolError::validation_error(format!("Parameter '{}' type error, expected string", key))
})
}
#[cfg(feature = "serde")]
impl<T: FromJsonValue> FromJsonValue for Option<T> {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
Ok(T::from_json_value_opt(args, key))
}
}
#[cfg(feature = "serde")]
impl<T: serde::de::DeserializeOwned> FromJsonValue for Vec<T> {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
let value = args.get(key).ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?;
serde_json::from_value(value.clone()).map_err(|e| {
ToolError::validation_error(format!("Parameter '{}' has wrong type: {}", key, e))
})
}
}
#[cfg(feature = "serde")]
#[inline(always)]
pub fn from_json_value_generic<T: serde::de::DeserializeOwned>(
args: &crate::serde_types::Value,
key: &str,
) -> Result<T, ToolError> {
let value = args.get(key).ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?;
serde_json::from_value(value.clone())
.map_err(|e| ToolError::validation_error(format!("Parameter '{}' type error: {}", key, e)))
}
#[cfg(feature = "serde")]
impl<V: serde::de::DeserializeOwned> FromJsonValue for std::collections::HashMap<String, V> {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
let value = args.get(key).ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?;
serde_json::from_value(value.clone()).map_err(|e| {
ToolError::validation_error(format!("Parameter '{}' type error: {}", key, e))
})
}
}
#[cfg(feature = "serde")]
impl<V: serde::de::DeserializeOwned> FromJsonValue for std::collections::BTreeMap<String, V> {
#[inline(always)]
fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
let value = args.get(key).ok_or_else(|| {
ToolError::validation_error(format!("Missing required parameter '{}'", key))
})?;
serde_json::from_value(value.clone()).map_err(|e| {
ToolError::validation_error(format!("Parameter '{}' type error: {}", key, e))
})
}
}
#[cfg(feature = "serde")]
pub mod config;
pub trait AsyncExecutor: Send + Sync {
fn block_on_dyn(
&self,
future: core::pin::Pin<Box<dyn core::future::Future<Output = ()> + Send>>,
) -> Box<dyn core::any::Any + Send>;
}
pub trait AsyncExecutorExt: AsyncExecutor {
fn block_on<F>(&self, future: F) -> F::Output
where
F: core::future::Future + Send + 'static,
F::Output: Send + 'static,
{
use core::any::Any;
use core::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
let slot: Arc<Mutex<Option<F::Output>>> = Arc::new(Mutex::new(None));
let ran: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
let slot_inner = slot.clone();
let ran_inner = ran.clone();
let wrapped = async move {
let output = future.await;
*slot_inner
.lock()
.expect("AsyncExecutor output slot poisoned") = Some(output);
ran_inner.store(true, Ordering::Release);
};
let _: Box<dyn Any + Send> = self.block_on_dyn(Box::pin(wrapped));
assert!(
ran.load(Ordering::Acquire),
"AsyncExecutor did not drive the future to completion"
);
let mut guard = slot.lock().expect("AsyncExecutor output slot poisoned");
guard
.take()
.expect("future reported complete but produced no output")
}
}
impl<T: AsyncExecutor + ?Sized> AsyncExecutorExt for T {}
static ASYNC_EXECUTOR: std::sync::OnceLock<Box<dyn AsyncExecutor>> = std::sync::OnceLock::new();
pub fn set_async_executor<E: AsyncExecutor + 'static>(executor: Box<E>) {
let trait_obj: Box<dyn AsyncExecutor> = executor;
let _ = ASYNC_EXECUTOR.set(trait_obj);
}
pub fn current_async_executor() -> Option<&'static dyn AsyncExecutor> {
ASYNC_EXECUTOR
.get()
.map(|b| b.as_ref() as &'static dyn AsyncExecutor)
}
#[cfg(feature = "serde")]
pub fn block_on_async<F>(future: F) -> Result<F::Output, ToolError>
where
F: core::future::Future + Send + 'static,
F::Output: Send + 'static,
{
if let Some(exec) = current_async_executor() {
return Ok(exec.block_on(future));
}
drop(future);
Err(ToolError::internal_error(block_on_async_error_message()))
}
#[cfg(not(feature = "serde"))]
pub fn block_on_async<F>(_future: F) -> Result<F, &'static str> {
Err(
"no async runtime available in no_std build; enable the `serde` feature \
and call `tokitai_core::set_async_executor(...)` to enable sync-from-async",
)
}
pub const fn block_on_async_error_message() -> &'static str {
"no async runtime registered; either call from within an async context, \
run inside a tokio runtime, or call `tokitai_core::set_async_executor(...)` \
before invoking"
}
#[cfg(feature = "serde")]
mod executor_internal {
use super::*;
pub struct NullExecutor;
impl AsyncExecutor for NullExecutor {
fn block_on_dyn(
&self,
_future: core::pin::Pin<Box<dyn core::future::Future<Output = ()> + Send>>,
) -> Box<dyn core::any::Any + Send> {
panic!(
"NullExecutor::block_on_dyn invoked; \
install a real AsyncExecutor via set_async_executor(...)"
)
}
}
}
#[cfg(feature = "serde")]
pub mod serde_types {
pub use alloc::string::String;
pub use serde_json::Value;
}
#[macro_export]
macro_rules! json_schema {
(
{
$($param_name:literal: {
type: $param_type:ident,
description: $description:literal,
required: $required:literal $(,)?
}),*
$(,)?
}
) => {{
const SCHEMA: &str = concat!(
"{\"type\":\"object\",\"properties\":{",
$({
concat!(
"\"", $param_name, "\":",
"{\"type\":\"", $crate::ParamType::$param_type.as_str(), "\",\"description\":\"", $description, "\"}"
)
},)*
"},\"required\":[",
$({
if $required { concat!("\"", $param_name, "\"") } else { "" }
},)*
"]}"
);
SCHEMA
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_param_type_from_rust_type() {
assert_eq!(ParamType::from_rust_type("String"), Some(ParamType::String));
assert_eq!(ParamType::from_rust_type("i32"), Some(ParamType::Integer));
assert_eq!(ParamType::from_rust_type("f64"), Some(ParamType::Number));
assert_eq!(ParamType::from_rust_type("bool"), Some(ParamType::Boolean));
assert_eq!(
ParamType::from_rust_type("Vec<i32>"),
Some(ParamType::Array)
);
}
#[test]
fn test_tool_definition_const() {
let tool = ToolDefinition::new("test", "A test tool", "{}");
assert_eq!(tool.name, "test");
assert_eq!(tool.description, "A test tool");
}
#[cfg(feature = "serde")]
#[test]
fn test_tool_definition_to_json() {
let tool = ToolDefinition::new("test", "A test tool", r#"{"type":"object"}"#);
let json = tool.to_json().unwrap();
assert!(json.contains(r#""name":"test""#));
}
#[cfg(feature = "serde")]
struct TestExecutor;
#[cfg(feature = "serde")]
impl AsyncExecutor for TestExecutor {
fn block_on_dyn(
&self,
future: core::pin::Pin<Box<dyn core::future::Future<Output = ()> + Send>>,
) -> Box<dyn core::any::Any + Send> {
futures::executor::block_on(future);
Box::new(())
}
}
#[cfg(feature = "serde")]
#[test]
#[serial_test::serial]
fn test_async_executor_lifecycle() {
if current_async_executor().is_none() {
let result: Result<(), ToolError> = block_on_async(async {});
let err = result
.expect_err("block_on_async should error when no AsyncExecutor is registered");
assert_eq!(err.kind, ToolErrorKind::InternalError);
let expected = block_on_async_error_message();
assert!(
err.message.contains(expected) || err.message.contains("no async runtime"),
"expected English error message, got: {:?}",
err.message
);
}
set_async_executor(Box::new(TestExecutor));
let exec = current_async_executor().expect("executor should be registered");
let _static_ref: &'static dyn AsyncExecutor = exec;
let string_result: String = exec.block_on(async { String::from("hello, executor") });
assert_eq!(string_result, "hello, executor");
let tuple_result: (i32, String) = exec.block_on(async { (42, String::from("typed")) });
assert_eq!(tuple_result, (42, String::from("typed")));
}
}