use std::{
path::{Path, PathBuf},
sync::Arc,
};
use mentra::ModelSelector;
#[cfg(feature = "mcp")]
use crate::mcp::{self, McpConfig, connections::McpConnections};
use crate::{
context::{ContextConfig, WorkspaceContext},
event::ContextFile,
hooks::{self, HookRunner, HooksConfig},
run::{LoadedSkill, RunError},
runtime::{Runtime, RuntimeBuilder, dispatch},
shell::ShellAccess,
skills::{self, SkillsConfig},
store,
templates::{self, Template, TemplatesConfig},
};
use super::Workspace;
pub struct WorkspaceBuilder {
path: PathBuf,
runtime: RuntimeSource,
model: Option<ModelSelector>,
context: ContextConfig,
skills: SkillsConfig,
#[cfg(feature = "mcp")]
mcp: McpConfig,
templates: TemplatesConfig,
hooks: HooksConfig,
shell: ShellAccess,
}
enum RuntimeSource {
Shared(Arc<Runtime>),
Private(RuntimeBuilder),
}
impl std::fmt::Debug for WorkspaceBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WorkspaceBuilder")
.field("path", &self.path)
.field(
"runtime",
match &self.runtime {
RuntimeSource::Shared(runtime) => runtime,
RuntimeSource::Private(recipe) => recipe,
},
)
.field("model", &self.model)
.field("context", &self.context)
.field("skills", &self.skills)
.field("templates", &self.templates)
.field("hooks", &self.hooks)
.field("shell", &self.shell)
.finish_non_exhaustive()
}
}
impl WorkspaceBuilder {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
runtime: RuntimeSource::Private(RuntimeBuilder::default()),
model: None,
context: ContextConfig::default(),
skills: SkillsConfig::default(),
#[cfg(feature = "mcp")]
mcp: McpConfig::default(),
templates: TemplatesConfig::default(),
hooks: HooksConfig::default(),
shell: ShellAccess::default(),
}
}
pub fn with_runtime(self, runtime: Arc<Runtime>) -> Self {
Self {
runtime: RuntimeSource::Shared(runtime),
..self
}
}
pub fn with_runtime_builder(self, runtime: RuntimeBuilder) -> Self {
Self {
runtime: RuntimeSource::Private(runtime),
..self
}
}
pub fn with_model(self, model: ModelSelector) -> Self {
Self {
model: Some(model),
..self
}
}
pub fn with_context(self, context: ContextConfig) -> Self {
Self { context, ..self }
}
pub fn with_skills(self, skills: SkillsConfig) -> Self {
Self { skills, ..self }
}
#[cfg(feature = "mcp")]
pub fn with_mcp(self, mcp: McpConfig) -> Self {
Self { mcp, ..self }
}
pub fn with_templates(self, templates: TemplatesConfig) -> Self {
Self { templates, ..self }
}
pub fn with_hooks(self, hooks: HooksConfig) -> Self {
Self { hooks, ..self }
}
pub fn with_shell(self, shell: ShellAccess) -> Self {
Self { shell, ..self }
}
pub async fn open(self) -> Result<Workspace, RunError> {
let context = WorkspaceContext::discover_with(&self.path, &self.context)?;
let loaded_hooks = hooks::load(&self.path, &self.hooks)?;
let shared = matches!(self.runtime, RuntimeSource::Shared(_));
let runtime = match self.runtime {
RuntimeSource::Shared(runtime) => runtime,
RuntimeSource::Private(recipe) => Arc::new(recipe.build_for(&self.path, self.shell)?),
};
let model = runtime.resolve_model(self.model).await?;
let skills_dirs = register_skills(runtime.mentra_runtime(), &self.path, &self.skills)?;
let skills = runtime
.mentra_runtime()
.skills()
.into_iter()
.map(|skill| LoadedSkill {
name: skill.name,
description: skill.description,
path: skill.path,
})
.collect();
let (templates_dirs, templates) = load_templates(&self.path, &self.templates)?;
let runner = runtime.interceptors().iter().cloned().fold(
HookRunner::new(&self.path, loaded_hooks),
|runner, interceptor| runner.with_interceptor(interceptor),
);
let hook_registration = runtime.register_workspace(dispatch::WorkspaceGuardEntry {
runner: Arc::new(runner),
shell: self.shell,
root: dispatch::canonical(&self.path),
shared,
});
#[cfg(feature = "mcp")]
let (mcp_connections, mcp_files, mcp_servers) = {
let (files, servers) = discovered_mcp(&self.path, &self.mcp)?;
let connections =
McpConnections::connect(Arc::clone(&runtime), &self.path, servers).await;
let names = connections.names().to_vec();
(connections, files, names)
};
#[cfg(not(feature = "mcp"))]
let (mcp_files, mcp_servers): (Vec<ContextFile>, Vec<String>) = (Vec::new(), Vec::new());
Ok(Workspace {
root: resolved_workspace(&self.path, &context),
agent: agent_config(&self.path, &context),
identifier: store::runtime_identifier(&self.path),
path: self.path,
provider: runtime.provider().to_string(),
runtime,
model,
context,
skills_dirs,
skills,
templates_dirs,
templates,
mcp_files,
mcp_servers,
hook_registration,
#[cfg(feature = "mcp")]
mcp_connections,
})
}
}
#[cfg(feature = "mcp")]
fn discovered_mcp(
workspace: &Path,
config: &McpConfig,
) -> Result<(Vec<ContextFile>, Vec<mcp::McpServer>), RunError> {
let files: Vec<ContextFile> = mcp::discover(workspace, config)?
.iter()
.map(|source| ContextFile {
path: source.path.clone(),
scope: source.scope.label(),
})
.collect();
Ok((files, mcp::servers(workspace, config)?))
}
fn register_skills(
runtime: &mentra::Runtime,
workspace: &Path,
config: &SkillsConfig,
) -> Result<Vec<PathBuf>, RunError> {
let sources = skills::discover(workspace, config);
let paths: Vec<PathBuf> = sources.iter().map(|source| source.path.clone()).collect();
runtime.register_skills_dirs(&paths)?;
Ok(paths)
}
pub(crate) fn load_templates(
workspace: &Path,
config: &TemplatesConfig,
) -> Result<(Vec<PathBuf>, Vec<Template>), RunError> {
let sources = templates::discover(workspace, config);
let dirs: Vec<PathBuf> = sources.iter().map(|source| source.path.clone()).collect();
Ok((dirs, templates::load_sources(&sources)?))
}
pub(crate) fn resolved_workspace(requested: &Path, context: &WorkspaceContext) -> PathBuf {
context
.root()
.map(Path::to_path_buf)
.unwrap_or_else(|| requested.to_path_buf())
}
fn agent_config(workspace: &Path, context: &WorkspaceContext) -> mentra::agent::AgentConfig {
mentra::agent::AgentConfig {
system: context.render(),
tool_profile: mentra::agent::ToolProfile::hide(REPLACED_TOOLS),
workspace: mentra::agent::WorkspaceConfig {
base_dir: workspace.to_path_buf(),
..Default::default()
},
..Default::default()
}
}
const REPLACED_TOOLS: [&str; 3] = ["shell", "background_run", "task"];
#[cfg(test)]
mod tests;