Skip to main content

hh_cli/agent/
config.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "lowercase")]
6pub enum AgentMode {
7    Primary,
8    Subagent,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AgentConfig {
13    pub name: String,
14    pub display_name: String,
15    pub description: String,
16    #[serde(default = "default_mode")]
17    pub mode: AgentMode,
18    #[serde(default)]
19    pub permission_overrides: BTreeMap<String, String>,
20    #[serde(default)]
21    pub model: Option<String>,
22    #[serde(default)]
23    pub color: Option<String>,
24    /// System prompt - can be set in frontmatter or from Markdown body
25    #[serde(default)]
26    pub system_prompt: Option<String>,
27}
28
29impl AgentConfig {
30    pub fn builtin_build() -> Self {
31        Self {
32            name: "build".to_string(),
33            display_name: "Build".to_string(),
34            description: "Build agent with standard permissions".to_string(),
35            mode: AgentMode::Primary,
36            system_prompt: Some(crate::core::system_prompt::build_system_prompt()),
37            permission_overrides: BTreeMap::new(),
38            model: None,
39            color: Some("blue".to_string()),
40        }
41    }
42
43    pub fn builtin_plan() -> Self {
44        let mut overrides = BTreeMap::new();
45        overrides.insert("write".to_string(), "deny".to_string());
46        overrides.insert("edit".to_string(), "deny".to_string());
47
48        Self {
49            name: "plan".to_string(),
50            display_name: "Plan".to_string(),
51            description: "Planning agent that analyzes without executing".to_string(),
52            mode: AgentMode::Primary,
53            system_prompt: Some(crate::core::system_prompt::plan_system_prompt()),
54            permission_overrides: overrides,
55            model: None,
56            color: Some("pink".to_string()),
57        }
58    }
59
60    pub fn builtin_explorer() -> Self {
61        let mut overrides = BTreeMap::new();
62        overrides.insert("write".to_string(), "deny".to_string());
63        overrides.insert("edit".to_string(), "deny".to_string());
64        overrides.insert("bash".to_string(), "deny".to_string());
65
66        Self {
67            name: "explorer".to_string(),
68            display_name: "Explorer".to_string(),
69            description:
70                "A fast, read-only agent for exploring codebases and answering code questions"
71                    .to_string(),
72            mode: AgentMode::Subagent,
73            system_prompt: Some(crate::core::system_prompt::explorer_system_prompt()),
74            permission_overrides: overrides,
75            model: None,
76            color: Some("cyan".to_string()),
77        }
78    }
79
80    pub fn builtin_general() -> Self {
81        let mut overrides = BTreeMap::new();
82        overrides.insert("read".to_string(), "allow".to_string());
83        overrides.insert("list".to_string(), "allow".to_string());
84        overrides.insert("glob".to_string(), "allow".to_string());
85        overrides.insert("grep".to_string(), "allow".to_string());
86        overrides.insert("write".to_string(), "allow".to_string());
87        overrides.insert("edit".to_string(), "allow".to_string());
88        overrides.insert("question".to_string(), "allow".to_string());
89        overrides.insert("bash".to_string(), "allow".to_string());
90        overrides.insert("web".to_string(), "allow".to_string());
91        overrides.insert("task".to_string(), "allow".to_string());
92        overrides.insert("todo_write".to_string(), "deny".to_string());
93        overrides.insert("todo_read".to_string(), "deny".to_string());
94
95        Self {
96            name: "general".to_string(),
97            display_name: "General".to_string(),
98            description:
99                "A general-purpose subagent for complex research and multi-step parallel tasks"
100                    .to_string(),
101            mode: AgentMode::Subagent,
102            system_prompt: Some(crate::core::system_prompt::general_system_prompt()),
103            permission_overrides: overrides,
104            model: None,
105            color: Some("green".to_string()),
106        }
107    }
108}
109
110fn default_mode() -> AgentMode {
111    AgentMode::Subagent
112}
113
114// Frontmatter structure for parsing agent Markdown files
115#[derive(Debug, Deserialize)]
116pub struct AgentFrontmatter {
117    pub display_name: String,
118    pub description: String,
119    #[serde(default = "default_mode")]
120    pub mode: AgentMode,
121    #[serde(default)]
122    pub model: Option<String>,
123    #[serde(default)]
124    pub color: Option<String>,
125    #[serde(default)]
126    pub tools: Option<BTreeMap<String, String>>,
127    #[serde(default)]
128    pub system_prompt: Option<String>,
129}
130
131impl AgentFrontmatter {
132    pub fn to_agent_config(&self, name: String, body: Option<String>) -> AgentConfig {
133        let mut permission_overrides = BTreeMap::new();
134        if let Some(tools) = &self.tools {
135            for (tool, policy) in tools {
136                permission_overrides.insert(tool.clone(), policy.clone());
137            }
138        }
139
140        // Use body as system prompt if provided, otherwise use frontmatter field
141        let system_prompt = if let Some(body) = body {
142            if body.trim().is_empty() {
143                self.system_prompt.clone()
144            } else {
145                Some(body)
146            }
147        } else {
148            self.system_prompt.clone()
149        };
150
151        AgentConfig {
152            name,
153            display_name: self.display_name.clone(),
154            description: self.description.clone(),
155            mode: self.mode,
156            permission_overrides,
157            model: self.model.clone(),
158            color: self.color.clone(),
159            system_prompt,
160        }
161    }
162}