1#![allow(dead_code)]
3
4use std::collections::HashSet;
5use std::path::Path;
6use std::sync::Mutex;
7
8use once_cell::sync::Lazy;
9
10use super::frontmatter_parser::parse_frontmatter;
11use super::loader::load_all_plugins_cache_only;
12use super::plugin_options_storage::{
13 load_plugin_options, substitute_plugin_variables, substitute_user_config_in_content,
14};
15use super::walk_plugin_markdown::{WalkPluginMarkdownOpts, walk_plugin_markdown};
16use crate::plugin::types::PluginManifest;
17
18static PLUGIN_AGENT_CACHE: Lazy<Mutex<Option<Vec<AgentDefinition>>>> =
19 Lazy::new(|| Mutex::new(None));
20
21#[derive(Clone, Debug)]
23pub struct AgentDefinition {
24 pub agent_type: String,
25 pub when_to_use: String,
26 pub tools: Option<Vec<String>>,
27 pub skills: Option<Vec<String>>,
28 pub color: Option<String>,
29 pub model: Option<String>,
30 pub background: Option<bool>,
31 pub system_prompt: String,
32 pub source: String,
33 pub filename: String,
34 pub plugin: String,
35 pub memory: Option<String>,
36 pub isolation: Option<String>,
37 pub effort: Option<u32>,
38 pub max_turns: Option<u32>,
39 pub disallowed_tools: Option<Vec<String>>,
40}
41
42pub async fn load_plugin_agents()
44-> Result<Vec<AgentDefinition>, Box<dyn std::error::Error + Send + Sync>> {
45 {
47 let cache = PLUGIN_AGENT_CACHE.lock().unwrap();
48 if let Some(ref agents) = *cache {
49 return Ok(agents.clone());
50 }
51 }
52
53 let plugin_result = load_all_plugins_cache_only().await?;
54
55 let mut all_agents = Vec::new();
56
57 for plugin in &plugin_result.enabled {
58 let mut loaded_paths = HashSet::new();
59
60 if let Some(ref agents_path) = plugin.agents_path {
62 if let Ok(agents) = load_agents_from_directory(
63 Path::new(agents_path),
64 &plugin.name,
65 &plugin.source,
66 &plugin.path,
67 &plugin.manifest,
68 &mut loaded_paths,
69 )
70 .await
71 {
72 log::debug!(
73 "Loaded {} agents from plugin {} default directory",
74 agents.len(),
75 plugin.name
76 );
77 all_agents.extend(agents);
78 }
79 }
80
81 if let Some(ref agents_paths) = plugin.agents_paths {
83 for agent_path in agents_paths {
84 if let Ok(agents) = load_agents_from_path(
85 agent_path,
86 &plugin.name,
87 &plugin.source,
88 &plugin.path,
89 &plugin.manifest,
90 &mut loaded_paths,
91 )
92 .await
93 {
94 all_agents.extend(agents);
95 }
96 }
97 }
98 }
99
100 log::debug!("Total plugin agents loaded: {}", all_agents.len());
101
102 {
104 let mut cache = PLUGIN_AGENT_CACHE.lock().unwrap();
105 *cache = Some(all_agents.clone());
106 }
107
108 Ok(all_agents)
109}
110
111async fn load_agents_from_directory(
112 agents_path: &Path,
113 plugin_name: &str,
114 source_name: &str,
115 plugin_path: &str,
116 plugin_manifest: &PluginManifest,
117 loaded_paths: &mut HashSet<String>,
118) -> Result<Vec<AgentDefinition>, Box<dyn std::error::Error + Send + Sync>> {
119 use std::sync::Arc;
120 use tokio::sync::Mutex;
121
122 let agents: Arc<Mutex<Vec<AgentDefinition>>> = Arc::new(Mutex::new(Vec::new()));
123
124 walk_plugin_markdown(
125 agents_path,
126 |full_path, namespace| {
127 let plugin_name = plugin_name.to_string();
128 let source_name = source_name.to_string();
129 let plugin_path = plugin_path.to_string();
130 let manifest = plugin_manifest.clone();
131 let agents = Arc::clone(&agents);
132
133 Box::pin(async move {
134 match load_agent_from_file(
135 &full_path,
136 &plugin_name,
137 &namespace,
138 &source_name,
139 &plugin_path,
140 &manifest,
141 &mut HashSet::new(),
142 )
143 .await
144 {
145 Ok(Some(agent)) => agents.lock().await.push(agent),
146 Ok(None) => {}
147 Err(e) => log::debug!("Failed to load agent from {:?}: {}", full_path, e),
148 }
149 })
150 },
151 WalkPluginMarkdownOpts {
152 stop_at_skill_dir: Some(false),
153 log_label: Some("agents".to_string()),
154 },
155 )
156 .await
157 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
158
159 Ok(Arc::try_unwrap(agents).unwrap().into_inner())
160}
161
162async fn load_agents_from_path(
163 agent_path: &str,
164 plugin_name: &str,
165 source_name: &str,
166 plugin_path: &str,
167 plugin_manifest: &PluginManifest,
168 loaded_paths: &mut HashSet<String>,
169) -> Result<Vec<AgentDefinition>, Box<dyn std::error::Error + Send + Sync>> {
170 load_agents_from_directory(
171 Path::new(agent_path),
172 plugin_name,
173 source_name,
174 plugin_path,
175 plugin_manifest,
176 loaded_paths,
177 )
178 .await
179}
180
181async fn load_agent_from_file(
182 file_path: &str,
183 plugin_name: &str,
184 namespace: &[String],
185 source_name: &str,
186 plugin_path: &str,
187 plugin_manifest: &PluginManifest,
188 loaded_paths: &mut HashSet<String>,
189) -> Result<Option<AgentDefinition>, Box<dyn std::error::Error + Send + Sync>> {
190 if loaded_paths.contains(file_path) {
191 return Ok(None);
192 }
193 loaded_paths.insert(file_path.to_string());
194
195 let content = tokio::fs::read_to_string(file_path)
196 .await
197 .map_err(|e| format!("Failed to read {}: {}", file_path, e))?;
198 let (frontmatter, markdown_content) = parse_frontmatter(&content, file_path);
199
200 let base_agent_name = match frontmatter.get("name").and_then(|v| v.as_str()) {
201 Some(name) => name.to_string(),
202 None => std::path::Path::new(file_path)
203 .file_stem()
204 .map(|s| s.to_string_lossy().to_string())
205 .unwrap_or_default(),
206 };
207
208 let mut name_parts = vec![plugin_name.to_string()];
210 name_parts.extend(namespace.iter().cloned());
211 name_parts.push(base_agent_name.clone());
212 let agent_type = name_parts.join(":");
213
214 let when_to_use = frontmatter
215 .get("description")
216 .or_else(|| frontmatter.get("when-to-use"))
217 .and_then(|v| v.as_str())
218 .unwrap_or(&format!("Agent from {} plugin", plugin_name))
219 .to_string();
220
221 let tools = parse_agent_tools_from_frontmatter(frontmatter.get("tools"));
222 let skills = parse_slash_command_tools_from_frontmatter(frontmatter.get("skills"));
223 let color = frontmatter
224 .get("color")
225 .and_then(|v| v.as_str())
226 .map(|s| s.to_string());
227
228 let model = frontmatter.get("model").and_then(|v| v.as_str()).map(|s| {
229 let trimmed = s.trim().to_lowercase();
230 if trimmed == "inherit" {
231 "inherit".to_string()
232 } else {
233 s.trim().to_string()
234 }
235 });
236
237 let background = frontmatter
238 .get("background")
239 .and_then(|v| v.as_str())
240 .map(|s| s == "true")
241 .or_else(|| frontmatter.get("background").and_then(|v| v.as_bool()));
242
243 let mut system_prompt =
244 substitute_plugin_variables(markdown_content.trim(), plugin_path, source_name);
245 if plugin_manifest.user_config.is_some() {
246 let options = load_plugin_options(source_name);
247 system_prompt = substitute_user_config_in_content(
248 &system_prompt,
249 &options,
250 plugin_manifest.user_config.as_ref().unwrap(),
251 );
252 }
253
254 Ok(Some(AgentDefinition {
255 agent_type,
256 when_to_use,
257 tools,
258 skills,
259 color,
260 model,
261 background,
262 system_prompt,
263 source: "plugin".to_string(),
264 filename: base_agent_name,
265 plugin: source_name.to_string(),
266 memory: None,
267 isolation: None,
268 effort: None,
269 max_turns: None,
270 disallowed_tools: None,
271 }))
272}
273
274fn parse_agent_tools_from_frontmatter(value: Option<&serde_json::Value>) -> Option<Vec<String>> {
275 match value {
276 Some(serde_json::Value::String(s)) => Some(
277 s.split(',')
278 .map(|s| s.trim().to_string())
279 .filter(|s| !s.is_empty())
280 .collect(),
281 ),
282 Some(serde_json::Value::Array(arr)) => Some(
283 arr.iter()
284 .filter_map(|v| v.as_str().map(|s| s.to_string()))
285 .collect(),
286 ),
287 _ => None,
288 }
289}
290
291fn parse_slash_command_tools_from_frontmatter(
292 value: Option<&serde_json::Value>,
293) -> Option<Vec<String>> {
294 parse_agent_tools_from_frontmatter(value)
295}
296
297pub fn clear_plugin_agent_cache() {
299 let mut cache = PLUGIN_AGENT_CACHE.lock().unwrap();
300 *cache = None;
301}