1use tokio::sync::mpsc;
6use tokio_util::sync::CancellationToken;
7
8use crate::llm::OllamaClient;
9use crate::state::app_state::AppState;
10
11pub async fn execute_due_tasks_public(
13 state: &AppState,
14 llm_client: &OllamaClient,
15 status_tx: &mpsc::UnboundedSender<String>,
16) {
17 execute_due_tasks(state, llm_client, status_tx, None).await
18}
19
20pub async fn execute_due_tasks_owned_public(
23 state: &AppState,
24 llm_client: &OllamaClient,
25 status_tx: &mpsc::UnboundedSender<String>,
26 owner: CancellationToken,
27) {
28 execute_due_tasks(state, llm_client, status_tx, Some(owner)).await
29}
30
31async fn execute_due_tasks(
33 state: &AppState,
34 llm_client: &OllamaClient,
35 status_tx: &mpsc::UnboundedSender<String>,
36 owner: Option<CancellationToken>,
37) {
38 for task in state.claim_due_tasks().await {
39 let state_clone = state.clone();
41 let llm_clone = llm_client.clone();
42 let status_tx_clone = status_tx.clone();
43 let task_clone = task.clone();
44 let owner = owner.clone();
45
46 state
47 .spawn_scheduled_task(task.id, async move {
48 if let Some(owner) = owner {
49 let cancelled_state = state_clone.clone();
50 let id = task_clone.id;
51 tokio::select! {
52 biased;
53 _ = owner.cancelled() => {
54 cancelled_state.update_task_status(id, crate::state::task::TaskStatus::Failed("execution owner closed".into())).await;
55 }
56 _ = execute_single_task(state_clone, llm_clone, status_tx_clone, task_clone) => {}
57 }
58 } else {
59 execute_single_task(state_clone, llm_clone, status_tx_clone, task_clone).await;
60 }
61 })
62 .await;
63 }
64}
65
66async fn build_task_actions(
78 state: &AppState,
79 scope: &crate::state::task::TaskScope,
80 protocol_actions: Vec<crate::llm::actions::ActionDefinition>,
81) -> Vec<crate::llm::actions::ActionDefinition> {
82 use crate::llm::actions::{
83 get_all_tool_actions, get_network_event_common_actions, get_network_event_tool_actions,
84 get_user_input_common_actions,
85 };
86 use crate::llm::prompt::PromptBuilder;
87 use crate::state::task::TaskScope;
88
89 let selected_mode = state.get_selected_scripting_mode().await;
90 let web_search_mode = state.get_web_search_mode().await;
91
92 let actions = match scope {
93 TaskScope::Global => {
94 let scripting_env = state.get_scripting_env().await;
96 let mut actions =
97 get_user_input_common_actions(selected_mode, &scripting_env, true, true);
98 actions.extend(get_all_tool_actions(web_search_mode));
99 actions
100 }
101 TaskScope::Server(_) | TaskScope::Connection(_, _) | TaskScope::Client(_) => {
102 let mut actions = get_network_event_common_actions();
104 actions.extend(protocol_actions);
105 actions.extend(get_network_event_tool_actions(web_search_mode));
106 actions
107 }
108 };
109
110 let has_scripting = selected_mode != crate::state::app_state::ScriptingMode::Off;
111 PromptBuilder::filter_actions_by_scripting_mode(actions, has_scripting)
112}
113
114async fn execute_single_task(
116 state: AppState,
117 llm_client: OllamaClient,
118 status_tx: mpsc::UnboundedSender<String>,
119 task: crate::state::ScheduledTask,
120) {
121 use crate::llm::prompt::PromptBuilder;
122 use crate::state::task::{TaskExecutionResult, TaskScope};
123
124 let _ = status_tx.send(format!("[TASK] Executing task '{}'", task.name));
125
126 let protocol_actions = match &task.scope {
128 TaskScope::Server(server_id) | TaskScope::Connection(server_id, _) => {
129 if let Some(protocol_name) = state.get_protocol_name(*server_id).await {
130 if let Some(protocol) =
131 crate::protocol::server_registry::registry().get(&protocol_name)
132 {
133 protocol.get_sync_actions()
134 } else {
135 Vec::new()
136 }
137 } else {
138 Vec::new()
139 }
140 }
141 TaskScope::Client(client_id) => {
142 if let Some(protocol_name) = state.get_protocol_name_for_client(*client_id).await {
143 if let Some(protocol) =
144 crate::protocol::client_registry::CLIENT_REGISTRY.get(&protocol_name)
145 {
146 crate::llm::actions::client_trait::client_llm_action_set(
154 protocol.as_ref(),
155 &state,
156 None,
157 )
158 } else {
159 Vec::new()
160 }
161 } else {
162 Vec::new()
163 }
164 }
165 TaskScope::Global => Vec::new(),
166 };
167
168 let task_actions = build_task_actions(&state, &task.scope, protocol_actions.clone()).await;
172
173 let prompt = PromptBuilder::build_task_execution_prompt(&state, &task, protocol_actions).await;
175
176 let model = match crate::llm::ensure_model_selected(
178 state.get_ollama_model().await,
179 &state.get_ollama_url().await,
180 )
181 .await
182 {
183 Ok(m) => m,
184 Err(e) => {
185 let error_msg = format!("Model selection failed: {}", e);
186 let _ = status_tx.send(format!(
187 "[ERROR] Failed to ensure model is selected for task execution: {}",
188 e
189 ));
190 state
192 .update_task_status(
193 task.id,
194 crate::state::task::TaskStatus::Failed(error_msg.clone()),
195 )
196 .await;
197 let result = TaskExecutionResult {
198 success: false,
199 actions: Vec::new(),
200 error: Some(error_msg),
201 };
202 state.record_task_execution(task.id, &result).await;
203 return;
204 }
205 };
206
207 let conversation_source = match &task.scope {
209 TaskScope::Global => crate::state::app_state::ConversationSource::Task {
210 task_name: task.name.clone(),
211 },
212 TaskScope::Server(server_id) => crate::state::app_state::ConversationSource::Task {
213 task_name: format!("{}#{}", task.name, server_id.as_u32()),
214 },
215 TaskScope::Connection(server_id, conn_id) => {
216 crate::state::app_state::ConversationSource::Task {
217 task_name: format!("{}#{}/{}", task.name, server_id.as_u32(), conn_id),
218 }
219 }
220 TaskScope::Client(client_id) => crate::state::app_state::ConversationSource::Task {
221 task_name: format!("{}@{}", task.name, client_id.as_u32()),
222 },
223 };
224
225 let truncated_instruction = crate::utils::truncate_for_log(&task.instruction, 27);
226
227 let rate_limiter = state.get_rate_limiter().await;
229
230 let mut conversation = crate::llm::ConversationHandler::new(
232 prompt.clone(),
233 std::sync::Arc::new(llm_client.clone()),
234 model.clone(),
235 rate_limiter,
236 crate::llm::RequestSource::Network, )
238 .with_native_tools(&task_actions)
239 .with_status_tx(status_tx.clone())
240 .with_tracking(state.clone(), conversation_source, truncated_instruction);
241
242 conversation.add_user_message("Execute the task.".to_string());
244
245 let web_search_mode = state.get_web_search_mode().await;
247 let actions = match conversation
248 .generate_with_tools_and_retry(
249 state.get_web_approval_channel().await,
250 web_search_mode,
251 task_actions,
252 )
253 .await
254 {
255 Ok(actions) => actions,
256 Err(e) => {
257 let error = format!("LLM call failed: {}", e);
259 let _ = status_tx.send(format!("[ERROR] Task '{}' failed: {}", task.name, error));
260
261 let result = TaskExecutionResult {
262 success: false,
263 actions: Vec::new(),
264 error: Some(error),
265 };
266
267 handle_task_failure(&state, &status_tx, task, result).await;
268 return;
269 }
270 };
271
272 let protocol = match &task.scope {
274 TaskScope::Server(server_id) | TaskScope::Connection(server_id, _) => state
275 .get_protocol_name(*server_id)
276 .await
277 .and_then(|name| crate::protocol::server_registry::registry().get(&name)),
278 TaskScope::Client(_client_id) => {
279 None
282 }
283 TaskScope::Global => None,
284 };
285
286 let (server_id, client_id) = match &task.scope {
288 TaskScope::Server(sid) | TaskScope::Connection(sid, _) => (Some(*sid), None),
289 TaskScope::Client(cid) => (None, Some(*cid)),
290 TaskScope::Global => (None, None),
291 };
292
293 match crate::llm::execute_actions(
295 actions.clone(),
296 &state,
297 protocol.as_deref(),
298 server_id,
299 client_id,
300 )
301 .await
302 {
303 Ok(_exec_result) => {
304 let _ = status_tx.send(format!(
306 "[TASK] Task '{}' completed successfully",
307 task.name
308 ));
309
310 let result = TaskExecutionResult {
311 success: true,
312 actions,
313 error: None,
314 };
315
316 handle_task_success(&state, &status_tx, task, result).await;
317 }
318 Err(e) => {
319 let error = format!("Action execution failed: {}", e);
321 let _ = status_tx.send(format!("[ERROR] Task '{}' failed: {}", task.name, error));
322
323 let result = TaskExecutionResult {
324 success: false,
325 actions,
326 error: Some(error),
327 };
328
329 handle_task_failure(&state, &status_tx, task, result).await;
330 }
331 }
332}
333
334async fn handle_task_success(
336 state: &AppState,
337 status_tx: &mpsc::UnboundedSender<String>,
338 task: crate::state::ScheduledTask,
339 result: crate::state::TaskExecutionResult,
340) {
341 use crate::state::task::{TaskStatus, TaskType};
342
343 state.record_task_execution(task.id, &result).await;
345
346 match &task.task_type {
347 TaskType::OneShot { .. } => {
348 state
350 .update_task_status(task.id, TaskStatus::Completed)
351 .await;
352 state.remove_task(task.id).await;
353 let _ = status_tx.send(format!(
354 "[TASK] One-shot task '{}' completed and removed",
355 task.name
356 ));
357 }
358 TaskType::Recurring {
359 interval_secs,
360 max_executions,
361 executions_count,
362 } => {
363 if let Some(max) = max_executions {
366 if executions_count.saturating_add(1) >= *max {
367 state
368 .update_task_status(task.id, TaskStatus::Completed)
369 .await;
370 state.remove_task(task.id).await;
371 let _ = status_tx.send(format!(
372 "[TASK] Recurring task '{}' reached max executions ({}) and removed",
373 task.name, max
374 ));
375 return;
376 }
377 }
378
379 let next = match crate::state::task::checked_task_deadline(*interval_secs) {
381 Ok(next) => next,
382 Err(error) => {
383 state
384 .update_task_status(task.id, TaskStatus::Failed(error.to_string()))
385 .await;
386 let _ = status_tx.send(format!("[ERROR] Task '{}': {}", task.name, error));
387 return;
388 }
389 };
390 state.update_task_next_execution(task.id, next).await;
391 state
392 .update_task_status(task.id, TaskStatus::Scheduled)
393 .await;
394 }
395 }
396}
397
398async fn handle_task_failure(
400 state: &AppState,
401 status_tx: &mpsc::UnboundedSender<String>,
402 task: crate::state::ScheduledTask,
403 result: crate::state::TaskExecutionResult,
404) {
405 use crate::state::task::TaskStatus;
406
407 const MAX_FAILURES: u64 = 5;
408 const BACKOFF_BASE_SECS: u64 = 60; state.record_task_execution(task.id, &result).await;
412
413 let failure_count = task.failure_count.saturating_add(1);
414
415 if failure_count >= MAX_FAILURES {
416 state
418 .update_task_status(
419 task.id,
420 TaskStatus::Failed(result.error.unwrap_or_else(|| "Unknown error".to_string())),
421 )
422 .await;
423 state.remove_task(task.id).await;
424 let _ = status_tx.send(format!(
425 "[ERROR] Task '{}' failed {} times, removing from schedule",
426 task.name, MAX_FAILURES
427 ));
428 } else {
429 let backoff_secs = BACKOFF_BASE_SECS * 2u64.pow((failure_count - 1) as u32);
431 let next = match crate::state::task::checked_task_deadline(backoff_secs) {
432 Ok(next) => next,
433 Err(error) => {
434 state
435 .update_task_status(task.id, TaskStatus::Failed(error.to_string()))
436 .await;
437 return;
438 }
439 };
440
441 state.update_task_next_execution(task.id, next).await;
442 state
443 .update_task_status(task.id, TaskStatus::Scheduled)
444 .await;
445
446 let _ = status_tx.send(format!(
447 "[WARN] Task '{}' failed (attempt {}/{}), retrying in {} seconds",
448 task.name, failure_count, MAX_FAILURES, backoff_secs
449 ));
450 }
451}