//! Skill perception system for the AI agent. //! //! Skills are injected through three modes: //! - Active: explicit user invocation, highest priority. //! - Passive: tool-call or event driven activation. //! - Auto: ambient keyword relevance. //! //! Vector search is merged by the message builder as a semantic auto signal. pub mod active; pub mod auto; pub mod passive; pub mod vector; pub use active::ActiveSkillAwareness; pub use auto::AutoSkillAwareness; pub use passive::PassiveSkillAwareness; pub use vector::{VectorActiveAwareness, VectorPassiveAwareness}; use crate::client::ChatRequestMessage; use std::collections::HashSet; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum SkillActivation { Active, Passive, Vector, Auto, } impl SkillActivation { fn label(self) -> &'static str { match self { SkillActivation::Active => "Active", SkillActivation::Passive => "Passive", SkillActivation::Vector => "Vector", SkillActivation::Auto => "Auto", } } pub fn rank(self) -> u8 { match self { SkillActivation::Active => 0, SkillActivation::Passive => 1, SkillActivation::Vector => 2, SkillActivation::Auto => 3, } } } /// A chunk of skill context ready to be injected into the message list. #[derive(Debug, Clone)] pub struct SkillContext { /// Stable skill identifier used for de-duplication across trigger sources. pub slug: String, /// Human-readable label shown to the AI, e.g. "Active skill: code-review" pub label: String, /// The actual skill content to inject. pub content: String, /// How this skill was selected. pub activation: SkillActivation, /// Optional relevance score. Active/passive matches use `None`. pub score: Option, } /// Converts skill context into a system message for injection. impl SkillContext { pub fn new( skill: &SkillEntry, activation: SkillActivation, reason: Option<&str>, content: String, score: Option, ) -> Self { let label = match reason { Some(reason) => format!("{} skill: {} ({})", activation.label(), skill.name, reason), None => format!("{} skill: {}", activation.label(), skill.name), }; Self { slug: skill.slug.clone(), label, content, activation, score, } } pub fn dedupe_key(&self) -> String { if !self.slug.trim().is_empty() { return normalize_skill_key(&self.slug); } normalize_skill_key(&self.label) } pub fn to_system_message(self) -> ChatRequestMessage { ChatRequestMessage::system(format!("[{}]\n{}", self.label, self.content)) } } pub fn normalize_skill_key(value: &str) -> String { value .trim() .to_lowercase() .replace(['_', ' '], "-") .chars() .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '/') .collect() } /// Unified perception service combining all three modes. #[derive(Debug, Clone)] pub struct PerceptionService { pub auto: AutoSkillAwareness, pub active: ActiveSkillAwareness, pub passive: PassiveSkillAwareness, } impl Default for PerceptionService { fn default() -> Self { Self { auto: AutoSkillAwareness::default(), active: ActiveSkillAwareness::default(), passive: PassiveSkillAwareness::default(), } } } impl PerceptionService { /// Inject relevant skill context into the message list based on current conversation state. /// /// - **auto**: Scans the current input and conversation history for skill-relevant keywords /// and injects matching skills that are enabled. /// - **active**: Checks if the user explicitly invoked a skill by slug (e.g. "用 code-review") /// and injects it. /// - **passive**: Checks if any tool-call events or prior observations mention a skill /// slug and injects the matching skill. /// /// Returns a list of system messages to prepend to the conversation. pub async fn inject_skills( &self, input: &str, history: &[String], tool_calls: &[ToolCallEvent], enabled_skills: &[SkillEntry], ) -> Vec { let mut results = Vec::new(); let mut seen = HashSet::new(); // Active: explicit skill invocation (highest priority) if let Some(skill) = self.active.detect(input, enabled_skills) { seen.insert(skill.dedupe_key()); results.push(skill); } // Passive: triggered by tool-call events for tc in tool_calls { if let Some(skill) = self.passive.detect(tc, enabled_skills) { if seen.insert(skill.dedupe_key()) { results.push(skill); } } } // Auto: keyword-based relevance matching let auto_results = self.auto.detect(input, history, enabled_skills).await; for skill in auto_results { if seen.insert(skill.dedupe_key()) { results.push(skill); } } results } } /// A tool-call event used for passive skill detection. #[derive(Debug, Clone)] pub struct ToolCallEvent { pub tool_name: String, pub arguments: String, } /// A skill entry from the database, used for matching. #[derive(Debug, Clone)] pub struct SkillEntry { pub slug: String, pub name: String, pub description: Option, pub content: String, }