Skip to main content

app_kit/workflow/
mod.rs

1//! Workflow authoring, execution, immutable state, and retained history.
2//!
3//! Parse strict versioned JSON with
4//! [`WorkflowDefinition::parse`](crate::workflow::WorkflowDefinition::parse), inspect its
5//! [`GraphDescription`](crate::workflow::GraphDescription), and transfer definitions
6//! through [`crate::AppConfig`].
7//! [`Workflow::start`](crate::workflow::Workflow::start) drives accepted work in the background;
8//! [`Workflow::prepare`](crate::workflow::Workflow::prepare) admits checkpoint zero for explicit
9//! [`WorkflowRun::execution`](crate::workflow::WorkflowRun::execution) or
10//! [`SyncExecution`](crate::workflow::SyncExecution). Synchronous calls require an outside thread and a runtime
11//! that stays driven elsewhere. Both lanes share the same commit authority.
12//!
13//! [`ExecutionEvent::Partial`](crate::workflow::ExecutionEvent::Partial) never commits state.
14//! [`ExecutionEvent::Committed`](crate::workflow::ExecutionEvent::Committed)
15//! identifies a durable boundary; action ordinals and checkpoint sequences are
16//! different coordinates. [`WorkflowRun::history`](crate::workflow::WorkflowRun::history)
17//! reads a fixed cutoff, and [`Workflow::restore`](crate::workflow::Workflow::restore)
18//! copies clean retained state into a new paused run.
19//! History remains until explicit [`WorkflowRun::delete`](crate::workflow::WorkflowRun::delete).
20//!
21//! Concrete behavior belongs to [`ActionBackend`],
22//! [`ConditionBackend`], [`StateTyping`],
23//! [`LifecycleHooks`] and [`ChildExecutor`].
24//! The child executor's
25//! associated future can remain local/non-Send without future erasure.
26//! [`StateSerialization`](crate::workflow::StateSerialization) separately registers JSON field/tag codecs only.
27//! Supporting DTOs describe inputs, results and controls; runtime ownership
28//! stays private. Run observers do not cancel work when dropped; execution owners
29//! revoke uncommitted advancement while preserving admitted native commits.
30
31//! # Execute, observe, restore and delete
32//!
33//! This local workflow admits a paused run, explicitly drives its lazy execution,
34//! and then reads immutable history. Merely obtaining a run snapshot does not
35//! advance a prepared run. Restore copies a retained checkpoint into another paused
36//! run; deletion removes only the selected terminal history, not its independent
37//! copy. The example needs no credentials, Agent value or subprocess.
38//!
39//! ```
40//! use agents_kit::Agent;
41//! use app_kit::{App, AppConfig, AppError, Shutdown, WorkspaceConfig};
42//! use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions,
43//!     RestoreOptions, ResumePosition, RunSelector, RunStatus, WorkflowDefinition, WorkflowState};
44//! use storage_kit::StateStore;
45//! use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
46//! use std::{num::NonZeroUsize, time::Duration};
47//! # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
48//! # runtime.block_on(async {
49//! let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
50//! let definition = WorkflowDefinition::parse(r#"{
51//!     "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
52//!     "state":{"count":0},"required":["count"],"reducers":{"count":"reject_concurrent"},
53//!     "entry":"increment","nodes":[{"id":"increment","step":{"kind":"state",
54//!     "mutations":[{"kind":"increment","target":"count","value":{"kind":"literal","value":1}}]}}],
55//!     "routes":{}
56//! }"#)?;
57//! let mut app = App::open(AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition])).await?;
58//! let outcome = async {
59//!     let workspace = app.create_workspace(WorkspaceConfig {
60//!         name: Some("history example".into()), root: std::env::current_dir()?,
61//!     }).await?;
62//!     let workflow = workspace.workflow("counter".parse()?).await?;
63//!     let mut run = workflow.prepare(WorkflowState::default()).await?;
64//!     assert!(matches!(run.snapshot().await?.status, RunStatus::Paused));
65//!     assert_eq!(run.checkpoint(CheckpointSelector::Sequence(0)).await?.state.values()["count"], 0);
66//!     let mut execution = run.execution(ExecutionOptions::default()).await?;
67//!     let mut committed_actions = 0;
68//!     while let Some(event) = execution.next().await? {
69//!         if let ExecutionEvent::Committed { outcomes, .. } = event {
70//!             committed_actions += outcomes.len();
71//!         }
72//!     }
73//!     let terminal = execution.finish().await?;
74//!     assert!(matches!(terminal.status, RunStatus::Succeeded));
75//!     assert_eq!(terminal.state.values()["count"], 1);
76//!     assert_eq!(committed_actions, 1);
77//!
78//!     let first = run.history(None, NonZeroUsize::new(1).unwrap()).await?;
79//!     assert_eq!(first.checkpoints[0].sequence, 0);
80//!     let cursor = first.next.expect("the action has a later checkpoint");
81//!     let later = run.history(Some(cursor), NonZeroUsize::new(10).unwrap()).await?;
82//!     assert!(later.checkpoints.iter().any(|checkpoint| checkpoint.action_ordinal == Some(0)));
83//!     let mut copy = workflow.restore(RestoreOptions {
84//!         source: RunSelector::Latest(run.id().clone()), checkpoint: CheckpointSelector::Action(0),
85//!         overrides: WorkflowState::default(), position: ResumePosition::AfterCompleted, fallback: None,
86//!     }).await?;
87//!     assert_ne!(copy.id(), run.id());
88//!     assert!(matches!(copy.snapshot().await?.status, RunStatus::Paused));
89//!     let copied = copy.checkpoint(CheckpointSelector::Sequence(0)).await?;
90//!     assert_eq!(copied.state.values()["count"], 1);
91//!     assert_eq!(&copied.lineage.as_ref().unwrap().source, run.id());
92//!     let copied_terminal = copy.execution(ExecutionOptions::default()).await?.finish().await?;
93//!     assert!(matches!(copied_terminal.status, RunStatus::Succeeded));
94//!     copy.delete(copied_terminal.checkpoint).await?;
95//!     let original_id = run.id().clone();
96//!     run.delete(terminal.checkpoint).await?;
97//!     assert!(matches!(workflow.open_run(RunSelector::Latest(original_id)).await, Err(AppError::NotFound(_))));
98//!     Ok::<(), Box<dyn std::error::Error>>(())
99//! }.await;
100//! let cleanup = app.shutdown(Shutdown::CancelAndDrain { timeout: Duration::from_secs(5) }).await;
101//! outcome?; // An execution error remains primary if shutdown also reports an error.
102//! cleanup?;
103//! # Ok::<(), Box<dyn std::error::Error>>(())
104//! # })?;
105//! # Ok::<(), Box<dyn std::error::Error>>(())
106//! ```
107//!
108//! A real Agent-backed workflow can stop at an actionable interaction rather than
109//! success: inspect [`crate::workflow::RunSnapshot::interactions`] and [`crate::workflow::RunStatus`] before choosing
110//! an explicit resolution. Caller actions own their external effects; a discarded
111//! uncommitted workflow state change does not undo external I/O.
112//!
113//! # Configure caller behavior
114//!
115//! [`crate::AppConfig`]'s configuration map links the complete implementations of
116//! all five behavior traits and their consuming attachment methods. Actions and
117//! conditions use exact declared keys/revisions; typing validates initial and
118//! resulting state; hooks observe lifecycle boundaries; child executors drive the
119//! already-admitted concrete child futures. None changes the lower Agent or
120//! StateStore associated child types. See [`crate::App::open`] for durable reopen
121//! and the cold migration boundary.
122
123use std::fmt;
124use std::num::NonZeroUsize;
125use std::str::FromStr;
126use std::sync::{Arc, RwLock};
127
128use agents_kit::AgentBackend;
129use agents_kit::notification::{ConversationNotification, NotificationResolution};
130use serde::{Deserialize, Deserializer, Serialize, Serializer};
131use serde_json::{Map, Value};
132use sha2::{Digest, Sha256};
133use storage_kit::StateStoreBackend;
134use uuid::Uuid;
135
136use crate::{AppError, IdentitySyntax};
137
138mod action;
139mod child;
140pub(crate) mod definition;
141mod execution;
142pub(crate) use execution::{MAX_FRONTIER, StagedOutcomeRecord};
143mod graph;
144mod history;
145pub(crate) mod interaction;
146mod lifecycle;
147pub(crate) mod run;
148mod serialization;
149mod state;
150
151pub use action::{
152    ActionBackend, ActionContext, ActionDeclaration, ActionEvent, ActionMode, ActionRequest,
153    ConditionBackend, ConditionDeclaration, NoActions, NoConditions,
154};
155pub use child::{ChildBatch, ChildCompletion, ChildExecutor, InlineExecutor};
156pub use history::{
157    ActionOutcome, Checkpoint, CheckpointKind, CheckpointSelector, HistoryCursor, HistoryPage,
158    RestoreOptions, ResumePosition, RunLineage,
159};
160pub use lifecycle::{LifecycleEvent, LifecycleHooks, NoHooks};
161pub use run::session::{Execution, SyncExecution};
162
163/// Immutable, ordered workflow values validated at each operation boundary.
164///
165/// ```
166/// use app_kit::workflow::{JsonState, StateChange, WorkflowState};
167/// use serde_json::{Map, Value};
168/// let before = WorkflowState::new(Map::from_iter([("count".into(), Value::from(1))]))?;
169/// let after = before.apply(&StateChange::Increment {
170///     values: vec![("count".into(), Value::from(1))],
171/// }, &JsonState::default())?;
172/// assert_eq!(before.values()["count"], 1);
173/// assert_eq!(after.values()["count"], 2);
174/// # Ok::<(), app_kit::AppError>(())
175/// ```
176///
177/// Changes go through the validated state operations:
178///
179/// ```compile_fail
180/// use app_kit::workflow::WorkflowState;
181/// let mut state = WorkflowState::default();
182/// state.values.clear();
183/// ```
184#[derive(Clone, Debug, Default, PartialEq)]
185pub struct WorkflowState {
186    values: Map<String, Value>,
187}
188
189/// Process-global field encoders, field decoders and tagged JSON decoders.
190///
191/// Registrations are exact names and replacements affect the next operation.
192/// Each operation retains one registry snapshot and invokes callbacks without
193/// holding its lock. The only construction entrypoint is [`Self::global`].
194///
195/// ```
196/// use app_kit::workflow::StateSerialization;
197/// StateSerialization::global().register_tag("text".into(), |value| {
198/// value.get("value").cloned().ok_or("missing text value")
199/// })?;
200/// # Ok::<(), app_kit::AppError>(())
201/// ```
202///
203/// Only field/tag JSON codecs enter this registry; actions, hooks, child executors
204/// and lower backends remain concrete configuration values. Registration does not
205/// persist callable identity; callers version their own portable codec payloads.
206pub struct StateSerialization {
207    registry: RwLock<Arc<serialization::SerializationRegistry>>,
208}
209
210/// A resolved immutable state operation; expressions remain private syntax.
211///
212/// Each payload is one atomic operation with at most 4,096 entries. Repeated
213/// Append/Extend/Increment keys apply in order; any later failure discards the
214/// whole candidate. Merge and Subset use full-state validation rather than
215/// primitive transition callbacks. Results never mutate the original state.
216pub enum StateChange {
217    /// Replaces values atomically in payload order, preserving existing positions.
218    Assign {
219        /// Complete values to replace or insert.
220        values: Map<String, Value>,
221    },
222    /// Appends one item per entry, creating arrays for missing keys.
223    Append {
224        /// Ordered entries; repeated keys append repeatedly within one operation.
225        values: Vec<(String, Value)>,
226    },
227    /// Extends arrays atomically without recursively flattening their items.
228    Extend {
229        /// Ordered entries; repeated keys preserve every supplied item.
230        values: Vec<(String, Vec<Value>)>,
231    },
232    /// Adds signed integer deltas to integer values, or zero for missing keys.
233    Increment {
234        /// Ordered deltas, each representable as i64; repeated keys fold in order.
235        values: Vec<(String, Value)>,
236    },
237    /// Removes keys atomically, preserving the order of other keys.
238    Remove {
239        /// Missing and repeated names are harmless.
240        keys: Vec<String>,
241    },
242    /// Keeps keys in their existing order; typing observes resolved Remove keys.
243    Wipe {
244        /// Keys to retain; missing and repeated names are harmless.
245        keep: Vec<String>,
246    },
247    /// Replaces overlapping values and appends new keys in incoming order.
248    Merge {
249        /// Values to merge into the current state.
250        state: WorkflowState,
251    },
252    /// Selects present keys in the requested order.
253    Subset {
254        /// Ordered keys to select; missing keys are ignored.
255        keys: Vec<String>,
256    },
257}
258
259/// Caller-owned typed construction and state validation.
260///
261/// Data is created and consumed locally and need not be Clone, Send or serializable.
262/// The default roundtrip checks successful reconstruction, not raw equality.
263///
264/// # Example
265///
266/// ```
267/// use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
268/// use serde_json::{Map, Value};
269/// struct Counter;
270/// impl StateTyping for Counter {
271///     type Data = i64;
272///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
273///         state.values().get("count").and_then(Value::as_i64)
274///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
275///     }
276///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
277///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
278///     }
279///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
280///         if let StateChange::Increment { values } = change {
281///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
282///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
283///             }
284///         }
285///         Ok(())
286///     }
287///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
288///         if self.construct_data(after)? < self.construct_data(before)? {
289///             return Err(AppError::InvalidInput("count cannot decrease".into()));
290///         }
291///         Ok(())
292///     }
293///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
294///         if self.construct_data(state)? < 0 {
295///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
296///         }
297///         Ok(())
298///     }
299/// }
300/// let before = Counter.construct_state(&2)?;
301/// let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
302/// let after = before.apply(&change, &Counter)?;
303/// assert_eq!(Counter.construct_data(&after)?, 3);
304/// assert!(Counter.validate_state(&Counter.construct_state(&-1)?).is_err());
305/// # Ok::<(), AppError>(())
306/// ```
307pub trait StateTyping: Send + Sync + 'static {
308    /// Caller-specific data produced from workflow values.
309    type Data;
310
311    /// Constructs caller data from a validated state.
312    ///
313    /// # Errors
314    ///
315    /// Return an error when the state cannot be represented as `Self::Data`.
316    ///
317    /// # Example
318    ///
319    /// ```
320    /// use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
321    /// use serde_json::{Map, Value};
322    /// struct Counter;
323    /// impl StateTyping for Counter {
324    ///     type Data = i64;
325    ///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
326    ///         state.values().get("count").and_then(Value::as_i64)
327    ///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
328    ///     }
329    ///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
330    ///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
331    ///     }
332    ///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
333    ///         if let StateChange::Increment { values } = change {
334    ///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
335    ///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
336    ///             }
337    ///         }
338    ///         Ok(())
339    ///     }
340    ///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
341    ///         if self.construct_data(after)? < self.construct_data(before)? {
342    ///             return Err(AppError::InvalidInput("count cannot decrease".into()));
343    ///         }
344    ///         Ok(())
345    ///     }
346    ///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
347    ///         if self.construct_data(state)? < 0 {
348    ///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
349    ///         }
350    ///         Ok(())
351    ///     }
352    /// }
353    /// let before = Counter.construct_state(&2)?;
354    /// let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
355    /// let after = before.apply(&change, &Counter)?;
356    /// assert_eq!(Counter.construct_data(&after)?, 3);
357    /// # Ok::<(), AppError>(())
358    /// ```
359    fn construct_data(&self, state: &WorkflowState) -> Result<Self::Data, AppError>;
360    /// Reconstructs workflow values from caller data.
361    ///
362    /// # Errors
363    ///
364    /// Return an error when caller data cannot produce a valid workflow state.
365    ///
366    /// # Example
367    ///
368    /// ```
369    /// use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
370    /// use serde_json::{Map, Value};
371    /// struct Counter;
372    /// impl StateTyping for Counter {
373    ///     type Data = i64;
374    ///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
375    ///         state.values().get("count").and_then(Value::as_i64)
376    ///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
377    ///     }
378    ///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
379    ///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
380    ///     }
381    ///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
382    ///         if let StateChange::Increment { values } = change {
383    ///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
384    ///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
385    ///             }
386    ///         }
387    ///         Ok(())
388    ///     }
389    ///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
390    ///         if self.construct_data(after)? < self.construct_data(before)? {
391    ///             return Err(AppError::InvalidInput("count cannot decrease".into()));
392    ///         }
393    ///         Ok(())
394    ///     }
395    ///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
396    ///         if self.construct_data(state)? < 0 {
397    ///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
398    ///         }
399    ///         Ok(())
400    ///     }
401    /// }
402    /// let before = Counter.construct_state(&2)?;
403    /// let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
404    /// let after = before.apply(&change, &Counter)?;
405    /// let reconstructed = Counter.construct_state(&3)?;
406    /// assert_eq!(reconstructed, after);
407    /// # Ok::<(), AppError>(())
408    /// ```
409    fn construct_state(&self, data: &Self::Data) -> Result<WorkflowState, AppError>;
410    /// Validates an operation before any candidate mutation.
411    ///
412    /// # Errors
413    ///
414    /// Return an error to reject the complete primitive operation before candidate
415    /// mutation. The default accepts it; Merge and Subset do not invoke this method.
416    ///
417    /// # Example
418    ///
419    /// ```
420    /// use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
421    /// use serde_json::{Map, Value};
422    /// struct Counter;
423    /// impl StateTyping for Counter {
424    ///     type Data = i64;
425    ///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
426    ///         state.values().get("count").and_then(Value::as_i64)
427    ///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
428    ///     }
429    ///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
430    ///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
431    ///     }
432    ///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
433    ///         if let StateChange::Increment { values } = change {
434    ///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
435    ///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
436    ///             }
437    ///         }
438    ///         Ok(())
439    ///     }
440    ///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
441    ///         if self.construct_data(after)? < self.construct_data(before)? {
442    ///             return Err(AppError::InvalidInput("count cannot decrease".into()));
443    ///         }
444    ///         Ok(())
445    ///     }
446    ///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
447    ///         if self.construct_data(state)? < 0 {
448    ///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
449    ///         }
450    ///         Ok(())
451    ///     }
452    /// }
453    /// let before = Counter.construct_state(&2)?;
454    /// let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
455    /// let after = before.apply(&change, &Counter)?;
456    /// Counter.validate_pre(&before, &change)?;
457    /// let decreasing = StateChange::Increment { values: vec![("count".into(), Value::from(-1))] };
458    /// assert!(Counter.validate_pre(&before, &decreasing).is_err());
459    /// assert_eq!(before.values()["count"], 2);
460    /// # Ok::<(), AppError>(())
461    /// ```
462    fn validate_pre(&self, _state: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
463        Ok(())
464    }
465    /// Validates the original and candidate states after the operation.
466    ///
467    /// # Errors
468    ///
469    /// Return an error to reject the complete candidate primitive transition. The
470    /// original state remains unchanged. The default accepts it; Merge and Subset
471    /// do not invoke this method.
472    ///
473    /// # Example
474    ///
475    /// ```
476    /// use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
477    /// use serde_json::{Map, Value};
478    /// struct Counter;
479    /// impl StateTyping for Counter {
480    ///     type Data = i64;
481    ///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
482    ///         state.values().get("count").and_then(Value::as_i64)
483    ///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
484    ///     }
485    ///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
486    ///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
487    ///     }
488    ///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
489    ///         if let StateChange::Increment { values } = change {
490    ///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
491    ///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
492    ///             }
493    ///         }
494    ///         Ok(())
495    ///     }
496    ///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
497    ///         if self.construct_data(after)? < self.construct_data(before)? {
498    ///             return Err(AppError::InvalidInput("count cannot decrease".into()));
499    ///         }
500    ///         Ok(())
501    ///     }
502    ///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
503    ///         if self.construct_data(state)? < 0 {
504    ///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
505    ///         }
506    ///         Ok(())
507    ///     }
508    /// }
509    /// let before = Counter.construct_state(&2)?;
510    /// let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
511    /// let after = before.apply(&change, &Counter)?;
512    /// Counter.validate_post(&before, &after, &change)?;
513    /// let decreasing = Counter.construct_state(&1)?;
514    /// assert!(Counter.validate_post(&before, &decreasing, &change).is_err());
515    /// assert_eq!(after.values()["count"], 3);
516    /// # Ok::<(), AppError>(())
517    /// ```
518    fn validate_post(
519        &self,
520        _before: &WorkflowState,
521        _after: &WorkflowState,
522        _change: &StateChange,
523    ) -> Result<(), AppError> {
524        Ok(())
525    }
526    /// Validates all candidate values after transition validation.
527    ///
528    /// # Errors
529    ///
530    /// Return an error when any full-state invariant is violated. The default accepts
531    /// the state; this method also runs for reconstructed Merge/Subset snapshots.
532    ///
533    /// # Example
534    ///
535    /// ```
536    /// use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
537    /// use serde_json::{Map, Value};
538    /// struct Counter;
539    /// impl StateTyping for Counter {
540    ///     type Data = i64;
541    ///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
542    ///         state.values().get("count").and_then(Value::as_i64)
543    ///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
544    ///     }
545    ///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
546    ///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
547    ///     }
548    ///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
549    ///         if let StateChange::Increment { values } = change {
550    ///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
551    ///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
552    ///             }
553    ///         }
554    ///         Ok(())
555    ///     }
556    ///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
557    ///         if self.construct_data(after)? < self.construct_data(before)? {
558    ///             return Err(AppError::InvalidInput("count cannot decrease".into()));
559    ///         }
560    ///         Ok(())
561    ///     }
562    ///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
563    ///         if self.construct_data(state)? < 0 {
564    ///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
565    ///         }
566    ///         Ok(())
567    ///     }
568    /// }
569    /// let before = Counter.construct_state(&2)?;
570    /// let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
571    /// let after = before.apply(&change, &Counter)?;
572    /// assert!(Counter.validate_state(&Counter.construct_state(&-1)?).is_err());
573    /// # Ok::<(), AppError>(())
574    /// ```
575    fn validate_state(&self, _state: &WorkflowState) -> Result<(), AppError> {
576        Ok(())
577    }
578    /// Verifies both caller conversions succeed; a different reconstruction is allowed.
579    ///
580    /// # Errors
581    ///
582    /// The default propagates either conversion error. Successful reconstruction is
583    /// sufficient; byte-for-byte or value equality is not required.
584    ///
585    /// # Example
586    ///
587    /// ```
588    /// use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
589    /// use serde_json::{Map, Value};
590    /// struct Counter;
591    /// impl StateTyping for Counter {
592    ///     type Data = i64;
593    ///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
594    ///         state.values().get("count").and_then(Value::as_i64)
595    ///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
596    ///     }
597    ///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
598    ///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
599    ///     }
600    ///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
601    ///         if let StateChange::Increment { values } = change {
602    ///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
603    ///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
604    ///             }
605    ///         }
606    ///         Ok(())
607    ///     }
608    ///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
609    ///         if self.construct_data(after)? < self.construct_data(before)? {
610    ///             return Err(AppError::InvalidInput("count cannot decrease".into()));
611    ///         }
612    ///         Ok(())
613    ///     }
614    ///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
615    ///         if self.construct_data(state)? < 0 {
616    ///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
617    ///         }
618    ///         Ok(())
619    ///     }
620    /// }
621    /// let before = Counter.construct_state(&2)?;
622    /// let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
623    /// let after = before.apply(&change, &Counter)?;
624    /// Counter.validate_roundtrip(&after)?;
625    /// assert_eq!(Counter.construct_data(&after)?, 3);
626    /// # Ok::<(), AppError>(())
627    /// ```
628    fn validate_roundtrip(&self, state: &WorkflowState) -> Result<(), AppError> {
629        let data = self.construct_data(state)?;
630        self.construct_state(&data).map(|_| ())
631    }
632}
633
634/// Raw JSON typing with no additional conversion or validation policy.
635///
636/// # Example
637///
638/// ```
639/// use app_kit::workflow::{JsonState, StateTyping, WorkflowState};
640/// let state = WorkflowState::new(serde_json::from_str(r#"{"count":2}"#)?)?;
641/// let data = JsonState.construct_data(&state)?;
642/// assert_eq!(data["count"], 2);
643/// assert_eq!(JsonState.construct_state(&data)?, state);
644/// # Ok::<(), Box<dyn std::error::Error>>(())
645/// ```
646#[derive(Clone, Copy, Debug, Default)]
647pub struct JsonState;
648
649impl StateTyping for JsonState {
650    type Data = Map<String, Value>;
651
652    fn construct_data(&self, state: &WorkflowState) -> Result<Self::Data, AppError> {
653        Ok(state.values.clone())
654    }
655
656    fn construct_state(&self, data: &Self::Data) -> Result<WorkflowState, AppError> {
657        WorkflowState::new(data.clone())
658    }
659
660    fn validate_roundtrip(&self, _state: &WorkflowState) -> Result<(), AppError> {
661        Ok(())
662    }
663}
664
665impl WorkflowState {
666    /// Validates ordered JSON values and takes ownership of them.
667    ///
668    /// ```
669    /// use app_kit::workflow::WorkflowState;
670    /// let values = serde_json::from_str(r#"{"first":1,"second":2}"#)?;
671    /// let state = WorkflowState::new(values)?;
672    /// assert_eq!(state.values().keys().collect::<Vec<_>>(), ["first", "second"]);
673    /// # Ok::<(), Box<dyn std::error::Error>>(())
674    /// ```
675    ///
676    /// # Errors
677    ///
678    /// Returns `InvalidInput` for invalid state keys or exceeded JSON byte, nesting,
679    /// collection, or text bounds. Ownership is accepted only after full validation.
680    pub fn new(values: Map<String, Value>) -> Result<Self, AppError> {
681        definition::JsonBoundary::validate_map(&values)?;
682        Ok(Self { values })
683    }
684
685    /// Borrows values in their insertion order.
686    ///
687    /// ```
688    /// use app_kit::workflow::WorkflowState;
689    /// let state = WorkflowState::default();
690    /// assert!(state.values().get("optional_input").is_none());
691    /// ```
692    pub fn values(&self) -> &Map<String, Value> {
693        &self.values
694    }
695
696    /// Produces a new state with the pinned operation-specific validation order.
697    ///
698    /// Primitive changes run pre/post transition validation, then full-state
699    /// validation and roundtrip. Merge and subset construct new snapshots and
700    /// run only full-state validation and roundtrip.
701    /// Any failure leaves this state and its retained clones unchanged.
702    ///
703    /// ```
704    /// use app_kit::workflow::{JsonState, StateChange, WorkflowState};
705    /// use serde_json::{Map, Value};
706    /// let state = WorkflowState::new(Map::from_iter([
707    /// ("count".into(), Value::from("unknown")),
708    /// ]))?;
709    /// let change = StateChange::Increment {
710    /// values: vec![("count".into(), Value::from(1))],
711    /// };
712    /// assert!(state.apply(&change, &JsonState).is_err());
713    /// assert_eq!(state.values()["count"], "unknown");
714    /// # Ok::<(), app_kit::AppError>(())
715    /// ```
716    ///
717    /// # Errors
718    ///
719    /// Returns `InvalidInput` for incompatible operand types, signed-integer delta or
720    /// range errors, invalid state, or excessive operation payloads. Caller typing
721    /// errors propagate unchanged; failure never mutates the original state.
722    ///
723    /// # Panics
724    ///
725    /// Panics if the caller-provided typing implementation panics. Direct state
726    /// operations do not contain caller panics; App admission/execution has its own
727    /// boundary for classifying them.
728    pub fn apply<T: StateTyping>(
729        &self,
730        change: &StateChange,
731        typing: &T,
732    ) -> Result<Self, AppError> {
733        if let StateChange::Wipe { keep } = change {
734            StateChange::validate_count(keep.len())?;
735            let keys = self
736                .values
737                .keys()
738                .filter(|key| !keep.contains(key))
739                .cloned()
740                .collect();
741            return self.apply(&StateChange::Remove { keys }, typing);
742        }
743        let snapshot = matches!(
744            change,
745            StateChange::Merge { .. } | StateChange::Subset { .. }
746        );
747        if !snapshot {
748            typing.validate_pre(self, change)?;
749        }
750        let candidate = Self::new(change.apply_values(&self.values)?)?;
751        if !snapshot {
752            typing.validate_post(self, &candidate, change)?;
753        }
754        candidate.validate_typing(typing)?;
755        Ok(candidate)
756    }
757
758    fn validate_typing<T: StateTyping>(&self, typing: &T) -> Result<(), AppError> {
759        typing.validate_state(self)?;
760        typing.validate_roundtrip(self)
761    }
762}
763
764/// A workflow selected in one application workspace.
765///
766/// # Example
767///
768/// ```
769/// # use agents_kit::Agent;
770/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
771/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
772/// #     RunStatus, WorkflowDefinition, WorkflowState};
773/// # use storage_kit::StateStore;
774/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
775/// #
776/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
777/// # runtime.block_on(async {
778/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
779/// # let definition = WorkflowDefinition::parse(r#"{
780/// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
781/// #     "state":{"count":0}, "required":["count"],
782/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
783/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
784/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
785/// #     ]}}], "routes":{}
786/// # }"#)?;
787/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
788/// # let mut app = App::open(config).await?;
789/// # let mut workspace = app.create_workspace(WorkspaceConfig {
790/// #     name: Some("local counter".into()), root: std::env::current_dir()?,
791/// # }).await?;
792/// # let workflow = workspace.workflow("counter".parse()?).await?;
793/// let run = workflow.start(ExecutionOptions::default()).await?;
794/// assert!(matches!(run.wait().await?.status, RunStatus::Succeeded));
795/// #
796/// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
797/// # Ok::<(), Box<dyn std::error::Error>>(())
798/// # })?;
799/// # Ok::<(), Box<dyn std::error::Error>>(())
800/// ```
801pub struct Workflow<
802    CS: StateStoreBackend,
803    A: AgentBackend,
804    X: ActionBackend = NoActions,
805    C: ConditionBackend = NoConditions,
806    T: StateTyping = JsonState,
807    H: LifecycleHooks = NoHooks,
808    E: ChildExecutor = InlineExecutor,
809> {
810    runtime: run::WorkflowRuntime<CS, A, X, C, T, H, E>,
811}
812
813/// A durable workflow-run handle.
814///
815/// This handle observes durable state and requests explicit controls. Dropping it
816/// or a `wait`/snapshot future does not cancel accepted work. [`Self::execution`]
817/// borrows it exclusively and returns an owner whose drop does revoke uncommitted
818/// advancement. Use [`Self::cancel`] for durable, monotonic cancellation.
819///
820/// # Example
821///
822/// ```
823/// # use agents_kit::Agent;
824/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
825/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
826/// #     RunStatus, WorkflowDefinition, WorkflowState};
827/// # use storage_kit::StateStore;
828/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
829/// #
830/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
831/// # runtime.block_on(async {
832/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
833/// # let definition = WorkflowDefinition::parse(r#"{
834/// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
835/// #     "state":{"count":0}, "required":["count"],
836/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
837/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
838/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
839/// #     ]}}], "routes":{}
840/// # }"#)?;
841/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
842/// # let mut app = App::open(config).await?;
843/// # let mut workspace = app.create_workspace(WorkspaceConfig {
844/// #     name: Some("local counter".into()), root: std::env::current_dir()?,
845/// # }).await?;
846/// # let workflow = workspace.workflow("counter".parse()?).await?;
847/// # let mut run = workflow.prepare(WorkflowState::default()).await?;
848/// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
849/// assert!(matches!(terminal.status, RunStatus::Succeeded));
850/// assert_eq!(run.snapshot().await?.state.values()["count"], 1);
851/// #
852/// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
853/// # Ok::<(), Box<dyn std::error::Error>>(())
854/// # })?;
855/// # Ok::<(), Box<dyn std::error::Error>>(())
856/// ```
857pub struct WorkflowRun<
858    CS: StateStoreBackend,
859    A: AgentBackend,
860    X: ActionBackend = NoActions,
861    C: ConditionBackend = NoConditions,
862    T: StateTyping = JsonState,
863    H: LifecycleHooks = NoHooks,
864    E: ChildExecutor = InlineExecutor,
865> {
866    runtime: run::WorkflowRunRuntime<CS, A, X, C, T, H, E>,
867}
868
869/// One opaque, parsed `app-kit.workflow/v2` definition.
870///
871/// Construct definitions through [`Self::parse`]; their internals cannot be
872/// copied through struct update syntax:
873///
874/// ```compile_fail
875/// use app_kit::workflow::WorkflowDefinition;
876/// fn reconstruct(definition: WorkflowDefinition) -> WorkflowDefinition {
877/// WorkflowDefinition { ..definition }
878/// }
879/// ```
880///
881/// Only the strict versioned language is accepted. Unknown/duplicate fields,
882/// invalid expressions, graph references and finite-bound violations fail before
883/// runtime work. Inspection returns passive metadata; composition runs the same
884/// compiler rather than exposing a mutable executable AST.
885///
886/// # Example
887///
888/// ```
889/// use app_kit::workflow::WorkflowDefinition;
890///
891/// let definition = WorkflowDefinition::parse(r#"{
892///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
893///     "state":{"count":0}, "required":["count"],
894///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
895///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
896///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
897///     ]}}], "routes":{}
898/// }"#)?;
899/// assert_eq!(definition.graph().entry, "increment");
900/// # Ok::<(), app_kit::AppError>(())
901/// ```
902pub struct WorkflowDefinition {
903    compiled: definition::CompiledDefinition,
904}
905
906/// A deterministic read-only graph export format.
907pub enum GraphFormat {
908    /// Portable graph metadata as JSON, without runtime state.
909    Json,
910    /// Graphviz DOT text; rendering requires no subprocess in this crate.
911    Dot,
912    /// Mermaid flowchart text with escaped caller labels.
913    Mermaid,
914}
915
916/// Passive graph metadata in authored node and transition order.
917///
918/// # Example
919///
920/// ```
921/// use app_kit::workflow::WorkflowDefinition;
922///
923/// let definition = WorkflowDefinition::parse(r#"{
924///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
925///     "state":{"count":0}, "required":["count"],
926///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
927///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
928///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
929///     ]}}], "routes":{}
930/// }"#)?;
931/// let graph = definition.graph();
932/// assert_eq!(graph.entry, "increment");
933/// assert_eq!(graph.nodes.len(), 1);
934/// # Ok::<(), app_kit::AppError>(())
935/// ```
936#[derive(Serialize)]
937pub struct GraphDescription {
938    /// Logical workflow identity.
939    pub id: WorkflowId,
940    /// Caller-selected definition revision.
941    pub revision: String,
942    /// First node's logical identity.
943    pub entry: String,
944    /// Authored node metadata, without executable bindings.
945    pub nodes: Vec<GraphNode>,
946    /// Ordered outgoing transitions, grouped by authored source-node order.
947    pub edges: Vec<GraphEdge>,
948}
949
950/// One passive node declaration in a graph projection.
951///
952/// # Example
953///
954/// ```
955/// use app_kit::workflow::WorkflowDefinition;
956///
957/// let definition = WorkflowDefinition::parse(r#"{
958///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
959///     "state":{"count":0}, "required":["count"],
960///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
961///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
962///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
963///     ]}}], "routes":{}
964/// }"#)?;
965/// let graph = definition.graph();
966/// let node = &graph.nodes[0];
967/// assert_eq!(node.kind, "state");
968/// assert_eq!(node.writes, ["count"]);
969/// # Ok::<(), app_kit::AppError>(())
970/// ```
971#[derive(Serialize)]
972pub struct GraphNode {
973    /// Graph-local node identity.
974    pub id: String,
975    /// Optional caller-facing label.
976    pub name: Option<String>,
977    /// Optional authored description.
978    pub description: Option<String>,
979    /// Authored selector tags, in declaration order.
980    pub tags: Vec<String>,
981    /// Closed workflow step kind rendered as text.
982    pub kind: String,
983    /// Top-level state keys read by this node.
984    pub reads: Vec<String>,
985    /// Top-level state keys written by this node.
986    pub writes: Vec<String>,
987    /// Required transient input names.
988    pub required_inputs: Vec<String>,
989    /// Optional transient input names.
990    pub optional_inputs: Vec<String>,
991    /// Actual caller source or canonical authored built-in definition.
992    pub source: Option<String>,
993}
994
995/// One ordered transition; a missing destination denotes path exhaustion.
996///
997/// # Example
998///
999/// ```
1000/// use app_kit::workflow::GraphEdge;
1001/// let end = GraphEdge { from: "increment".into(), to: None, condition: None };
1002/// assert!(end.to.is_none());
1003/// # Ok::<(), app_kit::AppError>(())
1004/// ```
1005#[derive(Serialize)]
1006pub struct GraphEdge {
1007    /// Source node identity.
1008    pub from: String,
1009    /// Destination node identity, or explicit end.
1010    pub to: Option<String>,
1011    /// Display expression, or the default true condition when absent.
1012    pub condition: Option<String>,
1013}
1014
1015/// An owned graph piece and its graph-local node-name prefix.
1016///
1017/// # Example
1018///
1019/// ```
1020/// use app_kit::workflow::WorkflowDefinition;
1021/// const SOURCE: &str = r#"{
1022///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1023///     "state":{"count":0}, "required":["count"],
1024///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1025///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1026///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1027///     ]}}], "routes":{}
1028/// }"#;
1029/// let definition = WorkflowDefinition::parse(SOURCE)?;
1030/// use app_kit::workflow::{GraphComposition, GraphConnection, GraphImport};
1031/// let composition = GraphComposition {
1032///     id: "composed".parse()?, revision: "r1".into(), entry: "a_increment".into(),
1033///     imports: vec![
1034///         GraphImport { definition, prefix: "a_".into() },
1035///         GraphImport { definition: WorkflowDefinition::parse(SOURCE)?, prefix: "b_".into() },
1036///     ],
1037///     connections: vec![GraphConnection { from: "a_increment".into(), to: "b_increment".into() }],
1038/// };
1039/// let composed = WorkflowDefinition::compose(composition)?;
1040/// assert_eq!(composed.graph().nodes.len(), 2);
1041/// assert_eq!(composed.graph().entry, "a_increment");
1042/// # Ok::<(), app_kit::AppError>(())
1043/// ```
1044pub struct GraphImport {
1045    /// Validated definition to import.
1046    pub definition: WorkflowDefinition,
1047    /// Prefix applied to nodes and their graph-local references.
1048    pub prefix: String,
1049}
1050
1051/// A default transition appended to an assembled graph.
1052///
1053/// # Example
1054///
1055/// ```
1056/// use app_kit::workflow::WorkflowDefinition;
1057/// const SOURCE: &str = r#"{
1058///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1059///     "state":{"count":0}, "required":["count"],
1060///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1061///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1062///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1063///     ]}}], "routes":{}
1064/// }"#;
1065/// let definition = WorkflowDefinition::parse(SOURCE)?;
1066/// use app_kit::workflow::{GraphComposition, GraphConnection, GraphImport};
1067/// let composition = GraphComposition {
1068///     id: "composed".parse()?, revision: "r1".into(), entry: "a_increment".into(),
1069///     imports: vec![
1070///         GraphImport { definition, prefix: "a_".into() },
1071///         GraphImport { definition: WorkflowDefinition::parse(SOURCE)?, prefix: "b_".into() },
1072///     ],
1073///     connections: vec![GraphConnection { from: "a_increment".into(), to: "b_increment".into() }],
1074/// };
1075/// let composed = WorkflowDefinition::compose(composition)?;
1076/// assert_eq!(composed.graph().nodes.len(), 2);
1077/// assert_eq!(composed.graph().entry, "a_increment");
1078/// # Ok::<(), app_kit::AppError>(())
1079/// ```
1080pub struct GraphConnection {
1081    /// Already-prefixed source node identity.
1082    pub from: String,
1083    /// Already-prefixed destination node identity.
1084    pub to: String,
1085}
1086
1087/// Definitions and explicit connections for one ordinary compiler invocation.
1088///
1089/// # Example
1090///
1091/// ```
1092/// use app_kit::workflow::WorkflowDefinition;
1093/// const SOURCE: &str = r#"{
1094///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1095///     "state":{"count":0}, "required":["count"],
1096///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1097///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1098///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1099///     ]}}], "routes":{}
1100/// }"#;
1101/// let definition = WorkflowDefinition::parse(SOURCE)?;
1102/// use app_kit::workflow::{GraphComposition, GraphConnection, GraphImport};
1103/// let composition = GraphComposition {
1104///     id: "composed".parse()?, revision: "r1".into(), entry: "a_increment".into(),
1105///     imports: vec![
1106///         GraphImport { definition, prefix: "a_".into() },
1107///         GraphImport { definition: WorkflowDefinition::parse(SOURCE)?, prefix: "b_".into() },
1108///     ],
1109///     connections: vec![GraphConnection { from: "a_increment".into(), to: "b_increment".into() }],
1110/// };
1111/// let composed = WorkflowDefinition::compose(composition)?;
1112/// assert_eq!(composed.graph().nodes.len(), 2);
1113/// assert_eq!(composed.graph().entry, "a_increment");
1114/// # Ok::<(), app_kit::AppError>(())
1115/// ```
1116pub struct GraphComposition {
1117    /// Identity of the assembled workflow.
1118    pub id: WorkflowId,
1119    /// Revision of the assembled workflow.
1120    pub revision: String,
1121    /// Already-prefixed entrypoint.
1122    pub entry: String,
1123    /// Graph pieces, preserving their declaration order.
1124    pub imports: Vec<GraphImport>,
1125    /// Ordered default transitions appended after imported transitions.
1126    pub connections: Vec<GraphConnection>,
1127}
1128
1129/// A validated caller-authored workflow identity.
1130///
1131/// Parsing and deserialization accept only ASCII `[a-z0-9][a-z0-9_-]{0,127}`.
1132///
1133/// # Example
1134///
1135/// ```
1136/// use app_kit::workflow::WorkflowId;
1137/// let id: WorkflowId = "counter".parse()?;
1138/// assert_eq!(id.to_string(), "counter");
1139/// assert!("invalid identity".parse::<WorkflowId>().is_err());
1140/// # Ok::<(), app_kit::AppError>(())
1141/// ```
1142#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1143pub struct WorkflowId(String);
1144
1145/// A validated root UUID or stable child SHA-256 identity.
1146///
1147/// Root identities use `run_` followed by a canonical lowercase UUIDv4. Child
1148/// identities also accept exactly 64 lowercase hexadecimal digits. One logical
1149/// identity may have multiple independent occurrences; use [`RunSelector::Exact`]
1150/// when the occurrence matters.
1151///
1152/// # Example
1153///
1154/// ```
1155/// use app_kit::workflow::RunId;
1156/// let id: RunId = "run_8a7c251e-b5b8-4d9e-8f0b-123456789abc".parse()?;
1157/// assert_eq!(id.to_string(), "run_8a7c251e-b5b8-4d9e-8f0b-123456789abc");
1158/// assert!("invalid identity".parse::<RunId>().is_err());
1159/// # Ok::<(), app_kit::AppError>(())
1160/// ```
1161#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1162pub struct RunId(String);
1163
1164/// Selects an exact occurrence or the latest native observation of a logical run.
1165#[derive(Clone, Debug, Eq, PartialEq)]
1166pub enum RunSelector {
1167    /// Last natively saved observation in this workflow and workspace scope.
1168    Latest(RunId),
1169    /// One admitted occurrence; an absent occurrence never falls back to Latest.
1170    Exact {
1171        /// Stable logical identity.
1172        id: RunId,
1173        /// Validated at method ingress as 32 lowercase hexadecimal digits.
1174        occurrence: String,
1175    },
1176}
1177
1178impl RunSelector {
1179    pub(crate) fn validate(&self) -> Result<(), AppError> {
1180        match self {
1181            Self::Latest(_) => Ok(()),
1182            Self::Exact { occurrence, .. } => RunId::validate_occurrence(occurrence),
1183        }
1184    }
1185
1186    pub(crate) fn id(&self) -> &RunId {
1187        match self {
1188            Self::Latest(id) | Self::Exact { id, .. } => id,
1189        }
1190    }
1191}
1192
1193/// Initial state, transient inputs and boundaries selected for one execution.
1194///
1195/// `state` patches a new run only through [`Workflow::start`]; an already admitted
1196/// run requires an empty state in its execution options. Inputs are transient and
1197/// do not become state automatically. Selectors are node ids or `@tag` names and
1198/// must match. `max_steps` counts committed action boundaries, not partial results
1199/// or control checkpoints; private finite safety bounds always remain.
1200///
1201/// # Example
1202///
1203/// ```
1204/// use app_kit::workflow::{ExecutionOptions, WorkflowState};
1205/// let options = ExecutionOptions {
1206///     state: WorkflowState::new(serde_json::from_str(r#"{"count":4}"#)?)?,
1207///     halt_after: vec!["increment".into()],
1208///     ..ExecutionOptions::default()
1209/// };
1210/// assert_eq!(options.state.values()["count"], 4);
1211/// # Ok::<(), Box<dyn std::error::Error>>(())
1212/// ```
1213#[derive(Clone, Default)]
1214pub struct ExecutionOptions {
1215    /// Initial patch for a new run; must be empty on an existing run.
1216    pub state: WorkflowState,
1217    /// Transient inputs reused by successive actions.
1218    pub inputs: Map<String, Value>,
1219    /// Node or tag selectors to stop before invocation.
1220    pub halt_before: Vec<String>,
1221    /// Node or tag selectors to stop after a committed boundary.
1222    pub halt_after: Vec<String>,
1223    /// Optional maximum number of committed action boundaries.
1224    pub max_steps: Option<NonZeroUsize>,
1225}
1226
1227impl ExecutionOptions {
1228    pub(crate) fn resolve_selectors(&mut self, graph: &GraphDescription) -> Result<(), AppError> {
1229        for selectors in [&mut self.halt_before, &mut self.halt_after] {
1230            let mut resolved = Vec::new();
1231            for selector in selectors.iter() {
1232                let mut matches = graph
1233                    .nodes
1234                    .iter()
1235                    .filter(|node| {
1236                        selector
1237                            .strip_prefix('@')
1238                            .map_or(node.id == *selector, |tag| {
1239                                node.tags.iter().any(|candidate| candidate == tag)
1240                            })
1241                    })
1242                    .peekable();
1243                if matches.peek().is_none() {
1244                    return Err(AppError::InvalidInput(
1245                        "execution selector has no matching node".into(),
1246                    ));
1247                }
1248                for node in matches {
1249                    if !resolved.contains(&node.id) {
1250                        resolved.push(node.id.clone());
1251                    }
1252                }
1253            }
1254            *selectors = resolved;
1255        }
1256        Ok(())
1257    }
1258}
1259
1260/// One transient result or committed execution boundary.
1261#[non_exhaustive]
1262pub enum ExecutionEvent {
1263    /// A source result which has not changed workflow state.
1264    Partial {
1265        /// Node whose source emitted this item.
1266        node: String,
1267        /// Transient output independent of workflow state.
1268        result: Map<String, Value>,
1269    },
1270    /// A durable action boundary with its complete ordered outcomes.
1271    Committed {
1272        /// Exact durable boundary sequence.
1273        checkpoint: u64,
1274        /// Complete results in canonical node order.
1275        outcomes: Vec<ActionOutcome>,
1276    },
1277    /// A selected boundary parked for a later execution.
1278    Halted {
1279        /// Selected next node or last committed node.
1280        node: String,
1281        /// Present only for a committed after-boundary, including an empty map.
1282        result: Option<Map<String, Value>>,
1283    },
1284}
1285
1286/// The current caller-visible run truth.
1287///
1288/// # Example
1289///
1290/// ```
1291/// # use agents_kit::Agent;
1292/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1293/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1294/// #     RunStatus, WorkflowDefinition, WorkflowState};
1295/// # use storage_kit::StateStore;
1296/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1297/// #
1298/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1299/// # runtime.block_on(async {
1300/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1301/// # let definition = WorkflowDefinition::parse(r#"{
1302/// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1303/// #     "state":{"count":0}, "required":["count"],
1304/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1305/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1306/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1307/// #     ]}}], "routes":{}
1308/// # }"#)?;
1309/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1310/// # let mut app = App::open(config).await?;
1311/// # let mut workspace = app.create_workspace(WorkspaceConfig {
1312/// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1313/// # }).await?;
1314/// # let workflow = workspace.workflow("counter".parse()?).await?;
1315/// # let mut run = workflow.prepare(WorkflowState::default()).await?;
1316/// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
1317/// assert_eq!(terminal.state.values()["count"], 1);
1318/// assert_eq!(terminal.last_action_ordinal, Some(0));
1319/// #
1320/// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1321/// # Ok::<(), Box<dyn std::error::Error>>(())
1322/// # })?;
1323/// # Ok::<(), Box<dyn std::error::Error>>(())
1324/// ```
1325#[non_exhaustive]
1326pub struct RunSnapshot {
1327    /// Identifies the run.
1328    pub id: RunId,
1329    /// Exact admitted execution occurrence of this logical run.
1330    pub occurrence: String,
1331    /// Fences explicit controls.
1332    pub checkpoint: u64,
1333    /// Projects lifecycle; actionable targets remain in `interactions`.
1334    pub status: RunStatus,
1335    /// Contains the current committed workflow state.
1336    pub state: WorkflowState,
1337    /// Canonical currently actionable controls.
1338    pub interactions: Vec<Interaction>,
1339    /// Last successfully committed action ordinal; controls do not advance it.
1340    pub last_action_ordinal: Option<u64>,
1341    /// Results from the latest committed wave, retained through later controls.
1342    pub outcomes: Vec<ActionOutcome>,
1343}
1344
1345/// The current stable run status.
1346///
1347/// Inspect [`crate::workflow::RunSnapshot::interactions`] alongside this lifecycle projection.
1348/// Halted retains the selected before/after boundary and exact optional result;
1349/// Paused denotes an ordinary parked control or prepared run. Neither status
1350/// alone means that every external effect is safe to retry.
1351#[non_exhaustive]
1352pub enum RunStatus {
1353    /// Execution can still advance without caller input.
1354    Running,
1355    /// A committed boundary is parked for explicit execution.
1356    Paused,
1357    /// An explicitly selected node boundary is retained.
1358    Halted {
1359        /// Selected next node or last committed node.
1360        node: String,
1361        /// Present only for a committed after-boundary, including an empty map.
1362        result: Option<Map<String, Value>>,
1363    },
1364    /// Explicit cancellation has committed and owned cleanup is still running.
1365    Cancelling,
1366    /// The terminal frontier succeeded.
1367    Succeeded,
1368    /// The run terminated with a durable failure.
1369    Failed(WorkflowFailure),
1370    /// Explicit cancellation converged.
1371    Cancelled,
1372}
1373
1374/// A caller-actionable workflow interaction.
1375#[derive(Clone)]
1376#[non_exhaustive]
1377pub enum Interaction {
1378    /// Requests one schema-checked caller value.
1379    Input {
1380        /// Identifies this run-scoped control.
1381        sequence: u64,
1382        /// Fences this input reservation independently of sibling checkpoints.
1383        generation: u64,
1384        /// Describes the requested value.
1385        message: String,
1386        /// Optionally constrains the response.
1387        schema: Option<Value>,
1388    },
1389    /// Exposes one exact lower-agent notification.
1390    Notification {
1391        /// Identifies this run-scoped control.
1392        sequence: u64,
1393        /// Fences this exact notification target independently of sibling checkpoints.
1394        generation: u64,
1395        /// Carries the exact portable lower notification.
1396        notification: Arc<ConversationNotification>,
1397    },
1398    /// Retains an external effect whose outcome has not been established.
1399    Unknown {
1400        /// Identifies this run-scoped interaction target.
1401        sequence: u64,
1402        /// Fences this target independently of sibling checkpoints.
1403        generation: u64,
1404        /// Reports whether authoritative evidence permits another attempt.
1405        retry_safe: bool,
1406    },
1407    /// Requests transient inputs for an action that has not started.
1408    Inputs {
1409        /// Identifies this run-scoped interaction target.
1410        sequence: u64,
1411        /// Fences this binding independently of sibling checkpoints.
1412        generation: u64,
1413        /// Names the exact missing input keys in canonical order.
1414        required: Vec<String>,
1415    },
1416}
1417
1418/// Settles one current interaction or unknown effect.
1419///
1420/// Use the interaction sequence and its target generation from the same observed
1421/// item. Sibling checkpoints do not invalidate that reservation. Unknown effects
1422/// are never automatically retried; Retry requires authoritative retry-safe
1423/// evidence, and Accept is an explicit caller settlement.
1424pub enum Resolution {
1425    /// Supplies one caller-input value.
1426    Input(Value),
1427    /// Binds exact missing inputs in the retained execution, without persisting values.
1428    Inputs(Map<String, Value>),
1429    /// Resolves one exact lower notification.
1430    Notification(NotificationResolution),
1431    /// Accepts an unknown effect and stages ordinary state contribution.
1432    Accept(Map<String, Value>),
1433    /// Fails an unknown effect without claiming its external outcome.
1434    Fail,
1435    /// Retries only after authoritative evidence proves retry safe.
1436    Retry,
1437}
1438
1439/// A stable terminal workflow-failure category.
1440#[non_exhaustive]
1441pub enum WorkflowFailure {
1442    /// Expression or routing evaluation failed.
1443    Evaluation(String),
1444    /// State validation or deterministic reduction failed.
1445    State(String),
1446    /// A lower agent operation failed safely.
1447    Agent(String),
1448    /// A caller-owned action source returned an error.
1449    Action(String),
1450    /// An opt-in lifecycle hook failed, retaining any prior execution failure.
1451    Hook {
1452        /// The caller-supplied hook error context.
1453        message: String,
1454        /// The execution failure observed before the hook failed.
1455        cause: Option<Box<WorkflowFailure>>,
1456    },
1457    /// A child or map relationship failed, or a child executor returned an error.
1458    ///
1459    /// An error returned by [`ChildExecutor::execute`]'s future has this category
1460    /// regardless of its [`AppError`] variant. Partial child results do not reduce
1461    /// the parent, and descendant cleanup retains ownership before publication.
1462    Child(String),
1463    /// Execution could not continue, including a contained child-executor panic.
1464    ///
1465    /// A panic while constructing or polling [`ChildExecutor::execute`]'s future
1466    /// has this category. It does not reduce the parent from partial child results;
1467    /// descendant cleanup retains ownership before terminal publication.
1468    Execution(String),
1469    /// A durable deadline expired.
1470    Timeout(String),
1471    /// A fixed private safety bound was exhausted.
1472    Limit(String),
1473}
1474
1475impl WorkflowDefinition {
1476    /// Parses and validates one strict versioned workflow document.
1477    ///
1478    /// # Errors
1479    ///
1480    /// Returns [`AppError::InvalidInput`] for malformed JSON, unsupported schema,
1481    /// unknown or duplicate members, invalid identities or expressions, inconsistent
1482    /// state/reducers, graph errors, or exceeded source/collection/depth limits.
1483    ///
1484    /// # Example
1485    ///
1486    /// ```
1487    /// use app_kit::workflow::WorkflowDefinition;
1488    ///
1489    /// let definition = WorkflowDefinition::parse(r#"{
1490    ///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1491    ///     "state":{"count":0}, "required":["count"],
1492    ///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1493    ///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1494    ///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1495    ///     ]}}], "routes":{}
1496    /// }"#)?;
1497    /// assert_eq!(definition.graph().id.to_string(), "counter");
1498    /// assert!(WorkflowDefinition::parse(r#"{"schema":"app-kit.workflow/v1"}"#).is_err());
1499    /// # Ok::<(), app_kit::AppError>(())
1500    /// ```
1501    pub fn parse(source: &str) -> Result<Self, AppError> {
1502        Ok(Self {
1503            compiled: definition::CompiledDefinition::parse(source)?,
1504        })
1505    }
1506
1507    pub(crate) fn into_compiled(self) -> definition::CompiledDefinition {
1508        self.compiled
1509    }
1510}
1511
1512impl<
1513    CS: StateStoreBackend,
1514    A: AgentBackend,
1515    X: ActionBackend,
1516    C: ConditionBackend,
1517    T: StateTyping,
1518    H: LifecycleHooks,
1519    E: ChildExecutor,
1520> Workflow<CS, A, X, C, T, H, E>
1521{
1522    pub(crate) fn from_runtime(runtime: run::WorkflowRuntime<CS, A, X, C, T, H, E>) -> Self {
1523        Self { runtime }
1524    }
1525
1526    /// Returns this workflow's stable identity.
1527    ///
1528    /// # Example
1529    ///
1530    /// ```
1531    /// # use agents_kit::Agent;
1532    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1533    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1534    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1535    /// # use storage_kit::StateStore;
1536    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1537    /// #
1538    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1539    /// # runtime.block_on(async {
1540    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1541    /// # let definition = WorkflowDefinition::parse(r#"{
1542    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1543    /// #     "state":{"count":0}, "required":["count"],
1544    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1545    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1546    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1547    /// #     ]}}], "routes":{}
1548    /// # }"#)?;
1549    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1550    /// # let mut app = App::open(config).await?;
1551    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1552    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1553    /// # }).await?;
1554    /// # let workflow = workspace.workflow("counter".parse()?).await?;
1555    /// assert_eq!(workflow.id().to_string(), "counter");
1556    /// #
1557    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1558    /// # Ok::<(), Box<dyn std::error::Error>>(())
1559    /// # })?;
1560    /// # Ok::<(), Box<dyn std::error::Error>>(())
1561    /// ```
1562    pub fn id(&self) -> &WorkflowId {
1563        self.runtime.id()
1564    }
1565
1566    /// Atomically admits one run at checkpoint zero.
1567    ///
1568    /// Schedules background advancement after admission. Dropping the returned run
1569    /// observer does not stop it. [`WorkflowRun::wait`] returns at an actionable or
1570    /// terminal boundary, so inspect status and interactions before assuming success.
1571    ///
1572    /// # Errors
1573    ///
1574    /// Returns `InvalidInput` for invalid initial state/options or missing bindings,
1575    /// `Busy` for exhausted admission/capacity, `Closed` for sealed ownership, or
1576    /// `Conflict` for stale workspace/revision evidence. Lower/storage failures use
1577    /// `Integration`; accepted execution failures appear in run status.
1578    ///
1579    /// # Example
1580    ///
1581    /// ```
1582    /// # use agents_kit::Agent;
1583    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1584    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1585    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1586    /// # use storage_kit::StateStore;
1587    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1588    /// #
1589    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1590    /// # runtime.block_on(async {
1591    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1592    /// # let definition = WorkflowDefinition::parse(r#"{
1593    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1594    /// #     "state":{"count":0}, "required":["count"],
1595    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1596    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1597    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1598    /// #     ]}}], "routes":{}
1599    /// # }"#)?;
1600    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1601    /// # let mut app = App::open(config).await?;
1602    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1603    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1604    /// # }).await?;
1605    /// # let workflow = workspace.workflow("counter".parse()?).await?;
1606    /// let run = workflow.start(ExecutionOptions::default()).await?;
1607    /// let snapshot = run.wait().await?;
1608    /// assert_eq!(snapshot.state.values()["count"], 1);
1609    /// #
1610    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1611    /// # Ok::<(), Box<dyn std::error::Error>>(())
1612    /// # })?;
1613    /// # Ok::<(), Box<dyn std::error::Error>>(())
1614    /// ```
1615    pub async fn start(
1616        &self,
1617        options: ExecutionOptions,
1618    ) -> Result<WorkflowRun<CS, A, X, C, T, H, E>, AppError> {
1619        Ok(WorkflowRun {
1620            runtime: self.runtime.start(options).await?,
1621        })
1622    }
1623
1624    /// Admits checkpoint zero without executing the entry node.
1625    ///
1626    /// The initial patch is validated and atomically stored using the same
1627    /// workspace and App admission fences as background execution. Observers
1628    /// can read the parked run, and cancellation uses its checkpoint fence.
1629    ///
1630    /// # Errors
1631    ///
1632    /// Returns `InvalidInput` for invalid initial state or missing bindings,
1633    /// `Busy` for exhausted admission/capacity, `Closed` for sealed ownership, or
1634    /// `Conflict` for stale workspace/revision evidence or exhausted bounded
1635    /// transaction-contention retries. Concurrent preparations are not guaranteed
1636    /// to all succeed. Lower/storage failures use `Integration`; accepted
1637    /// execution failures appear in run status.
1638    ///
1639    /// # Example
1640    ///
1641    /// ```
1642    /// # use agents_kit::Agent;
1643    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1644    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1645    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1646    /// # use storage_kit::StateStore;
1647    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1648    /// #
1649    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1650    /// # runtime.block_on(async {
1651    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1652    /// # let definition = WorkflowDefinition::parse(r#"{
1653    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1654    /// #     "state":{"count":0}, "required":["count"],
1655    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1656    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1657    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1658    /// #     ]}}], "routes":{}
1659    /// # }"#)?;
1660    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1661    /// # let mut app = App::open(config).await?;
1662    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1663    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1664    /// # }).await?;
1665    /// # let workflow = workspace.workflow("counter".parse()?).await?;
1666    /// let run = workflow.prepare(WorkflowState::default()).await?;
1667    /// let checkpoint = run.checkpoint(CheckpointSelector::Sequence(0)).await?;
1668    /// assert_eq!(checkpoint.state.values()["count"], 0);
1669    /// assert!(checkpoint.action_ordinal.is_none());
1670    /// #
1671    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1672    /// # Ok::<(), Box<dyn std::error::Error>>(())
1673    /// # })?;
1674    /// # Ok::<(), Box<dyn std::error::Error>>(())
1675    /// ```
1676    pub async fn prepare(
1677        &self,
1678        state: WorkflowState,
1679    ) -> Result<WorkflowRun<CS, A, X, C, T, H, E>, AppError> {
1680        Ok(WorkflowRun {
1681            runtime: self.runtime.prepare(state.values).await?,
1682        })
1683    }
1684
1685    /// Copies a clean retained boundary into a fresh, paused run.
1686    ///
1687    /// Source selection and new admission commit atomically. The source remains
1688    /// unchanged; actions run only after explicit execution of the returned run.
1689    ///
1690    /// # Errors
1691    ///
1692    /// Returns `NotFound` for absent source/checkpoint without fallback, `InvalidInput`
1693    /// for invalid overrides or selectors, `Busy` for exhausted capacity, and
1694    /// `Conflict` when the selected boundary is unsafe to copy. Sealing/revocation
1695    /// returns `Closed`; inconsistent history or storage failures return `Integration`.
1696    ///
1697    /// # Example
1698    ///
1699    /// ```
1700    /// # use agents_kit::Agent;
1701    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1702    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1703    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1704    /// # use storage_kit::StateStore;
1705    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1706    /// #
1707    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1708    /// # runtime.block_on(async {
1709    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1710    /// # let definition = WorkflowDefinition::parse(r#"{
1711    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1712    /// #     "state":{"count":0}, "required":["count"],
1713    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1714    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1715    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1716    /// #     ]}}], "routes":{}
1717    /// # }"#)?;
1718    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1719    /// # let mut app = App::open(config).await?;
1720    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1721    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1722    /// # }).await?;
1723    /// # let workflow = workspace.workflow("counter".parse()?).await?;
1724    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
1725    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
1726    /// use app_kit::workflow::{RestoreOptions, ResumePosition};
1727    /// let options = RestoreOptions {
1728    ///     source: RunSelector::Latest(run.id().clone()),
1729    ///     checkpoint: CheckpointSelector::Action(0),
1730    ///     overrides: WorkflowState::default(), position: ResumePosition::AfterCompleted,
1731    ///     fallback: None,
1732    /// };
1733    /// let copy = workflow.restore(options).await?;
1734    /// assert_ne!(copy.id(), run.id());
1735    /// let copied = copy.checkpoint(CheckpointSelector::Sequence(0)).await?;
1736    /// assert_eq!(copied.state.values()["count"], 1);
1737    /// assert_eq!(&copied.lineage.as_ref().unwrap().source, run.id());
1738    /// #
1739    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1740    /// # Ok::<(), Box<dyn std::error::Error>>(())
1741    /// # })?;
1742    /// # Ok::<(), Box<dyn std::error::Error>>(())
1743    /// ```
1744    pub async fn restore(
1745        &self,
1746        options: RestoreOptions,
1747    ) -> Result<WorkflowRun<CS, A, X, C, T, H, E>, AppError> {
1748        Ok(WorkflowRun {
1749            runtime: self.runtime.restore(options).await?,
1750        })
1751    }
1752
1753    /// Reopens one run by typed identity.
1754    ///
1755    /// # Errors
1756    ///
1757    /// Returns `InvalidInput` for a malformed exact occurrence, `NotFound` when the
1758    /// selected run/occurrence is absent, or `Closed` for sealed scope. Exact selection
1759    /// never falls back to Latest; inconsistent retained evidence is `Integration`.
1760    ///
1761    /// # Example
1762    ///
1763    /// ```
1764    /// # use agents_kit::Agent;
1765    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1766    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1767    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1768    /// # use storage_kit::StateStore;
1769    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1770    /// #
1771    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1772    /// # runtime.block_on(async {
1773    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1774    /// # let definition = WorkflowDefinition::parse(r#"{
1775    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1776    /// #     "state":{"count":0}, "required":["count"],
1777    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1778    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1779    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1780    /// #     ]}}], "routes":{}
1781    /// # }"#)?;
1782    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1783    /// # let mut app = App::open(config).await?;
1784    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1785    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1786    /// # }).await?;
1787    /// # let workflow = workspace.workflow("counter".parse()?).await?;
1788    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
1789    /// let id = run.id().clone();
1790    /// let reopened = workflow.open_run(RunSelector::Latest(id.clone())).await?;
1791    /// assert_eq!(reopened.id(), &id);
1792    /// #
1793    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1794    /// # Ok::<(), Box<dyn std::error::Error>>(())
1795    /// # })?;
1796    /// # Ok::<(), Box<dyn std::error::Error>>(())
1797    /// ```
1798    pub async fn open_run(
1799        &self,
1800        selector: RunSelector,
1801    ) -> Result<WorkflowRun<CS, A, X, C, T, H, E>, AppError> {
1802        Ok(WorkflowRun {
1803            runtime: self.runtime.open_run(selector).await?,
1804        })
1805    }
1806
1807    /// Lists runs by logical identity, then exact occurrence token.
1808    ///
1809    /// An Exact cursor continues after that occurrence; a Latest cursor skips
1810    /// every occurrence of its logical identity. This inventory order does not
1811    /// represent the native save order used when opening Latest.
1812    ///
1813    /// # Errors
1814    ///
1815    /// Returns `InvalidInput` for a limit above 1,000 or malformed cursor, `NotFound`
1816    /// for an unavailable cursor, and `Closed` or `Integration` for revoked or
1817    /// unreadable scope.
1818    ///
1819    /// # Example
1820    ///
1821    /// ```
1822    /// # use agents_kit::Agent;
1823    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1824    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1825    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1826    /// # use storage_kit::StateStore;
1827    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1828    /// #
1829    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1830    /// # runtime.block_on(async {
1831    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1832    /// # let definition = WorkflowDefinition::parse(r#"{
1833    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1834    /// #     "state":{"count":0}, "required":["count"],
1835    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1836    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1837    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1838    /// #     ]}}], "routes":{}
1839    /// # }"#)?;
1840    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1841    /// # let mut app = App::open(config).await?;
1842    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1843    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1844    /// # }).await?;
1845    /// # let workflow = workspace.workflow("counter".parse()?).await?;
1846    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
1847    /// let runs = workflow.list_runs(None, std::num::NonZeroUsize::new(10).unwrap()).await?;
1848    /// assert_eq!(runs.len(), 1);
1849    /// assert_eq!(&runs[0].id, run.id());
1850    /// #
1851    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1852    /// # Ok::<(), Box<dyn std::error::Error>>(())
1853    /// # })?;
1854    /// # Ok::<(), Box<dyn std::error::Error>>(())
1855    /// ```
1856    pub async fn list_runs(
1857        &self,
1858        after: Option<RunSelector>,
1859        limit: NonZeroUsize,
1860    ) -> Result<Vec<RunSnapshot>, AppError> {
1861        self.runtime.list_runs(after, limit).await
1862    }
1863}
1864
1865impl<
1866    CS: StateStoreBackend,
1867    A: AgentBackend,
1868    X: ActionBackend,
1869    C: ConditionBackend,
1870    T: StateTyping,
1871    H: LifecycleHooks,
1872    E: ChildExecutor,
1873> WorkflowRun<CS, A, X, C, T, H, E>
1874{
1875    /// Returns this run's stable identity.
1876    ///
1877    /// # Example
1878    ///
1879    /// ```
1880    /// # use agents_kit::Agent;
1881    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1882    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1883    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1884    /// # use storage_kit::StateStore;
1885    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1886    /// #
1887    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1888    /// # runtime.block_on(async {
1889    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1890    /// # let definition = WorkflowDefinition::parse(r#"{
1891    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1892    /// #     "state":{"count":0}, "required":["count"],
1893    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1894    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1895    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1896    /// #     ]}}], "routes":{}
1897    /// # }"#)?;
1898    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1899    /// # let mut app = App::open(config).await?;
1900    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1901    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1902    /// # }).await?;
1903    /// # let workflow = workspace.workflow("counter".parse()?).await?;
1904    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
1905    /// assert_eq!(run.snapshot().await?.id, *run.id());
1906    /// #
1907    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1908    /// # Ok::<(), Box<dyn std::error::Error>>(())
1909    /// # })?;
1910    /// # Ok::<(), Box<dyn std::error::Error>>(())
1911    /// ```
1912    pub fn id(&self) -> &RunId {
1913        self.runtime.id()
1914    }
1915
1916    /// Acquires exclusive caller-local advancement without constructing a source.
1917    ///
1918    /// A live execution keeps that borrow until its last use:
1919    ///
1920    /// ```compile_fail
1921    /// use agents_kit::AgentBackend;
1922    /// use app_kit::{AppError, workflow::{ExecutionOptions, RunSnapshot, WorkflowRun}};
1923    /// use storage_kit::StateStoreBackend;
1924    /// async fn overlapping<CS: StateStoreBackend, A: AgentBackend>(
1925    /// run: &mut WorkflowRun<CS, A>,
1926    /// ) -> Result<RunSnapshot, AppError> {
1927    /// let execution = run.execution(ExecutionOptions::default()).await?;
1928    /// run.snapshot().await?;
1929    /// execution.finish().await
1930    /// }
1931    /// ```
1932    ///
1933    /// # Errors
1934    ///
1935    /// Returns `InvalidInput` for a nonempty options state or invalid inputs/selectors,
1936    /// `Busy` if advancement or admission is already owned, `Conflict` for stale
1937    /// authority, and `Closed` after revocation. Storage failures use `Integration`.
1938    ///
1939    /// # Example
1940    ///
1941    /// ```
1942    /// # use agents_kit::Agent;
1943    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1944    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1945    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1946    /// # use storage_kit::StateStore;
1947    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1948    /// #
1949    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1950    /// # runtime.block_on(async {
1951    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1952    /// # let definition = WorkflowDefinition::parse(r#"{
1953    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1954    /// #     "state":{"count":0}, "required":["count"],
1955    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1956    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1957    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1958    /// #     ]}}], "routes":{}
1959    /// # }"#)?;
1960    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1961    /// # let mut app = App::open(config).await?;
1962    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1963    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1964    /// # }).await?;
1965    /// # let workflow = workspace.workflow("counter".parse()?).await?;
1966    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
1967    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
1968    /// assert_eq!(run.snapshot().await?.checkpoint, terminal.checkpoint);
1969    /// #
1970    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1971    /// # Ok::<(), Box<dyn std::error::Error>>(())
1972    /// # })?;
1973    /// # Ok::<(), Box<dyn std::error::Error>>(())
1974    /// ```
1975    pub async fn execution(
1976        &mut self,
1977        options: ExecutionOptions,
1978    ) -> Result<Execution<'_, CS, A, X, C, T, H, E>, AppError> {
1979        Execution::open(self, options, ActionMode::Asynchronous).await
1980    }
1981
1982    /// Loads the current run snapshot.
1983    ///
1984    /// # Errors
1985    ///
1986    /// Returns `Closed` for revoked App ownership or sealed/deleted scope, `NotFound`
1987    /// for absent retained evidence, or `Integration` for inconsistent or unreadable
1988    /// records. A newer App generation can invalidate this handle.
1989    ///
1990    /// # Example
1991    ///
1992    /// ```
1993    /// # use agents_kit::Agent;
1994    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1995    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1996    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1997    /// # use storage_kit::StateStore;
1998    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1999    /// #
2000    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
2001    /// # runtime.block_on(async {
2002    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
2003    /// # let definition = WorkflowDefinition::parse(r#"{
2004    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
2005    /// #     "state":{"count":0}, "required":["count"],
2006    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
2007    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
2008    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
2009    /// #     ]}}], "routes":{}
2010    /// # }"#)?;
2011    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
2012    /// # let mut app = App::open(config).await?;
2013    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
2014    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
2015    /// # }).await?;
2016    /// # let workflow = workspace.workflow("counter".parse()?).await?;
2017    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
2018    /// let snapshot = run.snapshot().await?;
2019    /// assert_eq!(snapshot.checkpoint, 0);
2020    /// assert!(matches!(snapshot.status, RunStatus::Paused));
2021    /// #
2022    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
2023    /// # Ok::<(), Box<dyn std::error::Error>>(())
2024    /// # })?;
2025    /// # Ok::<(), Box<dyn std::error::Error>>(())
2026    /// ```
2027    pub async fn snapshot(&self) -> Result<RunSnapshot, AppError> {
2028        self.runtime.snapshot().await
2029    }
2030
2031    /// Reads one immutable checkpoint without advancing the workflow.
2032    ///
2033    /// An action selector returns the latest successful or failed observation
2034    /// of that ordinal. Missing or explicitly deleted history returns NotFound.
2035    ///
2036    /// # Errors
2037    ///
2038    /// Returns `Closed` for revoked App ownership or sealed/deleted scope, `NotFound`
2039    /// for absent retained evidence, or `Integration` for inconsistent or unreadable
2040    /// records. A newer App generation can invalidate this handle.
2041    /// An explicit missing sequence/action is `NotFound`; action ordinals are distinct
2042    /// from checkpoint sequences and select the latest matching saved observation.
2043    ///
2044    /// # Example
2045    ///
2046    /// ```
2047    /// # use agents_kit::Agent;
2048    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
2049    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
2050    /// #     RunStatus, WorkflowDefinition, WorkflowState};
2051    /// # use storage_kit::StateStore;
2052    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
2053    /// #
2054    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
2055    /// # runtime.block_on(async {
2056    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
2057    /// # let definition = WorkflowDefinition::parse(r#"{
2058    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
2059    /// #     "state":{"count":0}, "required":["count"],
2060    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
2061    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
2062    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
2063    /// #     ]}}], "routes":{}
2064    /// # }"#)?;
2065    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
2066    /// # let mut app = App::open(config).await?;
2067    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
2068    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
2069    /// # }).await?;
2070    /// # let workflow = workspace.workflow("counter".parse()?).await?;
2071    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
2072    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
2073    /// let initial = run.checkpoint(CheckpointSelector::Sequence(0)).await?;
2074    /// let action = run.checkpoint(CheckpointSelector::Action(0)).await?;
2075    /// assert_eq!(initial.state.values()["count"], 0);
2076    /// assert_eq!(action.state.values()["count"], 1);
2077    /// #
2078    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
2079    /// # Ok::<(), Box<dyn std::error::Error>>(())
2080    /// # })?;
2081    /// # Ok::<(), Box<dyn std::error::Error>>(())
2082    /// ```
2083    pub async fn checkpoint(&self, selector: CheckpointSelector) -> Result<Checkpoint, AppError> {
2084        self.runtime.checkpoint(selector).await
2085    }
2086
2087    /// Reads a bounded page from a fixed retained-history cutoff.
2088    ///
2089    /// Limits are 1 through 1,000. A returned cursor preserves its cutoff and
2090    /// is valid only for this run creation and App generation. Reading a page
2091    /// never invokes actions, state typing, codecs or lower-agent recovery.
2092    ///
2093    /// # Errors
2094    ///
2095    /// Returns `Closed` for revoked App ownership or sealed/deleted scope, `NotFound`
2096    /// for absent retained evidence, or `Integration` for inconsistent or unreadable
2097    /// records. A newer App generation can invalidate this handle.
2098    /// An excessive page limit or a cursor from another run/occurrence/generation is
2099    /// `InvalidInput`. A captured cutoff never includes later appended checkpoints.
2100    ///
2101    /// # Example
2102    ///
2103    /// ```
2104    /// # use agents_kit::Agent;
2105    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
2106    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
2107    /// #     RunStatus, WorkflowDefinition, WorkflowState};
2108    /// # use storage_kit::StateStore;
2109    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
2110    /// #
2111    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
2112    /// # runtime.block_on(async {
2113    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
2114    /// # let definition = WorkflowDefinition::parse(r#"{
2115    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
2116    /// #     "state":{"count":0}, "required":["count"],
2117    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
2118    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
2119    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
2120    /// #     ]}}], "routes":{}
2121    /// # }"#)?;
2122    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
2123    /// # let mut app = App::open(config).await?;
2124    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
2125    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
2126    /// # }).await?;
2127    /// # let workflow = workspace.workflow("counter".parse()?).await?;
2128    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
2129    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
2130    /// let page = run.history(None, std::num::NonZeroUsize::new(1).unwrap()).await?;
2131    /// assert_eq!(page.checkpoints[0].sequence, 0);
2132    /// let next = run.history(page.next, std::num::NonZeroUsize::new(100).unwrap()).await?;
2133    /// assert!(next.checkpoints.iter().all(|checkpoint| checkpoint.sequence > 0));
2134    /// #
2135    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
2136    /// # Ok::<(), Box<dyn std::error::Error>>(())
2137    /// # })?;
2138    /// # Ok::<(), Box<dyn std::error::Error>>(())
2139    /// ```
2140    pub async fn history(
2141        &self,
2142        after: Option<HistoryCursor>,
2143        limit: NonZeroUsize,
2144    ) -> Result<HistoryPage, AppError> {
2145        self.runtime.history(after, limit).await
2146    }
2147
2148    /// Deletes terminal history through bounded, resumable pages.
2149    ///
2150    /// The expected checkpoint fences the terminal boundary. Dropping this
2151    /// operation's observer preserves admitted deletion and native commits.
2152    ///
2153    /// # Errors
2154    ///
2155    /// Returns `Conflict` for a stale checkpoint, `Busy` for nonterminal/unknown work
2156    /// or a child still referenced by an active parent, and `Closed` after sealing or
2157    /// revocation. Storage failures use `Integration`. A successful seal survives
2158    /// observer loss and deletion resumes in bounded pages after reopen.
2159    ///
2160    /// # Example
2161    ///
2162    /// ```
2163    /// # use agents_kit::Agent;
2164    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
2165    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
2166    /// #     RunStatus, WorkflowDefinition, WorkflowState};
2167    /// # use storage_kit::StateStore;
2168    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
2169    /// #
2170    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
2171    /// # runtime.block_on(async {
2172    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
2173    /// # let definition = WorkflowDefinition::parse(r#"{
2174    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
2175    /// #     "state":{"count":0}, "required":["count"],
2176    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
2177    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
2178    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
2179    /// #     ]}}], "routes":{}
2180    /// # }"#)?;
2181    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
2182    /// # let mut app = App::open(config).await?;
2183    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
2184    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
2185    /// # }).await?;
2186    /// # let workflow = workspace.workflow("counter".parse()?).await?;
2187    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
2188    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
2189    /// let id = run.id().clone();
2190    /// run.delete(terminal.checkpoint).await?;
2191    /// assert!(matches!(workflow.open_run(RunSelector::Latest(id)).await, Err(app_kit::AppError::NotFound(_))));
2192    /// #
2193    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
2194    /// # Ok::<(), Box<dyn std::error::Error>>(())
2195    /// # })?;
2196    /// # Ok::<(), Box<dyn std::error::Error>>(())
2197    /// ```
2198    pub async fn delete(&mut self, expected_checkpoint: u64) -> Result<(), AppError> {
2199        self.runtime.delete(expected_checkpoint).await
2200    }
2201
2202    /// Waits for the next actionable or terminal snapshot.
2203    ///
2204    /// This observes accepted work; it does not acquire execution ownership or resume
2205    /// a prepared run. A caller interaction or unknown effect can return before a
2206    /// terminal result. Dropping the observation future never requests cancellation.
2207    ///
2208    /// # Errors
2209    ///
2210    /// Returns `Closed` for revoked App ownership or sealed/deleted scope, `NotFound`
2211    /// for absent retained evidence, or `Integration` for inconsistent or unreadable
2212    /// records. A newer App generation can invalidate this handle.
2213    ///
2214    /// # Example
2215    ///
2216    /// ```
2217    /// # use agents_kit::Agent;
2218    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
2219    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
2220    /// #     RunStatus, WorkflowDefinition, WorkflowState};
2221    /// # use storage_kit::StateStore;
2222    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
2223    /// #
2224    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
2225    /// # runtime.block_on(async {
2226    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
2227    /// # let definition = WorkflowDefinition::parse(r#"{
2228    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
2229    /// #     "state":{"count":0}, "required":["count"],
2230    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
2231    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
2232    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
2233    /// #     ]}}], "routes":{}
2234    /// # }"#)?;
2235    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
2236    /// # let mut app = App::open(config).await?;
2237    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
2238    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
2239    /// # }).await?;
2240    /// # let workflow = workspace.workflow("counter".parse()?).await?;
2241    /// let background = workflow.start(ExecutionOptions::default()).await?;
2242    /// let observed = background.wait().await?;
2243    /// assert!(matches!(observed.status, RunStatus::Succeeded));
2244    /// #
2245    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
2246    /// # Ok::<(), Box<dyn std::error::Error>>(())
2247    /// # })?;
2248    /// # Ok::<(), Box<dyn std::error::Error>>(())
2249    /// ```
2250    pub async fn wait(&self) -> Result<RunSnapshot, AppError> {
2251        self.runtime.wait().await
2252    }
2253
2254    /// Commits explicit checkpoint-fenced cancellation.
2255    ///
2256    /// # Errors
2257    ///
2258    /// Returns `Conflict` for a stale checkpoint, `Busy` for unavailable admission,
2259    /// `Closed` after revocation, or `Integration` for unreadable/failed storage.
2260    /// Cancellation admission is durable; observer loss does not reverse it.
2261    ///
2262    /// # Example
2263    ///
2264    /// ```
2265    /// # use agents_kit::Agent;
2266    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
2267    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
2268    /// #     RunStatus, WorkflowDefinition, WorkflowState};
2269    /// # use storage_kit::StateStore;
2270    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
2271    /// #
2272    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
2273    /// # runtime.block_on(async {
2274    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
2275    /// # let definition = WorkflowDefinition::parse(r#"{
2276    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
2277    /// #     "state":{"count":0}, "required":["count"],
2278    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
2279    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
2280    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
2281    /// #     ]}}], "routes":{}
2282    /// # }"#)?;
2283    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
2284    /// # let mut app = App::open(config).await?;
2285    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
2286    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
2287    /// # }).await?;
2288    /// # let workflow = workspace.workflow("counter".parse()?).await?;
2289    /// # let mut run = workflow.prepare(WorkflowState::default()).await?;
2290    /// let before = run.snapshot().await?;
2291    /// run.cancel(before.checkpoint).await?;
2292    /// let cancelled = run.wait().await?;
2293    /// assert!(matches!(cancelled.status, RunStatus::Cancelled));
2294    /// #
2295    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
2296    /// # Ok::<(), Box<dyn std::error::Error>>(())
2297    /// # })?;
2298    /// # Ok::<(), Box<dyn std::error::Error>>(())
2299    /// ```
2300    pub async fn cancel(&self, expected_checkpoint: u64) -> Result<RunSnapshot, AppError> {
2301        self.runtime.cancel(expected_checkpoint).await
2302    }
2303
2304    /// Settles one exact interaction using its original target generation.
2305    ///
2306    /// Sibling checkpoints do not invalidate that generation. An identical
2307    /// decision replays its retained receipt; a different decision conflicts.
2308    /// Transient inputs replay only while their execution retains the values.
2309    ///
2310    /// # Errors
2311    ///
2312    /// Returns `InvalidInput` for a mismatched resolution or invalid schema/input,
2313    /// `Conflict` for a stale target generation, a different repeated decision, or an
2314    /// unsafe retry. Capacity, revocation, lower/storage failure and observation
2315    /// deadlines report `Busy`, `Closed`, `Integration`, or `Timeout`.
2316    ///
2317    /// # Example
2318    ///
2319    /// ```
2320    /// # use agents_kit::Agent;
2321    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
2322    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
2323    /// #     RunStatus, WorkflowDefinition, WorkflowState};
2324    /// # use storage_kit::StateStore;
2325    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
2326    /// #
2327    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
2328    /// # runtime.block_on(async {
2329    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
2330    /// # let definition = WorkflowDefinition::parse(r#"{
2331    /// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
2332    /// #  "state":{"answer":""},"required":["answer"],"reducers":{"answer":"reject_concurrent"},
2333    /// #  "entry":"ask","nodes":[{"id":"ask","step":{"kind":"input",
2334    /// #  "message":[{"kind":"text","text":"Review topic?"}],"target":"answer"}}],"routes":{}
2335    /// # }"#)?;
2336    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
2337    /// # let mut app = App::open(config).await?;
2338    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
2339    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
2340    /// # }).await?;
2341    /// # let workflow = workspace.workflow("counter".parse()?).await?;
2342    /// use app_kit::workflow::{Interaction, Resolution};
2343    /// let run = workflow.start(ExecutionOptions::default()).await?;
2344    /// let waiting = run.wait().await?;
2345    /// let Interaction::Input { sequence, generation, .. } = waiting.interactions[0] else {
2346    ///     panic!("the input workflow must request a value");
2347    /// };
2348    /// run.resolve(sequence, generation, Resolution::Input(serde_json::json!("ownership"))).await?;
2349    /// assert_eq!(run.wait().await?.state.values()["answer"], "ownership");
2350    /// #
2351    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
2352    /// # Ok::<(), Box<dyn std::error::Error>>(())
2353    /// # })?;
2354    /// # Ok::<(), Box<dyn std::error::Error>>(())
2355    /// ```
2356    pub async fn resolve(
2357        &self,
2358        sequence: u64,
2359        generation: u64,
2360        resolution: Resolution,
2361    ) -> Result<RunSnapshot, AppError> {
2362        self.runtime.resolve(sequence, generation, resolution).await
2363    }
2364}
2365
2366impl WorkflowId {
2367    pub(crate) fn as_str(&self) -> &str {
2368        &self.0
2369    }
2370}
2371
2372impl RunId {
2373    pub(crate) fn validate_occurrence(occurrence: &str) -> Result<(), AppError> {
2374        if occurrence.len() == 32
2375            && occurrence
2376                .bytes()
2377                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2378        {
2379            Ok(())
2380        } else {
2381            Err(AppError::InvalidInput(
2382                "run occurrence must contain 32 lowercase hexadecimal digits".into(),
2383            ))
2384        }
2385    }
2386
2387    pub(crate) fn generate() -> Self {
2388        Self(format!("run_{}", Uuid::new_v4().hyphenated()))
2389    }
2390
2391    fn child(&self, task_key: &str) -> Self {
2392        let mut digest = Sha256::new();
2393        digest.update(self.as_str().as_bytes());
2394        digest.update(b":");
2395        digest.update(task_key.as_bytes());
2396        Self(format!("{:x}", digest.finalize()))
2397    }
2398
2399    pub(crate) fn as_str(&self) -> &str {
2400        &self.0
2401    }
2402}
2403
2404impl fmt::Display for WorkflowId {
2405    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2406        formatter.write_str(&self.0)
2407    }
2408}
2409
2410impl FromStr for WorkflowId {
2411    type Err = AppError;
2412
2413    fn from_str(value: &str) -> Result<Self, Self::Err> {
2414        IdentitySyntax::Named("WorkflowId").parse(value).map(Self)
2415    }
2416}
2417
2418impl Serialize for WorkflowId {
2419    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2420    where
2421        S: Serializer,
2422    {
2423        serializer.serialize_str(&self.0)
2424    }
2425}
2426
2427impl<'de> Deserialize<'de> for WorkflowId {
2428    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2429    where
2430        D: Deserializer<'de>,
2431    {
2432        String::deserialize(deserializer)?
2433            .parse()
2434            .map_err(serde::de::Error::custom)
2435    }
2436}
2437
2438impl fmt::Display for RunId {
2439    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2440        formatter.write_str(&self.0)
2441    }
2442}
2443
2444impl FromStr for RunId {
2445    type Err = AppError;
2446
2447    fn from_str(value: &str) -> Result<Self, Self::Err> {
2448        if value.len() == 64
2449            && value
2450                .bytes()
2451                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2452        {
2453            return Ok(Self(value.to_owned()));
2454        }
2455        IdentitySyntax::Uuid {
2456            name: "RunId",
2457            prefix: "run_",
2458        }
2459        .parse(value)
2460        .map(Self)
2461    }
2462}
2463
2464impl Serialize for RunId {
2465    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2466    where
2467        S: Serializer,
2468    {
2469        serializer.serialize_str(&self.0)
2470    }
2471}
2472
2473impl<'de> Deserialize<'de> for RunId {
2474    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2475    where
2476        D: Deserializer<'de>,
2477    {
2478        String::deserialize(deserializer)?
2479            .parse()
2480            .map_err(serde::de::Error::custom)
2481    }
2482}