Skip to main content

rustyclaw_core/tasks/
model.rs

1//! Task model — core types for task representation.
2
3use serde::{Deserialize, Serialize};
4use std::time::{Duration, SystemTime};
5
6/// Unique identifier for a task.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct TaskId(pub u64);
9
10impl TaskId {
11    /// Generate a new unique task ID.
12    pub fn new() -> Self {
13        use std::sync::atomic::{AtomicU64, Ordering};
14        static COUNTER: AtomicU64 = AtomicU64::new(1);
15        Self(COUNTER.fetch_add(1, Ordering::SeqCst))
16    }
17}
18
19impl Default for TaskId {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl std::fmt::Display for TaskId {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "#{}", self.0)
28    }
29}
30
31/// Task status with progress tracking.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub enum TaskStatus {
34    /// Task is waiting to start
35    Pending,
36
37    /// Task is currently running
38    Running {
39        /// Optional progress (0.0 - 1.0)
40        progress: Option<f32>,
41        /// Optional status message
42        message: Option<String>,
43    },
44
45    /// Task is running but not receiving user attention
46    Background {
47        progress: Option<f32>,
48        message: Option<String>,
49    },
50
51    /// Task is paused (can be resumed)
52    Paused { reason: Option<String> },
53
54    /// Task completed successfully
55    Completed {
56        /// Summary of what was accomplished
57        summary: Option<String>,
58        /// Output/result data
59        output: Option<String>,
60    },
61
62    /// Task failed
63    Failed {
64        error: String,
65        /// Whether the task can be retried
66        retryable: bool,
67    },
68
69    /// Task was cancelled by user
70    Cancelled,
71
72    /// Task is waiting for user input
73    WaitingForInput { prompt: String },
74}
75
76impl TaskStatus {
77    /// Check if the task is in a terminal state.
78    pub fn is_terminal(&self) -> bool {
79        matches!(
80            self,
81            Self::Completed { .. } | Self::Failed { .. } | Self::Cancelled
82        )
83    }
84
85    /// Check if the task is running (foreground or background).
86    pub fn is_running(&self) -> bool {
87        matches!(self, Self::Running { .. } | Self::Background { .. })
88    }
89
90    /// Check if the task is in the foreground.
91    pub fn is_foreground(&self) -> bool {
92        matches!(self, Self::Running { .. } | Self::WaitingForInput { .. })
93    }
94
95    /// Get progress if available.
96    pub fn progress(&self) -> Option<f32> {
97        match self {
98            Self::Running { progress, .. } | Self::Background { progress, .. } => *progress,
99            Self::Completed { .. } => Some(1.0),
100            _ => None,
101        }
102    }
103
104    /// Get status message if available.
105    pub fn message(&self) -> Option<&str> {
106        match self {
107            Self::Running { message, .. } | Self::Background { message, .. } => message.as_deref(),
108            Self::Paused { reason } => reason.as_deref(),
109            Self::Completed { summary, .. } => summary.as_deref(),
110            Self::Failed { error, .. } => Some(error.as_str()),
111            Self::WaitingForInput { prompt } => Some(prompt.as_str()),
112            _ => None,
113        }
114    }
115}
116
117/// Kind of task — determines behavior and display.
118#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
119pub enum TaskKind {
120    /// Shell command execution
121    Command { command: String, pid: Option<u32> },
122
123    /// Sub-agent session
124    SubAgent {
125        session_key: String,
126        label: Option<String>,
127    },
128
129    /// Cron job execution
130    CronJob {
131        job_id: String,
132        job_name: Option<String>,
133    },
134
135    /// MCP tool call
136    McpTool { server: String, tool: String },
137
138    /// Browser automation
139    Browser { action: String, url: Option<String> },
140
141    /// File operation (download, upload, copy)
142    FileOp { operation: String, path: String },
143
144    /// Web request
145    WebRequest { url: String, method: String },
146
147    /// Generic/custom task
148    Custom {
149        name: String,
150        details: Option<String>,
151    },
152}
153
154impl TaskKind {
155    /// Get a short display name for the task kind.
156    pub fn display_name(&self) -> &str {
157        match self {
158            Self::Command { .. } => "Command",
159            Self::SubAgent { .. } => "Sub-agent",
160            Self::CronJob { .. } => "Cron job",
161            Self::McpTool { .. } => "MCP",
162            Self::Browser { .. } => "Browser",
163            Self::FileOp { .. } => "File",
164            Self::WebRequest { .. } => "Web",
165            Self::Custom { name, .. } => name.as_str(),
166        }
167    }
168
169    /// Get a detailed description.
170    pub fn description(&self) -> String {
171        match self {
172            Self::Command { command, pid } => {
173                if let Some(p) = pid {
174                    format!("{} (pid {})", command, p)
175                } else {
176                    command.clone()
177                }
178            }
179            Self::SubAgent { label, session_key } => {
180                label.clone().unwrap_or_else(|| session_key.clone())
181            }
182            Self::CronJob { job_name, job_id } => {
183                job_name.clone().unwrap_or_else(|| job_id.clone())
184            }
185            Self::McpTool { server, tool } => format!("{}:{}", server, tool),
186            Self::Browser { action, url } => {
187                if let Some(u) = url {
188                    format!("{} {}", action, u)
189                } else {
190                    action.clone()
191                }
192            }
193            Self::FileOp { operation, path } => format!("{} {}", operation, path),
194            Self::WebRequest { method, url } => format!("{} {}", method, url),
195            Self::Custom { name, details } => {
196                if let Some(d) = details {
197                    format!("{}: {}", name, d)
198                } else {
199                    name.clone()
200                }
201            }
202        }
203    }
204}
205
206/// Progress information for a task.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct TaskProgress {
209    /// Progress as fraction (0.0 - 1.0), None if indeterminate
210    pub fraction: Option<f32>,
211
212    /// Current step / total steps
213    pub steps: Option<(u32, u32)>,
214
215    /// Bytes processed / total bytes
216    pub bytes: Option<(u64, u64)>,
217
218    /// Items processed / total items  
219    pub items: Option<(u32, u32)>,
220
221    /// ETA in seconds
222    pub eta_secs: Option<u64>,
223
224    /// Current status message
225    pub message: Option<String>,
226}
227
228impl TaskProgress {
229    /// Create indeterminate progress.
230    pub fn indeterminate() -> Self {
231        Self {
232            fraction: None,
233            steps: None,
234            bytes: None,
235            items: None,
236            eta_secs: None,
237            message: None,
238        }
239    }
240
241    /// Create progress from a fraction.
242    pub fn fraction(f: f32) -> Self {
243        Self {
244            fraction: Some(f.clamp(0.0, 1.0)),
245            ..Self::indeterminate()
246        }
247    }
248
249    /// Create progress from steps.
250    pub fn steps(current: u32, total: u32) -> Self {
251        let frac = if total > 0 {
252            Some(current as f32 / total as f32)
253        } else {
254            None
255        };
256        Self {
257            fraction: frac,
258            steps: Some((current, total)),
259            ..Self::indeterminate()
260        }
261    }
262
263    /// Add a message to the progress.
264    pub fn with_message(mut self, msg: impl Into<String>) -> Self {
265        self.message = Some(msg.into());
266        self
267    }
268
269    /// Add ETA to the progress.
270    pub fn with_eta(mut self, secs: u64) -> Self {
271        self.eta_secs = Some(secs);
272        self
273    }
274}
275
276/// A task with full metadata.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct Task {
279    /// Unique task ID
280    pub id: TaskId,
281
282    /// What kind of task this is
283    pub kind: TaskKind,
284
285    /// Current status
286    pub status: TaskStatus,
287
288    /// When the task was created
289    #[serde(with = "system_time_serde")]
290    pub created_at: SystemTime,
291
292    /// When the task started running
293    #[serde(with = "option_system_time_serde")]
294    pub started_at: Option<SystemTime>,
295
296    /// When the task finished (completed/failed/cancelled)
297    #[serde(with = "option_system_time_serde")]
298    pub finished_at: Option<SystemTime>,
299
300    /// Session that owns this task
301    pub session_key: Option<String>,
302
303    /// User-provided label
304    pub label: Option<String>,
305
306    /// Short description of what the task is currently doing (agent-settable)
307    pub description: Option<String>,
308
309    /// Whether task output should stream to chat
310    pub stream_output: bool,
311
312    /// Accumulated output (if buffering)
313    #[serde(skip)]
314    pub output_buffer: String,
315}
316
317impl Task {
318    /// Create a new pending task.
319    pub fn new(kind: TaskKind) -> Self {
320        Self {
321            id: TaskId::new(),
322            kind,
323            status: TaskStatus::Pending,
324            created_at: SystemTime::now(),
325            started_at: None,
326            finished_at: None,
327            session_key: None,
328            label: None,
329            description: None,
330            stream_output: false,
331            output_buffer: String::new(),
332        }
333    }
334
335    /// Set the session key.
336    pub fn with_session(mut self, key: impl Into<String>) -> Self {
337        self.session_key = Some(key.into());
338        self
339    }
340
341    /// Set a label.
342    pub fn with_label(mut self, label: impl Into<String>) -> Self {
343        self.label = Some(label.into());
344        self
345    }
346
347    /// Set a description.
348    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
349        self.description = Some(desc.into());
350        self
351    }
352
353    /// Enable output streaming.
354    pub fn with_streaming(mut self) -> Self {
355        self.stream_output = true;
356        self
357    }
358
359    /// Mark task as running.
360    pub fn start(&mut self) {
361        self.started_at = Some(SystemTime::now());
362        self.status = TaskStatus::Running {
363            progress: None,
364            message: None,
365        };
366    }
367
368    /// Move task to background.
369    pub fn background(&mut self) {
370        if let TaskStatus::Running { progress, message } = &self.status {
371            self.status = TaskStatus::Background {
372                progress: *progress,
373                message: message.clone(),
374            };
375        }
376    }
377
378    /// Move task to foreground.
379    pub fn foreground(&mut self) {
380        if let TaskStatus::Background { progress, message } = &self.status {
381            self.status = TaskStatus::Running {
382                progress: *progress,
383                message: message.clone(),
384            };
385        }
386    }
387
388    /// Update progress.
389    pub fn update_progress(&mut self, progress: TaskProgress) {
390        match &mut self.status {
391            TaskStatus::Running {
392                progress: p,
393                message: m,
394            }
395            | TaskStatus::Background {
396                progress: p,
397                message: m,
398            } => {
399                *p = progress.fraction;
400                if progress.message.is_some() {
401                    *m = progress.message;
402                }
403            }
404            _ => {}
405        }
406    }
407
408    /// Mark task as completed.
409    pub fn complete(&mut self, summary: Option<String>) {
410        self.finished_at = Some(SystemTime::now());
411        let output = if self.output_buffer.is_empty() {
412            None
413        } else {
414            Some(std::mem::take(&mut self.output_buffer))
415        };
416        self.status = TaskStatus::Completed { summary, output };
417    }
418
419    /// Mark task as failed.
420    pub fn fail(&mut self, error: impl Into<String>, retryable: bool) {
421        self.finished_at = Some(SystemTime::now());
422        self.status = TaskStatus::Failed {
423            error: error.into(),
424            retryable,
425        };
426    }
427
428    /// Mark task as cancelled.
429    pub fn cancel(&mut self) {
430        self.finished_at = Some(SystemTime::now());
431        self.status = TaskStatus::Cancelled;
432    }
433
434    /// Get elapsed time since start.
435    pub fn elapsed(&self) -> Option<Duration> {
436        self.started_at.map(|start| {
437            let end = self.finished_at.unwrap_or_else(SystemTime::now);
438            end.duration_since(start).unwrap_or_default()
439        })
440    }
441
442    /// Get display label (user label or auto-generated).
443    pub fn display_label(&self) -> String {
444        self.label
445            .clone()
446            .unwrap_or_else(|| self.kind.description())
447    }
448
449    /// Get short description (agent-set, or fallback to label).
450    pub fn display_description(&self) -> String {
451        self.description
452            .clone()
453            .unwrap_or_else(|| self.display_label())
454    }
455}
456
457// Serde helpers for SystemTime
458mod system_time_serde {
459    use serde::{Deserialize, Deserializer, Serialize, Serializer};
460    use std::time::{SystemTime, UNIX_EPOCH};
461
462    pub fn serialize<S: Serializer>(time: &SystemTime, ser: S) -> Result<S::Ok, S::Error> {
463        let millis = time
464            .duration_since(UNIX_EPOCH)
465            .unwrap_or_default()
466            .as_millis() as u64;
467        millis.serialize(ser)
468    }
469
470    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<SystemTime, D::Error> {
471        let millis = u64::deserialize(de)?;
472        Ok(UNIX_EPOCH + std::time::Duration::from_millis(millis))
473    }
474}
475
476mod option_system_time_serde {
477    use serde::{Deserialize, Deserializer, Serialize, Serializer};
478    use std::time::{SystemTime, UNIX_EPOCH};
479
480    pub fn serialize<S: Serializer>(time: &Option<SystemTime>, ser: S) -> Result<S::Ok, S::Error> {
481        match time {
482            Some(t) => {
483                let millis = t.duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
484                Some(millis).serialize(ser)
485            }
486            None => None::<u64>.serialize(ser),
487        }
488    }
489
490    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Option<SystemTime>, D::Error> {
491        let millis: Option<u64> = Option::deserialize(de)?;
492        Ok(millis.map(|m| UNIX_EPOCH + std::time::Duration::from_millis(m)))
493    }
494}