Skip to main content

agent_code_lib/tools/
tasks.rs

1//! Task management tools.
2//!
3//! Full task lifecycle: create, update, get, list, stop, and read output.
4//! Tasks are tracked in the background task manager and persisted to
5//! the cache directory for retrieval.
6
7use async_trait::async_trait;
8use serde_json::json;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use super::{Tool, ToolContext, ToolResult};
12use crate::error::ToolError;
13
14static TASK_COUNTER: AtomicU64 = AtomicU64::new(1);
15
16pub struct TaskCreateTool;
17
18#[async_trait]
19impl Tool for TaskCreateTool {
20    fn name(&self) -> &'static str {
21        "TaskCreate"
22    }
23
24    fn description(&self) -> &'static str {
25        "Create a task to track progress on a piece of work."
26    }
27
28    fn input_schema(&self) -> serde_json::Value {
29        json!({
30            "type": "object",
31            "required": ["description"],
32            "properties": {
33                "description": {
34                    "type": "string",
35                    "description": "What needs to be done"
36                },
37                "status": {
38                    "type": "string",
39                    "enum": ["pending", "in_progress", "completed"],
40                    "default": "pending"
41                }
42            }
43        })
44    }
45
46    fn is_read_only(&self) -> bool {
47        true
48    }
49
50    fn is_concurrency_safe(&self) -> bool {
51        true
52    }
53
54    async fn call(
55        &self,
56        input: serde_json::Value,
57        _ctx: &ToolContext,
58    ) -> Result<ToolResult, ToolError> {
59        let description = input
60            .get("description")
61            .and_then(|v| v.as_str())
62            .ok_or_else(|| ToolError::InvalidInput("'description' is required".into()))?;
63
64        let status = input
65            .get("status")
66            .and_then(|v| v.as_str())
67            .unwrap_or("pending");
68
69        let id = TASK_COUNTER.fetch_add(1, Ordering::Relaxed);
70
71        Ok(ToolResult::success(format!(
72            "Task #{id} created: {description} [{status}]"
73        )))
74    }
75}
76
77pub struct TaskUpdateTool;
78
79#[async_trait]
80impl Tool for TaskUpdateTool {
81    fn name(&self) -> &'static str {
82        "TaskUpdate"
83    }
84
85    fn description(&self) -> &'static str {
86        "Update a task's status (pending, in_progress, completed)."
87    }
88
89    fn input_schema(&self) -> serde_json::Value {
90        json!({
91            "type": "object",
92            "required": ["id", "status"],
93            "properties": {
94                "id": { "type": "string", "description": "Task ID" },
95                "status": {
96                    "type": "string",
97                    "enum": ["pending", "in_progress", "completed"]
98                }
99            }
100        })
101    }
102
103    fn is_read_only(&self) -> bool {
104        true
105    }
106
107    fn is_concurrency_safe(&self) -> bool {
108        true
109    }
110
111    async fn call(
112        &self,
113        input: serde_json::Value,
114        _ctx: &ToolContext,
115    ) -> Result<ToolResult, ToolError> {
116        let id = input
117            .get("id")
118            .and_then(|v| v.as_str())
119            .ok_or_else(|| ToolError::InvalidInput("'id' is required".into()))?;
120
121        let status = input
122            .get("status")
123            .and_then(|v| v.as_str())
124            .ok_or_else(|| ToolError::InvalidInput("'status' is required".into()))?;
125
126        Ok(ToolResult::success(format!(
127            "Task #{id} updated to [{status}]"
128        )))
129    }
130}
131
132pub struct TaskGetTool;
133
134#[async_trait]
135impl Tool for TaskGetTool {
136    fn name(&self) -> &'static str {
137        "TaskGet"
138    }
139
140    fn description(&self) -> &'static str {
141        "Get details about a specific task by ID."
142    }
143
144    fn input_schema(&self) -> serde_json::Value {
145        json!({
146            "type": "object",
147            "required": ["id"],
148            "properties": {
149                "id": { "type": "string", "description": "Task ID to look up" }
150            }
151        })
152    }
153
154    fn is_read_only(&self) -> bool {
155        true
156    }
157
158    fn is_concurrency_safe(&self) -> bool {
159        true
160    }
161
162    async fn call(
163        &self,
164        input: serde_json::Value,
165        _ctx: &ToolContext,
166    ) -> Result<ToolResult, ToolError> {
167        let id = input
168            .get("id")
169            .and_then(|v| v.as_str())
170            .ok_or_else(|| ToolError::InvalidInput("'id' is required".into()))?;
171
172        // In a full implementation this would query the TaskManager.
173        Ok(ToolResult::success(format!(
174            "Task #{id}: status and details would be shown here. \
175             Use the background task manager for active task tracking."
176        )))
177    }
178}
179
180pub struct TaskListTool;
181
182#[async_trait]
183impl Tool for TaskListTool {
184    fn name(&self) -> &'static str {
185        "TaskList"
186    }
187
188    fn description(&self) -> &'static str {
189        "List all tasks in the current session."
190    }
191
192    fn input_schema(&self) -> serde_json::Value {
193        json!({
194            "type": "object",
195            "properties": {}
196        })
197    }
198
199    fn is_read_only(&self) -> bool {
200        true
201    }
202
203    fn is_concurrency_safe(&self) -> bool {
204        true
205    }
206
207    async fn call(
208        &self,
209        _input: serde_json::Value,
210        _ctx: &ToolContext,
211    ) -> Result<ToolResult, ToolError> {
212        let count = TASK_COUNTER.load(Ordering::Relaxed) - 1;
213        Ok(ToolResult::success(format!(
214            "{count} task(s) created this session."
215        )))
216    }
217}
218
219pub struct TaskStopTool;
220
221#[async_trait]
222impl Tool for TaskStopTool {
223    fn name(&self) -> &'static str {
224        "TaskStop"
225    }
226
227    fn description(&self) -> &'static str {
228        "Stop a running background task."
229    }
230
231    fn input_schema(&self) -> serde_json::Value {
232        json!({
233            "type": "object",
234            "required": ["id"],
235            "properties": {
236                "id": { "type": "string", "description": "Task ID to stop" }
237            }
238        })
239    }
240
241    fn is_read_only(&self) -> bool {
242        false
243    }
244
245    fn is_concurrency_safe(&self) -> bool {
246        true
247    }
248
249    async fn call(
250        &self,
251        input: serde_json::Value,
252        _ctx: &ToolContext,
253    ) -> Result<ToolResult, ToolError> {
254        let id = input
255            .get("id")
256            .and_then(|v| v.as_str())
257            .ok_or_else(|| ToolError::InvalidInput("'id' is required".into()))?;
258
259        Ok(ToolResult::success(format!("Task #{id} stop requested.")))
260    }
261}
262
263pub struct TaskOutputTool;
264
265#[async_trait]
266impl Tool for TaskOutputTool {
267    fn name(&self) -> &'static str {
268        "TaskOutput"
269    }
270
271    fn description(&self) -> &'static str {
272        "Read the output of a completed background task."
273    }
274
275    fn input_schema(&self) -> serde_json::Value {
276        json!({
277            "type": "object",
278            "required": ["id"],
279            "properties": {
280                "id": { "type": "string", "description": "Task ID to read output from" }
281            }
282        })
283    }
284
285    fn is_read_only(&self) -> bool {
286        true
287    }
288
289    fn is_concurrency_safe(&self) -> bool {
290        true
291    }
292
293    async fn call(
294        &self,
295        input: serde_json::Value,
296        _ctx: &ToolContext,
297    ) -> Result<ToolResult, ToolError> {
298        let id = input
299            .get("id")
300            .and_then(|v| v.as_str())
301            .ok_or_else(|| ToolError::InvalidInput("'id' is required".into()))?;
302
303        // Check the output file in the cache directory.
304        let output_path = dirs::cache_dir()
305            .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
306            .join("agent-code")
307            .join("tasks")
308            .join(format!("{id}.out"));
309
310        if output_path.exists() {
311            let content = std::fs::read_to_string(&output_path)
312                .map_err(|e| ToolError::ExecutionFailed(format!("Read failed: {e}")))?;
313            Ok(ToolResult::success(content))
314        } else {
315            Ok(ToolResult::success(format!(
316                "No output file found for task #{id}. It may still be running."
317            )))
318        }
319    }
320}