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}