tama 0.0.1

Multi-agent AI framework — build, run, and trace agent pipelines from the command line
Documentation
use anyhow::{Context, Result};
use genai::chat::Tool;
use serde::Deserialize;
use serde_json::json;

use crate::skill::parser::split_frontmatter;

pub fn definition() -> Tool {
    Tool::new("read_skill")
        .with_description(
            "Load full instructions for a skill. Call this first before using any skill.",
        )
        .with_schema(json!({
            "type": "object",
            "properties": {
                "name": { "type": "string", "description": "Skill name" }
            },
            "required": ["name"]
        }))
}

pub async fn execute(args: &serde_json::Value) -> Result<String> {
    let name = args["name"]
        .as_str()
        .context("read_skill: missing 'name'")?;
    let (body, _) = load_skill(name)?;
    Ok(body)
}

#[derive(Deserialize)]
struct SkillFrontmatter {
    #[serde(default)]
    description: String,
    #[serde(default)]
    tools: Vec<String>,
}

pub fn load_skill(name: &str) -> Result<(String, Vec<String>)> {
    let path = format!("skills/{name}/SKILL.md");
    let content = std::fs::read_to_string(&path)
        .with_context(|| format!("skill '{name}' not found at {path}"))?;

    let (yaml, body) =
        split_frontmatter(&content).with_context(|| format!("bad frontmatter in {path}"))?;
    let fm: SkillFrontmatter =
        serde_yaml::from_str(yaml).with_context(|| format!("invalid YAML in {path}"))?;

    Ok((format!("# Skill: {name}\n\n{body}"), fm.tools))
}

pub fn load_skill_description(skill_name: &str) -> String {
    let path = format!("skills/{skill_name}/SKILL.md");
    let Ok(content) = std::fs::read_to_string(&path) else {
        return format!("(no description for {skill_name})");
    };
    let Ok((yaml, _)) = split_frontmatter(&content) else {
        return format!("(no description for {skill_name})");
    };
    let Ok(fm) = serde_yaml::from_str::<SkillFrontmatter>(yaml) else {
        return format!("(no description for {skill_name})");
    };
    if fm.description.is_empty() {
        format!("(no description for {skill_name})")
    } else {
        fm.description
    }
}

pub fn extract_body(md: &str) -> &str {
    let s = md.trim_start();
    if let Some(rest) = s.strip_prefix("---") {
        if let Some(end) = rest.find("\n---") {
            return rest[end + 4..].trim_start_matches('\n');
        }
    }
    md
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn extract_body_after_frontmatter() {
        let md = "---\nname: s\n---\n\nThis is the body.\n";
        assert_eq!(extract_body(md), "This is the body.\n");
    }

    #[test]
    fn extract_body_no_frontmatter_returns_whole() {
        let md = "no frontmatter here";
        assert_eq!(extract_body(md), "no frontmatter here");
    }

    #[test]
    fn extract_body_empty_body() {
        let md = "---\nname: s\n---\n";
        assert_eq!(extract_body(md), "");
    }

    #[test]
    fn extract_body_leading_newlines_stripped() {
        let md = "---\nk: v\n---\n\n\nActual content";
        assert_eq!(extract_body(md), "Actual content");
    }

    #[test]
    fn parse_tools_block_sequence() {
        let md =
            "---\nname: mem-set\ndescription: Store a value.\ntools:\n  - mem_set\n---\n\nBody.\n";
        let (yaml, _) = split_frontmatter(md).unwrap();
        let fm: SkillFrontmatter = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(fm.tools, vec!["mem_set"]);
    }

    #[test]
    fn parse_tools_inline_sequence() {
        let md = "---\nname: mem-get\ndescription: Get a value.\ntools: [mem_get]\n---\n\nBody.\n";
        let (yaml, _) = split_frontmatter(md).unwrap();
        let fm: SkillFrontmatter = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(fm.tools, vec!["mem_get"]);
    }

    #[test]
    fn parse_tools_multiple() {
        let md = "---\nname: mem\ndescription: Memory.\ntools:\n  - mem_set\n  - mem_get\n---\n\nBody.\n";
        let (yaml, _) = split_frontmatter(md).unwrap();
        let fm: SkillFrontmatter = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(fm.tools, vec!["mem_set", "mem_get"]);
    }

    #[test]
    fn parse_tools_absent_returns_empty() {
        let md = "---\nname: no-tools\ndescription: Nothing.\n---\n\nBody.\n";
        let (yaml, _) = split_frontmatter(md).unwrap();
        let fm: SkillFrontmatter = serde_yaml::from_str(yaml).unwrap();
        assert!(fm.tools.is_empty());
    }
}