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#[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#[derive(Debug, Clone)]
35pub enum TaskScope {
36 Global,
38 Server(ServerId),
40 Connection(ServerId, ConnectionId),
43 Client(ClientId),
46}
47
48#[derive(Debug, Clone)]
50pub enum TaskType {
51 OneShot { delay_secs: u64 },
53 Recurring {
55 interval_secs: u64,
56 max_executions: Option<u64>,
57 executions_count: u64,
58 },
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum TaskStatus {
64 Scheduled,
66 Executing,
68 Completed,
70 Failed(String),
72}
73
74#[derive(Debug, Clone)]
76pub struct ScheduledTask {
77 pub id: TaskId,
79 pub name: String,
81 pub scope: TaskScope,
83 pub task_type: TaskType,
85 pub instruction: String,
87 pub context: Option<serde_json::Value>,
89 pub status: TaskStatus,
91 pub created_at: Instant,
93 pub next_execution: Instant,
95 pub last_error: Option<String>,
97 pub failure_count: u64,
99}
100
101impl ScheduledTask {
102 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 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 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 #[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 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 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 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 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 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
319pub struct TaskExecutionResult {
321 pub success: bool,
323 pub actions: Vec<serde_json::Value>,
325 pub error: Option<String>,
327}
328
329pub 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
343pub 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
355pub 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}