use super::{Skill, SkillExecution};
use anda_core::BoxError;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, BTreeSet},
path::PathBuf,
sync::Arc,
};
#[derive(Debug, Clone)]
pub struct SkillLimits {
pub max_depth: usize,
pub max_directories: usize,
pub max_entries: usize,
pub max_skills: usize,
pub max_total_bytes: usize,
pub catalog_bytes: usize,
pub response_bytes: usize,
}
impl Default for SkillLimits {
fn default() -> Self {
Self {
max_depth: 6,
max_directories: 2_000,
max_entries: 20_000,
max_skills: 1_024,
max_total_bytes: 32 * 1024 * 1024,
catalog_bytes: 8_000,
response_bytes: 32 * 1024,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillDiagnostic {
pub kind: String,
#[serde(serialize_with = "serialize_path")]
pub path: PathBuf,
pub message: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillLoadReport {
pub generation: u64,
pub loaded: usize,
pub rejected: usize,
pub truncated: bool,
pub diagnostics: Vec<SkillDiagnostic>,
}
impl SkillLoadReport {
pub(super) fn note(&mut self, kind: &str, path: PathBuf, message: impl ToString) {
if self.diagnostics.len() == 128 {
self.truncated = true;
return;
}
self.diagnostics.push(SkillDiagnostic {
kind: kind.into(),
path,
message: truncate(&message.to_string(), 1_024).into(),
});
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillMetadata {
#[serde(default)]
pub interface: SkillInterface,
#[serde(default)]
pub policy: SkillPolicy,
#[serde(default)]
pub dependencies: SkillDependencies,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillInterface {
pub display_name: Option<String>,
pub short_description: Option<String>,
pub default_prompt: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillPolicy {
#[serde(default = "default_true")]
pub allow_implicit_invocation: bool,
}
fn default_true() -> bool {
true
}
impl Default for SkillPolicy {
fn default() -> Self {
Self {
allow_implicit_invocation: true,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillDependencies {
#[serde(default)]
pub tools: Vec<SkillDependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillDependency {
#[serde(rename = "type")]
pub kind: String,
pub value: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillPreflight {
pub skill_id: String,
pub ready: bool,
pub missing: Vec<SkillDependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillSummary {
pub id: String,
pub source_id: String,
pub name: String,
pub description: String,
pub execution: SkillExecution,
pub callable: Option<String>,
#[serde(serialize_with = "serialize_path")]
pub base_dir: PathBuf,
pub active: bool,
pub metadata: SkillMetadata,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SkillCatalogSnapshot {
pub report: SkillLoadReport,
pub skills: Vec<SkillSummary>,
}
#[derive(Clone)]
pub(super) struct Entry {
pub id: String,
pub source_id: String,
pub rank: usize,
pub root: PathBuf,
pub canonical_root: PathBuf,
pub skill: Arc<Skill>,
pub metadata: SkillMetadata,
pub fingerprint: String,
}
#[derive(Default)]
pub(super) struct Catalog {
pub entries: BTreeMap<String, Entry>,
pub winners: BTreeMap<String, String>,
pub report: SkillLoadReport,
pub fingerprint: String,
}
impl Catalog {
pub fn summary(&self, entry: &Entry) -> SkillSummary {
let skill = &entry.skill;
let active = self.winners.get(&skill.agent_name) == Some(&entry.id);
SkillSummary {
id: entry.id.clone(),
source_id: entry.source_id.clone(),
name: skill.frontmatter.name.clone(),
description: skill.frontmatter.description.clone(),
execution: skill.execution,
callable: (active && skill.is_subagent())
.then(|| format!("{}{}", crate::context::SUB_AGENT_PREFIX, skill.agent_name)),
base_dir: skill.base_dir.clone(),
active,
metadata: entry.metadata.clone(),
}
}
pub fn snapshot(&self) -> SkillCatalogSnapshot {
let mut entries = self.entries.values().collect::<Vec<_>>();
entries.sort_by_key(|entry| (entry.rank, &entry.skill.base_dir));
SkillCatalogSnapshot {
report: self.report.clone(),
skills: entries
.into_iter()
.map(|entry| self.summary(entry))
.collect(),
}
}
pub fn resolve(&self, name: &str) -> Result<Option<&Entry>, BoxError> {
if let Some(entry) = self.entries.get(name) {
return Ok(Some(entry));
}
let agent = super::normalise_skill_agent_name(name);
if let Some(id) = self.winners.get(&agent) {
return Ok(self.entries.get(id));
}
if self
.entries
.values()
.any(|entry| entry.skill.frontmatter.name == name)
{
return Err(
format!("multiple skills named {name:?}; use skills_list and a skill ID").into(),
);
}
Ok(None)
}
}
impl SkillSummary {
pub fn preflight(
&self,
tools: &BTreeSet<String>,
providers: &BTreeSet<String>,
) -> SkillPreflight {
let missing = self
.metadata
.dependencies
.tools
.iter()
.filter(|dependency| !match dependency.kind.as_str() {
"tool" => tools.contains(&dependency.value),
"mcp" => providers.contains(&dependency.value),
_ => false,
})
.cloned()
.collect::<Vec<_>>();
SkillPreflight {
skill_id: self.id.clone(),
ready: missing.is_empty(),
missing,
}
}
}
pub(super) fn digest(bytes: &[u8]) -> String {
hex::encode(Sha256::digest(bytes))
}
pub(super) fn truncate(text: &str, bytes: usize) -> &str {
let mut end = bytes.min(text.len());
while !text.is_char_boundary(end) {
end -= 1;
}
&text[..end]
}
fn serialize_path<S: serde::Serializer>(
path: &std::path::Path,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&path.to_string_lossy())
}