Skip to main content

vtcode_config/core/
skills.rs

1//! Skills configuration
2//!
3//! Configuration for VT Code skills system, including rendering modes
4//! and discovery settings.
5//!
6//! ## Current Implementation Note
7//!
8//! As of v0.50.7, VT Code implements skills as **callable tools** (via Tool trait),
9//! not as prompt text in the system prompt. Skills are loaded on-demand via
10//! `/skills load <name>` commands and registered in the tool registry.
11//!
12//! The `prompt_format` and `render_mode` configs are currently **unused** but
13//! available for future features such as:
14//! - Optional skills summary in system prompt (opt-in via config flag)
15//! - Rich formatting for `/skills list` command output
16//! - Documentation generation
17//!
18//! Per Agent Skills specification: Skills are loaded on-demand, not auto-loaded.
19
20use serde::{Deserialize, Serialize};
21
22/// Bundled skills configuration.
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
25#[serde(rename_all = "kebab-case")]
26pub struct BundledSkillsConfig {
27    /// Enable bundled skills shipped with VT Code.
28    #[serde(default = "default_bundled_skills_enabled")]
29    pub enabled: bool,
30}
31
32impl Default for BundledSkillsConfig {
33    fn default() -> Self {
34        Self {
35            enabled: default_bundled_skills_enabled(),
36        }
37    }
38}
39
40/// Skills system configuration
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43#[serde(rename_all = "kebab-case")]
44pub struct SkillsConfig {
45    /// Bundled skills configuration
46    #[serde(default)]
47    pub bundled: BundledSkillsConfig,
48
49    /// Rendering mode for skills in system prompt
50    /// - "lean": Codex-style minimal (name + description + path only, 40-60% token savings)
51    /// - "full": Full metadata with version, author, native flags
52    #[serde(default = "default_render_mode")]
53    pub render_mode: SkillsRenderMode,
54
55    /// Prompt format for skills section (Agent Skills spec)
56    /// - "xml": XML wrapping for safety (Claude models default)
57    /// - "markdown": Plain markdown sections
58    #[serde(default = "default_prompt_format")]
59    pub prompt_format: PromptFormat,
60
61    /// Maximum number of skills to show in system prompt
62    #[serde(default = "default_max_skills_in_prompt")]
63    pub max_skills_in_prompt: usize,
64
65    /// Enable auto-trigger on $skill-name mentions
66    #[serde(default = "default_enable_auto_trigger")]
67    pub enable_auto_trigger: bool,
68
69    /// Enable description-based keyword matching for auto-trigger
70    #[serde(default = "default_enable_description_matching")]
71    pub enable_description_matching: bool,
72
73    /// Minimum keyword matches required for description-based trigger
74    #[serde(default = "default_min_keyword_matches")]
75    pub min_keyword_matches: usize,
76}
77
78impl Default for SkillsConfig {
79    fn default() -> Self {
80        Self {
81            bundled: BundledSkillsConfig::default(),
82            render_mode: default_render_mode(),
83            prompt_format: default_prompt_format(),
84            max_skills_in_prompt: default_max_skills_in_prompt(),
85            enable_auto_trigger: default_enable_auto_trigger(),
86            enable_description_matching: default_enable_description_matching(),
87            min_keyword_matches: default_min_keyword_matches(),
88        }
89    }
90}
91
92/// Skills rendering mode
93#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
94#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
95#[serde(rename_all = "lowercase")]
96pub enum SkillsRenderMode {
97    /// Lean mode (Codex-style): name + description + path only
98    #[default]
99    Lean,
100    /// Full mode: all metadata including version, author, native flags
101    Full,
102}
103
104/// Prompt format for skills section (Agent Skills spec)
105#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
106#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
107#[serde(rename_all = "lowercase")]
108pub enum PromptFormat {
109    /// XML wrapping for safety (Claude models default, per Agent Skills spec)
110    #[default]
111    Xml,
112    /// Plain markdown sections
113    Markdown,
114}
115
116fn default_render_mode() -> SkillsRenderMode {
117    SkillsRenderMode::Lean
118}
119
120fn default_bundled_skills_enabled() -> bool {
121    true
122}
123
124fn default_prompt_format() -> PromptFormat {
125    PromptFormat::Xml
126}
127
128fn default_max_skills_in_prompt() -> usize {
129    10
130}
131
132fn default_enable_auto_trigger() -> bool {
133    true
134}
135
136fn default_enable_description_matching() -> bool {
137    true
138}
139
140fn default_min_keyword_matches() -> usize {
141    2
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_default_skills_config() {
150        let config = SkillsConfig::default();
151        assert!(config.bundled.enabled);
152        assert_eq!(config.render_mode, SkillsRenderMode::Lean);
153        assert_eq!(config.prompt_format, PromptFormat::Xml);
154        assert_eq!(config.max_skills_in_prompt, 10);
155        assert!(config.enable_auto_trigger);
156        assert!(config.enable_description_matching);
157        assert_eq!(config.min_keyword_matches, 2);
158    }
159
160    #[test]
161    fn test_skills_render_mode_serde() {
162        // Test serialization
163        let lean = SkillsRenderMode::Lean;
164        let lean_json = serde_json::to_string(&lean).unwrap();
165        assert_eq!(lean_json, r#""lean""#);
166
167        let full = SkillsRenderMode::Full;
168        let full_json = serde_json::to_string(&full).unwrap();
169        assert_eq!(full_json, r#""full""#);
170
171        // Test deserialization
172        let lean_de: SkillsRenderMode = serde_json::from_str(r#""lean""#).unwrap();
173        assert_eq!(lean_de, SkillsRenderMode::Lean);
174
175        let full_de: SkillsRenderMode = serde_json::from_str(r#""full""#).unwrap();
176        assert_eq!(full_de, SkillsRenderMode::Full);
177    }
178
179    #[test]
180    fn test_prompt_format_serde() {
181        // Test serialization
182        let xml = PromptFormat::Xml;
183        let xml_json = serde_json::to_string(&xml).unwrap();
184        assert_eq!(xml_json, r#""xml""#);
185
186        let markdown = PromptFormat::Markdown;
187        let markdown_json = serde_json::to_string(&markdown).unwrap();
188        assert_eq!(markdown_json, r#""markdown""#);
189
190        // Test deserialization
191        let xml_de: PromptFormat = serde_json::from_str(r#""xml""#).unwrap();
192        assert_eq!(xml_de, PromptFormat::Xml);
193
194        let markdown_de: PromptFormat = serde_json::from_str(r#""markdown""#).unwrap();
195        assert_eq!(markdown_de, PromptFormat::Markdown);
196    }
197
198    #[test]
199    fn test_skills_config_serde() {
200        let config = SkillsConfig {
201            bundled: BundledSkillsConfig { enabled: false },
202            render_mode: SkillsRenderMode::Full,
203            prompt_format: PromptFormat::Markdown,
204            max_skills_in_prompt: 15,
205            enable_auto_trigger: false,
206            enable_description_matching: false,
207            min_keyword_matches: 3,
208        };
209
210        let json = serde_json::to_string_pretty(&config).unwrap();
211        let deserialized: SkillsConfig = serde_json::from_str(&json).unwrap();
212        assert_eq!(config, deserialized);
213    }
214
215    #[test]
216    fn test_skills_config_toml_parses_bundled_settings() {
217        let config: SkillsConfig = toml::from_str(
218            r#"
219            render-mode = "full"
220            prompt-format = "markdown"
221            max-skills-in-prompt = 15
222            enable-auto-trigger = false
223            enable-description-matching = false
224            min-keyword-matches = 3
225
226            [bundled]
227            enabled = false
228            "#,
229        )
230        .unwrap();
231
232        assert!(!config.bundled.enabled);
233        assert_eq!(config.render_mode, SkillsRenderMode::Full);
234        assert_eq!(config.prompt_format, PromptFormat::Markdown);
235        assert_eq!(config.max_skills_in_prompt, 15);
236        assert!(!config.enable_auto_trigger);
237        assert!(!config.enable_description_matching);
238        assert_eq!(config.min_keyword_matches, 3);
239    }
240}