oxios_kernel/tools/builtin/
cron_tool.rs1use std::sync::Arc;
16
17use async_trait::async_trait;
18use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
19use serde_json::{json, Value};
20use tokio::sync::oneshot;
21
22use crate::cron::CronScheduler;
23use crate::kernel_handle::KernelHandle;
24
25pub struct CronTool {
39 cron_scheduler: Arc<CronScheduler>,
40}
41
42impl CronTool {
43 pub fn from_kernel(kernel: &KernelHandle) -> Self {
47 Self {
48 cron_scheduler: kernel.infra.cron_scheduler.clone(),
49 }
50 }
51}
52
53impl std::fmt::Debug for CronTool {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("CronTool").finish()
56 }
57}
58
59#[async_trait]
60impl AgentTool for CronTool {
61 fn name(&self) -> &str {
62 "cron"
63 }
64
65 fn label(&self) -> &str {
66 "Cron"
67 }
68
69 fn description(&self) -> &'static str {
70 "Manage cron jobs — schedule recurring tasks. \
71 Actions: list, add, remove, trigger."
72 }
73
74 fn parameters_schema(&self) -> Value {
75 json!({
76 "type": "object",
77 "properties": {
78 "action": {
79 "type": "string",
80 "enum": ["list", "add", "remove", "trigger"],
81 "description": "Cron operation to perform"
82 },
83 "id": {
84 "type": "string",
85 "description": "Job UUID (required for remove and trigger)"
86 },
87 "expression": {
88 "type": "string",
89 "description": "Cron expression, e.g. '0 */6 * * *' (add action only)"
90 },
91 "task": {
92 "type": "string",
93 "description": "Goal description for the scheduled agent (add action only)"
94 }
95 },
96 "required": ["action"]
97 })
98 }
99
100 async fn execute(
101 &self,
102 _tool_call_id: &str,
103 params: Value,
104 _signal: Option<oneshot::Receiver<()>>,
105 _ctx: &ToolContext,
106 ) -> Result<AgentToolResult, String> {
107 let action = params
108 .get("action")
109 .and_then(|v| v.as_str())
110 .ok_or_else(|| "Missing required parameter: action".to_string())?;
111
112 match action {
113 "list" => {
114 let jobs = self.cron_scheduler.list_jobs();
115 if jobs.is_empty() {
116 return Ok(AgentToolResult::success("No cron jobs defined."));
117 }
118
119 let display: Vec<Value> = jobs
120 .iter()
121 .map(|job| {
122 json!({
123 "id": job.id.to_string(),
124 "name": job.name,
125 "schedule": job.schedule,
126 "goal": job.goal,
127 "enabled": job.enabled,
128 "run_count": job.run_count,
129 "last_success": job.last_success,
130 })
131 })
132 .collect();
133
134 Ok(AgentToolResult::success(
135 serde_json::to_string_pretty(
136 &json!({ "jobs": display, "count": display.len() }),
137 )
138 .unwrap_or_default(),
139 ))
140 }
141
142 "add" => {
143 let expression = params
144 .get("expression")
145 .and_then(|v| v.as_str())
146 .ok_or_else(|| "add requires 'expression' parameter".to_string())?;
147 let task = params
148 .get("task")
149 .and_then(|v| v.as_str())
150 .ok_or_else(|| "add requires 'task' parameter".to_string())?;
151
152 let job = crate::cron::CronJob::new(
153 format!("job_{}", uuid::Uuid::new_v4()),
154 expression.to_string(),
155 task.to_string(),
156 );
157
158 match self.cron_scheduler.add_job(job).await {
159 Ok(job_id) => Ok(AgentToolResult::success(
160 serde_json::to_string(&json!({
161 "job_id": job_id.to_string(),
162 "schedule": expression,
163 "goal": task,
164 }))
165 .unwrap_or_default(),
166 )),
167 Err(e) => Ok(AgentToolResult::error(format!(
168 "Failed to add cron job: {e}"
169 ))),
170 }
171 }
172
173 "remove" => {
174 let id_str = params
175 .get("id")
176 .and_then(|v| v.as_str())
177 .ok_or_else(|| "remove requires 'id' parameter".to_string())?;
178
179 let job_id = match uuid::Uuid::parse_str(id_str) {
180 Ok(id) => id,
181 Err(e) => return Ok(AgentToolResult::error(format!("Invalid job ID: {e}"))),
182 };
183
184 match self.cron_scheduler.remove_job(job_id).await {
185 Ok(()) => Ok(AgentToolResult::success(format!(
186 "Cron job '{id_str}' removed."
187 ))),
188 Err(e) => Ok(AgentToolResult::error(format!(
189 "Failed to remove cron job: {e}"
190 ))),
191 }
192 }
193
194 "trigger" => {
195 let id_str = params
196 .get("id")
197 .and_then(|v| v.as_str())
198 .ok_or_else(|| "trigger requires 'id' parameter".to_string())?;
199
200 let job_id = match uuid::Uuid::parse_str(id_str) {
201 Ok(id) => id,
202 Err(e) => return Ok(AgentToolResult::error(format!("Invalid job ID: {e}"))),
203 };
204
205 match self.cron_scheduler.trigger_job(job_id) {
206 Ok(job) => Ok(AgentToolResult::success(format!(
207 "Cron job '{}' ({}) triggered successfully.",
208 job.name, id_str
209 ))),
210 Err(e) => Ok(AgentToolResult::error(format!(
211 "Failed to trigger cron job: {e}"
212 ))),
213 }
214 }
215
216 other => Err(format!(
217 "Unknown cron action '{other}'. Valid: list, add, remove, trigger"
218 )),
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_schema_structure() {
229 let schema = json!({
230 "type": "object",
231 "properties": {
232 "action": {
233 "type": "string",
234 "enum": ["list", "add", "remove", "trigger"]
235 },
236 "id": { "type": "string" },
237 "expression": { "type": "string" },
238 "task": { "type": "string" }
239 },
240 "required": ["action"]
241 });
242
243 let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
244 assert_eq!(actions.len(), 4);
245 assert!(actions.iter().any(|a| a == "list"));
246 assert!(actions.iter().any(|a| a == "add"));
247 assert!(actions.iter().any(|a| a == "remove"));
248 assert!(actions.iter().any(|a| a == "trigger"));
249 }
250}