mod builder;
mod spec;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use mentra::{ModelInfo, Session, agent::AgentConfig, provider::ReasoningOptions};
pub use builder::WorkspaceBuilder;
pub use spec::RunSpec;
pub(crate) use builder::{load_templates, resolved_workspace};
pub(crate) use spec::DEFAULT_SESSION_NAME;
#[cfg(feature = "mcp")]
use crate::mcp::connections::McpConnections;
use crate::{
context::WorkspaceContext,
event::ContextFile,
fingerprint::{self, Snapshot},
run::{Effort, LoadedSkill, PreparedRun, RunContext, RunError},
runtime::{Runtime, dispatch::HookRegistration},
templates::Template,
};
pub struct Workspace {
path: PathBuf,
root: PathBuf,
runtime: Arc<Runtime>,
model: ModelInfo,
provider: String,
identifier: String,
context: WorkspaceContext,
agent: AgentConfig,
skills_dirs: Vec<PathBuf>,
skills: Vec<LoadedSkill>,
templates_dirs: Vec<PathBuf>,
templates: Vec<Template>,
mcp_files: Vec<ContextFile>,
mcp_servers: Vec<String>,
#[allow(dead_code, reason = "held for its Drop")]
hook_registration: HookRegistration,
#[cfg(feature = "mcp")]
#[allow(dead_code, reason = "held for its Drop")]
mcp_connections: McpConnections,
}
impl std::fmt::Debug for Workspace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Workspace")
.field("root", &self.root)
.field("provider", &self.provider)
.field("model", &self.model.id)
.field("context_files", &self.context.documents().len())
.field("skills", &self.skills.len())
.field("templates", &self.templates.len())
.field("mcp_servers", &self.mcp_servers)
.finish_non_exhaustive()
}
}
impl Workspace {
pub async fn open(path: impl Into<PathBuf>) -> Result<Self, RunError> {
Self::builder(path).open().await
}
pub fn builder(path: impl Into<PathBuf>) -> WorkspaceBuilder {
WorkspaceBuilder::new(path)
}
pub fn prepare(&self, spec: impl Into<RunSpec>) -> Result<PreparedRun, RunError> {
let spec = spec.into();
let mut session = self.runtime.mint(
spec.session_name.clone(),
self.model.clone(),
self.minted_agent(),
&self.identifier,
)?;
apply_effort(&mut session, spec.effort)?;
Ok(self.minted(session, spec))
}
pub fn resume(
&self,
agent_id: &str,
spec: impl Into<RunSpec>,
) -> Result<PreparedRun, RunError> {
let spec = spec.into();
let mut session = self.runtime.resume_minted(agent_id)?;
apply_effort(&mut session, spec.effort)?;
Ok(self.minted(session, spec))
}
pub fn fingerprint(&self) -> Snapshot {
fingerprint::snapshot(&self.root)
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn model(&self) -> &str {
&self.model.id
}
pub fn provider(&self) -> &str {
&self.provider
}
pub fn context(&self) -> &WorkspaceContext {
&self.context
}
pub fn skills(&self) -> &[LoadedSkill] {
&self.skills
}
pub fn templates(&self) -> &[Template] {
&self.templates
}
pub fn mcp_servers(&self) -> &[String] {
&self.mcp_servers
}
pub fn mentra_runtime(&self) -> &mentra::Runtime {
self.runtime.mentra_runtime()
}
#[cfg(feature = "mcp")]
fn minted_agent(&self) -> AgentConfig {
let mut agent = self.agent.clone();
for descriptor in self.runtime.mentra_runtime().tools() {
let name = &descriptor.provider.name;
if let Some((server, _)) = mentra::mcp::parse_mcp_tool_name(name)
&& !self.mcp_servers.iter().any(|own| own == server)
{
agent.tool_profile.hidden_tools.insert(name.clone());
}
}
agent
}
#[cfg(not(feature = "mcp"))]
fn minted_agent(&self) -> AgentConfig {
self.agent.clone()
}
fn minted(&self, session: Session, spec: RunSpec) -> PreparedRun {
let bounds = spec.turn_options();
PreparedRun::new(
session,
RunContext {
workspace: self.root.clone(),
prompt: spec.prompt,
provider: self.provider.clone(),
model: self.model.id.clone(),
context: self.context.clone(),
skills_dirs: self.skills_dirs.clone(),
skills: self.skills.clone(),
templates_dirs: self.templates_dirs.clone(),
templates: self.templates.clone(),
mcp_files: self.mcp_files.clone(),
mcp_servers: self.mcp_servers.clone(),
},
)
.with_bounds(bounds)
}
}
fn apply_effort(session: &mut Session, effort: Option<Effort>) -> Result<(), RunError> {
let Some(effort) = effort else {
return Ok(());
};
session.set_reasoning(Some(ReasoningOptions {
effort: Some(effort.into()),
summary: None,
}))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_workspace_can_be_shared_across_tasks() {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Workspace>();
assert_send_sync::<RunSpec>();
}
}