返回 DeepSeek-TUI-2026
automation.rs
根目录 / crates / tui / src / tools / automation.rs
1 //! Model-visible automation tools over `AutomationManager`.
2
3 use std::path::PathBuf;
4
5 use async_trait::async_trait;
6 use serde_json::{Value, json};
7
8 use crate::automation_manager::{
9 AutomationStatus, CreateAutomationRequest, UpdateAutomationRequest,
10 };
11 use crate::tools::spec::{
12 ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
13 optional_str, optional_u64, required_str,
14 };
15
16 pub struct AutomationCreateTool;
17 pub struct AutomationListTool;
18 pub struct AutomationReadTool;
19 pub struct AutomationUpdateTool;
20 pub struct AutomationPauseTool;
21 pub struct AutomationResumeTool;
22 pub struct AutomationDeleteTool;
23 pub struct AutomationRunTool;
24
25 #[async_trait]
26 impl ToolSpec for AutomationCreateTool {
27 fn name(&self) -> &'static str {
28 "automation_create"
29 }
30
31 fn description(&self) -> &'static str {
32 "Create a durable scheduled automation. Creation requires approval and recurrence is constrained to supported HOURLY/WEEKLY RRULE forms. Runs enqueue normal durable tasks."
33 }
34
35 fn input_schema(&self) -> Value {
36 json!({
37 "type": "object",
38 "properties": {
39 "name": { "type": "string" },
40 "prompt": { "type": "string" },
41 "rrule": {
42 "type": "string",
43 "description": "Supported: FREQ=HOURLY;INTERVAL=N[;BYDAY=MO,TU] or FREQ=WEEKLY;BYDAY=MO;BYHOUR=9;BYMINUTE=30"
44 },
45 "cwds": { "type": "array", "items": { "type": "string" } },
46 "paused": { "type": "boolean", "default": false }
47 },
48 "required": ["name", "prompt", "rrule"],
49 "additionalProperties": false
50 })
51 }
52
53 fn capabilities(&self) -> Vec<ToolCapability> {
54 vec![ToolCapability::RequiresApproval]
55 }
56
57 fn approval_requirement(&self) -> ApprovalRequirement {
58 ApprovalRequirement::Required
59 }
60
61 async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
62 let manager = context
63 .runtime
64 .automations
65 .as_ref()
66 .ok_or_else(|| ToolError::not_available("AutomationManager is not attached"))?;
67 let manager = manager.lock().await;
68 let req = CreateAutomationRequest {
69 name: required_str(&input, "name")?.to_string(),
70 prompt: required_str(&input, "prompt")?.to_string(),
71 rrule: required_str(&input, "rrule")?.to_string(),
72 cwds: string_array(&input, "cwds")?
73 .into_iter()
74 .map(PathBuf::from)
75 .collect(),
76 status: Some(
77 if input
78 .get("paused")
79 .and_then(Value::as_bool)
80 .unwrap_or(false)
81 {
82 AutomationStatus::Paused
83 } else {
84 AutomationStatus::Active
85 },
86 ),
87 };
88 let automation = manager
89 .create_automation(req)
90 .map_err(|e| ToolError::execution_failed(e.to_string()))?;
91 ToolResult::json(&automation).map_err(|e| ToolError::execution_failed(e.to_string()))
92 }
93 }
94
95 #[async_trait]
96 impl ToolSpec for AutomationListTool {
97 fn name(&self) -> &'static str {
98 "automation_list"
99 }
100
101 fn description(&self) -> &'static str {
102 "List durable automations with status, next run, and last run timestamps."
103 }
104
105 fn input_schema(&self) -> Value {
106 json!({
107 "type": "object",
108 "properties": {
109 "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
110 },
111 "additionalProperties": false
112 })
113 }
114
115 fn capabilities(&self) -> Vec<ToolCapability> {
116 vec![ToolCapability::ReadOnly]
117 }
118
119 async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
120 let manager = context
121 .runtime
122 .automations
123 .as_ref()
124 .ok_or_else(|| ToolError::not_available("AutomationManager is not attached"))?;
125 let manager = manager.lock().await;
126 let mut automations = manager
127 .list_automations()
128 .map_err(|e| ToolError::execution_failed(e.to_string()))?;
129 automations.truncate(optional_u64(&input, "limit", 50).clamp(1, 100) as usize);
130 ToolResult::json(&automations).map_err(|e| ToolError::execution_failed(e.to_string()))
131 }
132 }
133
134 #[async_trait]
135 impl ToolSpec for AutomationReadTool {
136 fn name(&self) -> &'static str {
137 "automation_read"
138 }
139
140 fn description(&self) -> &'static str {
141 "Read one durable automation plus recent run records."
142 }
143
144 fn input_schema(&self) -> Value {
145 automation_id_schema(true)
146 }
147
148 fn capabilities(&self) -> Vec<ToolCapability> {
149 vec![ToolCapability::ReadOnly]
150 }
151
152 fn approval_requirement(&self) -> ApprovalRequirement {
153 ApprovalRequirement::Auto
154 }
155
156 async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
157 let manager = context
158 .runtime
159 .automations
160 .as_ref()
161 .ok_or_else(|| ToolError::not_available("AutomationManager is not attached"))?;
162 let manager = manager.lock().await;
163 let id = required_str(&input, "automation_id")?;
164 let automation = manager
165 .get_automation(id)
166 .map_err(|e| ToolError::execution_failed(e.to_string()))?;
167 let runs = manager
168 .list_runs(id, Some(20))
169 .map_err(|e| ToolError::execution_failed(e.to_string()))?;
170 ToolResult::json(&json!({ "automation": automation, "recent_runs": runs }))
171 .map_err(|e| ToolError::execution_failed(e.to_string()))
172 }
173 }
174
175 #[async_trait]
176 impl ToolSpec for AutomationUpdateTool {
177 fn name(&self) -> &'static str {
178 "automation_update"
179 }
180
181 fn description(&self) -> &'static str {
182 "Update a durable automation. Requires approval; recurrence remains constrained to supported RRULE forms."
183 }
184
185 fn input_schema(&self) -> Value {
186 json!({
187 "type": "object",
188 "properties": {
189 "automation_id": { "type": "string" },
190 "name": { "type": "string" },
191 "prompt": { "type": "string" },
192 "rrule": { "type": "string" },
193 "cwds": { "type": "array", "items": { "type": "string" } },
194 "status": { "type": "string", "enum": ["active", "paused"] }
195 },
196 "required": ["automation_id"],
197 "additionalProperties": false
198 })
199 }
200
201 fn capabilities(&self) -> Vec<ToolCapability> {
202 vec![ToolCapability::RequiresApproval]
203 }
204
205 fn approval_requirement(&self) -> ApprovalRequirement {
206 ApprovalRequirement::Required
207 }
208
209 async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
210 let manager = context
211 .runtime
212 .automations
213 .as_ref()
214 .ok_or_else(|| ToolError::not_available("AutomationManager is not attached"))?;
215 let manager = manager.lock().await;
216 let status = optional_str(&input, "status").map(|value| match value {
217 "paused" => AutomationStatus::Paused,
218 _ => AutomationStatus::Active,
219 });
220 let req = UpdateAutomationRequest {
221 name: optional_str(&input, "name").map(ToString::to_string),
222 prompt: optional_str(&input, "prompt").map(ToString::to_string),
223 rrule: optional_str(&input, "rrule").map(ToString::to_string),
224 cwds: if input.get("cwds").is_some() {
225 Some(
226 string_array(&input, "cwds")?
227 .into_iter()
228 .map(PathBuf::from)
229 .collect(),
230 )
231 } else {
232 None
233 },
234 status,
235 };
236 let automation = manager
237 .update_automation(required_str(&input, "automation_id")?, req)
238 .map_err(|e| ToolError::execution_failed(e.to_string()))?;
239 ToolResult::json(&automation).map_err(|e| ToolError::execution_failed(e.to_string()))
240 }
241 }
242
243 macro_rules! write_automation_tool {
244 ($ty:ident, $name:literal, $desc:literal, $method:ident) => {
245 #[async_trait]
246 impl ToolSpec for $ty {
247 fn name(&self) -> &'static str {
248 $name
249 }
250 fn description(&self) -> &'static str {
251 $desc
252 }
253 fn input_schema(&self) -> Value {
254 automation_id_schema(true)
255 }
256 fn capabilities(&self) -> Vec<ToolCapability> {
257 vec![ToolCapability::RequiresApproval]
258 }
259 fn approval_requirement(&self) -> ApprovalRequirement {
260 ApprovalRequirement::Required
261 }
262 async fn execute(
263 &self,
264 input: Value,
265 context: &ToolContext,
266 ) -> Result<ToolResult, ToolError> {
267 let manager =
268 context.runtime.automations.as_ref().ok_or_else(|| {
269 ToolError::not_available("AutomationManager is not attached")
270 })?;
271 let manager = manager.lock().await;
272 let automation = manager
273 .$method(required_str(&input, "automation_id")?)
274 .map_err(|e| ToolError::execution_failed(e.to_string()))?;
275 ToolResult::json(&automation)
276 .map_err(|e| ToolError::execution_failed(e.to_string()))
277 }
278 }
279 };
280 }
281
282 write_automation_tool!(
283 AutomationPauseTool,
284 "automation_pause",
285 "Pause a durable automation. Requires approval.",
286 pause_automation
287 );
288 write_automation_tool!(
289 AutomationResumeTool,
290 "automation_resume",
291 "Resume a paused durable automation. Requires approval.",
292 resume_automation
293 );
294 write_automation_tool!(
295 AutomationDeleteTool,
296 "automation_delete",
297 "Delete a durable automation and its run history. Requires approval.",
298 delete_automation
299 );
300
301 #[async_trait]
302 impl ToolSpec for AutomationRunTool {
303 fn name(&self) -> &'static str {
304 "automation_run"
305 }
306
307 fn description(&self) -> &'static str {
308 "Run an automation now. The run enqueues a normal durable task and returns linked task/thread/turn ids as they become available."
309 }
310
311 fn input_schema(&self) -> Value {
312 automation_id_schema(true)
313 }
314
315 fn capabilities(&self) -> Vec<ToolCapability> {
316 vec![ToolCapability::RequiresApproval]
317 }
318
319 fn approval_requirement(&self) -> ApprovalRequirement {
320 ApprovalRequirement::Required
321 }
322
323 async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
324 let manager = context
325 .runtime
326 .automations
327 .as_ref()
328 .ok_or_else(|| ToolError::not_available("AutomationManager is not attached"))?;
329 let task_manager = context
330 .runtime
331 .task_manager
332 .as_ref()
333 .ok_or_else(|| ToolError::not_available("TaskManager is not attached"))?;
334 let manager = manager.lock().await;
335 let run = manager
336 .run_now(required_str(&input, "automation_id")?, task_manager)
337 .await
338 .map_err(|e| ToolError::execution_failed(e.to_string()))?;
339 ToolResult::json(&run).map_err(|e| ToolError::execution_failed(e.to_string()))
340 }
341 }
342
343 fn automation_id_schema(require_id: bool) -> Value {
344 let mut schema = json!({
345 "type": "object",
346 "properties": {
347 "automation_id": { "type": "string" }
348 },
349 "additionalProperties": false
350 });
351 if require_id {
352 schema["required"] = json!(["automation_id"]);
353 }
354 schema
355 }
356
357 fn string_array(input: &Value, field: &str) -> Result<Vec<String>, ToolError> {
358 Ok(input
359 .get(field)
360 .and_then(Value::as_array)
361 .map(|items| {
362 items
363 .iter()
364 .filter_map(Value::as_str)
365 .map(ToString::to_string)
366 .collect::<Vec<_>>()
367 })
368 .unwrap_or_default())
369 }
370
371 #[cfg(test)]
372 mod tests {
373 use super::*;
374 use crate::tools::spec::ToolSpec;
375
376 #[test]
377 fn create_schema_exposes_rrule() {
378 let schema = AutomationCreateTool.input_schema();
379 assert!(schema["properties"]["rrule"].is_object());
380 assert_eq!(schema["required"][0], "name");
381 }
382 }
383
383 lines RUST