brainos_backends/
scheduling.rs1pub fn capabilities(config: &brain::BrainConfig) -> Vec<intent::ToolDescriptor> {
6 use crate::capabilities::{backend, native, usage};
7 use intent::ToolAnnotations;
8 if !config.actions.scheduling.enabled {
9 return Vec::new();
10 }
11 let sched = || backend("scheduling");
12 vec![
13 native(
14 "schedule",
15 "create",
16 sched(),
17 ToolAnnotations::default(),
18 usage(
19 "The user wants something to happen later or on a recurring cadence.",
20 "For one-shot actions to run right now.",
21 &["actions.scheduling.enabled = true"],
22 "free / local row",
23 "\"Remind me to rotate the certs every 90 days\"",
24 ),
25 ),
26 native(
27 "schedule",
28 "cancel",
29 sched(),
30 ToolAnnotations::default(),
31 usage(
32 "The user wants to stop a previously scheduled item.",
33 "When the schedule id / description is unknown.",
34 &["actions.scheduling.enabled = true"],
35 "free / local row",
36 "\"Cancel the nightly backup reminder\"",
37 ),
38 ),
39 ]
40}
41
42#[derive(Clone)]
43pub struct DefaultSchedulingBackend {
44 pub db: storage::SqlitePool,
45 pub mode: brain::config::SchedulingMode,
46}
47
48#[async_trait::async_trait]
49impl cortex::actions::SchedulingBackend for DefaultSchedulingBackend {
50 async fn schedule(
51 &self,
52 description: &str,
53 cron: Option<&str>,
54 namespace: &str,
55 ) -> Result<cortex::actions::ScheduleOutcome, cortex::actions::ActionError> {
56 if self.mode != brain::config::SchedulingMode::PersistOnly {
57 return Err(cortex::actions::ActionError::InvalidArguments(format!(
58 "Unsupported scheduling mode: {:?}",
59 self.mode
60 )));
61 }
62
63 let metadata = serde_json::json!({
64 "source": "action_dispatcher",
65 "mode": "persist_only",
66 })
67 .to_string();
68
69 let schedule_id = self
70 .db
71 .insert_scheduled_intent(description, cron, namespace, Some(&metadata))
72 .map_err(|e| cortex::actions::ActionError::ExecutionFailed(e.to_string()))?;
73
74 Ok(cortex::actions::ScheduleOutcome {
75 schedule_id,
76 status: "scheduled".to_string(),
77 })
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[tokio::test]
86 async fn test_scheduling_backend_persists_intent() {
87 let db = storage::SqlitePool::open_memory().unwrap();
88 let backend = DefaultSchedulingBackend {
89 db: db.clone(),
90 mode: brain::config::SchedulingMode::PersistOnly,
91 };
92
93 let outcome = cortex::actions::SchedulingBackend::schedule(
94 &backend,
95 "ship release",
96 Some("0 9 * * 1-5"),
97 "work",
98 )
99 .await
100 .unwrap();
101
102 assert_eq!(outcome.status, "scheduled");
103 let intents = db.list_scheduled_intents(Some("work")).unwrap();
104 assert_eq!(intents.len(), 1);
105 assert_eq!(intents[0].id, outcome.schedule_id);
106 assert_eq!(intents[0].description, "ship release");
107 }
108}