Skip to main content

ai_agent/utils/plugins/
load_plugin_agents.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/loadPluginAgents.ts
2#![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/// Agent definition loaded from a plugin.
22#[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
42/// Load plugin agents from all enabled plugins.
43pub async fn load_plugin_agents()
44-> Result<Vec<AgentDefinition>, Box<dyn std::error::Error + Send + Sync>> {
45    // Return cached result if available
46    {
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        // Load agents from default agents directory
61        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        // Load agents from additional paths specified in manifest
82        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    // Cache the result
103    {
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    // Apply namespace prefixing
209    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
297/// Clear the plugin agent cache.
298pub fn clear_plugin_agent_cache() {
299    let mut cache = PLUGIN_AGENT_CACHE.lock().unwrap();
300    *cache = None;
301}