use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use serde::{Deserialize, Serialize};
use crate::{
agent::{AgentConfig, AgentStatus, SpawnedAgentSummary, TeammateIdentity},
background::BackgroundStore,
memory::MemoryStore,
memory::journal::AgentMemoryState,
provider::ProviderId,
runtime::TaskItem,
session::permission::RememberedRule,
team::TeamStore,
};
use super::error::RuntimeError;
static NEXT_STORE_ID: AtomicU64 = AtomicU64::new(1);
#[cfg(test)]
static NEXT_TEST_STORE_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedAgentRecord {
pub(crate) id: String,
pub(crate) runtime_identifier: String,
pub(crate) name: String,
pub(crate) model: String,
pub(crate) provider_id: ProviderId,
pub(crate) config: AgentConfig,
pub(crate) hidden_tools: HashSet<String>,
pub(crate) max_rounds: Option<usize>,
pub(crate) teammate_identity: Option<TeammateIdentity>,
pub(crate) rounds_since_task: usize,
pub(crate) idle_requested: bool,
pub(crate) status: AgentStatus,
pub(crate) subagents: Vec<SpawnedAgentSummary>,
}
#[derive(Debug, Clone)]
pub struct LoadedAgentState {
pub(crate) record: PersistedAgentRecord,
pub(crate) memory: AgentMemoryState,
pub(crate) created_at: Option<u64>,
pub(crate) updated_at: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskStateSnapshot {
pub(crate) tasks: Vec<TaskItem>,
}
pub trait AgentStore: Send + Sync {
fn allows_disk_artifacts(&self) -> bool {
true
}
fn prepare_recovery(&self) -> Result<(), RuntimeError>;
fn create_agent(
&self,
record: &PersistedAgentRecord,
memory: &AgentMemoryState,
) -> Result<(), RuntimeError>;
fn save_agent_record(&self, record: &PersistedAgentRecord) -> Result<(), RuntimeError>;
fn save_agent_memory(
&self,
agent_id: &str,
memory: &AgentMemoryState,
) -> Result<(), RuntimeError>;
fn load_agent(&self, agent_id: &str) -> Result<Option<LoadedAgentState>, RuntimeError>;
fn delete_agent(&self, agent_id: &str) -> Result<(), RuntimeError>;
fn list_agents(&self) -> Result<Vec<LoadedAgentState>, RuntimeError>;
fn list_agents_by_runtime(
&self,
runtime_identifier: &str,
) -> Result<Vec<LoadedAgentState>, RuntimeError>;
}
pub trait RunStore: Send + Sync {
fn start_run(&self, agent_id: &str) -> Result<String, RuntimeError>;
fn update_run_state(
&self,
run_id: &str,
state: &str,
error: Option<&str>,
) -> Result<(), RuntimeError>;
fn finish_run(&self, run_id: &str) -> Result<(), RuntimeError>;
fn fail_run(&self, run_id: &str, error: &str) -> Result<(), RuntimeError>;
}
pub trait TaskStore: Send + Sync {
fn load_tasks(&self, namespace: &Path) -> Result<Vec<TaskItem>, RuntimeError>;
fn capture_tasks(&self, namespace: &Path) -> Result<TaskStateSnapshot, RuntimeError>;
fn restore_tasks(
&self,
namespace: &Path,
snapshot: &TaskStateSnapshot,
) -> Result<(), RuntimeError>;
fn replace_tasks(&self, namespace: &Path, tasks: &[TaskItem]) -> Result<(), RuntimeError>;
fn mutate(
&self,
namespace: &Path,
mutation: &mut dyn FnMut(&mut Vec<TaskItem>) -> Result<(), RuntimeError>,
) -> Result<(), RuntimeError> {
let mut tasks = self.load_tasks(namespace)?;
mutation(&mut tasks)?;
self.replace_tasks(namespace, &tasks)
}
}
pub trait AuditStore: Send + Sync {
fn record_audit_event(
&self,
scope: &str,
event_type: &str,
payload: serde_json::Value,
) -> Result<(), RuntimeError>;
}
pub trait LeaseStore: Send + Sync {
fn acquire_lease(&self, key: &str, owner: &str, ttl: Duration) -> Result<bool, RuntimeError>;
fn release_lease(&self, key: &str, owner: &str) -> Result<(), RuntimeError>;
}
pub trait PermissionRuleStore: Send + Sync {
fn save_rules(
&self,
session_id: &str,
project_id: Option<&str>,
rules: &[RememberedRule],
) -> Result<(), RuntimeError>;
fn load_rules(
&self,
session_id: &str,
project_id: Option<&str>,
) -> Result<Vec<RememberedRule>, RuntimeError>;
fn clear_rules(&self, session_id: &str) -> Result<(), RuntimeError>;
}
pub trait RuntimeStore:
AgentStore
+ RunStore
+ TaskStore
+ AuditStore
+ LeaseStore
+ PermissionRuleStore
+ TeamStore
+ BackgroundStore
+ MemoryStore
+ Send
+ Sync
{
}
impl<T> RuntimeStore for T where
T: AgentStore
+ RunStore
+ TaskStore
+ AuditStore
+ LeaseStore
+ PermissionRuleStore
+ TeamStore
+ BackgroundStore
+ MemoryStore
+ Send
+ Sync
{
}
pub(crate) fn next_id(prefix: &str) -> String {
let counter = NEXT_STORE_ID.fetch_add(1, Ordering::Relaxed);
format!("{prefix}-{:x}-{:x}", now_nanos(), counter)
}
pub(crate) fn now_secs() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
pub(crate) fn now_nanos() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
}
#[cfg(not(test))]
pub(crate) fn default_store_dir() -> PathBuf {
crate::default_paths::workspace_default_paths().root_dir
}
#[cfg(test)]
thread_local! {
static DEFAULT_STORE_DIRS: std::cell::RefCell<Vec<PathBuf>> =
const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(test)]
pub(crate) fn default_store_dir() -> PathBuf {
let suffix = NEXT_TEST_STORE_ID.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir()
.join("mentra-test-runtime")
.join(format!("process-{}-{suffix}", std::process::id()));
DEFAULT_STORE_DIRS.with(|dirs| dirs.borrow_mut().push(dir.clone()));
dir
}
#[cfg(test)]
pub(crate) fn default_store_paths_on_this_thread() -> Vec<PathBuf> {
#[cfg(feature = "store-sqlite")]
let file_name = "runtime.sqlite";
#[cfg(not(feature = "store-sqlite"))]
let file_name = "agents";
DEFAULT_STORE_DIRS.with(|dirs| {
dirs.borrow()
.iter()
.map(|dir| dir.join(file_name))
.collect()
})
}