Skip to main content

brainos_backends/
scheduling.rs

1//! Scheduling backend — persists scheduled intents to SQLite.
2
3/// The capabilities this backend declares: schedule create/cancel, gated on
4/// `actions.scheduling.enabled`.
5pub 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}