swiftide_tasks/task.rs
1//! Tasks enable you to define a graph of interacting nodes.
2//!
3//! The nodes can be any type that implements the `TaskNode` trait, which defines how the node
4//! will be evaluated with its input and output.
5//!
6//! Prompt-like Swiftide primitives implement `TaskNode`, and it's easy to implement your own. Since
7//! how agents interact is subject to taste, we recommend implementing your own.
8//!
9//! # Examples
10//!
11//! ```no_run
12//! use swiftide_tasks::{NodeError, Task, TaskRunOutcome};
13//!
14//! # #[tokio::main(flavor = "current_thread")]
15//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! let mut task = Task::<i32, i32>::new();
17//!
18//! let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
19//! let finish =
20//! task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input * 2) });
21//!
22//! task.starts_with(start);
23//! task.register_transition(start, move |value| finish.transitions_with(value))?;
24//! task.register_transition(finish, task.transitions_to_finish())?;
25//!
26//! assert_eq!(task.run(2).await?, TaskRunOutcome::Completed(6));
27//! # Ok(())
28//! # }
29//! ```
30//!
31//! WARN: Here be dragons! This api is not stable yet. We are using it in production, and it is
32//! subject to rapid change. However, do not hesitate to open an issue if you find anything.
33use std::{any::Any, future::Future, pin::Pin, sync::Arc};
34
35use super::node::NodeId;
36use super::{
37 adapters::{AsyncFn, SyncFn},
38 errors::TaskError,
39 executor::{JoinHandler, NodeExecutor, TransitionHandler},
40 runtime::Runtime,
41 traits::{
42 AnyNodeExecutor, NodeArg, RegisterTransition, RegisterTransitionAsync, TaskNode,
43 TransitionResult,
44 },
45 transition::{
46 AnyJoinInput, AnyJoinTarget, ConcurrencyModel, JoinDefinition, JoinDestination, JoinInput,
47 JoinTarget, MappedJoinTarget, Transition,
48 },
49};
50
51type TypedNodeExecutor<T> =
52 NodeExecutor<<T as TaskNode>::Input, <T as TaskNode>::Output, <T as TaskNode>::Error>;
53
54/// The observable outcome of calling [`Task::run`] or [`Task::resume`].
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum TaskRunOutcome<Output> {
57 /// The task reached its finish transition and produced an output.
58 Completed(Output),
59 /// The task paused and can be continued with [`Task::resume`].
60 Paused,
61}
62
63/// The currently active task nodes that match a requested node type.
64#[derive(Debug)]
65pub enum CurrentNodes<'a, T> {
66 /// No current node has the requested type.
67 None,
68 /// Exactly one current node has the requested type.
69 One(&'a T),
70 /// Multiple current nodes have the requested type.
71 Multiple(Vec<&'a T>),
72}
73
74impl<'a, T> CurrentNodes<'a, T> {
75 fn from_matches(nodes: Vec<&'a T>) -> Self {
76 match nodes.len() {
77 0 => Self::None,
78 1 => Self::One(nodes[0]),
79 _ => Self::Multiple(nodes),
80 }
81 }
82}
83
84/// A typed task graph that can run sequential, branching, and joining workflows.
85///
86/// Register nodes with [`Task::register_node`] or [`Task::register_node_fn`], choose a start node
87/// with [`Task::starts_with`], connect nodes with [`Task::register_transition`], and then execute
88/// the task with [`Task::run`].
89///
90/// The task value stores runtime state as well as the graph definition, so reuse the same task
91/// when you need pause and resume behavior. Clone a task when you want a fresh runtime with the
92/// same graph definition.
93///
94/// # Examples
95///
96/// ```no_run
97/// use swiftide_tasks::{NodeError, Task, TaskRunOutcome};
98///
99/// # #[tokio::main(flavor = "current_thread")]
100/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
101/// let mut task = Task::<i32, i32>::new();
102///
103/// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
104/// let finish =
105/// task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input * 3) });
106///
107/// task.starts_with(start);
108/// task.register_transition(start, move |value| finish.transitions_with(value))?;
109/// task.register_transition(finish, task.transitions_to_finish())?;
110///
111/// assert_eq!(task.run(2).await?, TaskRunOutcome::Completed(9));
112/// # Ok(())
113/// # }
114/// ```
115#[derive(Debug)]
116pub struct Task<Input: NodeArg, Output: NodeArg> {
117 pub(crate) nodes: Vec<Arc<dyn AnyNodeExecutor>>,
118 pub(crate) start_node: Option<usize>,
119 pub(crate) runtime: Runtime,
120 pub(crate) default_concurrency_model: ConcurrencyModel,
121 _marker: std::marker::PhantomData<(Input, Output)>,
122}
123
124impl<From, F, R> RegisterTransition<From> for F
125where
126 From: TaskNode + 'static + ?Sized,
127 F: Fn(From::Output) -> R + Send + Sync + 'static,
128 R: TransitionResult<From> + 'static,
129{
130 fn register<Input: NodeArg + Clone, Output: NodeArg + Clone>(
131 self,
132 task: &mut Task<Input, Output>,
133 from: NodeId<From>,
134 ) -> Result<(), TaskError> {
135 let transition = Arc::new(self);
136 task.set_transition_handler(
137 from,
138 Arc::new(move |output: From::Output| {
139 let transition = transition.clone();
140 Box::pin(async move { transition(output).into_transition() })
141 }),
142 )
143 }
144}
145
146impl<From, F, Fut, R> RegisterTransitionAsync<From> for F
147where
148 From: TaskNode + 'static + ?Sized,
149 F: Fn(From::Output) -> Fut + Send + Sync + 'static,
150 Fut: Future<Output = R> + Send + 'static,
151 R: TransitionResult<From> + 'static,
152{
153 fn register_async<Input: NodeArg + Clone, Output: NodeArg + Clone>(
154 self,
155 task: &mut Task<Input, Output>,
156 from: NodeId<From>,
157 ) -> Result<(), TaskError> {
158 let transition = Arc::new(self);
159 task.set_transition_handler(
160 from,
161 Arc::new(move |output: From::Output| {
162 let transition = transition.clone();
163 Box::pin(async move { transition(output).await.into_transition() })
164 }),
165 )
166 }
167}
168
169impl<From, To, Payload> RegisterTransition<From> for JoinTarget<To, Payload>
170where
171 From: TaskNode + 'static + ?Sized,
172 From::Output: Into<Payload>,
173 Payload: NodeArg,
174 To: TaskNode<Input = JoinInput<Payload>> + 'static + ?Sized,
175{
176 fn register<Input: NodeArg + Clone, Output: NodeArg + Clone>(
177 self,
178 task: &mut Task<Input, Output>,
179 from: NodeId<From>,
180 ) -> Result<(), TaskError> {
181 task.set_join_handler(
182 from,
183 self.into_definition(),
184 Arc::new(move |output: From::Output| {
185 Box::pin(async move { Arc::new(output.into()) as Arc<dyn Any + Send + Sync> })
186 }),
187 )
188 }
189}
190
191impl<From, To> RegisterTransition<From> for AnyJoinTarget<To>
192where
193 From: TaskNode + 'static + ?Sized,
194 From::Output: NodeArg,
195 To: TaskNode<Input = AnyJoinInput> + 'static + ?Sized,
196{
197 fn register<Input: NodeArg + Clone, Output: NodeArg + Clone>(
198 self,
199 task: &mut Task<Input, Output>,
200 from: NodeId<From>,
201 ) -> Result<(), TaskError> {
202 task.set_join_handler(
203 from,
204 self.into_definition(),
205 Arc::new(move |output: From::Output| {
206 Box::pin(async move { Arc::new(output) as Arc<dyn Any + Send + Sync> })
207 }),
208 )
209 }
210}
211
212impl<From, To, Payload, F> RegisterTransition<From> for MappedJoinTarget<To, Payload, F>
213where
214 From: TaskNode + 'static + ?Sized,
215 To: TaskNode<Input = JoinInput<Payload>> + 'static + ?Sized,
216 F: Fn(From::Output) -> Payload + Send + Sync + 'static,
217 Payload: NodeArg,
218{
219 fn register<Input: NodeArg + Clone, Output: NodeArg + Clone>(
220 self,
221 task: &mut Task<Input, Output>,
222 from: NodeId<From>,
223 ) -> Result<(), TaskError> {
224 let MappedJoinTarget { join_target, map } = self;
225 let map = Arc::new(map);
226 task.set_join_handler(
227 from,
228 join_target.into_definition(),
229 Arc::new(move |output: From::Output| {
230 let map = map.clone();
231 Box::pin(async move { Arc::new(map(output)) as Arc<dyn Any + Send + Sync> })
232 }),
233 )
234 }
235}
236
237impl<From, To, Payload, F, Fut> RegisterTransitionAsync<From> for MappedJoinTarget<To, Payload, F>
238where
239 From: TaskNode + 'static + ?Sized,
240 To: TaskNode<Input = JoinInput<Payload>> + 'static + ?Sized,
241 F: Fn(From::Output) -> Fut + Send + Sync + 'static,
242 Fut: Future<Output = Payload> + Send + 'static,
243 Payload: NodeArg,
244{
245 fn register_async<Input: NodeArg + Clone, Output: NodeArg + Clone>(
246 self,
247 task: &mut Task<Input, Output>,
248 from: NodeId<From>,
249 ) -> Result<(), TaskError> {
250 let MappedJoinTarget { join_target, map } = self;
251 let map = Arc::new(map);
252 task.set_join_handler(
253 from,
254 join_target.into_definition(),
255 Arc::new(move |output: From::Output| {
256 let map = map.clone();
257 Box::pin(async move { Arc::new(map(output).await) as Arc<dyn Any + Send + Sync> })
258 }),
259 )
260 }
261}
262
263impl<Input: NodeArg, Output: NodeArg> Clone for Task<Input, Output> {
264 fn clone(&self) -> Self {
265 Self {
266 nodes: self
267 .nodes
268 .iter()
269 .map(|node_executor| {
270 Arc::<dyn AnyNodeExecutor>::from(dyn_clone::clone_box(&**node_executor))
271 })
272 .collect(),
273 start_node: self.start_node,
274 runtime: Runtime::new(),
275 default_concurrency_model: self.default_concurrency_model,
276 _marker: std::marker::PhantomData,
277 }
278 }
279}
280
281impl<Input: NodeArg + Clone, Output: NodeArg + Clone> Default for Task<Input, Output> {
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287impl<Input: NodeArg + Clone, Output: NodeArg + Clone> Task<Input, Output> {
288 /// Creates a new task with the default runtime behavior.
289 pub fn new() -> Self {
290 Self {
291 nodes: Vec::new(),
292 start_node: None,
293 runtime: Runtime::new(),
294 default_concurrency_model: ConcurrencyModel::Sequential,
295 _marker: std::marker::PhantomData,
296 }
297 }
298
299 /// Sets the default concurrency model for transitions that do not override it explicitly.
300 ///
301 /// # Examples
302 ///
303 /// ```no_run
304 /// use swiftide_tasks::{ConcurrencyModel, Task};
305 ///
306 /// let task = Task::<i32, i32>::new()
307 /// .with_default_concurrency_model(ConcurrencyModel::Parallel);
308 ///
309 /// let _ = task;
310 /// ```
311 #[must_use]
312 pub fn with_default_concurrency_model(
313 mut self,
314 default_concurrency_model: ConcurrencyModel,
315 ) -> Self {
316 self.default_concurrency_model = default_concurrency_model;
317 self.runtime.clear_state();
318 self
319 }
320
321 /// Marks the node where execution should start.
322 ///
323 /// # Examples
324 ///
325 /// ```no_run
326 /// use swiftide_tasks::{NodeError, Task};
327 ///
328 /// let mut task = Task::<i32, i32>::new();
329 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
330 ///
331 /// task.starts_with(start);
332 /// ```
333 pub fn starts_with<T: TaskNode<Input = Input> + Clone + 'static>(
334 &mut self,
335 node_id: NodeId<T>,
336 ) {
337 self.start_node = Some(node_id.id());
338 self.runtime.clear_state();
339 }
340
341 /// Returns a typed transition closure that finishes the task with the final output.
342 ///
343 /// Use this from the final node of a task. Fan-out branches cannot finish independently; when
344 /// multiple branches are conceptually terminal, route them to a join node and finish from that
345 /// join node instead.
346 ///
347 /// # Examples
348 ///
349 /// ```no_run
350 /// use swiftide_tasks::{NodeError, Task, TaskRunOutcome};
351 ///
352 /// # #[tokio::main(flavor = "current_thread")]
353 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
354 /// let mut task = Task::<i32, i32>::new();
355 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
356 ///
357 /// task.starts_with(start);
358 /// task.register_transition(start, task.transitions_to_finish())?;
359 ///
360 /// assert_eq!(task.run(2).await?, TaskRunOutcome::Completed(3));
361 /// # Ok(())
362 /// # }
363 /// ```
364 pub fn transitions_to_finish(&self) -> impl Fn(Output) -> Transition + Send + Sync + 'static {
365 |output| Transition::finish(output)
366 }
367
368 /// Starts the task from its configured start node.
369 ///
370 /// Returns [`TaskRunOutcome::Completed`] when the task reaches its finish transition, or
371 /// [`TaskRunOutcome::Paused`] when execution was intentionally paused.
372 ///
373 /// # Errors
374 ///
375 /// Returns an error when the task is already active, when the graph definition is incomplete,
376 /// or when a node evaluation or transition fails while running the task.
377 #[tracing::instrument(skip(self, input), name = "task.run", err)]
378 pub async fn run(
379 &mut self,
380 input: impl Into<Input>,
381 ) -> Result<TaskRunOutcome<Output>, TaskError> {
382 if self.runtime.is_live() {
383 return Err(TaskError::TaskActive);
384 }
385
386 let start_node = self.validate_transitions()?;
387 self.runtime
388 .run(
389 &self.nodes,
390 start_node,
391 self.default_concurrency_model,
392 input.into(),
393 )
394 .await
395 }
396
397 /// Resets runtime state while keeping the graph definition and last start input.
398 ///
399 /// After calling `reset`, use [`Task::resume`] to rerun the task from the start node with the
400 /// most recent input passed to [`Task::run`].
401 pub fn reset(&mut self) {
402 self.runtime
403 .reset(self.start_node, self.default_concurrency_model);
404 }
405
406 /// Continues a paused or reset task.
407 ///
408 /// # Errors
409 ///
410 /// Returns an error when the task graph is invalid, when there is no paused or reset state to
411 /// resume, or when a node evaluation or transition fails while continuing execution.
412 #[tracing::instrument(skip(self), name = "task.resume", err)]
413 pub async fn resume(&mut self) -> Result<TaskRunOutcome<Output>, TaskError> {
414 self.validate_transitions()?;
415
416 self.runtime
417 .resume(&self.nodes, self.default_concurrency_model)
418 .await
419 }
420
421 /// Returns the current paused or runnable nodes that have the requested type.
422 ///
423 /// Paused branches take precedence over runnable branches. Runnable branches are only returned
424 /// when no paused branches exist.
425 pub fn current_nodes<T: TaskNode + 'static>(&self) -> CurrentNodes<'_, T> {
426 let nodes = self
427 .runtime
428 .current_nodes()
429 .into_iter()
430 .filter_map(|node_id| self.nodes.get(node_id)?.node_as_any().downcast_ref::<T>())
431 .collect();
432
433 CurrentNodes::from_matches(nodes)
434 }
435
436 /// Registers a node in the task graph and returns its typed identifier.
437 ///
438 /// # Examples
439 ///
440 /// ```no_run
441 /// use async_trait::async_trait;
442 /// use swiftide_tasks::{DynNodeId, NodeError, Task, TaskNode};
443 ///
444 /// #[derive(Clone)]
445 /// struct Increment;
446 ///
447 /// #[async_trait]
448 /// impl TaskNode for Increment {
449 /// type Input = i32;
450 /// type Output = i32;
451 /// type Error = NodeError;
452 ///
453 /// async fn evaluate(
454 /// &self,
455 /// _node_id: &DynNodeId<Self>,
456 /// input: &Self::Input,
457 /// ) -> Result<Self::Output, Self::Error> {
458 /// Ok(*input + 1)
459 /// }
460 /// }
461 ///
462 /// let mut task = Task::<i32, i32>::new();
463 /// let start = task.register_node(Increment);
464 ///
465 /// let _ = start;
466 /// ```
467 pub fn register_node<T>(&mut self, node: T) -> NodeId<T>
468 where
469 T: TaskNode + 'static + Clone,
470 {
471 let id = self.nodes.len();
472 let node_id = NodeId::new(id, &node);
473 self.nodes.push(Arc::new(NodeExecutor::new(node, node_id)));
474 node_id
475 }
476
477 /// Registers a synchronous closure as a task node.
478 ///
479 /// This is the convenience entry point for examples, tests, and small bits of task glue.
480 /// For reusable domain logic, prefer implementing [`TaskNode`] directly and calling
481 /// [`Task::register_node`].
482 ///
483 /// # Examples
484 ///
485 /// ```no_run
486 /// use swiftide_tasks::{NodeError, Task};
487 ///
488 /// let mut task = Task::<i32, i32>::new();
489 /// let double = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> {
490 /// Ok(*input * 2)
491 /// });
492 ///
493 /// let _ = double;
494 /// ```
495 pub fn register_node_fn<F, I, O, E>(&mut self, f: F) -> NodeId<SyncFn<F, I, O, E>>
496 where
497 F: Fn(&I) -> Result<O, E> + Send + Sync + Clone + 'static,
498 I: NodeArg + Clone,
499 O: NodeArg + Clone,
500 E: std::error::Error + Send + Sync + 'static,
501 {
502 self.register_node(SyncFn::new(f))
503 }
504
505 /// Registers an asynchronous closure as a task node.
506 ///
507 /// # Examples
508 ///
509 /// ```no_run
510 /// use swiftide_tasks::{NodeError, Task};
511 ///
512 /// let mut task = Task::<i32, i32>::new();
513 /// let double = task.register_node_async_fn(|input: &i32| {
514 /// Box::pin(async move { Ok::<i32, NodeError>(*input * 2) })
515 /// });
516 ///
517 /// let _ = double;
518 /// ```
519 pub fn register_node_async_fn<F, I, O, E>(&mut self, f: F) -> NodeId<AsyncFn<F, I, O, E>>
520 where
521 F: for<'a> Fn(&'a I) -> Pin<Box<dyn Future<Output = Result<O, E>> + Send + 'a>>
522 + Send
523 + Sync
524 + Clone
525 + 'static,
526 I: NodeArg + Clone,
527 O: NodeArg + Clone,
528 E: std::error::Error + Send + Sync + 'static,
529 {
530 self.register_node(AsyncFn::new(f))
531 }
532
533 /// Registers how execution should continue after `from` completes.
534 ///
535 /// The transition may be:
536 /// - a closure returning a typed [`MarkedTransition`](crate::MarkedTransition)
537 /// - a closure returning a raw [`Transition`] for advanced control flow
538 /// - a [`JoinTarget`](crate::JoinTarget) built from a join node
539 /// - a mapped join target produced by [`JoinTarget::map`](crate::JoinTarget::map)
540 ///
541 /// # Examples
542 ///
543 /// ```no_run
544 /// use swiftide_tasks::{NodeError, Task};
545 ///
546 /// let mut task = Task::<i32, i32>::new();
547 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
548 /// let finish = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input * 2) });
549 ///
550 /// task.starts_with(start);
551 /// task.register_transition(start, move |value| finish.transitions_with(value))?;
552 /// # Ok::<(), Box<dyn std::error::Error>>(())
553 /// ```
554 ///
555 /// # Errors
556 ///
557 /// Returns an error when `from` is unknown, when the node already has a registered
558 /// transition, or when the transition type does not match the registered node type.
559 pub fn register_transition<From, R>(
560 &mut self,
561 from: NodeId<From>,
562 transition: R,
563 ) -> Result<(), TaskError>
564 where
565 From: TaskNode + 'static + ?Sized,
566 R: RegisterTransition<From>,
567 {
568 transition.register(self, from)
569 }
570
571 /// Registers an asynchronous transition or async join payload mapping for `from`.
572 ///
573 /// # Examples
574 ///
575 /// ```no_run
576 /// use swiftide_tasks::{NodeError, Task};
577 ///
578 /// let mut task = Task::<i32, i32>::new();
579 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
580 /// let finish = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input * 2) });
581 ///
582 /// task.starts_with(start);
583 /// task.register_transition_async(start, move |value| async move { finish.transitions_with(value) })?;
584 /// # Ok::<(), Box<dyn std::error::Error>>(())
585 /// ```
586 ///
587 /// # Errors
588 ///
589 /// Returns an error when `from` is unknown, when the node already has a registered
590 /// transition, or when the transition type does not match the registered node type.
591 pub fn register_transition_async<From, R>(
592 &mut self,
593 from: NodeId<From>,
594 transition: R,
595 ) -> Result<(), TaskError>
596 where
597 From: TaskNode + 'static + ?Sized,
598 R: RegisterTransitionAsync<From>,
599 {
600 transition.register_async(self, from)
601 }
602
603 fn validate_transitions(&self) -> Result<usize, TaskError> {
604 let start_node = self.start_node.ok_or(TaskError::NoSteps)?;
605
606 for (index, node_executor) in self.nodes.iter().enumerate() {
607 if !node_executor.transition_is_set() {
608 return Err(TaskError::missing_transition(index));
609 }
610 }
611
612 Ok(start_node)
613 }
614
615 fn set_transition_handler<From>(
616 &mut self,
617 from: NodeId<From>,
618 transition: TransitionHandler<From::Output>,
619 ) -> Result<(), TaskError>
620 where
621 From: TaskNode + 'static + ?Sized,
622 {
623 self.executor_mut(from)?.set_transition_handler(transition)
624 }
625
626 fn set_join_handler<From>(
627 &mut self,
628 from: NodeId<From>,
629 definition: JoinDefinition,
630 transition: JoinHandler<From::Output>,
631 ) -> Result<(), TaskError>
632 where
633 From: TaskNode + 'static + ?Sized,
634 {
635 self.executor_mut(from)?
636 .set_join_handler(definition, transition)
637 }
638
639 fn executor_mut<From>(
640 &mut self,
641 from: NodeId<From>,
642 ) -> Result<&mut TypedNodeExecutor<From>, TaskError>
643 where
644 From: TaskNode + 'static + ?Sized,
645 {
646 let node_executor = self
647 .nodes
648 .get_mut(from.id())
649 .ok_or_else(|| TaskError::missing_node(from.id()))?;
650 let node_executor = Arc::get_mut(node_executor).ok_or_else(|| {
651 TaskError::invalid_state(format!("Node {} is currently in use", from.id()))
652 })?;
653
654 let executor =
655 (node_executor as &mut dyn Any)
656 .downcast_mut::<NodeExecutor<From::Input, From::Output, From::Error>>();
657
658 let Some(executor) = executor else {
659 return Err(TaskError::invalid_state(format!(
660 "Transition registration type mismatch for node {}",
661 from.id()
662 )));
663 };
664
665 Ok(executor)
666 }
667}