Skip to main content

rustyclaw_core/gateway/
task_handler.rs

1//! Task handler — gateway-side task tool dispatch.
2//!
3//! Handles task_* tool calls by interacting with the shared TaskManager.
4
5use serde_json::{Value, json};
6use tracing::instrument;
7
8use super::SharedTaskManager;
9use crate::tasks::{
10    Task, TaskIcon, TaskId, TaskStatus, format_task_indicators, format_task_status,
11};
12
13/// Check if a tool name is a task tool.
14pub fn is_task_tool(name: &str) -> bool {
15    matches!(
16        name,
17        "task_list"
18            | "task_status"
19            | "task_foreground"
20            | "task_background"
21            | "task_cancel"
22            | "task_pause"
23            | "task_resume"
24            | "task_input"
25            | "task_describe"
26    )
27}
28
29/// Execute a task tool call.
30#[instrument(skip(task_mgr, args), fields(tool = %name))]
31pub async fn execute_task_tool(
32    name: &str,
33    args: &Value,
34    task_mgr: &SharedTaskManager,
35    session_key: Option<&str>,
36) -> Result<String, String> {
37    match name {
38        "task_list" => exec_task_list(args, task_mgr, session_key).await,
39        "task_status" => exec_task_status(args, task_mgr).await,
40        "task_foreground" => exec_task_foreground(args, task_mgr).await,
41        "task_background" => exec_task_background(args, task_mgr).await,
42        "task_cancel" => exec_task_cancel(args, task_mgr).await,
43        "task_pause" => exec_task_pause(args, task_mgr).await,
44        "task_resume" => exec_task_resume(args, task_mgr).await,
45        "task_input" => exec_task_input(args, task_mgr).await,
46        "task_describe" => exec_task_describe(args, task_mgr, session_key).await,
47        _ => Err(format!("Unknown task tool: {}", name)),
48    }
49}
50
51/// List active tasks.
52async fn exec_task_list(
53    args: &Value,
54    task_mgr: &SharedTaskManager,
55    current_session: Option<&str>,
56) -> Result<String, String> {
57    let session_filter = args.get("session").and_then(|v| v.as_str());
58    let include_completed = args
59        .get("includeCompleted")
60        .and_then(|v| v.as_bool())
61        .unwrap_or(false);
62
63    let tasks: Vec<Task> = if let Some(session) = session_filter {
64        task_mgr.for_session(session).await
65    } else if let Some(session) = current_session {
66        // Default to current session's tasks
67        task_mgr.for_session(session).await
68    } else {
69        task_mgr.all().await
70    };
71
72    let filtered: Vec<&Task> = tasks
73        .iter()
74        .filter(|t| include_completed || !t.status.is_terminal())
75        .collect();
76
77    if filtered.is_empty() {
78        return Ok(json!({
79            "tasks": [],
80            "message": "No active tasks"
81        })
82        .to_string());
83    }
84
85    let task_list: Vec<Value> = filtered
86        .iter()
87        .map(|t| {
88            json!({
89                "id": t.id.0,
90                "kind": t.kind.display_name(),
91                "label": t.display_label(),
92                "status": format_status_short(&t.status),
93                "foreground": t.status.is_foreground(),
94                "elapsed": t.elapsed().map(|d| d.as_secs()),
95                "progress": t.status.progress(),
96            })
97        })
98        .collect();
99
100    let indicators =
101        format_task_indicators(&filtered.iter().cloned().cloned().collect::<Vec<_>>(), 5);
102
103    Ok(json!({
104        "tasks": task_list,
105        "count": filtered.len(),
106        "indicators": indicators,
107    })
108    .to_string())
109}
110
111/// Get detailed task status.
112async fn exec_task_status(args: &Value, task_mgr: &SharedTaskManager) -> Result<String, String> {
113    let task_id = parse_task_id(args)?;
114
115    let task = task_mgr
116        .get(task_id)
117        .await
118        .ok_or_else(|| format!("Task {} not found", task_id))?;
119
120    Ok(json!({
121        "id": task.id.0,
122        "kind": task.kind.display_name(),
123        "kindDetails": task.kind.description(),
124        "label": task.display_label(),
125        "status": format_task_status(&task),
126        "statusCode": format_status_short(&task.status),
127        "foreground": task.status.is_foreground(),
128        "progress": task.status.progress(),
129        "message": task.status.message(),
130        "elapsed": task.elapsed().map(|d| d.as_secs()),
131        "session": task.session_key,
132        "output": if task.status.is_terminal() {
133            task.output_buffer.clone()
134        } else {
135            String::new()
136        },
137    })
138    .to_string())
139}
140
141/// Bring task to foreground.
142async fn exec_task_foreground(
143    args: &Value,
144    task_mgr: &SharedTaskManager,
145) -> Result<String, String> {
146    let task_id = parse_task_id(args)?;
147
148    task_mgr.set_foreground(task_id).await?;
149
150    let task = task_mgr
151        .get(task_id)
152        .await
153        .ok_or_else(|| format!("Task {} not found", task_id))?;
154
155    Ok(json!({
156        "success": true,
157        "id": task_id.0,
158        "label": task.display_label(),
159        "message": format!("Task {} is now in foreground", task_id),
160    })
161    .to_string())
162}
163
164/// Move task to background.
165async fn exec_task_background(
166    args: &Value,
167    task_mgr: &SharedTaskManager,
168) -> Result<String, String> {
169    let task_id = parse_task_id(args)?;
170
171    task_mgr.set_background(task_id).await?;
172
173    let task = task_mgr
174        .get(task_id)
175        .await
176        .ok_or_else(|| format!("Task {} not found", task_id))?;
177
178    Ok(json!({
179        "success": true,
180        "id": task_id.0,
181        "label": task.display_label(),
182        "message": format!("Task {} moved to background", task_id),
183    })
184    .to_string())
185}
186
187/// Cancel a task.
188async fn exec_task_cancel(args: &Value, task_mgr: &SharedTaskManager) -> Result<String, String> {
189    let task_id = parse_task_id(args)?;
190
191    task_mgr.cancel(task_id).await?;
192
193    Ok(json!({
194        "success": true,
195        "id": task_id.0,
196        "message": format!("Task {} cancelled", task_id),
197    })
198    .to_string())
199}
200
201/// Pause a task.
202async fn exec_task_pause(args: &Value, task_mgr: &SharedTaskManager) -> Result<String, String> {
203    let task_id = parse_task_id(args)?;
204
205    // Update status to paused
206    task_mgr
207        .update_status(task_id, TaskStatus::Paused { reason: None })
208        .await;
209
210    Ok(json!({
211        "success": true,
212        "id": task_id.0,
213        "message": format!("Task {} paused", task_id),
214        "note": "Not all task types support pause/resume",
215    })
216    .to_string())
217}
218
219/// Resume a paused task.
220async fn exec_task_resume(args: &Value, task_mgr: &SharedTaskManager) -> Result<String, String> {
221    let task_id = parse_task_id(args)?;
222
223    // Check if task exists and is paused
224    let task = task_mgr
225        .get(task_id)
226        .await
227        .ok_or_else(|| format!("Task {} not found", task_id))?;
228
229    if !matches!(task.status, TaskStatus::Paused { .. }) {
230        return Err(format!(
231            "Task {} is not paused (status: {})",
232            task_id,
233            format_status_short(&task.status)
234        ));
235    }
236
237    // Update status back to running
238    task_mgr
239        .update_status(
240            task_id,
241            TaskStatus::Running {
242                progress: None,
243                message: Some("Resumed".to_string()),
244            },
245        )
246        .await;
247
248    Ok(json!({
249        "success": true,
250        "id": task_id.0,
251        "message": format!("Task {} resumed", task_id),
252    })
253    .to_string())
254}
255
256/// Send input to a task.
257async fn exec_task_input(args: &Value, task_mgr: &SharedTaskManager) -> Result<String, String> {
258    let task_id = parse_task_id(args)?;
259    let input = args
260        .get("input")
261        .and_then(|v| v.as_str())
262        .ok_or("Missing required parameter: input")?;
263
264    let task = task_mgr
265        .get(task_id)
266        .await
267        .ok_or_else(|| format!("Task {} not found", task_id))?;
268
269    if !matches!(task.status, TaskStatus::WaitingForInput { .. }) {
270        return Err(format!(
271            "Task {} is not waiting for input (status: {})",
272            task_id,
273            format_status_short(&task.status)
274        ));
275    }
276
277    // TODO: Actually send input via TaskHandle
278    // This requires storing TaskHandles in TaskManager
279
280    Ok(json!({
281        "success": true,
282        "id": task_id.0,
283        "input": input,
284        "message": format!("Input sent to task {}", task_id),
285        "note": "Task input delivery not yet fully implemented",
286    })
287    .to_string())
288}
289
290/// Set task description.
291async fn exec_task_describe(
292    args: &Value,
293    task_mgr: &SharedTaskManager,
294    session_key: Option<&str>,
295) -> Result<String, String> {
296    let description = args
297        .get("description")
298        .and_then(|v| v.as_str())
299        .ok_or("Missing required parameter: description")?;
300
301    // Get task ID from args, or find the current session's active task
302    let task_id = if let Ok(id) = parse_task_id(args) {
303        id
304    } else if let Some(session) = session_key {
305        // Find the running task for this session
306        let tasks = task_mgr.for_session(session).await;
307        tasks
308            .iter()
309            .find(|t| matches!(t.status, TaskStatus::Running { .. }))
310            .map(|t| t.id)
311            .ok_or("No active task found for current session")?
312    } else {
313        return Err("No task ID provided and no session context".to_string());
314    };
315
316    task_mgr.set_description(task_id, description).await?;
317
318    Ok(json!({
319        "success": true,
320        "id": task_id.0,
321        "description": description,
322        "message": format!("Task {} description updated", task_id),
323    })
324    .to_string())
325}
326
327// ── Helpers ─────────────────────────────────────────────────────────────────
328
329fn parse_task_id(args: &Value) -> Result<TaskId, String> {
330    let id = args
331        .get("id")
332        .or_else(|| args.get("taskId"))
333        .and_then(|v| v.as_u64())
334        .ok_or("Missing required parameter: id (task ID)")?;
335
336    Ok(TaskId(id))
337}
338
339fn format_status_short(status: &TaskStatus) -> &'static str {
340    match status {
341        TaskStatus::Pending => "pending",
342        TaskStatus::Running { .. } => "running",
343        TaskStatus::Background { .. } => "background",
344        TaskStatus::Paused { .. } => "paused",
345        TaskStatus::Completed { .. } => "completed",
346        TaskStatus::Failed { .. } => "failed",
347        TaskStatus::Cancelled => "cancelled",
348        TaskStatus::WaitingForInput { .. } => "waiting_input",
349    }
350}
351
352/// Generate a system prompt section describing active tasks.
353pub async fn generate_task_prompt_section(
354    task_mgr: &SharedTaskManager,
355    session_key: &str,
356) -> Option<String> {
357    let tasks = task_mgr.for_session(session_key).await;
358    let active: Vec<_> = tasks.iter().filter(|t| !t.status.is_terminal()).collect();
359
360    if active.is_empty() {
361        return None;
362    }
363
364    let mut section = String::from("## Active Tasks\n");
365
366    for task in &active {
367        let icon = TaskIcon::from_status(&task.status);
368        let fg = if task.status.is_foreground() {
369            " [foreground]"
370        } else {
371            ""
372        };
373        // Show description if set, otherwise label
374        let desc = task.display_description();
375        section.push_str(&format!(
376            "- {} #{}: {}{}\n",
377            icon.emoji(),
378            task.id.0,
379            desc,
380            fg
381        ));
382    }
383
384    section.push_str(
385        "\nUse task_foreground/task_background to switch focus, task_cancel to stop.\n\
386         Use task_describe to update what your task is doing.\n",
387    );
388
389    Some(section)
390}