use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use salvor_graph::Graph;
use salvor_runtime::{Agent, ClockFn, RandomFn, RunCtx, Runtime, RuntimeError};
use salvor_store::EventStore;
use salvor_tools::mcp::McpServer;
use time::OffsetDateTime;
use tokio::task::JoinHandle;
use salvor_core::{EventEnvelope, RunId};
use crate::executor::ModelExecutor;
use crate::tool_registry::ToolRegistry;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefFormat {
Toml,
Json,
}
#[derive(Debug, Clone)]
pub struct AgentDefinition {
pub format: DefFormat,
pub body: Vec<u8>,
}
pub struct BuiltAgent {
pub agent: Agent,
pub servers: Vec<McpServer>,
}
pub type BuildFuture = Pin<Box<dyn Future<Output = Result<BuiltAgent, String>> + Send>>;
pub type AgentFactory = Arc<dyn Fn(AgentDefinition) -> BuildFuture + Send + Sync>;
#[derive(Debug, Clone)]
pub struct RegisteredAgent {
pub definition: AgentDefinition,
pub agent_hash: String,
pub name: Option<String>,
}
#[derive(Clone)]
pub struct AppState {
inner: Arc<Inner>,
}
struct Inner {
store: Arc<dyn EventStore>,
factory: AgentFactory,
model_executor: Option<Arc<dyn ModelExecutor>>,
tool_registry: Option<Arc<ToolRegistry>>,
hooks: Option<(ClockFn, RandomFn)>,
auth_token: Option<String>,
poll_interval: Duration,
agents: Mutex<HashMap<String, RegisteredAgent>>,
graphs: Mutex<HashMap<String, Graph>>,
active: Mutex<HashSet<RunId>>,
handles: Mutex<HashMap<RunId, JoinHandle<()>>>,
client_runs: Mutex<HashMap<RunId, ClientRunLease>>,
client_lease_ttl: Duration,
}
#[derive(Debug, Clone)]
pub struct ClientRunLease {
pub drive_token: String,
pub record_prompts: bool,
pub last_seen: OffsetDateTime,
}
impl AppState {
#[must_use]
pub fn new(store: Arc<dyn EventStore>, factory: AgentFactory) -> Self {
Self {
inner: Arc::new(Inner {
store,
factory,
model_executor: None,
tool_registry: None,
hooks: None,
auth_token: None,
poll_interval: Duration::from_millis(50),
agents: Mutex::new(HashMap::new()),
graphs: Mutex::new(HashMap::new()),
active: Mutex::new(HashSet::new()),
handles: Mutex::new(HashMap::new()),
client_runs: Mutex::new(HashMap::new()),
client_lease_ttl: Duration::from_secs(60),
}),
}
}
#[must_use]
pub fn with_client_lease_ttl(mut self, ttl: Duration) -> Self {
Arc::get_mut(&mut self.inner)
.expect("with_client_lease_ttl is called before the state is shared")
.client_lease_ttl = ttl;
self
}
#[must_use]
pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
Arc::get_mut(&mut self.inner)
.expect("with_auth_token is called before the state is shared")
.auth_token = Some(token.into());
self
}
#[must_use]
pub fn with_model_executor(mut self, executor: Arc<dyn ModelExecutor>) -> Self {
Arc::get_mut(&mut self.inner)
.expect("with_model_executor is called before the state is shared")
.model_executor = Some(executor);
self
}
#[must_use]
pub fn with_tool_registry(mut self, registry: Arc<ToolRegistry>) -> Self {
Arc::get_mut(&mut self.inner)
.expect("with_tool_registry is called before the state is shared")
.tool_registry = Some(registry);
self
}
#[must_use]
pub fn with_hooks(mut self, clock: ClockFn, random: RandomFn) -> Self {
Arc::get_mut(&mut self.inner)
.expect("with_hooks is called before the state is shared")
.hooks = Some((clock, random));
self
}
#[must_use]
pub fn with_poll_interval(mut self, interval: Duration) -> Self {
Arc::get_mut(&mut self.inner)
.expect("with_poll_interval is called before the state is shared")
.poll_interval = interval;
self
}
#[must_use]
pub fn store(&self) -> Arc<dyn EventStore> {
self.inner.store.clone()
}
#[must_use]
pub fn auth_token(&self) -> Option<&str> {
self.inner.auth_token.as_deref()
}
#[must_use]
pub fn poll_interval(&self) -> Duration {
self.inner.poll_interval
}
#[must_use]
pub fn model_executor(&self) -> Option<Arc<dyn ModelExecutor>> {
self.inner.model_executor.clone()
}
#[must_use]
pub fn tool_registry(&self) -> Option<Arc<ToolRegistry>> {
self.inner.tool_registry.clone()
}
#[must_use]
pub fn now(&self) -> OffsetDateTime {
match &self.inner.hooks {
Some((clock, _)) => clock(),
None => OffsetDateTime::now_utc(),
}
}
#[must_use]
pub fn runtime(&self) -> Runtime {
match &self.inner.hooks {
Some((clock, random)) => {
Runtime::with_hooks(self.inner.store.clone(), clock.clone(), random.clone())
}
None => Runtime::new(self.inner.store.clone()),
}
}
pub async fn build_agent(&self, definition: AgentDefinition) -> Result<BuiltAgent, String> {
(self.inner.factory)(definition).await
}
pub fn register_agent(&self, registered: RegisteredAgent) -> String {
let hash = registered.agent_hash.clone();
self.inner
.agents
.lock()
.expect("agents registry lock")
.insert(hash.clone(), registered);
hash
}
#[must_use]
pub fn agent(&self, hash: &str) -> Option<RegisteredAgent> {
self.inner
.agents
.lock()
.expect("agents registry lock")
.get(hash)
.cloned()
}
#[must_use]
pub fn agent_hashes(&self) -> Vec<String> {
let mut hashes: Vec<String> = self
.inner
.agents
.lock()
.expect("agents registry lock")
.keys()
.cloned()
.collect();
hashes.sort();
hashes
}
pub fn store_graph(&self, hash: String, graph: Graph) -> bool {
let mut graphs = self.inner.graphs.lock().expect("graphs registry lock");
if graphs.contains_key(&hash) {
return false;
}
graphs.insert(hash, graph);
true
}
#[must_use]
pub fn graph(&self, hash: &str) -> Option<Graph> {
self.inner
.graphs
.lock()
.expect("graphs registry lock")
.get(hash)
.cloned()
}
#[must_use]
pub fn graph_hashes(&self) -> Vec<String> {
let mut hashes: Vec<String> = self
.inner
.graphs
.lock()
.expect("graphs registry lock")
.keys()
.cloned()
.collect();
hashes.sort();
hashes
}
pub fn run_ctx(&self, run_id: RunId, log: Vec<EventEnvelope>) -> Result<RunCtx, RuntimeError> {
match &self.inner.hooks {
Some((clock, random)) => RunCtx::with_hooks(
self.inner.store.clone(),
run_id,
log,
clock.clone(),
random.clone(),
),
None => RunCtx::new(self.inner.store.clone(), run_id, log),
}
}
pub fn begin_run(&self, run_id: RunId) {
self.inner
.active
.lock()
.expect("active runs lock")
.insert(run_id);
}
pub fn set_handle(&self, run_id: RunId, handle: JoinHandle<()>) {
self.inner
.handles
.lock()
.expect("handles lock")
.insert(run_id, handle);
}
pub fn end_run(&self, run_id: RunId) {
self.inner
.active
.lock()
.expect("active runs lock")
.remove(&run_id);
self.inner
.handles
.lock()
.expect("handles lock")
.remove(&run_id);
}
#[must_use]
pub fn is_run_active(&self, run_id: RunId) -> bool {
self.inner
.active
.lock()
.expect("active runs lock")
.contains(&run_id)
}
pub fn lease_client_run(&self, run_id: RunId, record_prompts: bool) -> String {
let drive_token = format!("dt_{}", uuid::Uuid::new_v4().simple());
self.inner
.client_runs
.lock()
.expect("client runs lock")
.insert(
run_id,
ClientRunLease {
drive_token: drive_token.clone(),
record_prompts,
last_seen: self.now(),
},
);
drive_token
}
pub fn touch_client_run(&self, run_id: RunId) {
let now = self.now();
if let Some(lease) = self
.inner
.client_runs
.lock()
.expect("client runs lock")
.get_mut(&run_id)
{
lease.last_seen = now;
}
}
#[must_use]
pub fn client_run_driver_live(&self, run_id: RunId) -> bool {
let now = self.now();
let leases = self.inner.client_runs.lock().expect("client runs lock");
match leases.get(&run_id) {
Some(lease) => (now - lease.last_seen).unsigned_abs() < self.inner.client_lease_ttl,
None => false,
}
}
#[must_use]
pub fn client_run(&self, run_id: RunId) -> Option<ClientRunLease> {
self.inner
.client_runs
.lock()
.expect("client runs lock")
.get(&run_id)
.cloned()
}
#[must_use]
pub fn is_client_run(&self, run_id: RunId) -> bool {
self.inner
.client_runs
.lock()
.expect("client runs lock")
.contains_key(&run_id)
}
pub fn abort_all(&self) {
let mut handles = self.inner.handles.lock().expect("handles lock");
for (_, handle) in handles.drain() {
handle.abort();
}
self.inner.active.lock().expect("active runs lock").clear();
}
}