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