//! Skill entity โ€” reusable, model-agnostic workflows assignable to agents. //! //! A [`Skill`] is IdeA's universal equivalent of a CLI's slash-command, but //! without any dependency on a particular model's `/command` syntax //! (ARCHITECTURE ยง14.2). Assigned skills are injected as plain text into the //! agent's generated convention file at activation โ€” there is no proprietary //! CLI mechanism involved. use serde::{Deserialize, Serialize}; use crate::error::DomainError; use crate::ids::SkillId; use crate::markdown::MarkdownDoc; /// Where a skill lives, which also selects the store used to resolve it. /// /// - [`SkillScope::Global`] skills are stored in the global IDE store /// (`/IdeA/skills/`) and reusable across projects. /// - [`SkillScope::Project`] skills are stored under `.ideai/skills/` and are /// specific to one project. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum SkillScope { /// Reusable across projects (global IDE store). Global, /// Specific to a single project (`.ideai/skills/`). Project, } /// A reusable workflow assignable to one or more agents. /// /// Invariants enforced here: /// - `name` non-empty, /// - `content_md` non-empty (an empty skill carries no behaviour). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Skill { /// Stable identifier. pub id: SkillId, /// Display name (also used as the `.md` file stem on disk). pub name: String, /// Markdown body โ€” the workflow injected into an agent's convention file. pub content_md: MarkdownDoc, /// Scope (selects the backing store). pub scope: SkillScope, } impl Skill { /// Builds a validated skill. /// /// # Errors /// - [`DomainError::EmptyField`] if `name` is empty, /// - [`DomainError::EmptyField`] if `content_md` is empty. pub fn new( id: SkillId, name: impl Into, content_md: MarkdownDoc, scope: SkillScope, ) -> Result { let name = name.into(); crate::validation::non_empty(&name, "skill.name")?; if content_md.is_empty() { return Err(DomainError::EmptyField { field: "skill.content_md", }); } Ok(Self { id, name, content_md, scope, }) } /// Returns a copy of this skill with replaced content, re-validating the /// non-empty invariant. /// /// # Errors /// [`DomainError::EmptyField`] if `content_md` is empty. pub fn with_content(&self, content_md: MarkdownDoc) -> Result { Skill::new(self.id, self.name.clone(), content_md, self.scope) } } /// A reference from an agent to one assigned skill. /// /// Stored in the [`crate::agent::ManifestEntry`]: an agent carries 0..N of these. /// The `scope` is kept alongside the id so the application layer knows which /// store to resolve the skill from without a global lookup. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SkillRef { /// The assigned skill. pub skill_id: SkillId, /// Scope of the assigned skill (selects its store). pub scope: SkillScope, } impl SkillRef { /// Builds a reference to an assigned skill. #[must_use] pub const fn new(skill_id: SkillId, scope: SkillScope) -> Self { Self { skill_id, scope } } } impl From<&Skill> for SkillRef { fn from(skill: &Skill) -> Self { Self::new(skill.id, skill.scope) } }