haki-agents 0.1.0

Agent definitions, spawner, and concurrent subagent dispatch for haki
Documentation
//! Agent definitions — roles, system prompts, and tool filters.

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AgentRole {
    /// General-purpose: all tools, full execution.
    General,
    /// Planner: no exec tools, only read + reason.
    Planner,
    /// Explorer: read-only file and search tools.
    Explorer,
    /// Builder: full tool access for implementation work.
    Builder,
    /// Reviewer: read + diff tools, no write.
    Reviewer,
}

impl AgentRole {
    pub fn system_prompt(&self) -> &str {
        match self {
            Self::General => "You are a helpful coding assistant with access to all tools.",
            Self::Planner => "You are a planning agent. Reason carefully and produce structured plans. Do not execute code.",
            Self::Explorer => "You are an exploration agent. Read files and search the codebase to answer questions. Do not write or execute.",
            Self::Builder => "You are a builder agent. Implement the specified changes using the available tools.",
            Self::Reviewer => "You are a code reviewer. Analyze diffs and files to provide actionable feedback. Do not write files.",
        }
    }

    /// Tool names this role is allowed to use. Empty means all tools.
    pub fn allowed_tools(&self) -> Vec<&'static str> {
        match self {
            Self::General | Self::Builder => vec![],
            Self::Planner => vec!["read_file", "glob", "grep"],
            Self::Explorer => vec!["read_file", "glob", "grep", "fetch_url"],
            Self::Reviewer => vec!["read_file", "glob", "grep"],
        }
    }
}

#[derive(Debug, Clone)]
pub struct AgentDef {
    pub id: String,
    pub name: String,
    pub role: AgentRole,
    pub model_override: Option<String>,
}

impl AgentDef {
    pub fn new(name: impl Into<String>, role: AgentRole) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            name: name.into(),
            role,
            model_override: None,
        }
    }

    pub fn with_model(mut self, model: impl Into<String>) -> Self {
        self.model_override = Some(model.into());
        self
    }

    /// Built-in agent definitions.
    pub fn builtins() -> Vec<AgentDef> {
        vec![
            AgentDef::new("planner", AgentRole::Planner),
            AgentDef::new("explorer", AgentRole::Explorer),
            AgentDef::new("builder", AgentRole::Builder),
            AgentDef::new("reviewer", AgentRole::Reviewer),
        ]
    }
}

// ─── Tests ────────────────────────────────────────────────────────────────────

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

    #[test]
    fn planner_has_no_exec_tools() {
        let tools = AgentRole::Planner.allowed_tools();
        assert!(!tools.contains(&"bash"));
        assert!(!tools.contains(&"write_file"));
    }

    #[test]
    fn builder_has_all_tools() {
        let tools = AgentRole::Builder.allowed_tools();
        assert!(tools.is_empty(), "empty means all tools allowed");
    }

    #[test]
    fn builtins_are_four_agents() {
        assert_eq!(AgentDef::builtins().len(), 4);
    }

    #[test]
    fn agent_def_new_has_unique_id() {
        let a = AgentDef::new("test", AgentRole::General);
        let b = AgentDef::new("test", AgentRole::General);
        assert_ne!(a.id, b.id);
    }
}