Skip to main content

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}