luft_core/contract/skill.rs
1//! Skill contract (§1.5) — reusable agent-facing guidance, shared at the
2//! library level.
3//!
4//! A [`Skill`] bundles markdown content (and optional reference files) that a
5//! *consuming* crate embeds via `include_str!` and hands to whatever agent it
6//! drives. `luft-core` only defines the shape; it owns no content itself —
7//! see `luft_skills::WORKFLOW_SKILL` for the first instance. Consumers with
8//! their own skill format (e.g. a `BuiltinSkill` with triggers and tool
9//! requirements) build their richer type from these fields rather than
10//! parsing/duplicating the markdown.
11
12/// A named piece of agent-facing guidance, plus any files it references.
13///
14/// All fields are `&'static str` because every known instance is compiled in
15/// via `include_str!` — there is no owned-`String` constructor because a
16/// dynamically-built `Skill` has no crate that would outlive the borrow.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Skill {
19 /// Short identifier, e.g. `"workflow"`.
20 pub name: &'static str,
21 /// One-line summary of when this skill applies.
22 pub description: &'static str,
23 /// The skill body (markdown). No required frontmatter — consumers that
24 /// need YAML frontmatter (name/triggers/tags) wrap this content rather
25 /// than expecting it embedded here.
26 pub content: &'static str,
27 /// Bundled reference files as `(relative_path, content)` pairs, e.g.
28 /// `("references/examples.md", "...")`. Empty for skills with no
29 /// supporting files.
30 pub references: &'static [(&'static str, &'static str)],
31}
32
33#[cfg(test)]
34mod tests {
35 use super::*;
36
37 const SAMPLE: Skill = Skill {
38 name: "sample",
39 description: "a sample skill",
40 content: "# Sample\n\nBody.",
41 references: &[("references/extra.md", "extra content")],
42 };
43
44 #[test]
45 fn fields_are_reachable() {
46 assert_eq!(SAMPLE.name, "sample");
47 assert_eq!(SAMPLE.description, "a sample skill");
48 assert!(SAMPLE.content.contains("Body"));
49 assert_eq!(SAMPLE.references.len(), 1);
50 assert_eq!(SAMPLE.references[0].0, "references/extra.md");
51 }
52
53 #[test]
54 fn empty_references_is_valid() {
55 const NO_REFS: Skill = Skill {
56 name: "bare",
57 description: "no references",
58 content: "content",
59 references: &[],
60 };
61 assert!(NO_REFS.references.is_empty());
62 }
63
64 #[test]
65 fn skill_is_copy_and_comparable() {
66 let copied = SAMPLE;
67 assert_eq!(SAMPLE, copied);
68 }
69}