Skip to main content

netget/state/
task.rs

1use crate::utils::clock::Instant;
2use serde::{Deserialize, Serialize};
3use std::time::Duration;
4
5use super::client::ClientId;
6use super::server::ServerId;
7use crate::server::connection::ConnectionId;
8
9/// Unique identifier for a scheduled task
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct TaskId(u64);
12
13impl TaskId {
14    pub fn new(id: u64) -> Self {
15        Self(id)
16    }
17
18    pub fn as_u64(&self) -> u64 {
19        self.0
20    }
21
22    pub fn from_string(s: &str) -> Option<Self> {
23        s.parse::<u64>().ok().map(Self)
24    }
25}
26
27impl std::fmt::Display for TaskId {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33/// Task execution scope
34#[derive(Debug, Clone)]
35pub enum TaskScope {
36    /// Global task - uses user input prompt with all common actions
37    Global,
38    /// Server-scoped task - uses server's instruction and protocol actions
39    Server(ServerId),
40    /// Connection-scoped task - uses server's instruction and protocol actions
41    /// for a specific connection. Automatically cleaned up when connection closes.
42    Connection(ServerId, ConnectionId),
43    /// Client-scoped task - uses client's instruction and protocol actions
44    /// Automatically cleaned up when the client is removed.
45    Client(ClientId),
46}
47
48/// Task type
49#[derive(Debug, Clone)]
50pub enum TaskType {
51    /// Execute once after delay
52    OneShot { delay_secs: u64 },
53    /// Execute repeatedly at interval
54    Recurring {
55        interval_secs: u64,
56        max_executions: Option<u64>,
57        executions_count: u64,
58    },
59}
60
61/// Task status
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum TaskStatus {
64    /// Scheduled, waiting to execute
65    Scheduled,
66    /// Currently executing
67    Executing,
68    /// Completed (one-shot) or cancelled
69    Completed,
70    /// Failed with error
71    Failed(String),
72}
73
74/// Scheduled task state
75#[derive(Debug, Clone)]
76pub struct ScheduledTask {
77    /// Unique task ID
78    pub id: TaskId,
79    /// Human-readable task ID string (e.g., "cleanup_logs")
80    pub name: String,
81    /// Task scope (global or server-specific)
82    pub scope: TaskScope,
83    /// Task type (one-shot or recurring)
84    pub task_type: TaskType,
85    /// Instruction for LLM
86    pub instruction: String,
87    /// Optional context data
88    pub context: Option<serde_json::Value>,
89    /// Current status
90    pub status: TaskStatus,
91    /// When task was created
92    pub created_at: Instant,
93    /// When task will execute next
94    pub next_execution: Instant,
95    /// Previous execution error (for retry logic)
96    pub last_error: Option<String>,
97    /// Number of consecutive failures
98    pub failure_count: u64,
99}
100
101impl ScheduledTask {
102    /// Create a new one-shot task
103    pub fn new_one_shot(
104        id: TaskId,
105        name: String,
106        scope: TaskScope,
107        delay_secs: u64,
108        instruction: String,
109        context: Option<serde_json::Value>,
110    ) -> anyhow::Result<Self> {
111        let now = Instant::now();
112        let next_execution = checked_task_deadline(delay_secs)?;
113        Ok(Self {
114            id,
115            name,
116            scope,
117            task_type: TaskType::OneShot { delay_secs },
118            instruction,
119            context,
120            status: TaskStatus::Scheduled,
121            created_at: now,
122            next_execution,
123            last_error: None,
124            failure_count: 0,
125        })
126    }
127
128    /// Create a new recurring task
129    pub fn new_recurring(
130        id: TaskId,
131        name: String,
132        scope: TaskScope,
133        interval_secs: u64,
134        max_executions: Option<u64>,
135        instruction: String,
136        context: Option<serde_json::Value>,
137    ) -> anyhow::Result<Self> {
138        let now = Instant::now();
139        anyhow::ensure!(
140            interval_secs > 0,
141            "recurring interval_secs must be greater than zero"
142        );
143        anyhow::ensure!(
144            max_executions != Some(0),
145            "max_executions must be greater than zero"
146        );
147        let next_execution = checked_task_deadline(interval_secs)?;
148        Ok(Self {
149            id,
150            name,
151            scope,
152            task_type: TaskType::Recurring {
153                interval_secs,
154                max_executions,
155                executions_count: 0,
156            },
157            instruction,
158            context,
159            status: TaskStatus::Scheduled,
160            created_at: now,
161            next_execution,
162            last_error: None,
163            failure_count: 0,
164        })
165    }
166
167    /// Create a new connection-scoped one-shot task
168    pub fn new_connection_one_shot(
169        id: TaskId,
170        name: String,
171        server_id: ServerId,
172        connection_id: ConnectionId,
173        delay_secs: u64,
174        instruction: String,
175        context: Option<serde_json::Value>,
176    ) -> anyhow::Result<Self> {
177        Self::new_one_shot(
178            id,
179            name,
180            TaskScope::Connection(server_id, connection_id),
181            delay_secs,
182            instruction,
183            context,
184        )
185    }
186
187    /// Create a new connection-scoped recurring task
188    #[allow(clippy::too_many_arguments)]
189    pub fn new_connection_recurring(
190        id: TaskId,
191        name: String,
192        server_id: ServerId,
193        connection_id: ConnectionId,
194        interval_secs: u64,
195        max_executions: Option<u64>,
196        instruction: String,
197        context: Option<serde_json::Value>,
198    ) -> anyhow::Result<Self> {
199        Self::new_recurring(
200            id,
201            name,
202            TaskScope::Connection(server_id, connection_id),
203            interval_secs,
204            max_executions,
205            instruction,
206            context,
207        )
208    }
209
210    /// Build a nested startup/update task before mutating its owner. Explicit
211    /// initial delays apply to recurring tasks too; omission starts them immediately.
212    pub fn from_definition(
213        definition: &crate::llm::actions::common::ServerTaskDefinition,
214        scope: TaskScope,
215    ) -> anyhow::Result<Self> {
216        let next_execution = checked_task_deadline(definition.delay_secs.unwrap_or(0))?;
217        let mut task = if definition.recurring {
218            Self::new_recurring(
219                TaskId::new(0),
220                definition.task_id.clone(),
221                scope,
222                definition.interval_secs.unwrap_or(60),
223                definition.max_executions,
224                definition.instruction.clone(),
225                definition.context.clone(),
226            )?
227        } else {
228            Self::new_one_shot(
229                TaskId::new(0),
230                definition.task_id.clone(),
231                scope,
232                definition.delay_secs.unwrap_or(0),
233                definition.instruction.clone(),
234                definition.context.clone(),
235            )?
236        };
237        task.next_execution = next_execution;
238        Ok(task)
239    }
240
241    /// Get the interval for recurring tasks
242    pub fn interval_secs(&self) -> Option<u64> {
243        match &self.task_type {
244            TaskType::Recurring { interval_secs, .. } => Some(*interval_secs),
245            _ => None,
246        }
247    }
248
249    /// Get execution count for recurring tasks
250    pub fn executions_count(&self) -> u64 {
251        match &self.task_type {
252            TaskType::Recurring {
253                executions_count, ..
254            } => *executions_count,
255            _ => 0,
256        }
257    }
258
259    /// Check if recurring task has reached max executions
260    pub fn is_max_executions_reached(&self) -> bool {
261        match &self.task_type {
262            TaskType::Recurring {
263                max_executions: Some(max),
264                executions_count,
265                ..
266            } => executions_count >= max,
267            _ => false,
268        }
269    }
270
271    /// Format task for display in prompts
272    pub fn format_for_prompt(&self) -> String {
273        let scope_str = match &self.scope {
274            TaskScope::Global => "global".to_string(),
275            TaskScope::Server(sid) => format!("server #{}", sid.as_u32()),
276            TaskScope::Connection(sid, cid) => {
277                format!("connection {} on server #{}", cid, sid.as_u32())
278            }
279            TaskScope::Client(cid) => format!("client #{}", cid.as_u32()),
280        };
281
282        let type_str = match &self.task_type {
283            TaskType::OneShot { delay_secs } => {
284                format!("one-shot ({}s delay)", delay_secs)
285            }
286            TaskType::Recurring {
287                interval_secs,
288                max_executions,
289                executions_count,
290            } => {
291                if let Some(max) = max_executions {
292                    format!(
293                        "recurring ({}s interval, {}/{} executions)",
294                        interval_secs, executions_count, max
295                    )
296                } else {
297                    format!(
298                        "recurring ({}s interval, {} executions)",
299                        interval_secs, executions_count
300                    )
301                }
302            }
303        };
304
305        let next_in = if self.next_execution > Instant::now() {
306            let secs = (self.next_execution - Instant::now()).as_secs();
307            format!("{}s", secs)
308        } else {
309            "now".to_string()
310        };
311
312        format!(
313            "- Task '{}' (ID: {}): {} scope, {}, next in {}, status: {:?}",
314            self.name, self.id, scope_str, type_str, next_in, self.status
315        )
316    }
317}
318
319/// Task execution result
320pub struct TaskExecutionResult {
321    /// Whether execution succeeded
322    pub success: bool,
323    /// Actions returned by LLM/script
324    pub actions: Vec<serde_json::Value>,
325    /// Optional error message
326    pub error: Option<String>,
327}
328
329/// Format duration as human-readable string
330pub fn format_duration(duration: Duration) -> String {
331    let secs = duration.as_secs();
332    if secs < 60 {
333        format!("{}s", secs)
334    } else if secs < 3600 {
335        format!("{}m", secs / 60)
336    } else if secs < 86400 {
337        format!("{}h", secs / 3600)
338    } else {
339        format!("{}d", secs / 86400)
340    }
341}
342
343/// Reject unrepresentable delays instead of panicking, clamping, or executing early.
344pub fn checked_task_deadline(seconds: u64) -> anyhow::Result<Instant> {
345    Instant::now()
346        .checked_add(Duration::from_secs(seconds))
347        .ok_or_else(|| {
348            anyhow::anyhow!(
349                "scheduled task delay/interval of {} seconds exceeds the supported clock range",
350                seconds
351            )
352        })
353}
354
355/// Validate and prepare a complete batch atomically, before any instance mutation.
356pub fn prepare_tasks(
357    definitions: Option<&[crate::llm::actions::common::ServerTaskDefinition]>,
358    scope: TaskScope,
359) -> anyhow::Result<Vec<ScheduledTask>> {
360    definitions
361        .unwrap_or_default()
362        .iter()
363        .map(|definition| ScheduledTask::from_definition(definition, scope.clone()))
364        .collect()
365}