Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,17 +350,18 @@ async fn command_gateway(port: u16, verbose: bool) -> Result<()> {
let channel_manager = ChannelManager::new(&config, bus.clone());

// Initialize RBAC manager if configured (must be before channel registrations)
let rbac_manager: Option<Arc<RbacManager>> = if let Ok(Some(rbac_config)) = config.get_rbac_config() {
if rbac_config.enabled {
let workspace = config.workspace_path();
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
Some(Arc::new(RbacManager::new(rbac_config, workspace, home)))
let rbac_manager: Option<Arc<RbacManager>> =
if let Ok(Some(rbac_config)) = config.get_rbac_config() {
if rbac_config.enabled {
let workspace = config.workspace_path();
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
Some(Arc::new(RbacManager::new(rbac_config, workspace, home)))
} else {
None
}
} else {
None
}
} else {
None
};
};

// register dingtalk channel if enabled
if config.channels.dingtalk.enabled {
Expand Down Expand Up @@ -413,11 +414,16 @@ async fn command_gateway(port: u16, verbose: bool) -> Result<()> {
match if let Some(ref rbac) = rbac_manager {
DiscordChannel::with_rbac(
config.channels.discord.clone(),
config.skills.clone(),
bus.clone(),
Some(rbac.clone()),
)
} else {
DiscordChannel::new(config.channels.discord.clone(), bus.clone())
DiscordChannel::new(
config.channels.discord.clone(),
config.skills.clone(),
bus.clone(),
)
} {
Ok(discord) => {
channel_manager.register_channel(Arc::new(discord)).await;
Expand Down
69 changes: 42 additions & 27 deletions core/src/agent/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use mofa_sdk::skills::SkillsManager;
use serde_json::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::debug;

/// Builds the context (system prompt + messages) for the agent
Expand All @@ -23,8 +22,8 @@ use tracing::debug;
/// Memory (long-term and daily notes) is handled by mofa's PromptContext.
#[derive(Clone)]
pub struct ContextBuilder {
/// Skills manager (from mofa-sdk)
skills: Arc<SkillsManager>,
/// Skill search paths, in priority order
search_dirs: Vec<PathBuf>,
/// Workspace path (cached)
workspace: PathBuf,
}
Expand All @@ -38,7 +37,6 @@ impl ContextBuilder {
pub fn new(config: &Config) -> Self {
let workspace = config.workspace_path();

// Create SkillsManager with workspace + builtin skills
let workspace_skills = workspace.join("skills");
let mut builtin_skills = SkillsManager::find_builtin_skills();

Expand Down Expand Up @@ -95,31 +93,36 @@ impl ContextBuilder {
}
}

let skills = if let Some(builtin) = builtin_skills {
tracing::info!("Using both workspace and builtin skills");
let manager = SkillsManager::with_search_dirs(vec![workspace_skills.clone(), builtin])
.unwrap_or_else(|_| SkillsManager::new(&workspace_skills).unwrap());
// Log the number of skills found
let all_metadata = manager.get_all_metadata();
tracing::info!("Found {} skills", all_metadata.len());
for meta in &all_metadata {
tracing::info!(" - {} (requires: {:?})", meta.name, meta.requires);
}
manager
} else {
tracing::info!("No builtin skills found, using only workspace skills");
SkillsManager::new(&workspace_skills).unwrap()
};
let mut search_dirs = vec![workspace_skills.clone()];
search_dirs.push(crate::config::get_data_dir().join("skills").join("hub"));

if let Some(builtin) = builtin_skills {
search_dirs.push(builtin);
}

Self {
skills: Arc::new(skills),
search_dirs,
workspace,
}
}

fn build_skills_manager(&self) -> SkillsManager {
let workspace_skills = self.workspace.join("skills");
let manager = SkillsManager::with_search_dirs(self.search_dirs.clone())
.unwrap_or_else(|_| SkillsManager::new(&workspace_skills).unwrap());
let all_metadata = manager.get_all_metadata();
debug!(
"Loaded {} skills from {:?}",
all_metadata.len(),
self.search_dirs
);
manager
}

/// Build the system prompt from bootstrap files, memory, and skills
pub async fn build_system_prompt(&self, skill_names: Option<&[String]>) -> Result<String> {
let mut parts = Vec::new();
let skills = self.build_skills_manager();

// Build base prompt using mofa's PromptContextBuilder
// Memory context (long-term + today's notes) is integrated automatically
Expand Down Expand Up @@ -149,9 +152,9 @@ impl ContextBuilder {
parts.push(base_prompt);

// Add skills - progressive loading
let always_skills = self.skills.get_always_skills_async().await;
let always_skills = skills.get_always_skills_async().await;
if !always_skills.is_empty() {
let always_content = self.skills.load_skills_for_context(&always_skills).await;
let always_content = skills.load_skills_for_context(&always_skills).await;
if !always_content.is_empty() {
parts.push(format!("# Active Skills\n\n{}", always_content));
}
Expand All @@ -160,15 +163,15 @@ impl ContextBuilder {
// Requested skills
if let Some(names) = skill_names {
if !names.is_empty() {
let skills_content = self.skills.load_skills_for_context(names).await;
let skills_content = skills.load_skills_for_context(names).await;
if !skills_content.is_empty() {
parts.push(format!("# Requested Skills\n\n{}", skills_content));
}
}
}

// Skills summary
let skills_summary = self.skills.build_skills_summary().await;
let skills_summary = skills.build_skills_summary().await;
if !skills_summary.is_empty() {
parts.push(format!(
r#"# Available Skills
Expand Down Expand Up @@ -325,9 +328,21 @@ Read the skill's SKILL.md file using the `read_file` tool to learn how to use it
pub fn workspace(&self) -> &Path {
&self.workspace
}
}

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

#[test]
fn context_builder_includes_shared_hub_search_dir() {
let builder = ContextBuilder::new(&Config::default());
let expected = crate::config::get_data_dir().join("skills").join("hub");

/// Get the skills manager
pub fn skills(&self) -> &Arc<SkillsManager> {
&self.skills
assert!(
builder.search_dirs.contains(&expected),
"expected shared hub path {expected:?} in search dirs {:?}",
builder.search_dirs
);
}
}
Loading