graph_flow/task.rs
1//! Task definition and execution control.
2//!
3//! This module contains the core [`Task`] trait and related types for defining
4//! workflow steps and controlling execution flow.
5//!
6//! # Examples
7//!
8//! ## Basic Task Implementation
9//!
10//! ```rust
11//! use graph_flow::{Task, TaskResult, NextAction, Context};
12//! use async_trait::async_trait;
13//!
14//! struct HelloTask;
15//!
16//! #[async_trait]
17//! impl Task for HelloTask {
18//! fn id(&self) -> &str {
19//! "hello_task"
20//! }
21//!
22//! async fn run(&self, context: Context) -> graph_flow::Result<TaskResult> {
23//! let name: String = context.get("name").await.unwrap_or("World".to_string());
24//! let greeting = format!("Hello, {}!", name);
25//!
26//! // Store result for next task
27//! context.set("greeting", greeting.clone()).await;
28//!
29//! Ok(TaskResult::new(Some(greeting), NextAction::Continue))
30//! }
31//! }
32//! ```
33//!
34//! ## Task with Different Control Flow
35//!
36//! ```rust
37//! # use graph_flow::{Task, TaskResult, NextAction, Context};
38//! # use async_trait::async_trait;
39//! struct ConditionalTask;
40//!
41//! #[async_trait]
42//! impl Task for ConditionalTask {
43//! fn id(&self) -> &str {
44//! "conditional_task"
45//! }
46//!
47//! async fn run(&self, context: Context) -> graph_flow::Result<TaskResult> {
48//! let user_input: Option<String> = context.get("user_input").await;
49//!
50//! match user_input {
51//! Some(input) if !input.is_empty() => {
52//! // Process input and continue automatically
53//! context.set("processed", input.to_uppercase()).await;
54//! Ok(TaskResult::new(
55//! Some("Input processed".to_string()),
56//! NextAction::ContinueAndExecute
57//! ))
58//! }
59//! _ => {
60//! // Wait for user input
61//! Ok(TaskResult::new(
62//! Some("Please provide input".to_string()),
63//! NextAction::WaitForInput
64//! ))
65//! }
66//! }
67//! }
68//! }
69//! ```
70
71use async_trait::async_trait;
72use serde::{Deserialize, Serialize};
73
74use crate::{context::Context, error::Result};
75
76/// Result of a task execution.
77///
78/// Contains the response to send to the user and the next action to take.
79/// The `task_id` field is automatically set by the graph execution engine.
80///
81/// # Examples
82///
83/// ```rust
84/// use graph_flow::{TaskResult, NextAction};
85///
86/// // Basic task result
87/// let result = TaskResult::new(
88/// Some("Task completed successfully".to_string()),
89/// NextAction::Continue
90/// );
91///
92/// // Task result with status message
93/// let result = TaskResult::new_with_status(
94/// Some("Data validated".to_string()),
95/// NextAction::Continue,
96/// Some("All validation checks passed".to_string())
97/// );
98///
99/// // Convenience methods
100/// let result = TaskResult::move_to_next(); // Continue to next task
101/// let result = TaskResult::move_to_next_direct(); // Continue and execute immediately
102/// ```
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct TaskResult {
105 /// Response to send to the user
106 pub response: Option<String>,
107 /// Next action to take
108 pub next_action: NextAction,
109 /// ID of the task that generated this result
110 pub task_id: String,
111 /// Optional status message that describes the current state of the task
112 pub status_message: Option<String>,
113}
114
115impl TaskResult {
116 /// Create a new TaskResult with the given response and next action.
117 ///
118 /// The task_id will be set automatically by the graph execution engine.
119 ///
120 /// # Examples
121 ///
122 /// ```rust
123 /// use graph_flow::{TaskResult, NextAction};
124 ///
125 /// let result = TaskResult::new(
126 /// Some("Hello, World!".to_string()),
127 /// NextAction::Continue
128 /// );
129 /// ```
130 pub fn new(response: Option<String>, next_action: NextAction) -> Self {
131 Self {
132 response,
133 next_action,
134 task_id: String::new(),
135 status_message: None,
136 }
137 }
138
139 /// Create a new TaskResult with response, next action, and status message.
140 ///
141 /// The status message is used to describe the current state of the task.
142 /// It's only persisted in the context but not returned to the user.
143 /// Specifically aimed at debugging and logging.
144 ///
145 /// # Examples
146 ///
147 /// ```rust
148 /// use graph_flow::{TaskResult, NextAction};
149 ///
150 /// let result = TaskResult::new_with_status(
151 /// Some("Data processed".to_string()),
152 /// NextAction::Continue,
153 /// Some("Processing completed with 95% confidence".to_string())
154 /// );
155 /// ```
156 pub fn new_with_status(
157 response: Option<String>,
158 next_action: NextAction,
159 status_message: Option<String>,
160 ) -> Self {
161 Self {
162 response,
163 next_action,
164 task_id: String::new(),
165 status_message,
166 }
167 }
168
169 /// Create a TaskResult that moves to the next task (step-by-step execution).
170 ///
171 /// This is a convenience method equivalent to:
172 /// ```rust
173 /// # use graph_flow::{TaskResult, NextAction};
174 /// TaskResult::new(None, NextAction::Continue);
175 /// ```
176 pub fn move_to_next() -> Self {
177 Self {
178 response: None,
179 next_action: NextAction::Continue,
180 task_id: String::new(),
181 status_message: None,
182 }
183 }
184
185 /// Create a TaskResult that moves to the next task and executes it immediately.
186 ///
187 /// This is a convenience method equivalent to:
188 /// ```rust
189 /// # use graph_flow::{TaskResult, NextAction};
190 /// TaskResult::new(None, NextAction::ContinueAndExecute);
191 /// ```
192 pub fn move_to_next_direct() -> Self {
193 Self {
194 response: None,
195 next_action: NextAction::ContinueAndExecute,
196 task_id: String::new(),
197 status_message: None,
198 }
199 }
200}
201
202/// Defines what should happen after a task completes.
203///
204/// This enum controls the flow of execution in your workflow graph.
205/// Different variants provide different execution behaviors.
206///
207/// # Examples
208///
209/// ```rust
210/// use graph_flow::{NextAction, TaskResult};
211///
212/// // Step-by-step execution (pause after this task)
213/// let result = TaskResult::new(
214/// Some("Step 1 complete".to_string()),
215/// NextAction::Continue
216/// );
217///
218/// // Continuous execution (run next task immediately)
219/// let result = TaskResult::new(
220/// Some("Processing...".to_string()),
221/// NextAction::ContinueAndExecute
222/// );
223///
224/// // Wait for user input
225/// let result = TaskResult::new(
226/// Some("Please provide more information".to_string()),
227/// NextAction::WaitForInput
228/// );
229///
230/// // Jump to specific task
231/// let result = TaskResult::new(
232/// Some("Redirecting to error handler".to_string()),
233/// NextAction::GoTo("error_handler".to_string())
234/// );
235///
236/// // End the workflow
237/// let result = TaskResult::new(
238/// Some("Workflow completed!".to_string()),
239/// NextAction::End
240/// );
241/// ```
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
243pub enum NextAction {
244 /// Continue to the next task in the default path (step-by-step execution).
245 ///
246 /// The workflow will pause after this task and wait for the next
247 /// execution call. This gives you control over when the next task runs.
248 ///
249 /// Best for: Interactive applications, web services, debugging
250 Continue,
251
252 /// Continue to the next task and execute it immediately (continuous execution).
253 ///
254 /// The workflow will automatically proceed to the next task without
255 /// pausing. This creates a recursive execution until the workflow
256 /// reaches `End`, `WaitForInput`, or an error.
257 ///
258 /// Best for: Batch processing, automated workflows
259 ContinueAndExecute,
260
261 /// Go to a specific task by ID.
262 ///
263 /// Jump directly to the specified task, skipping the normal edge-based
264 /// flow. Useful for error handling, loops, or dynamic routing.
265 ///
266 /// # Examples
267 ///
268 /// ```rust
269 /// # use graph_flow::{NextAction, TaskResult};
270 /// // Jump to error handler
271 /// let result = TaskResult::new(
272 /// Some("Error detected, routing to handler".to_string()),
273 /// NextAction::GoTo("error_handler".to_string())
274 /// );
275 ///
276 /// // Create a retry loop
277 /// let result = TaskResult::new(
278 /// Some("Retrying...".to_string()),
279 /// NextAction::GoTo("validation_task".to_string())
280 /// );
281 /// ```
282 GoTo(String),
283
284 /// Go back to the previous task.
285 ///
286 /// Note: This was never implemented - it behaves like `WaitForInput`
287 /// (stays at the current task). It will be removed in a future release.
288 #[deprecated(
289 since = "0.5.2",
290 note = "GoBack is not implemented (it behaves like WaitForInput) and will be removed; \
291 use NextAction::GoTo(task_id) for explicit navigation"
292 )]
293 GoBack,
294
295 /// End the graph execution.
296 ///
297 /// Terminates the workflow completely. No further tasks will be executed.
298 End,
299
300 /// Wait for user input before continuing.
301 ///
302 /// Pauses the workflow and waits for external input. The workflow
303 /// will stay at the current task until new data is provided and
304 /// execution is resumed.
305 ///
306 /// Best for: Human-in-the-loop workflows, interactive applications
307 WaitForInput,
308}
309
310/// Core trait that all tasks must implement.
311///
312/// Tasks are the building blocks of your workflow. Each task represents
313/// a unit of work that can access shared context and control the flow
314/// of execution.
315///
316/// # Examples
317///
318/// ## Basic Task
319///
320/// ```rust
321/// use graph_flow::{Task, TaskResult, NextAction, Context};
322/// use async_trait::async_trait;
323///
324/// struct GreetingTask;
325///
326/// #[async_trait]
327/// impl Task for GreetingTask {
328/// fn id(&self) -> &str {
329/// "greeting"
330/// }
331///
332/// async fn run(&self, context: Context) -> graph_flow::Result<TaskResult> {
333/// let name: String = context.get("name").await.unwrap_or("World".to_string());
334/// let greeting = format!("Hello, {}!", name);
335///
336/// Ok(TaskResult::new(Some(greeting), NextAction::Continue))
337/// }
338/// }
339/// ```
340///
341/// ## Task with Default ID
342///
343/// ```rust
344/// # use graph_flow::{Task, TaskResult, NextAction, Context};
345/// # use async_trait::async_trait;
346/// struct DefaultIdTask;
347///
348/// #[async_trait]
349/// impl Task for DefaultIdTask {
350/// // id() is automatically implemented using the type name
351///
352/// async fn run(&self, context: Context) -> graph_flow::Result<TaskResult> {
353/// Ok(TaskResult::new(None, NextAction::End))
354/// }
355/// }
356/// ```
357///
358/// ## Complex Task with Error Handling
359///
360/// ```rust
361/// # use graph_flow::{Task, TaskResult, NextAction, Context, GraphError};
362/// # use async_trait::async_trait;
363/// struct ValidationTask {
364/// max_retries: usize,
365/// }
366///
367/// #[async_trait]
368/// impl Task for ValidationTask {
369/// fn id(&self) -> &str {
370/// "validator"
371/// }
372///
373/// async fn run(&self, context: Context) -> graph_flow::Result<TaskResult> {
374/// let data: Option<String> = context.get("data").await;
375/// let retry_count: usize = context.get("retry_count").await.unwrap_or(0);
376///
377/// match data {
378/// Some(data) if self.validate(&data) => {
379/// context.set("retry_count", 0).await; // Reset counter
380/// Ok(TaskResult::new(
381/// Some("Validation passed".to_string()),
382/// NextAction::Continue
383/// ))
384/// }
385/// Some(_) if retry_count < self.max_retries => {
386/// context.set("retry_count", retry_count + 1).await;
387/// Ok(TaskResult::new(
388/// Some("Validation failed, retrying...".to_string()),
389/// NextAction::GoTo("data_input".to_string())
390/// ))
391/// }
392/// _ => {
393/// Err(GraphError::TaskExecutionFailed(
394/// "Validation failed after max retries".to_string()
395/// ))
396/// }
397/// }
398/// }
399/// }
400///
401/// impl ValidationTask {
402/// fn validate(&self, data: &str) -> bool {
403/// !data.is_empty() && data.len() > 5
404/// }
405/// }
406/// ```
407#[async_trait]
408pub trait Task: Send + Sync {
409 /// Unique identifier for this task.
410 ///
411 /// By default, this returns the type name of the implementing struct.
412 /// Override this method if you need a custom identifier.
413 ///
414 /// # Examples
415 ///
416 /// ```rust
417 /// # use graph_flow::Task;
418 /// # use async_trait::async_trait;
419 /// # use graph_flow::{TaskResult, NextAction, Context};
420 /// // Using default implementation (type name)
421 /// struct MyTask;
422 ///
423 /// #[async_trait]
424 /// impl Task for MyTask {
425 /// // id() will return "my_module::MyTask"
426 /// async fn run(&self, _context: Context) -> graph_flow::Result<TaskResult> {
427 /// Ok(TaskResult::new(None, NextAction::End))
428 /// }
429 /// }
430 ///
431 /// // Using custom ID
432 /// struct CustomTask;
433 ///
434 /// #[async_trait]
435 /// impl Task for CustomTask {
436 /// fn id(&self) -> &str {
437 /// "custom_task_id"
438 /// }
439 ///
440 /// async fn run(&self, _context: Context) -> graph_flow::Result<TaskResult> {
441 /// Ok(TaskResult::new(None, NextAction::End))
442 /// }
443 /// }
444 /// ```
445 fn id(&self) -> &str {
446 std::any::type_name::<Self>()
447 }
448
449 /// Execute the task with the given context.
450 ///
451 /// This is where you implement your task's logic. You have access to
452 /// the shared context for reading input data and storing results.
453 ///
454 /// # Parameters
455 ///
456 /// * `context` - Shared context containing workflow state and data
457 ///
458 /// # Returns
459 ///
460 /// Returns a `Result<TaskResult>` where:
461 /// - `Ok(TaskResult)` indicates successful execution
462 /// - `Err(GraphError)` indicates an error that should stop the workflow
463 ///
464 /// # Examples
465 ///
466 /// ```rust
467 /// # use graph_flow::{Task, TaskResult, NextAction, Context};
468 /// # use async_trait::async_trait;
469 /// struct DataProcessor;
470 ///
471 /// #[async_trait]
472 /// impl Task for DataProcessor {
473 /// fn id(&self) -> &str {
474 /// "data_processor"
475 /// }
476 ///
477 /// async fn run(&self, context: Context) -> graph_flow::Result<TaskResult> {
478 /// // Read input from context
479 /// let input: String = context.get("raw_data").await
480 /// .unwrap_or_default();
481 ///
482 /// // Process the data
483 /// let processed = self.process_data(&input).await?;
484 ///
485 /// // Store result for next task
486 /// context.set("processed_data", processed.clone()).await;
487 ///
488 /// // Return result with next action
489 /// Ok(TaskResult::new(
490 /// Some(format!("Processed {} bytes", processed.len())),
491 /// NextAction::Continue
492 /// ))
493 /// }
494 /// }
495 ///
496 /// impl DataProcessor {
497 /// async fn process_data(&self, input: &str) -> graph_flow::Result<String> {
498 /// // Your processing logic here
499 /// Ok(input.to_uppercase())
500 /// }
501 /// }
502 /// ```
503 async fn run(&self, context: Context) -> Result<TaskResult>;
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use async_trait::async_trait;
510
511 struct TestTaskWithDefaultId;
512
513 #[async_trait]
514 impl Task for TestTaskWithDefaultId {
515 async fn run(&self, _context: Context) -> Result<TaskResult> {
516 Ok(TaskResult::new(None, NextAction::End))
517 }
518 }
519
520 struct TestTaskWithCustomId;
521
522 #[async_trait]
523 impl Task for TestTaskWithCustomId {
524 fn id(&self) -> &str {
525 "custom_task_id"
526 }
527
528 async fn run(&self, _context: Context) -> Result<TaskResult> {
529 Ok(TaskResult::new(None, NextAction::End))
530 }
531 }
532
533 #[test]
534 fn test_default_id_implementation() {
535 let task = TestTaskWithDefaultId;
536 assert_eq!(task.id(), "graph_flow::task::tests::TestTaskWithDefaultId");
537 }
538
539 #[test]
540 fn test_custom_id_override() {
541 let task = TestTaskWithCustomId;
542 assert_eq!(task.id(), "custom_task_id");
543 }
544}