mod builder;
pub(crate) mod control;
mod error;
pub(crate) mod handle;
mod hybrid_store;
mod intrinsic;
mod skill;
mod store;
pub(crate) mod task;
use std::{any::Any, path::Path, sync::Arc};
use tokio::sync::broadcast;
use crate::{
agent::{Agent, AgentConfig, AgentSpawnOptions, AgentStatus},
provider::{Provider, ProviderRegistry},
runtime::{builder::RuntimeBuilder, skill::SkillLoadError},
session::{
Session, SessionEvent, SessionId, SessionMetadata,
permission::{PendingPermissionStore, SessionToolAuthorizer},
},
tool::ExecutableTool,
};
use mentra_provider::{BuiltinProvider, ModelInfo, ModelSelector, ProviderDescriptor, ProviderId};
pub use control::{
AuditHook, AuditLogHook, CancellationFlag, CancellationToken, CommandOutput, CommandRequest,
CommandSpec, ExecOutput, RunOptions, RuntimeExecutor, RuntimeHook, RuntimeHookEvent,
RuntimeHooks, RuntimePolicy, is_transient_provider_error, is_transient_runtime_error,
};
pub use error::{ErrorCategory, RuntimeError};
pub(crate) use handle::RuntimeHandle;
pub use hybrid_store::HybridRuntimeStore;
pub(crate) use intrinsic::RuntimeIntrinsicTool;
pub use store::{
AgentStore, AuditStore, LeaseStore, PermissionRuleStore, RunStore, RuntimeStore,
SqliteRuntimeStore, TaskStore,
};
pub(crate) use store::{LoadedAgentState, PersistedAgentRecord, TaskStateSnapshot};
pub(crate) use task::TaskIntrinsicTool;
pub use task::{TaskItem, TaskStatus};
pub struct Runtime {
handle: RuntimeHandle,
provider_registry: ProviderRegistry,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PersistedAgentSummary {
pub id: String,
pub runtime_identifier: String,
pub name: String,
pub is_teammate: bool,
pub status: AgentStatus,
pub history_len: usize,
}
impl Runtime {
pub fn builder() -> RuntimeBuilder {
RuntimeBuilder::new(true)
}
pub fn empty_builder() -> RuntimeBuilder {
RuntimeBuilder::new(false)
}
pub fn register_tool<T>(&self, tool: T)
where
T: ExecutableTool + 'static,
{
self.handle.register_tool(tool);
}
pub fn tools(&self) -> Vec<crate::tool::RuntimeToolDescriptor> {
let tool_names = self
.handle
.tools()
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
let mut tools = tool_names
.into_iter()
.filter_map(|name| self.handle.get_tool_descriptor(&name))
.collect::<Vec<_>>();
tools.sort_by(|left, right| left.provider.name.cmp(&right.provider.name));
tools
}
pub fn tool_descriptor(&self, name: &str) -> Option<crate::tool::RuntimeToolDescriptor> {
self.handle.get_tool_descriptor(name)
}
pub fn register_context(&self, context: Arc<dyn Any + Send + Sync>) {
self.handle.register_app_context(context);
}
pub fn app_context<T>(&self) -> Result<Arc<T>, String>
where
T: Any + Send + Sync + 'static,
{
self.handle.app_context::<T>()
}
pub fn register_skills_dir(&self, path: impl AsRef<Path>) -> Result<(), SkillLoadError> {
self.handle
.register_skill_loader(skill::SkillLoader::from_dir(path)?);
Ok(())
}
pub fn spawn(&self, name: impl Into<String>, model: ModelInfo) -> Result<Agent, RuntimeError> {
self.spawn_with_config(name, model, AgentConfig::default())
}
pub fn spawn_with_config(
&self,
name: impl Into<String>,
model: ModelInfo,
config: AgentConfig,
) -> Result<Agent, RuntimeError> {
Agent::new(
self.handle.clone(),
model.id,
name.into(),
config,
self.provider_registry
.get_provider(Some(&model.provider))
.ok_or_else(|| RuntimeError::ProviderNotFound(Some(model.provider.clone())))?,
AgentSpawnOptions::default(),
)
}
pub fn resume_agent(&self, agent_id: &str) -> Result<Agent, RuntimeError> {
let Some(state) = self.handle.store().load_agent(agent_id)? else {
return Err(RuntimeError::Store(format!(
"No persisted agent with id '{agent_id}'"
)));
};
let provider = self
.provider_registry
.get_provider(Some(&state.record.provider_id))
.ok_or_else(|| {
RuntimeError::ProviderNotFound(Some(state.record.provider_id.clone()))
})?;
Agent::from_loaded(self.handle.clone(), state, provider)
}
pub fn resume(&self, runtime_identifier: &str) -> Result<Vec<Agent>, RuntimeError> {
let states = self
.handle
.store()
.list_agents_by_runtime(runtime_identifier)?;
let mut agents = Vec::new();
for state in states {
let provider = self
.provider_registry
.get_provider(Some(&state.record.provider_id))
.ok_or_else(|| {
RuntimeError::ProviderNotFound(Some(state.record.provider_id.clone()))
})?;
let agent = Agent::from_loaded(self.handle.clone(), state, provider)?;
if agent.is_teammate() {
agent.revive_teammate_actor()?;
} else {
agents.push(agent);
}
}
Ok(agents)
}
pub fn list_persisted_agents(
&self,
runtime_identifier: &str,
) -> Result<Vec<PersistedAgentSummary>, RuntimeError> {
self.handle
.store()
.list_agents_by_runtime(runtime_identifier)
.map(|states| {
states
.into_iter()
.map(|state| PersistedAgentSummary {
id: state.record.id,
runtime_identifier: state.record.runtime_identifier,
name: state.record.name,
is_teammate: state.record.teammate_identity.is_some(),
status: state.record.status,
history_len: state.memory.transcript.len(),
})
.collect()
})
}
pub fn resume_all(&self) -> Result<Vec<Agent>, RuntimeError> {
let states = self.handle.store().list_agents()?;
let mut agents = Vec::new();
for state in states {
let provider = self
.provider_registry
.get_provider(Some(&state.record.provider_id))
.ok_or_else(|| {
RuntimeError::ProviderNotFound(Some(state.record.provider_id.clone()))
})?;
agents.push(Agent::from_loaded(self.handle.clone(), state, provider)?);
}
Ok(agents)
}
}
impl Runtime {
pub fn providers(&self) -> Vec<ProviderDescriptor> {
self.provider_registry.descriptors()
}
pub fn register_provider(
&mut self,
id: BuiltinProvider,
api_key: impl Into<String>,
) -> Result<(), String> {
self.provider_registry
.register_builtin_provider(id, api_key)
}
pub fn register_ollama(&mut self) {
self.provider_registry.register_ollama();
}
pub fn register_lmstudio(&mut self) {
self.provider_registry.register_lmstudio();
}
pub fn register_provider_instance<P>(&mut self, provider: P)
where
P: Provider + 'static,
{
self.provider_registry.register_provider_instance(provider);
}
pub async fn list_models(
&self,
provider: Option<&ProviderId>,
) -> Result<Vec<ModelInfo>, RuntimeError> {
self.provider_registry
.get_provider(provider)
.ok_or_else(|| RuntimeError::ProviderNotFound(provider.cloned()))?
.list_models()
.await
.map_err(RuntimeError::FailedToListModels)
}
pub async fn resolve_model(
&self,
provider: impl Into<ProviderId>,
selector: ModelSelector,
) -> Result<ModelInfo, RuntimeError> {
let provider = provider.into();
if self
.provider_registry
.get_provider(Some(&provider))
.is_none()
{
return Err(RuntimeError::ProviderNotFound(Some(provider)));
}
match selector {
ModelSelector::Id(id) => Ok(ModelInfo::new(id, provider)),
ModelSelector::NewestAvailable => {
let mut models = self.list_models(Some(&provider)).await?;
models.sort_by(|left, right| {
right
.created_at
.cmp(&left.created_at)
.then_with(|| left.id.cmp(&right.id))
});
models
.into_iter()
.next()
.ok_or(RuntimeError::NoModelsAvailable(provider))
}
}
}
}
impl Runtime {
pub fn create_session(
&self,
name: impl Into<String>,
model: ModelInfo,
) -> Result<Session, RuntimeError> {
self.create_session_with_config(name, model, AgentConfig::default())
}
pub fn create_session_with_config(
&self,
name: impl Into<String>,
model: ModelInfo,
config: AgentConfig,
) -> Result<Session, RuntimeError> {
self.create_session_full(name, model, config, None)
}
pub fn create_session_full(
&self,
name: impl Into<String>,
model: ModelInfo,
config: AgentConfig,
project_id: Option<String>,
) -> Result<Session, RuntimeError> {
let name = name.into();
let session_id = SessionId::new();
let metadata = SessionMetadata::new(session_id.clone(), &name, &model.id);
let (event_tx, _) = broadcast::channel(512);
let rule_store = crate::session::RuleStore::new();
let pending_permissions = PendingPermissionStore::new();
let session_handle =
self.handle
.with_tool_authorizer(Arc::new(SessionToolAuthorizer::new(
self.handle.execution.tool_authorizer.clone(),
event_tx.clone(),
pending_permissions.clone(),
rule_store.clone(),
)));
let provider = self
.provider_registry
.get_provider(Some(&model.provider))
.ok_or_else(|| RuntimeError::ProviderNotFound(Some(model.provider.clone())))?;
let agent = Agent::new(
session_handle,
model.id.clone(),
name.clone(),
config,
provider,
AgentSpawnOptions::default(),
)?;
let mut session = Session::new_with_parts(
session_id.clone(),
metadata,
agent,
event_tx,
rule_store,
pending_permissions,
project_id,
);
let started = SessionEvent::SessionStarted { session_id };
let _rx = session.subscribe();
session.emit_started(started);
Ok(session)
}
pub fn resume_session(&self, agent_id: &str) -> Result<Session, RuntimeError> {
self.resume_session_with_project(agent_id, None)
}
pub fn resume_session_with_project(
&self,
agent_id: &str,
project_id: Option<String>,
) -> Result<Session, RuntimeError> {
let session_id = SessionId::new();
let (event_tx, _) = broadcast::channel(512);
let rule_store = crate::session::RuleStore::new();
let pending_permissions = PendingPermissionStore::new();
let session_handle =
self.handle
.with_tool_authorizer(Arc::new(SessionToolAuthorizer::new(
self.handle.execution.tool_authorizer.clone(),
event_tx.clone(),
pending_permissions.clone(),
rule_store.clone(),
)));
let Some(state) = self.handle.store().load_agent(agent_id)? else {
return Err(RuntimeError::Store(format!(
"No persisted agent with id '{agent_id}'"
)));
};
let provider = self
.provider_registry
.get_provider(Some(&state.record.provider_id))
.ok_or_else(|| {
RuntimeError::ProviderNotFound(Some(state.record.provider_id.clone()))
})?;
let agent = Agent::from_loaded(session_handle, state, provider)?;
let metadata = SessionMetadata::new(session_id.clone(), agent.name(), agent.model());
let session = Session::new_with_parts(
session_id,
metadata,
agent,
event_tx,
rule_store,
pending_permissions,
project_id,
);
Ok(session)
}
}