Skip to main content

oxios_kernel/tools/builtin/
cron_tool.rs

1//! Cron tool — wraps `InfraApi` cron methods behind the `AgentTool` interface.
2//!
3//! Provides agents with cron scheduling capabilities.
4//! Actions: list, add, remove, trigger.
5//!
6//! ## Example
7//!
8//! ```json
9//! { "action": "list" }
10//! { "action": "add", "expression": "0 */6 * * *", "task": "Review open PRs" }
11//! { "action": "remove", "id": "job-uuid" }
12//! { "action": "trigger", "id": "job-uuid" }
13//! ```
14
15use async_trait::async_trait;
16use std::sync::Arc;
17
18use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
19use serde_json::{Value, json};
20
21use crate::cron::CronScheduler;
22use crate::kernel_handle::KernelHandle;
23
24/// Agent tool for cron scheduling.
25///
26/// Wraps the cron-related methods of the `InfraApi` domain. Allows agents
27/// to list, create, remove, and manually trigger cron jobs.
28///
29/// ## Actions
30///
31/// | Action    | Description              | Required params           | Optional params |
32/// |-----------|--------------------------|---------------------------|-----------------|
33/// | `list`    | List all cron jobs       | —                         | —               |
34/// | `add`     | Add a new cron job       | `expression`, `task`      | —               |
35/// | `remove`  | Remove a cron job        | `id`                      | —               |
36/// | `trigger` | Manually trigger a job   | `id`                      | —               |
37pub struct CronTool {
38    cron_scheduler: Arc<CronScheduler>,
39}
40
41impl CronTool {
42    /// Create a new `CronTool` from a `KernelHandle`.
43    ///
44    /// Extracts the `CronScheduler` Arc from the kernel's Infra API.
45    pub fn from_kernel(kernel: &KernelHandle) -> Self {
46        Self {
47            cron_scheduler: kernel.infra.cron_scheduler.clone(),
48        }
49    }
50}
51
52impl std::fmt::Debug for CronTool {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("CronTool").finish()
55    }
56}
57
58#[async_trait]
59
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<tokio::sync::oneshot::Receiver<()>>,
105        _ctx: &ToolContext,
106    ) -> Result<AgentToolResult, oxi_sdk::ToolError> {
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) => {
182                        return Ok(AgentToolResult::error(format!("Invalid job ID: {e}")));
183                    }
184                };
185
186                match self.cron_scheduler.remove_job(job_id).await {
187                    Ok(()) => Ok(AgentToolResult::success(format!(
188                        "Cron job '{id_str}' removed."
189                    ))),
190                    Err(e) => Ok(AgentToolResult::error(format!(
191                        "Failed to remove cron job: {e}"
192                    ))),
193                }
194            }
195
196            "trigger" => {
197                let id_str = params
198                    .get("id")
199                    .and_then(|v| v.as_str())
200                    .ok_or_else(|| "trigger requires 'id' parameter".to_string())?;
201
202                let job_id = match uuid::Uuid::parse_str(id_str) {
203                    Ok(id) => id,
204                    Err(e) => {
205                        return Ok(AgentToolResult::error(format!("Invalid job ID: {e}")));
206                    }
207                };
208
209                match self.cron_scheduler.trigger_job(job_id) {
210                    Ok(job) => Ok(AgentToolResult::success(format!(
211                        "Cron job '{}' ({}) triggered successfully.",
212                        job.name, id_str
213                    ))),
214                    Err(e) => Ok(AgentToolResult::error(format!(
215                        "Failed to trigger cron job: {e}"
216                    ))),
217                }
218            }
219
220            other => Err(format!(
221                "Unknown cron action '{other}'. Valid: list, add, remove, trigger"
222            )),
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_schema_structure() {
233        let schema = json!({
234            "type": "object",
235            "properties": {
236                "action": {
237                    "type": "string",
238                    "enum": ["list", "add", "remove", "trigger"]
239                },
240                "id": { "type": "string" },
241                "expression": { "type": "string" },
242                "task": { "type": "string" }
243            },
244            "required": ["action"]
245        });
246
247        let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
248        assert_eq!(actions.len(), 4);
249        assert!(actions.iter().any(|a| a == "list"));
250        assert!(actions.iter().any(|a| a == "add"));
251        assert!(actions.iter().any(|a| a == "remove"));
252        assert!(actions.iter().any(|a| a == "trigger"));
253    }
254}