use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolKind {
Read,
Edit,
Delete,
ListDir,
Move,
Search,
Lsp,
Execute,
Plan,
WebSearch,
WebFetch,
Background,
Skill,
Memory,
Goal,
Other,
}
impl ToolKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Edit => "edit",
Self::Delete => "delete",
Self::ListDir => "list_dir",
Self::Move => "move",
Self::Search => "search",
Self::Lsp => "lsp",
Self::Execute => "execute",
Self::Plan => "plan",
Self::WebSearch => "web_search",
Self::WebFetch => "web_fetch",
Self::Background => "background",
Self::Skill => "skill",
Self::Memory => "memory",
Self::Goal => "goal",
Self::Other => "other",
}
}
}
impl std::fmt::Display for ToolKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolNamespace {
Builtin,
Mcp,
Skill,
Extension,
Alias,
Other,
}
impl ToolNamespace {
pub fn as_str(self) -> &'static str {
match self {
Self::Builtin => "builtin",
Self::Mcp => "mcp",
Self::Skill => "skill",
Self::Extension => "extension",
Self::Alias => "alias",
Self::Other => "other",
}
}
}
impl std::fmt::Display for ToolNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanonicalToolMeta {
pub kind: ToolKind,
pub namespace: ToolNamespace,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub side_effects: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_bucket: Option<TokenBucket>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenBucket {
Small,
Medium,
Large,
}
impl CanonicalToolMeta {
pub fn new(kind: ToolKind, namespace: ToolNamespace) -> Self {
Self {
kind,
namespace,
label: None,
side_effects: None,
token_bucket: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_kind_round_trips() {
for kind in [
ToolKind::Read,
ToolKind::Edit,
ToolKind::Delete,
ToolKind::ListDir,
ToolKind::Move,
ToolKind::Search,
ToolKind::Lsp,
ToolKind::Execute,
ToolKind::Plan,
ToolKind::WebSearch,
ToolKind::WebFetch,
ToolKind::Background,
ToolKind::Skill,
ToolKind::Memory,
ToolKind::Goal,
ToolKind::Other,
] {
let json = serde_json::to_string(&kind).unwrap();
let back: ToolKind = serde_json::from_str(&json).unwrap();
assert_eq!(kind, back);
assert!(!kind.as_str().is_empty());
}
}
#[test]
fn tool_namespace_round_trips() {
for ns in [
ToolNamespace::Builtin,
ToolNamespace::Mcp,
ToolNamespace::Skill,
ToolNamespace::Extension,
ToolNamespace::Alias,
ToolNamespace::Other,
] {
let json = serde_json::to_string(&ns).unwrap();
let back: ToolNamespace = serde_json::from_str(&json).unwrap();
assert_eq!(ns, back);
assert!(!ns.as_str().is_empty());
}
}
#[test]
fn canonical_tool_meta_serializes_without_label() {
let meta = CanonicalToolMeta::new(ToolKind::Search, ToolNamespace::Builtin);
let json = serde_json::to_string(&meta).unwrap();
assert!(!json.contains("label"));
assert!(json.contains("search"));
assert!(json.contains("builtin"));
}
#[test]
fn canonical_tool_meta_serializes_with_optional_fields() {
let mut meta = CanonicalToolMeta::new(ToolKind::Execute, ToolNamespace::Builtin);
meta.label = Some("Shell".to_string());
meta.side_effects = Some("mutating".to_string());
meta.token_bucket = Some(TokenBucket::Medium);
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("Shell"));
assert!(json.contains("mutating"));
assert!(json.contains("medium"));
}
}