//! Skill index middleware — injects a lightweight skill index into the system prompt. //! //! Instead of embedding full skill descriptions (which can consume ~2000 tokens for 70+ skills), //! this middleware injects only skill IDs and one-line triggers (~600 tokens). The LLM can then //! call the `skill_load` tool on demand to retrieve full skill details when needed. use async_trait::async_trait; use zclaw_types::Result; use crate::middleware::{AgentMiddleware, MiddlewareContext, MiddlewareDecision}; use crate::tool::{SkillIndexEntry, SkillExecutor}; use std::sync::Arc; /// Middleware that injects a lightweight skill index into the system prompt. /// /// The index format is compact: /// ```text /// ## Skills (index — use skill_load for details) /// - finance-tracker: 财务分析、财报解读 [数据分析] /// - senior-developer: 代码开发、架构设计 [开发工程] /// ``` pub struct SkillIndexMiddleware { /// Pre-built skill index entries, constructed at chain creation time. entries: Vec, } impl SkillIndexMiddleware { pub fn new(entries: Vec) -> Self { Self { entries } } /// Build index entries from a skill executor that supports listing. pub fn from_executor(executor: &Arc) -> Self { Self { entries: executor.list_skill_index(), } } } #[async_trait] impl AgentMiddleware for SkillIndexMiddleware { fn name(&self) -> &str { "skill_index" } fn priority(&self) -> i32 { 200 } async fn before_completion(&self, ctx: &mut MiddlewareContext) -> Result { if self.entries.is_empty() { return Ok(MiddlewareDecision::Continue); } let mut index = String::from("\n\n## Skills (index — call skill_load for details)\n\n"); for entry in &self.entries { let triggers = if entry.triggers.is_empty() { String::new() } else { format!(" — {}", entry.triggers.join(", ")) }; index.push_str(&format!("- **{}**: {}{}\n", entry.id, entry.description, triggers)); } ctx.system_prompt.push_str(&index); Ok(MiddlewareDecision::Continue) } }