oxios_kernel/tools/kernel/
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: {}",
169 e
170 ))),
171 }
172 }
173
174 "remove" => {
175 let id_str = params
176 .get("id")
177 .and_then(|v| v.as_str())
178 .ok_or_else(|| "remove requires 'id' parameter".to_string())?;
179
180 let job_id = match uuid::Uuid::parse_str(id_str) {
181 Ok(id) => id,
182 Err(e) => return Ok(AgentToolResult::error(format!("Invalid job ID: {e}"))),
183 };
184
185 match self.cron_scheduler.remove_job(job_id).await {
186 Ok(()) => Ok(AgentToolResult::success(format!(
187 "Cron job '{}' removed.",
188 id_str
189 ))),
190 Err(e) => Ok(AgentToolResult::error(format!(
191 "Failed to remove cron job: {}",
192 e
193 ))),
194 }
195 }
196
197 "trigger" => {
198 let id_str = params
199 .get("id")
200 .and_then(|v| v.as_str())
201 .ok_or_else(|| "trigger requires 'id' parameter".to_string())?;
202
203 let job_id = match uuid::Uuid::parse_str(id_str) {
204 Ok(id) => id,
205 Err(e) => return Ok(AgentToolResult::error(format!("Invalid job ID: {e}"))),
206 };
207
208 match self.cron_scheduler.trigger_job(job_id) {
209 Ok(job) => Ok(AgentToolResult::success(format!(
210 "Cron job '{}' ({}) triggered successfully.",
211 job.name, id_str
212 ))),
213 Err(e) => Ok(AgentToolResult::error(format!(
214 "Failed to trigger cron job: {}",
215 e
216 ))),
217 }
218 }
219
220 other => Err(format!(
221 "Unknown cron action '{}'. Valid: list, add, remove, trigger",
222 other
223 )),
224 }
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_schema_structure() {
234 let schema = json!({
235 "type": "object",
236 "properties": {
237 "action": {
238 "type": "string",
239 "enum": ["list", "add", "remove", "trigger"]
240 },
241 "id": { "type": "string" },
242 "expression": { "type": "string" },
243 "task": { "type": "string" }
244 },
245 "required": ["action"]
246 });
247
248 let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
249 assert_eq!(actions.len(), 4);
250 assert!(actions.iter().any(|a| a == "list"));
251 assert!(actions.iter().any(|a| a == "add"));
252 assert!(actions.iter().any(|a| a == "remove"));
253 assert!(actions.iter().any(|a| a == "trigger"));
254 }
255}