Skip to main content

app_kit/workflow/
child.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use crate::AppError;
6use crate::workflow::ActionMode;
7use crate::workflow::definition::CompiledDefinition;
8use crate::workflow::run::RunRecord;
9
10/// Caller-selected execution of a complete, already-admitted child batch.
11///
12/// The returned future may be local and need not be Unpin. App retains ownership
13/// of admission and validates exact returned membership before reducing results
14/// in original child order, independently of the executor's polling order.
15///
16/// The associated GAT future is the App-specific exception to ordinary RPITIT
17/// methods. Both the child futures and returned future may be non-Send and
18/// non-Unpin; retain their concrete types. App owns admission and validates exact
19/// completion membership regardless of custom polling order. A custom executor
20/// must return every admitted completion once or report failure.
21///
22/// # Example
23///
24/// ```
25/// use agents_kit::Agent;
26/// use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
27/// use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
28///     RunStatus, WorkflowDefinition, WorkflowState};
29/// use storage_kit::StateStore;
30/// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
31/// use app_kit::{AppError, workflow::{ActionMode, ChildBatch, ChildCompletion, ChildExecutor}};
32/// use futures_util::FutureExt;
33/// use std::future::Future;
34/// struct InspectChildren;
35/// impl InspectChildren {
36///     fn inspect(result: Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError> {
37///         let completed = result?;
38///         assert_eq!(completed.len(), 1);
39///         assert_eq!(completed[0].ordinal(), 0);
40///         Ok(completed)
41///     }
42/// }
43/// impl ChildExecutor for InspectChildren {
44///     type Execution<'a, F> = futures_util::future::Map<ChildBatch<F>,
45///         fn(Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError>>
46///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
47///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
48///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
49///         assert_eq!(batch.mode(), ActionMode::Asynchronous);
50///         assert_eq!(batch.len(), 1);
51///         assert!(!batch.is_empty());
52///         batch.map(Self::inspect as fn(_) -> _)
53///     }
54/// }
55/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
56/// # runtime.block_on(async {
57/// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
58/// let definition = WorkflowDefinition::parse(r#"{
59///  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
60///  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
61///  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
62///  "input":{"count":{"kind":"literal","value":4}},
63///  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
64/// }"#)?;
65/// let child = WorkflowDefinition::parse(r#"{
66///     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
67///     "state":{"count":0}, "required":["count"],
68///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
69///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
70///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
71///     ]}}], "routes":{}
72/// }"#)?;
73/// let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
74/// let mut app = App::open(config).await?;
75/// let mut workspace = app.create_workspace(WorkspaceConfig {
76///     name: Some("local counter".into()), root: std::env::current_dir()?,
77/// }).await?;
78/// let workflow = workspace.workflow("counter".parse()?).await?;
79/// let mut run = workflow.prepare(WorkflowState::default()).await?;
80/// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
81/// assert_eq!(terminal.state.values()["sum"], 5);
82/// #
83/// app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
84/// # Ok::<(), Box<dyn std::error::Error>>(())
85/// # })?;
86/// # Ok::<(), Box<dyn std::error::Error>>(())
87/// ```
88pub trait ChildExecutor: Send + Sync + 'static {
89    /// The exact caller future for this batch, without future erasure.
90    type Execution<'a, F>: Future<Output = Result<Vec<ChildCompletion>, AppError>> + 'a
91    where
92        Self: 'a,
93        F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
94
95    /// Selects how the admitted child futures are polled and awaited.
96    ///
97    /// App supplies already admitted work; this method chooses polling and collection,
98    /// not admission or new child identity. Pipeline nodes use their own synchronous
99    /// state handoff through the same kernel and bypass this seam.
100    ///
101    /// # Errors
102    ///
103    /// Any `Err` returned by the execution future becomes
104    /// [`WorkflowFailure::Child`](crate::workflow::WorkflowFailure::Child),
105    /// regardless of the [`AppError`] variant. Input and admission operations may
106    /// classify the same variant differently. A panic while constructing or
107    /// polling the future is contained as
108    /// [`WorkflowFailure::Execution`](crate::workflow::WorkflowFailure::Execution).
109    /// Neither failure reduces the parent from partial child results. Existing
110    /// descendant cleanup retains ownership before terminal publication.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// # use agents_kit::Agent;
116    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
117    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
118    /// #     RunStatus, WorkflowDefinition, WorkflowState};
119    /// # use storage_kit::StateStore;
120    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
121    /// use app_kit::{AppError, workflow::{ActionMode, ChildBatch, ChildCompletion, ChildExecutor}};
122    /// use futures_util::FutureExt;
123    /// use std::future::Future;
124    /// struct InspectChildren;
125    /// impl InspectChildren {
126    ///     fn inspect(result: Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError> {
127    ///         let completed = result?;
128    ///         assert_eq!(completed.len(), 1);
129    ///         assert_eq!(completed[0].ordinal(), 0);
130    ///         Ok(completed)
131    ///     }
132    /// }
133    /// impl ChildExecutor for InspectChildren {
134    ///     type Execution<'a, F> = futures_util::future::Map<ChildBatch<F>,
135    ///         fn(Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError>>
136    ///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
137    ///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
138    ///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
139    ///         assert_eq!(batch.mode(), ActionMode::Asynchronous);
140    ///         assert_eq!(batch.len(), 1);
141    ///         assert!(!batch.is_empty());
142    ///         batch.map(Self::inspect as fn(_) -> _)
143    ///     }
144    /// }
145    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
146    /// # runtime.block_on(async {
147    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
148    /// # let definition = WorkflowDefinition::parse(r#"{
149    /// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
150    /// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
151    /// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
152    /// #  "input":{"count":{"kind":"literal","value":4}},
153    /// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
154    /// # }"#)?;
155    /// # let child = WorkflowDefinition::parse(r#"{
156    /// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
157    /// #     "state":{"count":0}, "required":["count"],
158    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
159    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
160    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
161    /// #     ]}}], "routes":{}
162    /// # }"#)?;
163    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
164    /// # let mut app = App::open(config).await?;
165    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
166    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
167    /// # }).await?;
168    /// # let workflow = workspace.workflow("counter".parse()?).await?;
169    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
170    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
171    /// assert_eq!(terminal.state.values()["sum"], 5);
172    /// #
173    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
174    /// # Ok::<(), Box<dyn std::error::Error>>(())
175    /// # })?;
176    /// # Ok::<(), Box<dyn std::error::Error>>(())
177    /// ```
178    fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
179    where
180        F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
181}
182
183/// A non-Clone owner of concrete, pinned, admitted child futures.
184///
185/// Async execution polls all admitted children and awaits every result before
186/// collecting in original order. Sync execution polls sequentially and stops
187/// on the first error. Before its first poll a custom executor can transfer the exact pins with
188/// [`Self::into_tasks`]. No child future needs Send or Unpin.
189///
190/// # Example
191///
192/// ```
193/// # use agents_kit::Agent;
194/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
195/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
196/// #     RunStatus, WorkflowDefinition, WorkflowState};
197/// # use storage_kit::StateStore;
198/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
199/// use app_kit::{AppError, workflow::{ActionMode, ChildBatch, ChildCompletion, ChildExecutor}};
200/// use futures_util::FutureExt;
201/// use std::future::Future;
202/// struct InspectChildren;
203/// impl InspectChildren {
204///     fn inspect(result: Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError> {
205///         let completed = result?;
206///         assert_eq!(completed.len(), 1);
207///         assert_eq!(completed[0].ordinal(), 0);
208///         Ok(completed)
209///     }
210/// }
211/// impl ChildExecutor for InspectChildren {
212///     type Execution<'a, F> = futures_util::future::Map<ChildBatch<F>,
213///         fn(Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError>>
214///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
215///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
216///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
217///         assert_eq!(batch.mode(), ActionMode::Asynchronous);
218///         assert_eq!(batch.len(), 1);
219///         assert!(!batch.is_empty());
220///         batch.map(Self::inspect as fn(_) -> _)
221///     }
222/// }
223/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
224/// # runtime.block_on(async {
225/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
226/// # let definition = WorkflowDefinition::parse(r#"{
227/// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
228/// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
229/// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
230/// #  "input":{"count":{"kind":"literal","value":4}},
231/// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
232/// # }"#)?;
233/// # let child = WorkflowDefinition::parse(r#"{
234/// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
235/// #     "state":{"count":0}, "required":["count"],
236/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
237/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
238/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
239/// #     ]}}], "routes":{}
240/// # }"#)?;
241/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
242/// # let mut app = App::open(config).await?;
243/// # let mut workspace = app.create_workspace(WorkspaceConfig {
244/// #     name: Some("local counter".into()), root: std::env::current_dir()?,
245/// # }).await?;
246/// # let workflow = workspace.workflow("counter".parse()?).await?;
247/// # let mut run = workflow.prepare(WorkflowState::default()).await?;
248/// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
249/// assert_eq!(terminal.state.values()["sum"], 5);
250/// #
251/// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
252/// # Ok::<(), Box<dyn std::error::Error>>(())
253/// # })?;
254/// # Ok::<(), Box<dyn std::error::Error>>(())
255/// ```
256pub struct ChildBatch<F> {
257    mode: ActionMode,
258    tasks: Vec<ChildSlot<F>>,
259    state: ChildBatchState,
260}
261
262enum ChildSlot<F> {
263    Pending(Pin<Box<F>>),
264    Completed(Result<ChildCompletion, AppError>),
265}
266
267enum ChildBatchState {
268    Unpolled,
269    Polling,
270    Finished,
271}
272
273/// Exact terminal child evidence produced only by an admitted child future.
274///
275/// This value cannot be constructed or cloned by a caller. The private record
276/// retains the original run and parent membership for App's validation.
277///
278/// ```compile_fail
279/// use app_kit::workflow::ChildCompletion;
280///
281/// fn duplicate(completion: ChildCompletion) -> (ChildCompletion, ChildCompletion) {
282/// (completion.clone(), completion)
283/// }
284/// ```
285///
286/// Struct update syntax also cannot construct completion evidence:
287///
288/// ```compile_fail
289/// use app_kit::workflow::ChildCompletion;
290/// fn reconstruct(completion: ChildCompletion) -> ChildCompletion {
291/// ChildCompletion { ..completion }
292/// }
293/// ```
294///
295/// # Example
296///
297/// ```
298/// # use agents_kit::Agent;
299/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
300/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
301/// #     RunStatus, WorkflowDefinition, WorkflowState};
302/// # use storage_kit::StateStore;
303/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
304/// use app_kit::{AppError, workflow::{ActionMode, ChildBatch, ChildCompletion, ChildExecutor}};
305/// use futures_util::FutureExt;
306/// use std::future::Future;
307/// struct InspectChildren;
308/// impl InspectChildren {
309///     fn inspect(result: Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError> {
310///         let completed = result?;
311///         assert_eq!(completed.len(), 1);
312///         assert_eq!(completed[0].ordinal(), 0);
313///         Ok(completed)
314///     }
315/// }
316/// impl ChildExecutor for InspectChildren {
317///     type Execution<'a, F> = futures_util::future::Map<ChildBatch<F>,
318///         fn(Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError>>
319///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
320///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
321///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
322///         assert_eq!(batch.mode(), ActionMode::Asynchronous);
323///         assert_eq!(batch.len(), 1);
324///         assert!(!batch.is_empty());
325///         batch.map(Self::inspect as fn(_) -> _)
326///     }
327/// }
328/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
329/// # runtime.block_on(async {
330/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
331/// # let definition = WorkflowDefinition::parse(r#"{
332/// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
333/// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
334/// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
335/// #  "input":{"count":{"kind":"literal","value":4}},
336/// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
337/// # }"#)?;
338/// # let child = WorkflowDefinition::parse(r#"{
339/// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
340/// #     "state":{"count":0}, "required":["count"],
341/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
342/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
343/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
344/// #     ]}}], "routes":{}
345/// # }"#)?;
346/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
347/// # let mut app = App::open(config).await?;
348/// # let mut workspace = app.create_workspace(WorkspaceConfig {
349/// #     name: Some("local counter".into()), root: std::env::current_dir()?,
350/// # }).await?;
351/// # let workflow = workspace.workflow("counter".parse()?).await?;
352/// # let mut run = workflow.prepare(WorkflowState::default()).await?;
353/// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
354/// assert_eq!(terminal.state.values()["sum"], 5);
355/// #
356/// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
357/// # Ok::<(), Box<dyn std::error::Error>>(())
358/// # })?;
359/// # Ok::<(), Box<dyn std::error::Error>>(())
360/// ```
361pub struct ChildCompletion {
362    ordinal: usize,
363    record: Box<RunRecord>,
364}
365
366impl<F> ChildBatch<F> {
367    pub(super) fn new(tasks: Vec<F>, mode: ActionMode) -> Result<Self, AppError> {
368        if mode == ActionMode::Both {
369            return Err(AppError::InvalidInput(
370                "child execution requires an actual mode".into(),
371            ));
372        }
373        Ok(Self {
374            mode,
375            tasks: tasks
376                .into_iter()
377                .map(|task| ChildSlot::Pending(Box::pin(task)))
378                .collect(),
379            state: ChildBatchState::Unpolled,
380        })
381    }
382
383    /// Returns the actual synchronous or asynchronous execution mode.
384    ///
385    /// This never returns the declaration-only [`ActionMode::Both`] value.
386    ///
387    /// # Example
388    ///
389    /// ```
390    /// # use agents_kit::Agent;
391    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
392    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
393    /// #     RunStatus, WorkflowDefinition, WorkflowState};
394    /// # use storage_kit::StateStore;
395    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
396    /// use app_kit::{AppError, workflow::{ActionMode, ChildBatch, ChildCompletion, ChildExecutor}};
397    /// use futures_util::FutureExt;
398    /// use std::future::Future;
399    /// struct InspectChildren;
400    /// impl InspectChildren {
401    ///     fn inspect(result: Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError> {
402    ///         let completed = result?;
403    ///         assert_eq!(completed.len(), 1);
404    ///         assert_eq!(completed[0].ordinal(), 0);
405    ///         Ok(completed)
406    ///     }
407    /// }
408    /// impl ChildExecutor for InspectChildren {
409    ///     type Execution<'a, F> = futures_util::future::Map<ChildBatch<F>,
410    ///         fn(Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError>>
411    ///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
412    ///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
413    ///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
414    ///         assert_eq!(batch.mode(), ActionMode::Asynchronous);
415    ///         assert_eq!(batch.len(), 1);
416    ///         assert!(!batch.is_empty());
417    ///         batch.map(Self::inspect as fn(_) -> _)
418    ///     }
419    /// }
420    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
421    /// # runtime.block_on(async {
422    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
423    /// # let definition = WorkflowDefinition::parse(r#"{
424    /// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
425    /// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
426    /// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
427    /// #  "input":{"count":{"kind":"literal","value":4}},
428    /// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
429    /// # }"#)?;
430    /// # let child = WorkflowDefinition::parse(r#"{
431    /// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
432    /// #     "state":{"count":0}, "required":["count"],
433    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
434    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
435    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
436    /// #     ]}}], "routes":{}
437    /// # }"#)?;
438    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
439    /// # let mut app = App::open(config).await?;
440    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
441    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
442    /// # }).await?;
443    /// # let workflow = workspace.workflow("counter".parse()?).await?;
444    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
445    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
446    /// assert_eq!(terminal.state.values()["sum"], 5);
447    /// #
448    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
449    /// # Ok::<(), Box<dyn std::error::Error>>(())
450    /// # })?;
451    /// # Ok::<(), Box<dyn std::error::Error>>(())
452    /// ```
453    pub fn mode(&self) -> ActionMode {
454        self.mode
455    }
456
457    /// Returns the number of child tasks or completed results still owned here.
458    ///
459    /// After the Future returns its results or an error, the batch owns none.
460    ///
461    /// # Example
462    ///
463    /// ```
464    /// # use agents_kit::Agent;
465    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
466    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
467    /// #     RunStatus, WorkflowDefinition, WorkflowState};
468    /// # use storage_kit::StateStore;
469    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
470    /// use app_kit::{AppError, workflow::{ActionMode, ChildBatch, ChildCompletion, ChildExecutor}};
471    /// use futures_util::FutureExt;
472    /// use std::future::Future;
473    /// struct InspectChildren;
474    /// impl InspectChildren {
475    ///     fn inspect(result: Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError> {
476    ///         let completed = result?;
477    ///         assert_eq!(completed.len(), 1);
478    ///         assert_eq!(completed[0].ordinal(), 0);
479    ///         Ok(completed)
480    ///     }
481    /// }
482    /// impl ChildExecutor for InspectChildren {
483    ///     type Execution<'a, F> = futures_util::future::Map<ChildBatch<F>,
484    ///         fn(Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError>>
485    ///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
486    ///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
487    ///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
488    ///         assert_eq!(batch.mode(), ActionMode::Asynchronous);
489    ///         assert_eq!(batch.len(), 1);
490    ///         assert!(!batch.is_empty());
491    ///         batch.map(Self::inspect as fn(_) -> _)
492    ///     }
493    /// }
494    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
495    /// # runtime.block_on(async {
496    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
497    /// # let definition = WorkflowDefinition::parse(r#"{
498    /// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
499    /// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
500    /// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
501    /// #  "input":{"count":{"kind":"literal","value":4}},
502    /// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
503    /// # }"#)?;
504    /// # let child = WorkflowDefinition::parse(r#"{
505    /// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
506    /// #     "state":{"count":0}, "required":["count"],
507    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
508    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
509    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
510    /// #     ]}}], "routes":{}
511    /// # }"#)?;
512    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
513    /// # let mut app = App::open(config).await?;
514    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
515    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
516    /// # }).await?;
517    /// # let workflow = workspace.workflow("counter".parse()?).await?;
518    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
519    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
520    /// assert_eq!(terminal.state.values()["sum"], 5);
521    /// #
522    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
523    /// # Ok::<(), Box<dyn std::error::Error>>(())
524    /// # })?;
525    /// # Ok::<(), Box<dyn std::error::Error>>(())
526    /// ```
527    pub fn len(&self) -> usize {
528        self.tasks.len()
529    }
530
531    /// Reports whether this batch still owns any child task or result.
532    ///
533    /// # Example
534    ///
535    /// ```
536    /// # use agents_kit::Agent;
537    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
538    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
539    /// #     RunStatus, WorkflowDefinition, WorkflowState};
540    /// # use storage_kit::StateStore;
541    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
542    /// use app_kit::{AppError, workflow::{ActionMode, ChildBatch, ChildCompletion, ChildExecutor}};
543    /// use futures_util::FutureExt;
544    /// use std::future::Future;
545    /// struct InspectChildren;
546    /// impl InspectChildren {
547    ///     fn inspect(result: Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError> {
548    ///         let completed = result?;
549    ///         assert_eq!(completed.len(), 1);
550    ///         assert_eq!(completed[0].ordinal(), 0);
551    ///         Ok(completed)
552    ///     }
553    /// }
554    /// impl ChildExecutor for InspectChildren {
555    ///     type Execution<'a, F> = futures_util::future::Map<ChildBatch<F>,
556    ///         fn(Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError>>
557    ///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
558    ///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
559    ///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
560    ///         assert_eq!(batch.mode(), ActionMode::Asynchronous);
561    ///         assert_eq!(batch.len(), 1);
562    ///         assert!(!batch.is_empty());
563    ///         batch.map(Self::inspect as fn(_) -> _)
564    ///     }
565    /// }
566    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
567    /// # runtime.block_on(async {
568    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
569    /// # let definition = WorkflowDefinition::parse(r#"{
570    /// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
571    /// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
572    /// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
573    /// #  "input":{"count":{"kind":"literal","value":4}},
574    /// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
575    /// # }"#)?;
576    /// # let child = WorkflowDefinition::parse(r#"{
577    /// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
578    /// #     "state":{"count":0}, "required":["count"],
579    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
580    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
581    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
582    /// #     ]}}], "routes":{}
583    /// # }"#)?;
584    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
585    /// # let mut app = App::open(config).await?;
586    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
587    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
588    /// # }).await?;
589    /// # let workflow = workspace.workflow("counter".parse()?).await?;
590    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
591    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
592    /// assert_eq!(terminal.state.values()["sum"], 5);
593    /// #
594    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
595    /// # Ok::<(), Box<dyn std::error::Error>>(())
596    /// # })?;
597    /// # Ok::<(), Box<dyn std::error::Error>>(())
598    /// ```
599    pub fn is_empty(&self) -> bool {
600        self.len() == 0
601    }
602
603    /// Transfers the exact pinned child futures before this batch is polled.
604    ///
605    /// Once polling begins, returns Conflict and drops the consumed batch,
606    /// including its remaining futures and retained completion evidence. This
607    /// never moves a child out of its pin or creates a second execution owner.
608    ///
609    /// # Errors
610    ///
611    /// Returns [`AppError::Conflict`] once any batch polling has begun, even if no
612    /// child has completed. The consumed batch is dropped on error, preserving pin
613    /// safety and preventing duplicate completion ownership.
614    ///
615    /// # Example
616    ///
617    /// ```
618    /// # use agents_kit::Agent;
619    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
620    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
621    /// #     RunStatus, WorkflowDefinition, WorkflowState};
622    /// # use storage_kit::StateStore;
623    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
624    /// use app_kit::{AppError, workflow::{ChildBatch, ChildCompletion, ChildExecutor}};
625    /// use futures_util::FutureExt;
626    /// use std::{future::Future, pin::Pin};
627    /// struct InspectChildren;
628    /// impl InspectChildren {
629    ///     fn collect(results: Vec<Result<ChildCompletion, AppError>>) -> Result<Vec<ChildCompletion>, AppError> {
630    ///         results.into_iter().collect()
631    ///     }
632    /// }
633    /// impl ChildExecutor for InspectChildren {
634    ///     type Execution<'a, F> = futures_util::future::Map<futures_util::future::JoinAll<Pin<Box<F>>>,
635    ///         fn(Vec<Result<ChildCompletion, AppError>>) -> Result<Vec<ChildCompletion>, AppError>>
636    ///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
637    ///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
638    ///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
639    ///         // App delivers an unpolled batch. Keep each concrete future in its original pin.
640    ///         let tasks = batch.into_tasks().expect("batch has not been polled");
641    ///         futures_util::future::join_all(tasks).map(Self::collect as fn(_) -> _)
642    ///     }
643    /// }
644    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
645    /// # runtime.block_on(async {
646    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
647    /// # let definition = WorkflowDefinition::parse(r#"{
648    /// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
649    /// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
650    /// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
651    /// #  "input":{"count":{"kind":"literal","value":4}},
652    /// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
653    /// # }"#)?;
654    /// # let child = WorkflowDefinition::parse(r#"{
655    /// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
656    /// #     "state":{"count":0}, "required":["count"],
657    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
658    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
659    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
660    /// #     ]}}], "routes":{}
661    /// # }"#)?;
662    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
663    /// # let mut app = App::open(config).await?;
664    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
665    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
666    /// # }).await?;
667    /// # let workflow = workspace.workflow("counter".parse()?).await?;
668    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
669    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
670    /// assert_eq!(terminal.state.values()["sum"], 5);
671    /// #
672    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
673    /// # Ok::<(), Box<dyn std::error::Error>>(())
674    /// # })?;
675    /// # Ok::<(), Box<dyn std::error::Error>>(())
676    /// ```
677    pub fn into_tasks(self) -> Result<Vec<Pin<Box<F>>>, AppError> {
678        match self.state {
679            ChildBatchState::Unpolled => self
680                .tasks
681                .into_iter()
682                .map(|slot| match slot {
683                    ChildSlot::Pending(task) => Ok(task),
684                    ChildSlot::Completed(_) => Err(AppError::Conflict(
685                        "child batch polling already began".into(),
686                    )),
687                })
688                .collect(),
689            ChildBatchState::Polling | ChildBatchState::Finished => Err(AppError::Conflict(
690                "child batch polling already began".into(),
691            )),
692        }
693    }
694}
695
696impl<F: Future<Output = Result<ChildCompletion, AppError>>> Future for ChildBatch<F> {
697    type Output = Result<Vec<ChildCompletion>, AppError>;
698
699    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
700        let batch = self.get_mut();
701        if matches!(batch.state, ChildBatchState::Unpolled) {
702            batch.state = ChildBatchState::Polling;
703        }
704        let ChildBatchState::Polling = batch.state else {
705            return Poll::Ready(Err(AppError::Conflict(
706                "child batch already finished".into(),
707            )));
708        };
709        let mut pending = false;
710        for slot in &mut batch.tasks {
711            let ChildSlot::Pending(task) = slot else {
712                continue;
713            };
714            let result = match task.as_mut().poll(cx) {
715                Poll::Pending if batch.mode == ActionMode::Synchronous => return Poll::Pending,
716                Poll::Pending => {
717                    pending = true;
718                    continue;
719                }
720                Poll::Ready(result) => result,
721            };
722            // Dropping the completed future releases its resources while its
723            // exact completion evidence remains owned until the batch returns.
724            match (batch.mode, result) {
725                (ActionMode::Synchronous, Err(error)) => {
726                    batch.tasks.clear();
727                    batch.state = ChildBatchState::Finished;
728                    return Poll::Ready(Err(error));
729                }
730                (_, result) => *slot = ChildSlot::Completed(result),
731            }
732        }
733        if pending {
734            return Poll::Pending;
735        }
736        let results = std::mem::take(&mut batch.tasks)
737            .into_iter()
738            .map(|slot| match slot {
739                ChildSlot::Completed(result) => result,
740                ChildSlot::Pending(_) => Err(AppError::Integration(
741                    "child completion is unavailable".into(),
742                )),
743            })
744            .collect();
745        batch.state = ChildBatchState::Finished;
746        Poll::Ready(results)
747    }
748}
749
750impl ChildCompletion {
751    pub(super) fn admitted(
752        record: RunRecord,
753        parent: &RunRecord,
754        definition: &CompiledDefinition,
755    ) -> Result<Self, AppError> {
756        record.validate_child(parent, definition)?;
757        if !record.checkpoint.is_terminal() {
758            return Err(AppError::Conflict(
759                "child has no terminal completion".into(),
760            ));
761        }
762        let ordinal = record
763            .parent
764            .as_ref()
765            .ok_or_else(|| AppError::Integration("child parent evidence unavailable".into()))?
766            .index;
767        Ok(Self {
768            ordinal,
769            record: Box::new(record),
770        })
771    }
772
773    /// Returns this child's ordinal in the original admitted batch.
774    ///
775    /// # Example
776    ///
777    /// ```
778    /// # use agents_kit::Agent;
779    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
780    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
781    /// #     RunStatus, WorkflowDefinition, WorkflowState};
782    /// # use storage_kit::StateStore;
783    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
784    /// use app_kit::{AppError, workflow::{ActionMode, ChildBatch, ChildCompletion, ChildExecutor}};
785    /// use futures_util::FutureExt;
786    /// use std::future::Future;
787    /// struct InspectChildren;
788    /// impl InspectChildren {
789    ///     fn inspect(result: Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError> {
790    ///         let completed = result?;
791    ///         assert_eq!(completed.len(), 1);
792    ///         assert_eq!(completed[0].ordinal(), 0);
793    ///         Ok(completed)
794    ///     }
795    /// }
796    /// impl ChildExecutor for InspectChildren {
797    ///     type Execution<'a, F> = futures_util::future::Map<ChildBatch<F>,
798    ///         fn(Result<Vec<ChildCompletion>, AppError>) -> Result<Vec<ChildCompletion>, AppError>>
799    ///     where Self: 'a, F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
800    ///     fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
801    ///     where F: Future<Output = Result<ChildCompletion, AppError>> + 'a {
802    ///         assert_eq!(batch.mode(), ActionMode::Asynchronous);
803    ///         assert_eq!(batch.len(), 1);
804    ///         assert!(!batch.is_empty());
805    ///         batch.map(Self::inspect as fn(_) -> _)
806    ///     }
807    /// }
808    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
809    /// # runtime.block_on(async {
810    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
811    /// # let definition = WorkflowDefinition::parse(r#"{
812    /// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
813    /// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
814    /// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
815    /// #  "input":{"count":{"kind":"literal","value":4}},
816    /// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
817    /// # }"#)?;
818    /// # let child = WorkflowDefinition::parse(r#"{
819    /// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
820    /// #     "state":{"count":0}, "required":["count"],
821    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
822    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
823    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
824    /// #     ]}}], "routes":{}
825    /// # }"#)?;
826    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child]).with_child_executor(InspectChildren);
827    /// # let mut app = App::open(config).await?;
828    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
829    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
830    /// # }).await?;
831    /// # let workflow = workspace.workflow("counter".parse()?).await?;
832    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
833    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
834    /// assert_eq!(terminal.state.values()["sum"], 5);
835    /// #
836    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
837    /// # Ok::<(), Box<dyn std::error::Error>>(())
838    /// # })?;
839    /// # Ok::<(), Box<dyn std::error::Error>>(())
840    /// ```
841    pub fn ordinal(&self) -> usize {
842        self.ordinal
843    }
844
845    pub(super) fn record(&self) -> &RunRecord {
846        &self.record
847    }
848
849    pub(super) fn into_record(self) -> RunRecord {
850        *self.record
851    }
852}
853
854/// Default executor that applies the batch's actual execution-mode policy.
855///
856/// # Example
857///
858/// ```
859/// # use agents_kit::Agent;
860/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
861/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
862/// #     RunStatus, WorkflowDefinition, WorkflowState};
863/// # use storage_kit::StateStore;
864/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
865/// #
866/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
867/// # runtime.block_on(async {
868/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
869/// # let definition = WorkflowDefinition::parse(r#"{
870/// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
871/// #  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
872/// #  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
873/// #  "input":{"count":{"kind":"literal","value":4}},
874/// #  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
875/// # }"#)?;
876/// # let child = WorkflowDefinition::parse(r#"{
877/// #     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
878/// #     "state":{"count":0}, "required":["count"],
879/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
880/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
881/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
882/// #     ]}}], "routes":{}
883/// # }"#)?;
884/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child])
885/// #     .with_child_executor(app_kit::workflow::InlineExecutor);
886/// # let mut app = App::open(config).await?;
887/// # let mut workspace = app.create_workspace(WorkspaceConfig {
888/// #     name: Some("local counter".into()), root: std::env::current_dir()?,
889/// # }).await?;
890/// # let workflow = workspace.workflow("counter".parse()?).await?;
891/// # let mut run = workflow.prepare(WorkflowState::default()).await?;
892/// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
893/// assert_eq!(terminal.state.values()["sum"], 5);
894/// #
895/// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
896/// # Ok::<(), Box<dyn std::error::Error>>(())
897/// # })?;
898/// # Ok::<(), Box<dyn std::error::Error>>(())
899/// ```
900#[derive(Default)]
901pub struct InlineExecutor;
902
903impl ChildExecutor for InlineExecutor {
904    type Execution<'a, F>
905        = ChildBatch<F>
906    where
907        Self: 'a,
908        F: Future<Output = Result<ChildCompletion, AppError>> + 'a;
909
910    fn execute<'a, F>(&'a self, batch: ChildBatch<F>) -> Self::Execution<'a, F>
911    where
912        F: Future<Output = Result<ChildCompletion, AppError>> + 'a,
913    {
914        batch
915    }
916}