Skip to main content

adk_skill/
model.rs

1use adk_core::Tool;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use std::sync::Arc;
5
6/// Frontmatter metadata for a skill, following the `agentskills.io` specification.
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8#[serde(default)]
9pub struct SkillFrontmatter {
10    /// A required short identifier (1-64 chars) containing only lowercase letters, numbers, and hyphens.
11    pub name: String,
12    /// A required concise description of what the skill does and when an agent should use it.
13    pub description: String,
14    /// An optional version identifier for the skill (e.g., "1.0.0").
15    pub version: Option<String>,
16    /// An optional license identifier or reference to a bundled license file.
17    pub license: Option<String>,
18    /// Optional environment requirements (e.g., "Requires system packages, network access").
19    pub compatibility: Option<String>,
20    /// A collection of categorizing labels for discovery and filtering.
21    #[serde(default)]
22    pub tags: Vec<String>,
23    /// Experimental: A list of space-delimited pre-approved tools the skill may use.
24    #[serde(default, rename = "allowed-tools")]
25    pub allowed_tools: Vec<String>,
26    /// Optional list of paths to supporting resources (e.g., "references/data.json").
27    #[serde(default)]
28    pub references: Vec<String>,
29    /// If true, the skill is only included when explicitly invoked by name.
30    pub trigger: Option<bool>,
31    /// Optional hint text displayed for slash command guided input.
32    pub hint: Option<String>,
33    /// Arbitrary key-value mapping for custom extension metadata.
34    #[serde(default)]
35    pub metadata: std::collections::HashMap<String, serde_json::Value>,
36    /// File glob patterns that determine when this skill activates (e.g., `["*.rs", "Cargo.toml"]`).
37    #[serde(default)]
38    pub triggers: Vec<String>,
39}
40
41/// A parsed skill before it is assigned an ID and indexed.
42#[derive(Debug, Clone)]
43pub struct ParsedSkill {
44    /// The unique identifier from the frontmatter or filename.
45    pub name: String,
46    /// Description of the skill's purpose.
47    pub description: String,
48    /// Optional versioning string.
49    pub version: Option<String>,
50    /// Optional license information.
51    pub license: Option<String>,
52    /// Optional compatibility requirements.
53    pub compatibility: Option<String>,
54    /// Discovery tags.
55    pub tags: Vec<String>,
56    /// Pre-approved tool names.
57    pub allowed_tools: Vec<String>,
58    /// Supporting resource paths.
59    pub references: Vec<String>,
60    /// Whether the skill requires explicit invocation.
61    pub trigger: bool,
62    /// Guided input hint.
63    pub hint: Option<String>,
64    /// Extension metadata.
65    pub metadata: std::collections::HashMap<String, serde_json::Value>,
66    /// File glob patterns for activation triggers.
67    pub triggers: Vec<String>,
68    /// The raw Markdown body content (instructions).
69    pub body: String,
70}
71
72/// A fully indexed skill document with a content-based unique ID.
73#[derive(Debug, Clone, Serialize)]
74pub struct SkillDocument {
75    /// A unique ID derived from the name and content hash.
76    pub id: String,
77    /// The canonical name of the skill.
78    pub name: String,
79    /// Description used for agent discovery.
80    pub description: String,
81    /// Semantic version.
82    pub version: Option<String>,
83    /// License tag.
84    pub license: Option<String>,
85    /// Environment constraints.
86    pub compatibility: Option<String>,
87    /// List of discovery tags.
88    pub tags: Vec<String>,
89    /// Tools allowed for this skill.
90    pub allowed_tools: Vec<String>,
91    /// External resources required by the skill.
92    pub references: Vec<String>,
93    /// If true, requires explicit `@name` invocation.
94    pub trigger: bool,
95    /// Input guidance for users.
96    pub hint: Option<String>,
97    /// Custom extension metadata.
98    pub metadata: std::collections::HashMap<String, serde_json::Value>,
99    /// The instructional Markdown body.
100    pub body: String,
101    /// File system path where the skill was loaded from.
102    pub path: PathBuf,
103    /// SHA256 hash of the content.
104    pub hash: String,
105    /// Optional Unix timestamp of last file modification.
106    pub last_modified: Option<i64>,
107    /// File glob patterns for activation triggers.
108    pub triggers: Vec<String>,
109}
110
111impl SkillDocument {
112    /// Engineers a full system instruction from the skill body, with truncation
113    /// and optionally including tool capability hints.
114    pub fn engineer_instruction(&self, max_chars: usize, active_tools: &[Arc<dyn Tool>]) -> String {
115        let mut body = self.body.clone();
116        if body.chars().count() > max_chars {
117            body = body.chars().take(max_chars).collect();
118            body.push_str("\n[... truncated]");
119        }
120
121        let mut parts = Vec::new();
122        parts.push(format!("[skill:{}]", self.name));
123        parts.push(format!("# {}\n{}", self.name, self.description));
124
125        // Tool capability hint (so the LLM knows what it can do)
126        if !active_tools.is_empty() {
127            let names: Vec<_> = active_tools.iter().map(|t: &Arc<dyn Tool>| t.name()).collect();
128            parts.push(format!("You have access to the following tools: {}.", names.join(", ")));
129        }
130
131        parts.push(format!("## Instructions\n{}", body));
132        parts.push("[/skill]".to_string());
133
134        parts.join("\n\n")
135    }
136
137    /// Engineers a lightweight prompt block for Tier 1 injection.
138    pub fn engineer_prompt_block(&self, max_chars: usize) -> String {
139        let mut body = self.body.clone();
140        if body.chars().count() > max_chars {
141            body = body.chars().take(max_chars).collect();
142        }
143        format!("[skill:{}]\n{}\n[/skill]", self.name, body)
144    }
145}
146
147/// A lightweight summary of a skill, excluding the heavy body content.
148#[derive(Debug, Clone, Serialize)]
149pub struct SkillSummary {
150    /// Content-based unique ID.
151    pub id: String,
152    /// Skill name.
153    pub name: String,
154    /// Discovery description.
155    pub description: String,
156    /// Optional version.
157    pub version: Option<String>,
158    /// Optional license.
159    pub license: Option<String>,
160    /// Optional compatibility.
161    pub compatibility: Option<String>,
162    /// Discovery tags.
163    pub tags: Vec<String>,
164    /// Allowed tools.
165    pub allowed_tools: Vec<String>,
166    /// External references.
167    pub references: Vec<String>,
168    /// Trigger status.
169    pub trigger: bool,
170    /// Guided hint.
171    pub hint: Option<String>,
172    /// Extension metadata.
173    pub metadata: std::collections::HashMap<String, serde_json::Value>,
174    /// Associated file path.
175    pub path: PathBuf,
176    /// Content signature.
177    pub hash: String,
178    /// Last modified timestamp.
179    pub last_modified: Option<i64>,
180    /// File glob patterns for activation triggers.
181    pub triggers: Vec<String>,
182}
183
184impl From<&SkillDocument> for SkillSummary {
185    fn from(value: &SkillDocument) -> Self {
186        Self {
187            id: value.id.clone(),
188            name: value.name.clone(),
189            description: value.description.clone(),
190            version: value.version.clone(),
191            license: value.license.clone(),
192            compatibility: value.compatibility.clone(),
193            tags: value.tags.clone(),
194            allowed_tools: value.allowed_tools.clone(),
195            references: value.references.clone(),
196            trigger: value.trigger,
197            hint: value.hint.clone(),
198            metadata: value.metadata.clone(),
199            path: value.path.clone(),
200            hash: value.hash.clone(),
201            last_modified: value.last_modified,
202            triggers: value.triggers.clone(),
203        }
204    }
205}
206
207/// A collection of indexed skills, providing efficient access to metadata and summaries.
208#[derive(Debug, Clone, Default)]
209pub struct SkillIndex {
210    skills: Vec<SkillDocument>,
211}
212
213impl SkillIndex {
214    pub fn new(skills: Vec<SkillDocument>) -> Self {
215        Self { skills }
216    }
217
218    pub fn is_empty(&self) -> bool {
219        self.skills.is_empty()
220    }
221
222    pub fn len(&self) -> usize {
223        self.skills.len()
224    }
225
226    /// Returns the raw list of fully indexed skill documents.
227    pub fn skills(&self) -> &[SkillDocument] {
228        &self.skills
229    }
230
231    /// Returns a list of lightweight skill summaries, suitable for passing to agents or UI components.
232    pub fn summaries(&self) -> Vec<SkillSummary> {
233        self.skills.iter().map(SkillSummary::from).collect()
234    }
235
236    /// Find a skill by its canonical name.
237    pub fn find_by_name(&self, name: &str) -> Option<&SkillDocument> {
238        self.skills.iter().find(|s| s.name == name)
239    }
240
241    /// Find a skill by its unique ID (name + hash).
242    pub fn find_by_id(&self, id: &str) -> Option<&SkillDocument> {
243        self.skills.iter().find(|s| s.id == id)
244    }
245}
246
247/// Criteria used to filter and score skills during selection.
248#[derive(Debug, Clone)]
249pub struct SelectionPolicy {
250    /// Number of top-scoring matches to return.
251    pub top_k: usize,
252    /// Minimum score threshold for a skill to be included.
253    pub min_score: f32,
254    /// Optional list of tags that MUST be present on the skill.
255    pub include_tags: Vec<String>,
256    /// Optional list of tags that MUST NOT be present on the skill.
257    pub exclude_tags: Vec<String>,
258}
259
260impl Default for SelectionPolicy {
261    fn default() -> Self {
262        Self { top_k: 1, min_score: 1.0, include_tags: Vec::new(), exclude_tags: Vec::new() }
263    }
264}
265
266/// A ranked result representing a skill that matched a selection query.
267#[derive(Debug, Clone, Serialize)]
268pub struct SkillMatch {
269    /// Numerical relevance score calculated using weighted lexical overlap.
270    ///
271    /// The algorithm weights matches as follows:
272    /// - **Name Match**: +4.0
273    /// - **Description Match**: +2.5
274    /// - **Tag Match**: +2.0
275    /// - **Instruction Body Match**: +1.0
276    ///
277    /// The final score is normalized by `sqrt(unique_token_count)` of the body to
278    /// ensure long-form instructions do not unfairly drown out concise skills.
279    pub score: f32,
280    /// The lightweight summary of the matched skill.
281    pub skill: SkillSummary,
282}