deepseek/agent/builtin_tools/
cron_list.rs1use 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 #[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 #[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}