use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use async_trait::async_trait;
use meerkat_core::EventEnvelope;
use meerkat_core::event::AgentEvent;
use meerkat_core::skills::{SkillError, SourceIdentityRegistry};
use meerkat_core::types::SessionId;
use tokio::sync::{Mutex, Notify, broadcast};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionState {
Idle,
Running,
ShuttingDown,
}
impl SessionState {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Running => "running",
Self::ShuttingDown => "shutting_down",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionInfo {
pub session_id: SessionId,
pub state: SessionState,
pub labels: BTreeMap<String, String>,
}
#[derive(Clone)]
pub struct PendingSessionEventStreams {
pub events: broadcast::Sender<EventEnvelope<AgentEvent>>,
pub receiver_dropped: Arc<Notify>,
}
pub struct PendingSessionEventStreamDrop {
pub receiver_dropped: Arc<Notify>,
}
impl Drop for PendingSessionEventStreamDrop {
fn drop(&mut self) {
self.receiver_dropped.notify_one();
}
}
#[derive(Clone, Default)]
pub struct SkillIdentityRegistryState {
pub generation: u64,
pub registry: SourceIdentityRegistry,
}
#[allow(clippy::missing_errors_doc)]
pub fn build_skill_identity_registry(
config: &meerkat_core::Config,
context_root: Option<&std::path::Path>,
user_root: Option<&std::path::Path>,
) -> Result<SourceIdentityRegistry, SkillError> {
#[cfg(not(target_arch = "wasm32"))]
{
let _ = (context_root, user_root);
config.skills.build_source_identity_registry()
}
#[cfg(target_arch = "wasm32")]
{
let _ = (context_root, user_root);
config.skills.build_source_identity_registry()
}
}
#[async_trait]
pub trait ArchiveRuntimeMcpState: Send + Sync {
async fn cleanup(&self, session_id: &SessionId);
}
#[async_trait]
pub trait ArchiveRuntimeMobState: Send + Sync {
async fn cleanup(
&self,
session_id: &SessionId,
) -> Result<(), meerkat_core::service::SessionError>;
async fn has_retained_cleanup(&self, session_id: &SessionId) -> bool;
}
#[derive(Clone)]
pub struct ArchiveRuntimeCleanup {
pub runtime_adapter: Arc<meerkat_runtime::MeerkatMachine>,
pub pending_session_event_streams:
Option<Arc<Mutex<HashMap<SessionId, PendingSessionEventStreams>>>>,
pub mcp_state: Option<Arc<dyn ArchiveRuntimeMcpState>>,
pub mob_state: Option<Arc<dyn ArchiveRuntimeMobState>>,
}
impl ArchiveRuntimeCleanup {
pub async fn has_retained_mob_cleanup(&self, session_id: &SessionId) -> bool {
if let Some(mob_state) = self.mob_state.as_ref()
&& mob_state.has_retained_cleanup(session_id).await
{
return true;
}
false
}
#[cfg(all(feature = "session-store", not(target_arch = "wasm32")))]
pub async fn archive_service(
&self,
service: &crate::PersistentSessionService<crate::service_factory::FactoryAgentBuilder>,
session_id: &SessionId,
) -> Result<(), meerkat_core::service::SessionError> {
service
.archive_with_machine_protocol(
session_id,
meerkat_session::MachineSessionArchiveProtocol::from_machine(
self.runtime_adapter.as_ref(),
),
)
.await
}
pub async fn run(
&self,
session_id: &SessionId,
) -> Result<(), meerkat_core::service::SessionError> {
self.runtime_adapter.unregister_session(session_id).await;
if let Some(streams) = self.pending_session_event_streams.as_ref() {
streams.lock().await.remove(session_id);
}
if let Some(mcp_state) = self.mcp_state.as_ref() {
mcp_state.cleanup(session_id).await;
}
if let Some(mob_state) = self.mob_state.as_ref() {
mob_state.cleanup(session_id).await?;
}
#[cfg(feature = "comms")]
self.runtime_adapter.abort_comms_drain(session_id).await;
Ok(())
}
}
#[cfg(all(feature = "session-store", not(target_arch = "wasm32")))]
mod ops {
use std::sync::Arc;
use meerkat_core::service::SessionError;
use meerkat_core::types::SessionId;
use meerkat_runtime::MeerkatMachine;
use crate::PersistentSessionService;
use crate::service_factory::FactoryAgentBuilder;
use crate::session_runtime::admission::{
StagedCapacityAdmissions, discard_staged_capacity_admission,
};
use crate::{StagedSessionRegistry, session_runtime::recovery::RecoveryContext};
pub struct RuntimeStateOps<'a> {
pub service: &'a Arc<PersistentSessionService<FactoryAgentBuilder>>,
pub staged_sessions: &'a Arc<StagedSessionRegistry>,
pub staged_capacity_admissions: &'a StagedCapacityAdmissions,
pub runtime_adapter: &'a Arc<MeerkatMachine>,
}
impl RuntimeStateOps<'_> {
pub async fn discard_live_session(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
let result = self.service.discard_live_session(session_id).await;
if result.is_ok() && !self.staged_sessions.contains(session_id).await {
discard_staged_capacity_admission(self.staged_capacity_admissions, session_id);
}
result
}
pub async fn discard_stale_live_session(&self, session_id: &SessionId) {
let _ = self.discard_live_session(session_id).await;
self.runtime_adapter.unregister_session(session_id).await;
}
pub async fn live_session_is_stale(
&self,
session_id: &SessionId,
recovery_ctx: &RecoveryContext<'_>,
) -> Result<bool, SessionError> {
if self
.service
.synchronize_live_session_from_durable_authority_if_needed(session_id)
.await?
{
return Ok(false);
}
let live = match self.service.export_live_session(session_id).await {
Ok(session) => session,
Err(SessionError::NotFound { .. }) => {
return Ok(recovery_ctx
.load_persisted_session(session_id)
.await?
.is_some());
}
Err(err) => return Err(err),
};
let Some(stored) = recovery_ctx.load_persisted_session(session_id).await? else {
return Ok(false);
};
Ok(stored.messages().len() > live.messages().len())
}
}
}
#[cfg(all(feature = "session-store", not(target_arch = "wasm32")))]
pub use ops::RuntimeStateOps;