use std::path::Path;
use std::sync::Arc;
use meerkat::surface::{
NoopScheduleMobHost, ScheduleHostHandle, SurfaceScheduleMobHost,
spawn_runtime_backed_schedule_host_with_mobs,
};
use meerkat::{
Config, FactoryAgentBuilder, PersistentSessionService, ScheduleService, ScheduleToolDispatcher,
SessionAgentBuilder, SqliteScheduleStore,
};
use meerkat_core::service::SessionBuildOptions;
use meerkat_mob_mcp::{MobMcpScheduleHost, MobMcpState};
use meerkat_runtime::MeerkatMachine;
pub const SCHEDULE_STORE_FILE: &str = "schedule.sqlite";
#[must_use]
pub fn attach_schedule_tools(
builder: &FactoryAgentBuilder,
state_dir: &Path,
) -> Option<ScheduleService> {
let path = state_dir.join(SCHEDULE_STORE_FILE);
let store = match SqliteScheduleStore::open(&path) {
Ok(store) => store,
Err(error) => {
tracing::warn!(
path = %path.display(),
error = %error,
"failed to open schedule store; schedule tools disabled for this gateway",
);
return None;
}
};
let service = ScheduleService::new(Arc::new(store));
meerkat::surface::set_default_schedule_tools(
builder,
Some(Arc::new(ScheduleToolDispatcher::new(service.clone()))),
);
Some(service)
}
#[must_use]
pub fn spawn_schedule_host<B: SessionAgentBuilder + 'static>(
service: Arc<PersistentSessionService<B>>,
adapter: Arc<MeerkatMachine>,
schedule_service: ScheduleService,
mob_state: Option<Arc<MobMcpState>>,
owner_id: impl Into<String>,
) -> Option<ScheduleHostHandle> {
let mob_host: Arc<dyn SurfaceScheduleMobHost> = match mob_state {
Some(state) => Arc::new(MobMcpScheduleHost::new(state)),
None => Arc::new(NoopScheduleMobHost::new(
"scheduled mob targets are not supported: no mob runtime",
)),
};
spawn_runtime_backed_schedule_host_with_mobs(
service,
adapter,
Config::default(),
schedule_service,
SessionBuildOptions::default(),
mob_host,
owner_id,
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use meerkat::AgentFactory;
#[test]
fn attach_schedule_tools_populates_the_builder_slot_and_opens_the_store() {
let dir = tempfile::tempdir().expect("tempdir");
let factory = AgentFactory::new(dir.path());
let builder = FactoryAgentBuilder::new(factory, Config::default());
assert!(
builder.default_schedule_tools.read().unwrap().is_none(),
"slot should start empty"
);
let service = attach_schedule_tools(&builder, dir.path());
assert!(service.is_some(), "a durable schedule service is created");
assert!(
builder.default_schedule_tools.read().unwrap().is_some(),
"the dispatcher is installed so override_schedule=Enable members compose schedule tools",
);
assert!(
dir.path().join(SCHEDULE_STORE_FILE).exists(),
"the durable store file is created",
);
}
}