#![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};
#[cfg(feature = "serde")]
pub mod dynamic;
#[cfg(feature = "serde")]
pub use dynamic::{
is_tenant_denied, DynamicHandler, DynamicToolProvider, DynamicToolRegistry,
TENANT_DENIED_KIND_HINT,
};
#[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>,
#[cfg(feature = "serde")]
pub description_explicit: bool,
#[cfg(not(feature = "serde"))]
pub description_explicit: bool,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "serde")]
pub baked_examples: Option<serde_json::Value>,
#[cfg(not(feature = "serde"))]
pub baked_examples: Option<&'static str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "serde")]
pub usage_hint: Option<alloc::string::String>,
#[cfg(not(feature = "serde"))]
pub usage_hint: Option<&'static str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "serde")]
pub since: Option<alloc::string::String>,
#[cfg(not(feature = "serde"))]
pub since: Option<&'static str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "serde")]
pub until: Option<alloc::string::String>,
#[cfg(not(feature = "serde"))]
pub until: Option<&'static str>,
}
pub const CONFIG_PRIORITY_DOC: &[&str] = &[
"#[tool(desc = \"...\")] (compile-time, attribute-supplied)",
"doc comment (compile-time, /// lines above the method)",
"tokitai! config block (runtime, applies via GLOBAL_CONFIG_REGISTRY)",
"synthesized default (compile-time, \"调用 <method> 方法\")",
];
#[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,
description_explicit: false,
baked_examples: None,
usage_hint: None,
since: None,
until: 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,
description_explicit: false,
baked_examples: None,
usage_hint: None,
since: None,
until: 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,
description_explicit: false,
baked_examples: None,
usage_hint: None,
since: None,
until: None,
}
}
#[must_use]
pub fn with_description_explicit(mut self) -> Self {
self.description_explicit = true;
self
}
#[cfg(feature = "serde")]
pub fn with_usage_hint(mut self, hint: impl Into<alloc::string::String>) -> Self {
self.usage_hint = Some(hint.into());
self
}
#[cfg(not(feature = "serde"))]
pub fn with_usage_hint(mut self, hint: &'static str) -> Self {
self.usage_hint = Some(hint);
self
}
#[cfg(feature = "serde")]
#[must_use]
pub fn with_baked_examples(mut self, examples: serde_json::Value) -> Self {
if let serde_json::Value::Array(ref arr) = examples {
if arr.is_empty() {
self.baked_examples = None;
return self;
}
}
self.baked_examples = Some(examples);
self
}
#[cfg(not(feature = "serde"))]
#[must_use]
pub fn with_baked_examples(mut self, _examples: &'static str) -> Self {
self
}
#[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")]
#[must_use]
pub fn with_since(mut self, since: impl Into<alloc::string::String>) -> Self {
self.since = Some(since.into());
self
}
#[cfg(not(feature = "serde"))]
#[must_use]
pub fn with_since(mut self, since: &'static str) -> Self {
self.since = Some(since);
self
}
#[cfg(feature = "serde")]
#[must_use]
pub fn with_until(mut self, until: impl Into<alloc::string::String>) -> Self {
self.until = Some(until.into());
self
}
#[cfg(not(feature = "serde"))]
#[must_use]
pub fn with_until(mut self, until: &'static str) -> Self {
self.until = Some(until);
self
}
pub fn is_in_interval(&self, current_version: Option<&str>) -> bool {
if self.since.is_none() && self.until.is_none() {
return true;
}
let Some(current) = current_version else {
return true;
};
if let Some(since) = self.since.as_deref() {
if !version_gte(current, since) {
return false;
}
}
if let Some(until) = self.until.as_deref() {
if version_gte(current, until) {
return false;
}
}
true
}
#[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) => {
if !self.description_explicit {
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.merge_baked_examples_into(self.parse_input_schema_or_empty());
let description = self.deprecated_description_suffix();
serde_json::json!({
"type": "function",
"function": {
"name": self.name,
"description": description,
"parameters": parameters,
}
})
}
#[cfg(feature = "serde")]
pub fn to_anthropic_tool(&self) -> serde_json::Value {
let input_schema = self.merge_baked_examples_into(self.parse_input_schema_or_empty());
let description = self.deprecated_description_suffix();
serde_json::json!({
"name": self.name,
"description": description,
"input_schema": input_schema,
})
}
#[cfg(feature = "serde")]
pub fn to_mcp_tool(&self) -> serde_json::Value {
let input_schema = self.merge_baked_examples_into(self.parse_input_schema_or_empty());
let description = self.deprecated_description_suffix();
let mut envelope = serde_json::json!({
"name": self.name,
"description": description,
"inputSchema": input_schema,
});
if self.deprecated_since.is_some() || self.remove_in.is_some() || self.replaced_by.is_some()
{
let mut meta = serde_json::Map::new();
meta.insert("deprecated".to_string(), serde_json::Value::Bool(true));
if let Some(since) = self.deprecated_since.as_deref() {
meta.insert(
"deprecatedSince".to_string(),
serde_json::Value::String(since.to_string()),
);
}
if let Some(remove_in) = self.remove_in.as_deref() {
meta.insert(
"removeIn".to_string(),
serde_json::Value::String(remove_in.to_string()),
);
}
if let Some(replaced_by) = self.replaced_by.as_deref() {
meta.insert(
"replacedBy".to_string(),
serde_json::Value::String(replaced_by.to_string()),
);
}
envelope["_meta"] = serde_json::Value::Object(meta);
}
envelope
}
#[cfg(feature = "serde")]
fn merge_baked_examples_into(&self, mut schema: serde_json::Value) -> serde_json::Value {
if let Some(serde_json::Value::Array(arr)) = self.baked_examples.as_ref() {
if !arr.is_empty() {
if let serde_json::Value::Object(map) = &mut schema {
map.insert(
"examples".to_string(),
serde_json::Value::Array(arr.clone()),
);
}
}
}
schema
}
#[cfg(feature = "serde")]
fn deprecated_description_suffix(&self) -> alloc::string::String {
if self.deprecated_since.is_none() && self.remove_in.is_none() && self.replaced_by.is_none()
{
return self.description.clone();
}
let mut suffix = alloc::string::String::from(" [DEPRECATED");
if let Some(since) = self.deprecated_since.as_deref() {
suffix.push_str(&alloc::format!(" since={}", since));
}
if let Some(remove_in) = self.remove_in.as_deref() {
suffix.push_str(&alloc::format!(" remove_in={}", remove_in));
}
if let Some(replaced_by) = self.replaced_by.as_deref() {
if !replaced_by.is_empty() {
suffix.push_str(&alloc::format!(" replaced_by={}", replaced_by));
}
}
suffix.push(']');
let mut out = self.description.clone();
out.push_str(&suffix);
out
}
#[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!({}))
}
pub fn is_removed(&self, current_version: Option<&str>) -> bool {
let (Some(remove_in), Some(current)) = (self.remove_in.as_deref(), current_version) else {
return false;
};
match (parse_semver(remove_in), parse_semver(current)) {
(Some(rm), Some(cur)) => cur >= rm,
_ => false,
}
}
#[cfg(feature = "serde")]
pub fn removed_error(&self, current_version: Option<&str>) -> ToolError {
let remove_in = self.remove_in.as_deref().unwrap_or("?");
let message = match (current_version, self.replaced_by.as_deref()) {
(Some(cur), Some(repl)) if !repl.is_empty() => alloc::format!(
"tool `{}` was removed in version {} (current: {}); use `{}` instead",
self.name,
remove_in,
cur,
repl
),
(Some(cur), _) => alloc::format!(
"tool `{}` was removed in version {} (current: {})",
self.name,
remove_in,
cur
),
(None, Some(repl)) if !repl.is_empty() => alloc::format!(
"tool `{}` was removed in version {}; use `{}` instead",
self.name,
remove_in,
repl
),
(None, _) => {
alloc::format!("tool `{}` was removed in version {}", self.name, remove_in)
}
};
ToolError::removed(message)
}
}
pub(crate) fn parse_semver(s: &str) -> Option<(u32, u32, u32)> {
let s = s.trim();
let s = s.strip_prefix('v').unwrap_or(s);
let core = s.split(['-', '+']).next().unwrap_or(s);
let mut parts = core.split('.');
let major = parts.next()?.parse::<u32>().ok()?;
let minor = parts.next()?.parse::<u32>().ok()?;
let patch = parts.next()?.parse::<u32>().ok()?;
if parts.next().is_some() {
return None;
}
Some((major, minor, patch))
}
pub(crate) fn version_gte(current: &str, other: &str) -> bool {
match (parse_semver(current), parse_semver(other)) {
(Some(a), Some(b)) => a >= b,
_ => current.trim() >= other.trim(),
}
}
#[cfg(feature = "serde")]
static CURRENT_VERSION: std::sync::Mutex<Option<alloc::string::String>> =
std::sync::Mutex::new(None);
#[cfg(feature = "serde")]
pub fn set_current_version(version: impl Into<alloc::string::String>) {
if let Ok(mut guard) = CURRENT_VERSION.lock() {
*guard = Some(version.into());
}
}
#[cfg(feature = "serde")]
pub fn clear_current_version() {
if let Ok(mut guard) = CURRENT_VERSION.lock() {
*guard = None;
}
}
#[cfg(feature = "serde")]
pub fn current_version() -> Option<alloc::string::String> {
CURRENT_VERSION.lock().ok().and_then(|guard| guard.clone())
}
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,
}
}
pub fn removed(message: &'static str) -> Self {
Self {
kind: ToolErrorKind::Removed,
message,
}
}
pub fn truncated() -> Self {
Self {
kind: ToolErrorKind::Truncated,
message: "tool result exceeded the result_truncate_bytes budget",
}
}
}
#[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(),
}
}
pub fn removed(message: impl Into<crate::serde_types::String>) -> Self {
Self {
kind: ToolErrorKind::Removed,
message: message.into(),
}
}
pub fn truncated_with(original_bytes: usize, kept_bytes: usize) -> Self {
Self {
kind: ToolErrorKind::Truncated,
message: format!(
"tool result exceeded the result_truncate_bytes budget: \
original {} bytes, kept {} bytes",
original_bytes, kept_bytes
),
}
}
}
#[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,
Removed = 4,
Truncated = 5,
}
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>;
fn block_on_for(&self) -> Option<&'static dyn AsyncExecutor> {
None
}
}
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)
}
pub fn block_on_for_executor() -> Option<&'static dyn AsyncExecutor> {
ASYNC_EXECUTOR.get().and_then(|b| b.block_on_for())
}
#[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) = block_on_for_executor() {
return Ok(exec.block_on(future));
}
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")]
pub struct AsyncSleep {
deadline: std::time::Instant,
armed: bool,
}
#[cfg(feature = "serde")]
impl AsyncSleep {
#[must_use]
pub fn new(dur: std::time::Duration) -> Self {
Self {
deadline: std::time::Instant::now() + dur,
armed: false,
}
}
pub fn remaining(&self) -> std::time::Duration {
self.deadline
.saturating_duration_since(std::time::Instant::now())
}
}
#[cfg(feature = "serde")]
impl core::future::Future for AsyncSleep {
type Output = ();
fn poll(
mut self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> core::task::Poll<()> {
if std::time::Instant::now() >= self.deadline {
return core::task::Poll::Ready(());
}
let remaining = self.remaining();
if !self.armed {
let waker = cx.waker().clone();
std::thread::spawn(move || {
std::thread::park_timeout(remaining);
waker.wake();
});
self.armed = true;
}
core::task::Poll::Pending
}
}
#[cfg(feature = "serde")]
#[must_use]
pub fn async_sleep(dur: std::time::Duration) -> AsyncSleep {
AsyncSleep::new(dur)
}
#[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
}};
}
pub type CapabilityManifest = alloc::vec::Vec<(
alloc::string::String,
alloc::vec::Vec<alloc::string::String>,
)>;
pub trait CapabilityManifestProvider {
fn capability_manifest() -> &'static [(&'static str, &'static [&'static str])] {
&[]
}
}
pub fn capability_in_allowlist(declared: &str, allowlist: &[alloc::string::String]) -> bool {
for entry in allowlist {
if let Some(prefix) = entry.strip_suffix('*') {
if prefix.is_empty() {
return true;
}
if declared.starts_with(prefix) {
return true;
}
} else if entry.as_str() == declared {
return true;
}
}
false
}
#[cfg(test)]
mod capability_manifest_tests {
use super::*;
#[test]
fn exact_match_succeeds() {
let allowlist = alloc::vec!["net:egress:smtp".to_string()];
assert!(capability_in_allowlist("net:egress:smtp", &allowlist));
}
#[test]
fn exact_match_misses() {
let allowlist = alloc::vec!["net:egress:smtp".to_string()];
assert!(!capability_in_allowlist("net:egress:http", &allowlist));
}
#[test]
fn wildcard_prefix_matches() {
let allowlist = alloc::vec!["db:read:*".to_string()];
assert!(capability_in_allowlist("db:read:sales", &allowlist));
assert!(capability_in_allowlist("db:read:any_resource", &allowlist));
}
#[test]
fn wildcard_prefix_does_not_match_unrelated() {
let allowlist = alloc::vec!["db:read:*".to_string()];
assert!(!capability_in_allowlist("db:write:sales", &allowlist));
assert!(!capability_in_allowlist("net:egress:smtp", &allowlist));
}
#[test]
fn empty_allowlist_fails_closed() {
let allowlist: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
assert!(!capability_in_allowlist("db:read:sales", &allowlist));
}
#[test]
fn bare_star_matches_anything() {
let allowlist = alloc::vec!["*".to_string()];
assert!(capability_in_allowlist("db:read:sales", &allowlist));
assert!(capability_in_allowlist("process:exec", &allowlist));
}
}
pub const CORE_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const fn assert_compatible_with(expected: &str) {
let expected_bytes = expected.as_bytes();
let mut exp_offset: usize = 0;
let mut exp_new_len = expected_bytes.len();
if !expected_bytes.is_empty() {
let first = expected_bytes[0];
if first == b'v' || first == b'V' {
exp_offset = 1;
exp_new_len = expected_bytes.len() - 1;
}
}
let expected_tail = unsafe {
core::slice::from_raw_parts(expected_bytes.as_ptr().add(exp_offset), exp_new_len)
};
let expected_str = unsafe { core::str::from_utf8_unchecked(expected_tail) };
let core_bytes = CORE_VERSION.as_bytes();
let mut core_offset: usize = 0;
let mut core_new_len = core_bytes.len();
if !core_bytes.is_empty() {
let first = core_bytes[0];
if first == b'v' || first == b'V' {
core_offset = 1;
core_new_len = core_bytes.len() - 1;
}
}
let core_tail =
unsafe { core::slice::from_raw_parts(core_bytes.as_ptr().add(core_offset), core_new_len) };
let core_str_stripped = unsafe { core::str::from_utf8_unchecked(core_tail) };
let expected_parts = parse_semver_const(expected_str);
let core_parts = parse_semver_const(core_str_stripped);
match (expected_parts, core_parts) {
(Some(ep), Some(cp)) => {
let (emaj, emin, epat, eary) = ep;
let (cmaj, cmin, cpat, _) = cp;
let _ = cpat; if eary >= 1 && cmaj != emaj {
panic!(concat!(
"tokitai-core version mismatch: major drift. ",
"compiled CORE_VERSION=",
env!("CARGO_PKG_VERSION"),
" differs from the caller's `expected` argument on the major component. ",
"See https://docs.rs/tokitai-core for the migration guide."
));
}
if eary >= 2 && cmin != emin {
panic!(concat!(
"tokitai-core version mismatch: minor drift. ",
"compiled CORE_VERSION=",
env!("CARGO_PKG_VERSION"),
" differs from the caller's `expected` argument on the minor component. ",
"See https://docs.rs/tokitai-core for the migration guide."
));
}
if eary >= 3 && cpat != epat {
panic!(concat!(
"tokitai-core version mismatch: patch drift. ",
"compiled CORE_VERSION=",
env!("CARGO_PKG_VERSION"),
" differs from the caller's `expected` argument on the patch component. ",
"See https://docs.rs/tokitai-core for the migration guide."
));
}
}
_ => {
panic!(concat!(
"tokitai-core version mismatch: invalid SemVer literal. ",
"compiled CORE_VERSION=",
env!("CARGO_PKG_VERSION"),
" (one or both sides failed to parse). ",
"See https://docs.rs/tokitai-core for the migration guide."
));
}
}
}
pub fn assert_compatible_with_runtime(expected: &str) {
let expected_str = expected
.strip_prefix('v')
.or_else(|| expected.strip_prefix('V'))
.unwrap_or(expected);
let core_str_stripped = CORE_VERSION
.strip_prefix('v')
.or_else(|| CORE_VERSION.strip_prefix('V'))
.unwrap_or(CORE_VERSION);
let expected_parts = parse_semver_const(expected_str);
let core_parts = parse_semver_const(core_str_stripped);
match (expected_parts, core_parts) {
(Some(ep), Some(cp)) => {
let (emaj, emin, epat, eary) = ep;
let (cmaj, cmin, cpat, _) = cp;
if eary >= 1 && cmaj != emaj {
panic!(
"tokitai-core version mismatch: expected={}, actual={} (major drift). See https://docs.rs/tokitai-core for the migration guide.",
expected, CORE_VERSION
);
}
if eary >= 2 && cmin != emin {
panic!(
"tokitai-core version mismatch: expected={}, actual={} (minor drift). See https://docs.rs/tokitai-core for the migration guide.",
expected, CORE_VERSION
);
}
if eary >= 3 && cpat != epat {
panic!(
"tokitai-core version mismatch: expected={}, actual={} (patch drift). See https://docs.rs/tokitai-core for the migration guide.",
expected, CORE_VERSION
);
}
}
_ => panic!(
"tokitai-core version mismatch: expected={}, actual={} (invalid SemVer literal on one or both sides). See https://docs.rs/tokitai-core for the migration guide.",
expected, CORE_VERSION
),
}
}
const fn parse_semver_const(s: &str) -> Option<(u64, u64, u64, usize)> {
let bytes = s.as_bytes();
let mut i: usize = 0;
let mut parts: [u64; 3] = [0, 0, 0];
let mut arity: usize = 0;
let mut end = bytes.len();
if end == 0 {
return None;
}
while i < end {
let b = bytes[i];
if b == b'-' || b == b'+' {
end = i;
break;
}
i += 1;
}
i = 0;
while i <= end {
let at_end = i == end;
let is_dot = !at_end && bytes[i] == b'.';
if at_end || is_dot {
if i == 0 {
return None;
}
if bytes[i - 1] == b'.' {
return None;
}
if arity >= 3 {
return None;
}
arity += 1;
if at_end {
break;
}
} else {
let b = bytes[i];
if b < b'0' || b > b'9' {
return None;
}
if b == b'0' {
let prev_is_dot = i == 0 || bytes[i - 1] == b'.';
if prev_is_dot {
let next_is_dot_or_end = i + 1 == end || bytes[i + 1] == b'.';
if !next_is_dot_or_end {
return None;
}
}
}
if arity >= 3 {
return None;
}
let slot = &mut parts[arity];
let mul = match slot.checked_mul(10) {
Some(v) => v,
None => return None,
};
let add = match mul.checked_add((b - b'0') as u64) {
Some(v) => v,
None => return None,
};
*slot = add;
}
i += 1;
}
Some((parts[0], parts[1], parts[2], arity))
}
#[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");
}
#[test]
fn parse_semver_const_rejects_extra_component() {
assert_eq!(parse_semver_const("0.0.0.0"), None);
assert_eq!(parse_semver_const("1.2.3.4"), None);
assert_eq!(parse_semver_const("0.5.1.2.3"), None);
assert_eq!(parse_semver_const("0.5.1"), Some((0, 5, 1, 3)));
}
#[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")));
}
}