Skip to main content

deepseek/agent/builtin_tools/
cron_list.rs

1//! `CronList` tool — return all scheduled tasks.
2
3use std::sync::{Arc, Mutex};
4
5use async_trait::async_trait;
6use serde_json::{json, Value};
7
8use crate::agent::scheduler::{Schedule, Scheduler};
9use crate::agent::tool::{Tool, ToolDefinition};
10
11pub struct CronListTool {
12    pub scheduler: Arc<Mutex<Scheduler>>,
13}
14
15impl CronListTool {
16    pub fn new(scheduler: Arc<Mutex<Scheduler>>) -> Self {
17        Self { scheduler }
18    }
19}
20
21#[async_trait]
22impl Tool for CronListTool {
23    fn name(&self) -> &str {
24        "CronList"
25    }
26
27    fn definition(&self) -> ToolDefinition {
28        ToolDefinition {
29            name: self.name().to_string(),
30            description: "List every scheduled task in the current session, with \
31                          schedule, prompt, next fire time (UTC), and expiry."
32                .into(),
33            parameters: json!({
34                "type": "object",
35                "properties": {},
36            }),
37        }
38    }
39
40    fn read_only_hint(&self) -> bool {
41        true
42    }
43
44    async fn call_json(&self, _args: Value) -> Result<String, String> {
45        let sched = self
46            .scheduler
47            .lock()
48            .map_err(|_| "CronList: scheduler lock poisoned".to_string())?;
49
50        let tasks: Vec<_> = sched
51            .list()
52            .into_iter()
53            .map(|t| {
54                let schedule = match &t.schedule {
55                    Schedule::Cron(c) => json!({"kind": "cron", "expr": c.as_str()}),
56                    Schedule::Once { at } => json!({"kind": "once", "at": at}),
57                    Schedule::Dynamic => json!({"kind": "dynamic"}),
58                };
59                json!({
60                    "task_id": t.id.as_str(),
61                    "schedule": schedule,
62                    "prompt": t.prompt,
63                    "recurring": t.recurring,
64                    "next_fire": t.next_fire,
65                    "expires_at": t.expires_at,
66                    "created_at": t.created_at,
67                })
68            })
69            .collect();
70
71        Ok(serde_json::to_string(&json!({
72            "session_id": sched.session_id(),
73            "count": tasks.len(),
74            "cap": sched.cap(),
75            "disabled": sched.is_disabled(),
76            "tasks": tasks,
77        }))
78        .unwrap())
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::agent::scheduler::Scheduler;
86    use chrono::{Duration, Utc};
87    use serde_json::Value;
88
89    fn fresh_sched(session: &str) -> Arc<Mutex<Scheduler>> {
90        std::env::remove_var("CLAUDE_CODE_DISABLE_CRON");
91        std::env::remove_var("DEEPSEEK_LOOP_DISABLE_CRON");
92        Arc::new(Mutex::new(Scheduler::new(session)))
93    }
94
95    #[test]
96    fn definition_advertises_read_only_and_no_args() {
97        let sched = fresh_sched("def");
98        let tool = CronListTool::new(sched);
99        let def = tool.definition();
100        assert_eq!(def.name, "CronList");
101        assert!(tool.read_only_hint());
102        let props = def.parameters.get("properties").unwrap();
103        assert!(props.as_object().unwrap().is_empty());
104    }
105
106    #[tokio::test]
107    async fn empty_scheduler_lists_zero_tasks() {
108        let sched = fresh_sched("empty");
109        let tool = CronListTool::new(sched);
110        let raw = tool.call_json(json!({})).await.unwrap();
111        let v: Value = serde_json::from_str(&raw).unwrap();
112        assert_eq!(v["count"].as_u64().unwrap(), 0);
113        assert_eq!(v["session_id"].as_str().unwrap(), "empty");
114        assert_eq!(v["disabled"].as_bool().unwrap(), false);
115        assert!(v["cap"].as_u64().unwrap() >= 50);
116        assert_eq!(v["tasks"].as_array().unwrap().len(), 0);
117    }
118
119    #[tokio::test]
120    async fn lists_all_three_schedule_kinds() {
121        let sched = fresh_sched("kinds");
122        {
123            let mut s = sched.lock().unwrap();
124            let cron = CronExpr_for_test("*/10 * * * *");
125            s.create(Schedule::Cron(Box::new(cron)), "ping cron", true)
126                .unwrap();
127            let when = Utc::now() + Duration::hours(1);
128            s.create(Schedule::Once { at: when }, "one shot", false)
129                .unwrap();
130            s.create(Schedule::Dynamic, "dyn", true).unwrap();
131        }
132        let tool = CronListTool::new(sched);
133        let raw = tool.call_json(json!({})).await.unwrap();
134        let v: Value = serde_json::from_str(&raw).unwrap();
135        assert_eq!(v["count"].as_u64().unwrap(), 3);
136        let kinds: Vec<&str> = v["tasks"]
137            .as_array()
138            .unwrap()
139            .iter()
140            .map(|t| t["schedule"]["kind"].as_str().unwrap())
141            .collect();
142        assert!(kinds.contains(&"cron"));
143        assert!(kinds.contains(&"once"));
144        assert!(kinds.contains(&"dynamic"));
145    }
146
147    // (`disabled` flag JSON surface is trivial — env-var driven Scheduler
148    // construction races with parallel tests that also touch the env var,
149    // and `disabled` is module-private. The env-var detection itself is
150    // covered by `mod.rs::schedule_is_disabled_when_either_var_set`.)
151
152    #[tokio::test]
153    async fn task_payload_includes_id_prompt_recurring() {
154        let sched = fresh_sched("payload");
155        {
156            let mut s = sched.lock().unwrap();
157            let cron = CronExpr_for_test("0 9 * * *");
158            s.create(Schedule::Cron(Box::new(cron)), "morning check", true)
159                .unwrap();
160        }
161        let tool = CronListTool::new(sched);
162        let raw = tool.call_json(json!({})).await.unwrap();
163        let v: Value = serde_json::from_str(&raw).unwrap();
164        let task = &v["tasks"][0];
165        assert_eq!(task["task_id"].as_str().unwrap().len(), 8);
166        assert_eq!(task["prompt"].as_str().unwrap(), "morning check");
167        assert_eq!(task["recurring"].as_bool().unwrap(), true);
168        assert!(task["next_fire"].is_string());
169    }
170
171    // Helper kept inside tests so it doesn't widen the public surface.
172    #[allow(non_snake_case)]
173    fn CronExpr_for_test(expr: &str) -> crate::agent::scheduler::CronExpr {
174        crate::agent::scheduler::CronExpr::parse(expr).unwrap()
175    }
176}