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