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