mod agent;
mod delegated;
mod fs_util;
mod leases;
mod rules;
mod runs;
#[cfg(test)]
mod tests;
mod transcript_log;
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, Mutex, MutexGuard},
};
use super::error::RuntimeError;
use super::store::default_store_dir;
use super::volatile_store::VolatileRuntimeStore;
use transcript_log::TranscriptLogIndex;
const SCHEMA_VERSION: u32 = 1;
#[derive(Clone)]
pub struct FileRuntimeStore {
root: PathBuf,
volatile: VolatileRuntimeStore,
transcript_logs: Arc<Mutex<HashMap<String, Arc<Mutex<TranscriptLogIndex>>>>>,
rules_state: Arc<Mutex<rules::RulesState>>,
held_leases: Arc<Mutex<HashMap<String, leases::HeldLease>>>,
runs_lock: Arc<Mutex<()>>,
}
impl FileRuntimeStore {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
volatile: VolatileRuntimeStore::new(),
transcript_logs: Arc::new(Mutex::new(HashMap::new())),
rules_state: Arc::new(Mutex::new(rules::RulesState::default())),
held_leases: Arc::new(Mutex::new(HashMap::new())),
runs_lock: Arc::new(Mutex::new(())),
}
}
pub fn default_root() -> PathBuf {
default_store_dir()
}
pub fn root(&self) -> &Path {
&self.root
}
pub(crate) fn agents_dir(&self) -> PathBuf {
self.root.join("agents")
}
pub(crate) fn agent_dir(&self, agent_id: &str) -> PathBuf {
self.agents_dir().join(fs_util::encode_component(agent_id))
}
pub(crate) fn rules_path(&self) -> PathBuf {
self.root.join("rules.json")
}
pub(crate) fn rules_lock_path(&self) -> PathBuf {
self.root.join("rules.lock")
}
pub(crate) fn runs_path(&self) -> PathBuf {
self.root.join("runs.jsonl")
}
pub(crate) fn leases_dir(&self) -> PathBuf {
self.root.join("leases")
}
fn transcript_log(&self, agent_id: &str) -> Arc<Mutex<TranscriptLogIndex>> {
let mut logs = lock_unpoisoned(&self.transcript_logs);
logs.entry(agent_id.to_string())
.or_insert_with(|| Arc::new(Mutex::new(TranscriptLogIndex::default())))
.clone()
}
fn forget_transcript_log(&self, agent_id: &str) {
lock_unpoisoned(&self.transcript_logs).remove(agent_id);
}
#[cfg(all(test, not(feature = "store-sqlite")))]
pub(crate) fn release_all_leases(&self) {
lock_unpoisoned(&self.held_leases).clear();
}
}
impl Default for FileRuntimeStore {
fn default() -> Self {
Self::new(Self::default_root())
}
}
fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn store_error(context: &str, error: impl std::fmt::Display) -> RuntimeError {
RuntimeError::Store(format!("{context}: {error}"))
}
fn parse_versioned<T: serde::de::DeserializeOwned>(
contents: &str,
file: &str,
) -> Result<T, RuntimeError> {
#[derive(serde::Deserialize)]
struct SchemaOnly {
schema: u32,
}
let schema: SchemaOnly = serde_json::from_str(contents)
.map_err(|error| store_error(&format!("parse {file}"), error))?;
if schema.schema > SCHEMA_VERSION {
return Err(RuntimeError::Store(format!(
"{file} schema {} is newer than this build understands ({SCHEMA_VERSION})",
schema.schema
)));
}
serde_json::from_str(contents).map_err(|error| store_error(&format!("parse {file}"), error))
}
fn to_pretty_json<T: serde::Serialize>(value: &T) -> Result<String, RuntimeError> {
serde_json::to_string_pretty(value).map_err(|error| RuntimeError::Store(error.to_string()))
}