Skip to main content

vtcode_utility_tool_specs/
tool_kind.rs

1//! Tool semantic taxonomy — the "what does this tool do" dimension.
2//!
3//! This is complementary to the surface-level `ToolKind` in `vtcode-core`
4//! (Function / Mcp / Custom). These variants answer "what category of work
5//! does this tool perform", which is useful for:
6//! - capability negotiation ("does this session support LSP tools?")
7//! - token-budget policy (search tools are cheap, goal tools are expensive)
8//! - UI grouping (file tools vs shell tools vs web tools)
9
10use serde::{Deserialize, Serialize};
11
12/// Semantic category of a tool — what it does, not how it is called.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ToolKind {
16    /// Read-only file inspection (read_file, view, cat, etc.)
17    Read,
18    /// File creation and mutation (write_file, apply_patch, edit, etc.)
19    Edit,
20    /// File or directory deletion
21    Delete,
22    /// Directory or file listing (ls, find, tree, etc.)
23    ListDir,
24    /// File or path movement (mv, rename, etc.)
25    Move,
26    /// Code or text search (grep, rg, search, etc.)
27    Search,
28    /// Language-server protocol operations (goto, hover, diagnostics, etc.)
29    Lsp,
30    /// Shell or system command execution
31    Execute,
32    /// Planning, tracking, or goal management
33    Plan,
34    /// Web search
35    WebSearch,
36    /// Web fetch / scrape
37    WebFetch,
38    /// Background or scheduled task management
39    Background,
40    /// Skill loading or invocation
41    Skill,
42    /// Memory search or retrieval
43    Memory,
44    /// Goal or objective management
45    Goal,
46    /// Catch-all for unclassified tools
47    Other,
48}
49
50impl ToolKind {
51    /// Short stable label used in telemetry and grouping.
52    pub fn as_str(self) -> &'static str {
53        match self {
54            Self::Read => "read",
55            Self::Edit => "edit",
56            Self::Delete => "delete",
57            Self::ListDir => "list_dir",
58            Self::Move => "move",
59            Self::Search => "search",
60            Self::Lsp => "lsp",
61            Self::Execute => "execute",
62            Self::Plan => "plan",
63            Self::WebSearch => "web_search",
64            Self::WebFetch => "web_fetch",
65            Self::Background => "background",
66            Self::Skill => "skill",
67            Self::Memory => "memory",
68            Self::Goal => "goal",
69            Self::Other => "other",
70        }
71    }
72}
73
74impl std::fmt::Display for ToolKind {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.write_str(self.as_str())
77    }
78}
79
80/// Logical namespace for a tool — who "owns" it.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum ToolNamespace {
84    /// Built-in vtcode tool
85    Builtin,
86    /// MCP server tool
87    Mcp,
88    /// Skill-provided tool
89    Skill,
90    /// Extension or plugin tool
91    Extension,
92    /// User-defined alias or wrapper
93    Alias,
94    /// Unknown / fallback
95    Other,
96}
97
98impl ToolNamespace {
99    /// Short stable label used in telemetry and grouping.
100    pub fn as_str(self) -> &'static str {
101        match self {
102            Self::Builtin => "builtin",
103            Self::Mcp => "mcp",
104            Self::Skill => "skill",
105            Self::Extension => "extension",
106            Self::Alias => "alias",
107            Self::Other => "other",
108        }
109    }
110}
111
112impl std::fmt::Display for ToolNamespace {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.write_str(self.as_str())
115    }
116}
117
118/// Canonical metadata envelope attached to a tool registration.
119///
120/// The `_meta` wrapper keeps the envelope distinct from the tool's functional
121/// schema so downstream consumers can strip or override it without affecting
122/// parameter validation.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct CanonicalToolMeta {
125    /// Semantic category of the tool.
126    pub kind: ToolKind,
127    /// Logical owner of the tool.
128    pub namespace: ToolNamespace,
129    /// Human-readable label for UI grouping.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub label: Option<String>,
132    /// Short description of the tool's side-effects (mutating, read-only, etc.).
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub side_effects: Option<String>,
135    /// Estimated token cost bucket (small < 500, medium < 2000, large >= 2000).
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub token_bucket: Option<TokenBucket>,
138}
139
140/// Rough token-cost bucket for a tool invocation (input + output estimate).
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum TokenBucket {
144    Small,
145    Medium,
146    Large,
147}
148
149impl CanonicalToolMeta {
150    /// Create a minimal metadata entry with kind and namespace.
151    pub fn new(kind: ToolKind, namespace: ToolNamespace) -> Self {
152        Self {
153            kind,
154            namespace,
155            label: None,
156            side_effects: None,
157            token_bucket: None,
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn tool_kind_round_trips() {
168        for kind in [
169            ToolKind::Read,
170            ToolKind::Edit,
171            ToolKind::Delete,
172            ToolKind::ListDir,
173            ToolKind::Move,
174            ToolKind::Search,
175            ToolKind::Lsp,
176            ToolKind::Execute,
177            ToolKind::Plan,
178            ToolKind::WebSearch,
179            ToolKind::WebFetch,
180            ToolKind::Background,
181            ToolKind::Skill,
182            ToolKind::Memory,
183            ToolKind::Goal,
184            ToolKind::Other,
185        ] {
186            let json = serde_json::to_string(&kind).unwrap();
187            let back: ToolKind = serde_json::from_str(&json).unwrap();
188            assert_eq!(kind, back);
189            assert!(!kind.as_str().is_empty());
190        }
191    }
192
193    #[test]
194    fn tool_namespace_round_trips() {
195        for ns in [
196            ToolNamespace::Builtin,
197            ToolNamespace::Mcp,
198            ToolNamespace::Skill,
199            ToolNamespace::Extension,
200            ToolNamespace::Alias,
201            ToolNamespace::Other,
202        ] {
203            let json = serde_json::to_string(&ns).unwrap();
204            let back: ToolNamespace = serde_json::from_str(&json).unwrap();
205            assert_eq!(ns, back);
206            assert!(!ns.as_str().is_empty());
207        }
208    }
209
210    #[test]
211    fn canonical_tool_meta_serializes_without_label() {
212        let meta = CanonicalToolMeta::new(ToolKind::Search, ToolNamespace::Builtin);
213        let json = serde_json::to_string(&meta).unwrap();
214        assert!(!json.contains("label"));
215        assert!(json.contains("search"));
216        assert!(json.contains("builtin"));
217    }
218
219    #[test]
220    fn canonical_tool_meta_serializes_with_optional_fields() {
221        let mut meta = CanonicalToolMeta::new(ToolKind::Execute, ToolNamespace::Builtin);
222        meta.label = Some("Shell".to_string());
223        meta.side_effects = Some("mutating".to_string());
224        meta.token_bucket = Some(TokenBucket::Medium);
225        let json = serde_json::to_string(&meta).unwrap();
226        assert!(json.contains("Shell"));
227        assert!(json.contains("mutating"));
228        assert!(json.contains("medium"));
229    }
230}