1#![allow(missing_docs)]
13mod merge;
14pub mod registry;
15pub mod skills;
16pub mod variables;
17
18use std::collections::HashMap;
19
20use serde::Deserialize;
21
22use crate::config::AgentConfig;
23use crate::error::Error;
24
25#[allow(missing_docs)]
27#[derive(Debug, Clone, Deserialize)]
28pub struct TemplateMeta {
29 pub description: String,
31 #[serde(default = "default_version")]
32 pub version: String,
33 #[serde(default)]
34 pub tags: Vec<String>,
35 #[serde(default)]
37 pub extends: Option<String>,
38}
39
40fn default_version() -> String {
41 "1.0".into()
42}
43
44#[allow(missing_docs)]
47#[derive(Debug, Clone, Default, Deserialize)]
48pub struct PartialAgentConfig {
49 pub system_prompt: Option<String>,
50 pub max_tokens: Option<u32>,
51 pub max_turns: Option<usize>,
52 pub tool_profile: Option<String>,
53 pub dangerous_tools: Option<bool>,
54 pub max_identical_tool_calls: Option<u32>,
55 pub max_fuzzy_identical_tool_calls: Option<u32>,
56 pub max_tool_calls_per_turn: Option<u32>,
57 pub reasoning_effort: Option<String>,
58 pub enable_reflection: Option<bool>,
59 pub tool_timeout_seconds: Option<u64>,
60 pub max_tool_output_bytes: Option<usize>,
61 pub run_timeout_seconds: Option<u64>,
62 pub tool_output_compression_threshold: Option<usize>,
63 pub max_tools_per_turn: Option<usize>,
64 pub response_cache_size: Option<usize>,
65 pub max_total_tokens: Option<u64>,
66}
67
68#[derive(Debug, Clone, Deserialize)]
70pub struct AgentTemplate {
71 pub meta: TemplateMeta,
72 #[serde(default)]
73 pub agent: PartialAgentConfig,
74}
75
76pub fn resolve_agent_config(
82 config: &AgentConfig,
83 variables: &HashMap<String, String>,
84) -> Result<AgentConfig, Error> {
85 let mut resolved = if let Some(ref template_name) = config.template {
86 let template = merge::resolve_template_chain(template_name)?;
88 merge::apply_template(config, &template)
90 } else {
91 config.clone_config()
92 };
93
94 if !config.skills.is_empty() {
96 let skills_section = skills::load_skills(&config.skills)?;
97 resolved.system_prompt = format!("{}{skills_section}", resolved.system_prompt);
98 }
99
100 let workspace = variables.get("workspace").map(|s| s.as_str());
102 let all_vars =
103 variables::build_variables(&resolved.name, &resolved.description, workspace, variables);
104 resolved.system_prompt = variables::substitute(&resolved.system_prompt, &all_vars);
105
106 Ok(resolved)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn test_config(name: &str) -> AgentConfig {
114 AgentConfig {
115 name: name.into(),
116 description: "test agent".into(),
117 system_prompt: String::new(),
118 mcp_servers: vec![],
119 a2a_agents: vec![],
120 context_strategy: None,
121 summarize_threshold: None,
122 tool_timeout_seconds: None,
123 max_tool_output_bytes: None,
124 max_turns: None,
125 max_tokens: None,
126 response_schema: None,
127 run_timeout_seconds: None,
128 provider: None,
129 reasoning_effort: None,
130 enable_reflection: None,
131 tool_output_compression_threshold: None,
132 max_tools_per_turn: None,
133 tool_profile: None,
134 max_identical_tool_calls: None,
135 max_fuzzy_identical_tool_calls: None,
136 max_tool_calls_per_turn: None,
137 session_prune: None,
138 recursive_summarization: None,
139 reflection_threshold: None,
140 consolidate_on_exit: None,
141 max_total_tokens: None,
142 guardrails: None,
143 response_cache_size: None,
144 mcp_resources: Default::default(),
145 dangerous_tools: false,
146 audit_mode: None,
147 builtin_tools: None,
148 template: None,
149 skills: vec![],
150 }
151 }
152
153 #[test]
154 fn resolve_no_template_passthrough() {
155 let config = test_config("plain");
156 let resolved = resolve_agent_config(&config, &HashMap::new()).unwrap();
157 assert_eq!(resolved.name, "plain");
158 assert!(resolved.system_prompt.is_empty());
159 }
160
161 #[test]
162 fn resolve_with_template() {
163 let mut config = test_config("my-coder");
164 config.template = Some("coder".into());
165 let resolved = resolve_agent_config(&config, &HashMap::new()).unwrap();
166
167 assert!(!resolved.system_prompt.is_empty());
168 assert!(resolved.max_tokens.is_some());
169 assert!(resolved.template.is_none()); }
171
172 #[test]
173 fn resolve_with_skills() {
174 let mut config = test_config("skilled");
175 config.skills = vec!["rust-expert".into()];
176 let resolved = resolve_agent_config(&config, &HashMap::new()).unwrap();
177
178 assert!(resolved.system_prompt.contains("Loaded Skills"));
179 assert!(resolved.system_prompt.contains("rust-expert"));
180 }
181
182 #[test]
183 fn resolve_with_template_and_skills() {
184 let mut config = test_config("full");
185 config.template = Some("coder".into());
186 config.skills = vec!["rust-expert".into()];
187 let resolved = resolve_agent_config(&config, &HashMap::new()).unwrap();
188
189 assert!(resolved.system_prompt.contains("Loaded Skills"));
191 assert!(resolved.system_prompt.len() > 100);
192 }
193
194 #[test]
195 fn resolve_with_variables() {
196 let mut config = test_config("var-test");
197 config.system_prompt = "Hello {agent_name}, project: {project}".into();
198
199 let mut vars = HashMap::new();
200 vars.insert("project".into(), "heartbit".into());
201 let resolved = resolve_agent_config(&config, &vars).unwrap();
202
203 assert_eq!(resolved.system_prompt, "Hello var-test, project: heartbit");
204 }
205
206 #[test]
207 fn resolve_variables_in_template_prompt() {
208 let mut config = test_config("tmpl-var");
209 config.template = Some("coder".into());
210 let resolved = resolve_agent_config(&config, &HashMap::new()).unwrap();
211
212 assert!(!resolved.system_prompt.is_empty());
216 }
217
218 #[test]
219 fn resolve_unknown_template_error() {
220 let mut config = test_config("bad");
221 config.template = Some("nonexistent-template".into());
222 let err = resolve_agent_config(&config, &HashMap::new()).unwrap_err();
223 assert!(err.to_string().contains("unknown template"));
224 }
225
226 #[test]
227 fn resolve_unknown_skill_error() {
228 let mut config = test_config("bad");
229 config.skills = vec!["nonexistent-skill".into()];
230 let err = resolve_agent_config(&config, &HashMap::new()).unwrap_err();
231 assert!(err.to_string().contains("unknown skill"));
232 }
233
234 #[test]
235 fn resolve_user_override_wins() {
236 let mut config = test_config("override");
237 config.template = Some("coder".into());
238 config.max_turns = Some(5);
239 let resolved = resolve_agent_config(&config, &HashMap::new()).unwrap();
240
241 assert_eq!(resolved.max_turns, Some(5));
242 }
243
244 #[test]
245 fn backward_compat_no_template_no_skills() {
246 let mut config = test_config("legacy");
247 config.system_prompt = "I am a legacy agent.".into();
248 config.max_tokens = Some(2048);
249 let resolved = resolve_agent_config(&config, &HashMap::new()).unwrap();
250
251 assert_eq!(resolved.system_prompt, "I am a legacy agent.");
252 assert_eq!(resolved.max_tokens, Some(2048));
253 }
254}