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//! After the [introductory workflow](crate#local-workflow), use these operations
34//! for explicit control and retained results:
35//!
36//! - Prepare a paused run, then drive
37//!   [`WorkflowRun::execution`](crate::workflow::WorkflowRun::execution);
38//!   reading a snapshot alone never advances it.
39//! - Page immutable [`WorkflowRun::history`](crate::workflow::WorkflowRun::history)
40//!   at a fixed cutoff.
41//! - [`Workflow::restore`] copies a checkpoint into an independent paused run;
42//!   [`WorkflowRun::delete`](crate::workflow::WorkflowRun::delete) removes only
43//!   the selected run's retained history.
44//!
45//! The complete example below joins these three tasks without credentials, an
46//! Agent value or a subprocess.
47//!
48//! ```
49//! use agents_kit::Agent;
50//! use app_kit::{App, AppConfig, AppError, Shutdown, WorkspaceConfig};
51//! use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions,
52//!     RestoreOptions, ResumePosition, RunSelector, RunStatus, WorkflowDefinition, WorkflowState};
53//! use storage_kit::StateStore;
54//! use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
55//! use std::{num::NonZeroUsize, time::Duration};
56//! # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
57//! # runtime.block_on(async {
58//! let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
59//! let definition = WorkflowDefinition::parse(r#"{
60//!     "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
61//!     "state":{"count":0},"required":["count"],"reducers":{"count":"reject_concurrent"},
62//!     "entry":"increment","nodes":[{"id":"increment","step":{"kind":"state",
63//!     "mutations":[{"kind":"increment","target":"count","value":{"kind":"literal","value":1}}]}}],
64//!     "routes":{}
65//! }"#)?;
66//! let mut app = App::open(AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition])).await?;
67//! let outcome = async {
68//!     let workspace = app.create_workspace(WorkspaceConfig {
69//!         name: Some("history example".into()), root: std::env::current_dir()?,
70//!     }).await?;
71//!     let workflow = workspace.workflow("counter".parse()?).await?;
72//!     let mut run = workflow.prepare(WorkflowState::default()).await?;
73//!     assert!(matches!(run.snapshot().await?.status, RunStatus::Paused));
74//!     assert_eq!(run.checkpoint(CheckpointSelector::Sequence(0)).await?.state.values()["count"], 0);
75//!     let mut execution = run.execution(ExecutionOptions::default()).await?;
76//!     let mut committed_actions = 0;
77//!     while let Some(event) = execution.next().await? {
78//!         if let ExecutionEvent::Committed { outcomes, .. } = event {
79//!             committed_actions += outcomes.len();
80//!         }
81//!     }
82//!     let terminal = execution.finish().await?;
83//!     assert!(matches!(terminal.status, RunStatus::Succeeded));
84//!     assert_eq!(terminal.state.values()["count"], 1);
85//!     assert_eq!(committed_actions, 1);
86//!
87//!     // Read history in bounded pages.
88//!     let first = run.history(None, NonZeroUsize::new(1).unwrap()).await?;
89//!     assert_eq!(first.checkpoints[0].sequence, 0);
90//!     let cursor = first.next.expect("the action has a later checkpoint");
91//!     let later = run.history(Some(cursor), NonZeroUsize::new(10).unwrap()).await?;
92//!     assert!(later.checkpoints.iter().any(|checkpoint| checkpoint.action_ordinal == Some(0)));
93//!     // Restore an independent run, then remove both histories explicitly.
94//!     let mut copy = workflow.restore(RestoreOptions {
95//!         source: RunSelector::Latest(run.id().clone()), checkpoint: CheckpointSelector::Action(0),
96//!         overrides: WorkflowState::default(), position: ResumePosition::AfterCompleted, fallback: None,
97//!     }).await?;
98//!     assert_ne!(copy.id(), run.id());
99//!     assert!(matches!(copy.snapshot().await?.status, RunStatus::Paused));
100//!     let copied = copy.checkpoint(CheckpointSelector::Sequence(0)).await?;
101//!     assert_eq!(copied.state.values()["count"], 1);
102//!     assert_eq!(&copied.lineage.as_ref().unwrap().source, run.id());
103//!     let copied_terminal = copy.execution(ExecutionOptions::default()).await?.finish().await?;
104//!     assert!(matches!(copied_terminal.status, RunStatus::Succeeded));
105//!     copy.delete(copied_terminal.checkpoint).await?;
106//!     let original_id = run.id().clone();
107//!     run.delete(terminal.checkpoint).await?;
108//!     assert!(matches!(workflow.open_run(RunSelector::Latest(original_id)).await, Err(AppError::NotFound(_))));
109//!     Ok::<(), Box<dyn std::error::Error>>(())
110//! }.await;
111//! let cleanup = app.shutdown(Shutdown::CancelAndDrain { timeout: Duration::from_secs(5) }).await;
112//! if let Err(error) = &cleanup { eprintln!("App shutdown: {error}"); }
113//! outcome?; // An execution error remains primary if shutdown also reports an error.
114//! cleanup?;
115//! # Ok::<(), Box<dyn std::error::Error>>(())
116//! # })?;
117//! # Ok::<(), Box<dyn std::error::Error>>(())
118//! ```
119//!
120//! A real Agent-backed workflow can stop at an actionable interaction rather than
121//! success: inspect [`crate::workflow::RunSnapshot::interactions`] and [`crate::workflow::RunStatus`] before choosing
122//! an explicit resolution. Caller actions own their external effects; a discarded
123//! uncommitted workflow state change does not undo external I/O.
124//!
125//! # Configure caller behavior
126//!
127//! [`crate::AppConfig`]'s configuration map links the complete implementations of
128//! all five behavior traits and their consuming attachment methods. Actions and
129//! conditions use exact declared keys/revisions; typing validates initial and
130//! resulting state; hooks observe lifecycle boundaries; child executors drive the
131//! already-admitted concrete child futures. None changes the lower Agent or
132//! StateStore associated child types. See [`crate::App::open`] for durable reopen
133//! and the cold migration boundary.
134
135use std::fmt;
136use std::num::NonZeroUsize;
137use std::str::FromStr;
138use std::sync::{Arc, RwLock};
139
140use agents_kit::AgentBackend;
141use agents_kit::notification::{ConversationNotification, NotificationResolution};
142use serde::{Deserialize, Deserializer, Serialize, Serializer};
143use serde_json::{Map, Value};
144use sha2::{Digest, Sha256};
145use storage_kit::StateStoreBackend;
146use uuid::Uuid;
147
148use crate::{AppError, IdentitySyntax};
149
150mod action;
151mod child;
152pub(crate) mod definition;
153mod execution;
154pub(crate) use execution::{MAX_FRONTIER, StagedOutcomeRecord};
155mod graph;
156mod history;
157pub(crate) mod interaction;
158mod lifecycle;
159pub(crate) mod run;
160mod serialization;
161mod state;
162
163pub use action::{
164    ActionBackend, ActionContext, ActionDeclaration, ActionEvent, ActionMode, ActionRequest,
165    ConditionBackend, ConditionDeclaration, NoActions, NoConditions,
166};
167pub use child::{ChildBatch, ChildCompletion, ChildExecutor, InlineExecutor};
168pub use history::{
169    ActionOutcome, Checkpoint, CheckpointKind, CheckpointSelector, HistoryCursor, HistoryPage,
170    RestoreOptions, ResumePosition, RunLineage,
171};
172pub use lifecycle::{LifecycleEvent, LifecycleHooks, NoHooks};
173pub use run::session::{Execution, SyncExecution};
174
175/// Immutable, ordered workflow values validated at each operation boundary.
176///
177/// ```
178/// use app_kit::workflow::{JsonState, StateChange, WorkflowState};
179/// use serde_json::{Map, Value};
180/// let before = WorkflowState::new(Map::from_iter([("count".into(), Value::from(1))]))?;
181/// let after = before.apply(&StateChange::Increment {
182///     values: vec![("count".into(), Value::from(1))],
183/// }, &JsonState::default())?;
184/// assert_eq!(before.values()["count"], 1);
185/// assert_eq!(after.values()["count"], 2);
186/// # Ok::<(), app_kit::AppError>(())
187/// ```
188///
189/// Changes go through the validated state operations:
190///
191/// ```compile_fail
192/// use app_kit::workflow::WorkflowState;
193/// let mut state = WorkflowState::default();
194/// state.values.clear();
195/// ```
196#[derive(Clone, Debug, Default, PartialEq)]
197pub struct WorkflowState {
198    values: Map<String, Value>,
199}
200
201/// Process-global field encoders, field decoders and tagged JSON decoders.
202///
203/// Registrations are exact names and replacements affect the next operation.
204/// Each operation retains one registry snapshot and invokes callbacks without
205/// holding its lock. The only construction entrypoint is [`Self::global`].
206///
207/// ```
208/// use app_kit::workflow::StateSerialization;
209/// StateSerialization::global().register_tag("text".into(), |value| {
210/// value.get("value").cloned().ok_or("missing text value")
211/// })?;
212/// # Ok::<(), app_kit::AppError>(())
213/// ```
214///
215/// Only field/tag JSON codecs enter this registry; actions, hooks, child executors
216/// and lower backends remain concrete configuration values. Registration does not
217/// persist callable identity; callers version their own portable codec payloads.
218pub struct StateSerialization {
219    registry: RwLock<Arc<serialization::SerializationRegistry>>,
220}
221
222/// A resolved immutable state operation; expressions remain private syntax.
223///
224/// Each payload is one atomic operation with at most 4,096 entries. Repeated
225/// Append/Extend/Increment keys apply in order; any later failure discards the
226/// whole candidate. Merge and Subset use full-state validation rather than
227/// primitive transition callbacks. Results never mutate the original state.
228pub enum StateChange {
229    /// Replaces values atomically in payload order, preserving existing positions.
230    Assign {
231        /// Complete values to replace or insert.
232        values: Map<String, Value>,
233    },
234    /// Appends one item per entry, creating arrays for missing keys.
235    Append {
236        /// Ordered entries; repeated keys append repeatedly within one operation.
237        values: Vec<(String, Value)>,
238    },
239    /// Extends arrays atomically without recursively flattening their items.
240    Extend {
241        /// Ordered entries; repeated keys preserve every supplied item.
242        values: Vec<(String, Vec<Value>)>,
243    },
244    /// Adds signed integer deltas to integer values, or zero for missing keys.
245    Increment {
246        /// Ordered deltas, each representable as i64; repeated keys fold in order.
247        values: Vec<(String, Value)>,
248    },
249    /// Removes keys atomically, preserving the order of other keys.
250    Remove {
251        /// Missing and repeated names are harmless.
252        keys: Vec<String>,
253    },
254    /// Keeps keys in their existing order; typing observes resolved Remove keys.
255    Wipe {
256        /// Keys to retain; missing and repeated names are harmless.
257        keep: Vec<String>,
258    },
259    /// Replaces overlapping values and appends new keys in incoming order.
260    Merge {
261        /// Values to merge into the current state.
262        state: WorkflowState,
263    },
264    /// Selects present keys in the requested order.
265    Subset {
266        /// Ordered keys to select; missing keys are ignored.
267        keys: Vec<String>,
268    },
269}
270
271/// Caller-owned typed construction and state validation.
272///
273/// Data is created and consumed locally and need not be Clone, Send or serializable.
274/// The default roundtrip checks successful reconstruction, not raw equality.
275///
276/// # Example
277///
278/// ```
279/// use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
280/// use serde_json::{Map, Value};
281/// struct Counter;
282/// impl StateTyping for Counter {
283///     type Data = i64;
284///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
285///         state.values().get("count").and_then(Value::as_i64)
286///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
287///     }
288///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
289///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
290///     }
291///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
292///         if let StateChange::Increment { values } = change {
293///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
294///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
295///             }
296///         }
297///         Ok(())
298///     }
299///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
300///         if self.construct_data(after)? < self.construct_data(before)? {
301///             return Err(AppError::InvalidInput("count cannot decrease".into()));
302///         }
303///         Ok(())
304///     }
305///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
306///         if self.construct_data(state)? < 0 {
307///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
308///         }
309///         Ok(())
310///     }
311/// }
312/// let before = Counter.construct_state(&2)?;
313/// let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
314/// let after = before.apply(&change, &Counter)?;
315/// assert_eq!(Counter.construct_data(&after)?, 3);
316/// assert!(Counter.validate_state(&Counter.construct_state(&-1)?).is_err());
317/// # Ok::<(), AppError>(())
318/// ```
319pub trait StateTyping: Send + Sync + 'static {
320    /// Caller-specific data produced from workflow values.
321    type Data;
322
323    /// Constructs caller data from a validated state.
324    ///
325    /// # Errors
326    ///
327    /// Return an error when the state cannot be represented as `Self::Data`.
328    ///
329    /// See [the complete typing implementation](StateTyping).
330    fn construct_data(&self, state: &WorkflowState) -> Result<Self::Data, AppError>;
331    /// Reconstructs workflow values from caller data.
332    ///
333    /// # Errors
334    ///
335    /// Return an error when caller data cannot produce a valid workflow state.
336    ///
337    /// See [the complete typing implementation](StateTyping).
338    fn construct_state(&self, data: &Self::Data) -> Result<WorkflowState, AppError>;
339    /// Validates an operation before any candidate mutation.
340    ///
341    /// # Errors
342    ///
343    /// Return an error to reject the complete primitive operation before candidate
344    /// mutation. The default accepts it; Merge and Subset do not invoke this method.
345    ///
346    /// See [the complete typing implementation](StateTyping).
347    fn validate_pre(&self, _state: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
348        Ok(())
349    }
350    /// Validates the original and candidate states after the operation.
351    ///
352    /// # Errors
353    ///
354    /// Return an error to reject the complete candidate primitive transition. The
355    /// original state remains unchanged. The default accepts it; Merge and Subset
356    /// do not invoke this method.
357    ///
358    /// See [the complete typing implementation](StateTyping).
359    fn validate_post(
360        &self,
361        _before: &WorkflowState,
362        _after: &WorkflowState,
363        _change: &StateChange,
364    ) -> Result<(), AppError> {
365        Ok(())
366    }
367    /// Validates all candidate values after transition validation.
368    ///
369    /// # Errors
370    ///
371    /// Return an error when any full-state invariant is violated. The default accepts
372    /// the state; this method also runs for reconstructed Merge/Subset snapshots.
373    ///
374    /// See [the complete typing implementation](StateTyping).
375    fn validate_state(&self, _state: &WorkflowState) -> Result<(), AppError> {
376        Ok(())
377    }
378    /// Verifies both caller conversions succeed; a different reconstruction is allowed.
379    ///
380    /// # Errors
381    ///
382    /// The default propagates either conversion error. Successful reconstruction is
383    /// sufficient; byte-for-byte or value equality is not required.
384    ///
385    /// See [the complete typing implementation](StateTyping).
386    fn validate_roundtrip(&self, state: &WorkflowState) -> Result<(), AppError> {
387        let data = self.construct_data(state)?;
388        self.construct_state(&data).map(|_| ())
389    }
390}
391
392/// Raw JSON typing with no additional conversion or validation policy.
393///
394/// # Example
395///
396/// ```
397/// use app_kit::workflow::{JsonState, StateTyping, WorkflowState};
398/// let state = WorkflowState::new(serde_json::from_str(r#"{"count":2}"#)?)?;
399/// let data = JsonState.construct_data(&state)?;
400/// assert_eq!(data["count"], 2);
401/// assert_eq!(JsonState.construct_state(&data)?, state);
402/// # Ok::<(), Box<dyn std::error::Error>>(())
403/// ```
404#[derive(Clone, Copy, Debug, Default)]
405pub struct JsonState;
406
407impl StateTyping for JsonState {
408    type Data = Map<String, Value>;
409
410    fn construct_data(&self, state: &WorkflowState) -> Result<Self::Data, AppError> {
411        Ok(state.values.clone())
412    }
413
414    fn construct_state(&self, data: &Self::Data) -> Result<WorkflowState, AppError> {
415        WorkflowState::new(data.clone())
416    }
417
418    fn validate_roundtrip(&self, _state: &WorkflowState) -> Result<(), AppError> {
419        Ok(())
420    }
421}
422
423impl WorkflowState {
424    /// Validates ordered JSON values and takes ownership of them.
425    ///
426    /// ```
427    /// use app_kit::workflow::WorkflowState;
428    /// let values = serde_json::from_str(r#"{"first":1,"second":2}"#)?;
429    /// let state = WorkflowState::new(values)?;
430    /// assert_eq!(state.values().keys().collect::<Vec<_>>(), ["first", "second"]);
431    /// # Ok::<(), Box<dyn std::error::Error>>(())
432    /// ```
433    ///
434    /// # Errors
435    ///
436    /// Returns `InvalidInput` for invalid state keys or exceeded JSON byte, nesting,
437    /// collection, or text bounds. Ownership is accepted only after full validation.
438    pub fn new(values: Map<String, Value>) -> Result<Self, AppError> {
439        definition::JsonBoundary::validate_map(&values)?;
440        Ok(Self { values })
441    }
442
443    /// Borrows values in their insertion order.
444    ///
445    /// ```
446    /// use app_kit::workflow::WorkflowState;
447    /// let state = WorkflowState::default();
448    /// assert!(state.values().get("optional_input").is_none());
449    /// ```
450    pub fn values(&self) -> &Map<String, Value> {
451        &self.values
452    }
453
454    /// Produces a new state with the pinned operation-specific validation order.
455    ///
456    /// Primitive changes run pre/post transition validation, then full-state
457    /// validation and roundtrip. Merge and subset construct new snapshots and
458    /// run only full-state validation and roundtrip.
459    /// Any failure leaves this state and its retained clones unchanged.
460    ///
461    /// ```
462    /// use app_kit::workflow::{JsonState, StateChange, WorkflowState};
463    /// use serde_json::{Map, Value};
464    /// let state = WorkflowState::new(Map::from_iter([
465    /// ("count".into(), Value::from("unknown")),
466    /// ]))?;
467    /// let change = StateChange::Increment {
468    /// values: vec![("count".into(), Value::from(1))],
469    /// };
470    /// assert!(state.apply(&change, &JsonState).is_err());
471    /// assert_eq!(state.values()["count"], "unknown");
472    /// # Ok::<(), app_kit::AppError>(())
473    /// ```
474    ///
475    /// # Errors
476    ///
477    /// Returns `InvalidInput` for incompatible operand types, signed-integer delta or
478    /// range errors, invalid state, or excessive operation payloads. Caller typing
479    /// errors propagate unchanged; failure never mutates the original state.
480    ///
481    /// # Panics
482    ///
483    /// Panics if the caller-provided typing implementation panics. Direct state
484    /// operations do not contain caller panics; App admission/execution has its own
485    /// boundary for classifying them.
486    pub fn apply<T: StateTyping>(
487        &self,
488        change: &StateChange,
489        typing: &T,
490    ) -> Result<Self, AppError> {
491        if let StateChange::Wipe { keep } = change {
492            StateChange::validate_count(keep.len())?;
493            let keys = self
494                .values
495                .keys()
496                .filter(|key| !keep.contains(key))
497                .cloned()
498                .collect();
499            return self.apply(&StateChange::Remove { keys }, typing);
500        }
501        let snapshot = matches!(
502            change,
503            StateChange::Merge { .. } | StateChange::Subset { .. }
504        );
505        if !snapshot {
506            typing.validate_pre(self, change)?;
507        }
508        let candidate = Self::new(change.apply_values(&self.values)?)?;
509        if !snapshot {
510            typing.validate_post(self, &candidate, change)?;
511        }
512        candidate.validate_typing(typing)?;
513        Ok(candidate)
514    }
515
516    fn validate_typing<T: StateTyping>(&self, typing: &T) -> Result<(), AppError> {
517        typing.validate_state(self)?;
518        typing.validate_roundtrip(self)
519    }
520}
521
522/// A workflow selected in one application workspace.
523///
524/// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
525pub struct Workflow<
526    CS: StateStoreBackend,
527    A: AgentBackend,
528    X: ActionBackend = NoActions,
529    C: ConditionBackend = NoConditions,
530    T: StateTyping = JsonState,
531    H: LifecycleHooks = NoHooks,
532    E: ChildExecutor = InlineExecutor,
533> {
534    runtime: run::WorkflowRuntime<CS, A, X, C, T, H, E>,
535}
536
537/// A durable workflow-run handle.
538///
539/// This handle observes durable state and requests explicit controls. Dropping it
540/// or a `wait`/snapshot future does not cancel accepted work. [`Self::execution`]
541/// borrows it exclusively and returns an owner whose drop does revoke uncommitted
542/// advancement. Use [`Self::cancel`] for durable, monotonic cancellation.
543///
544/// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
545pub struct WorkflowRun<
546    CS: StateStoreBackend,
547    A: AgentBackend,
548    X: ActionBackend = NoActions,
549    C: ConditionBackend = NoConditions,
550    T: StateTyping = JsonState,
551    H: LifecycleHooks = NoHooks,
552    E: ChildExecutor = InlineExecutor,
553> {
554    runtime: run::WorkflowRunRuntime<CS, A, X, C, T, H, E>,
555}
556
557/// One opaque, parsed `app-kit.workflow/v2` definition.
558///
559/// Construct definitions through [`Self::parse`]; their internals cannot be
560/// copied through struct update syntax:
561///
562/// ```compile_fail
563/// use app_kit::workflow::WorkflowDefinition;
564/// fn reconstruct(definition: WorkflowDefinition) -> WorkflowDefinition {
565/// WorkflowDefinition { ..definition }
566/// }
567/// ```
568///
569/// Only the strict versioned language is accepted. Unknown/duplicate fields,
570/// invalid expressions, graph references and finite-bound violations fail before
571/// runtime work. Inspection returns passive metadata; composition runs the same
572/// compiler rather than exposing a mutable executable AST.
573///
574/// # Example
575///
576/// ```
577/// use app_kit::workflow::WorkflowDefinition;
578///
579/// let definition = WorkflowDefinition::parse(r#"{
580///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
581///     "state":{"count":0}, "required":["count"],
582///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
583///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
584///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
585///     ]}}], "routes":{}
586/// }"#)?;
587/// assert_eq!(definition.graph().entry, "increment");
588/// # Ok::<(), app_kit::AppError>(())
589/// ```
590pub struct WorkflowDefinition {
591    compiled: definition::CompiledDefinition,
592}
593
594/// A deterministic read-only graph export format.
595pub enum GraphFormat {
596    /// Portable graph metadata as JSON, without runtime state.
597    Json,
598    /// Graphviz DOT text; rendering requires no subprocess in this crate.
599    Dot,
600    /// Mermaid flowchart text with escaped caller labels.
601    Mermaid,
602}
603
604/// Passive graph metadata in authored node and transition order.
605///
606/// # Example
607///
608/// ```
609/// use app_kit::workflow::WorkflowDefinition;
610///
611/// let definition = WorkflowDefinition::parse(r#"{
612///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
613///     "state":{"count":0}, "required":["count"],
614///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
615///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
616///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
617///     ]}}], "routes":{}
618/// }"#)?;
619/// let graph = definition.graph();
620/// assert_eq!(graph.entry, "increment");
621/// assert_eq!(graph.nodes.len(), 1);
622/// # Ok::<(), app_kit::AppError>(())
623/// ```
624#[derive(Serialize)]
625pub struct GraphDescription {
626    /// Logical workflow identity.
627    pub id: WorkflowId,
628    /// Caller-selected definition revision.
629    pub revision: String,
630    /// First node's logical identity.
631    pub entry: String,
632    /// Authored node metadata, without executable bindings.
633    pub nodes: Vec<GraphNode>,
634    /// Ordered outgoing transitions, grouped by authored source-node order.
635    pub edges: Vec<GraphEdge>,
636}
637
638/// One passive node declaration in a graph projection.
639///
640/// # Example
641///
642/// ```
643/// use app_kit::workflow::WorkflowDefinition;
644///
645/// let definition = WorkflowDefinition::parse(r#"{
646///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
647///     "state":{"count":0}, "required":["count"],
648///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
649///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
650///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
651///     ]}}], "routes":{}
652/// }"#)?;
653/// let graph = definition.graph();
654/// let node = &graph.nodes[0];
655/// assert_eq!(node.kind, "state");
656/// assert_eq!(node.writes, ["count"]);
657/// # Ok::<(), app_kit::AppError>(())
658/// ```
659#[derive(Serialize)]
660pub struct GraphNode {
661    /// Graph-local node identity.
662    pub id: String,
663    /// Optional caller-facing label.
664    pub name: Option<String>,
665    /// Optional authored description.
666    pub description: Option<String>,
667    /// Authored selector tags, in declaration order.
668    pub tags: Vec<String>,
669    /// Closed workflow step kind rendered as text.
670    pub kind: String,
671    /// Top-level state keys read by this node.
672    pub reads: Vec<String>,
673    /// Top-level state keys written by this node.
674    pub writes: Vec<String>,
675    /// Required transient input names.
676    pub required_inputs: Vec<String>,
677    /// Optional transient input names.
678    pub optional_inputs: Vec<String>,
679    /// Actual caller source or canonical authored built-in definition.
680    pub source: Option<String>,
681}
682
683/// One ordered transition; a missing destination denotes path exhaustion.
684///
685/// # Example
686///
687/// ```
688/// use app_kit::workflow::GraphEdge;
689/// let end = GraphEdge { from: "increment".into(), to: None, condition: None };
690/// assert!(end.to.is_none());
691/// # Ok::<(), app_kit::AppError>(())
692/// ```
693#[derive(Serialize)]
694pub struct GraphEdge {
695    /// Source node identity.
696    pub from: String,
697    /// Destination node identity, or explicit end.
698    pub to: Option<String>,
699    /// Display expression, or the default true condition when absent.
700    pub condition: Option<String>,
701}
702
703/// An owned graph piece and its graph-local node-name prefix.
704///
705/// See [`WorkflowDefinition::compose`] for a complete composition example.
706pub struct GraphImport {
707    /// Validated definition to import.
708    pub definition: WorkflowDefinition,
709    /// Prefix applied to nodes and their graph-local references.
710    pub prefix: String,
711}
712
713/// A default transition appended to an assembled graph.
714///
715/// See [`WorkflowDefinition::compose`] for a complete composition example.
716pub struct GraphConnection {
717    /// Already-prefixed source node identity.
718    pub from: String,
719    /// Already-prefixed destination node identity.
720    pub to: String,
721}
722
723/// Definitions and explicit connections for one ordinary compiler invocation.
724///
725/// See [`WorkflowDefinition::compose`] for a complete composition example.
726pub struct GraphComposition {
727    /// Identity of the assembled workflow.
728    pub id: WorkflowId,
729    /// Revision of the assembled workflow.
730    pub revision: String,
731    /// Already-prefixed entrypoint.
732    pub entry: String,
733    /// Graph pieces, preserving their declaration order.
734    pub imports: Vec<GraphImport>,
735    /// Ordered default transitions appended after imported transitions.
736    pub connections: Vec<GraphConnection>,
737}
738
739/// A validated caller-authored workflow identity.
740///
741/// Parsing and deserialization accept only ASCII `[a-z0-9][a-z0-9_-]{0,127}`.
742///
743/// # Example
744///
745/// ```
746/// use app_kit::workflow::WorkflowId;
747/// let id: WorkflowId = "counter".parse()?;
748/// assert_eq!(id.to_string(), "counter");
749/// assert!("invalid identity".parse::<WorkflowId>().is_err());
750/// # Ok::<(), app_kit::AppError>(())
751/// ```
752#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
753pub struct WorkflowId(String);
754
755/// A validated root UUID or stable child SHA-256 identity.
756///
757/// Root identities use `run_` followed by a canonical lowercase UUIDv4. Child
758/// identities also accept exactly 64 lowercase hexadecimal digits. One logical
759/// identity may have multiple independent occurrences; use [`RunSelector::Exact`]
760/// when the occurrence matters.
761///
762/// # Example
763///
764/// ```
765/// use app_kit::workflow::RunId;
766/// let id: RunId = "run_8a7c251e-b5b8-4d9e-8f0b-123456789abc".parse()?;
767/// assert_eq!(id.to_string(), "run_8a7c251e-b5b8-4d9e-8f0b-123456789abc");
768/// assert!("invalid identity".parse::<RunId>().is_err());
769/// # Ok::<(), app_kit::AppError>(())
770/// ```
771#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
772pub struct RunId(String);
773
774/// Selects an exact occurrence or the latest native observation of a logical run.
775#[derive(Clone, Debug, Eq, PartialEq)]
776pub enum RunSelector {
777    /// Last natively saved observation in this workflow and workspace scope.
778    Latest(RunId),
779    /// One admitted occurrence; an absent occurrence never falls back to Latest.
780    Exact {
781        /// Stable logical identity.
782        id: RunId,
783        /// Validated at method ingress as 32 lowercase hexadecimal digits.
784        occurrence: String,
785    },
786}
787
788impl RunSelector {
789    pub(crate) fn validate(&self) -> Result<(), AppError> {
790        match self {
791            Self::Latest(_) => Ok(()),
792            Self::Exact { occurrence, .. } => RunId::validate_occurrence(occurrence),
793        }
794    }
795
796    pub(crate) fn id(&self) -> &RunId {
797        match self {
798            Self::Latest(id) | Self::Exact { id, .. } => id,
799        }
800    }
801}
802
803/// Initial state, transient inputs and boundaries selected for one execution.
804///
805/// `state` patches a new run only through [`Workflow::start`]; an already admitted
806/// run requires an empty state in its execution options. Inputs are transient and
807/// do not become state automatically. Selectors are node ids or `@tag` names and
808/// must match. `max_steps` counts committed action boundaries, not partial results
809/// or control checkpoints; private finite safety bounds always remain.
810///
811/// # Example
812///
813/// ```
814/// use app_kit::workflow::{ExecutionOptions, WorkflowState};
815/// let options = ExecutionOptions {
816///     state: WorkflowState::new(serde_json::from_str(r#"{"count":4}"#)?)?,
817///     halt_after: vec!["increment".into()],
818///     ..ExecutionOptions::default()
819/// };
820/// assert_eq!(options.state.values()["count"], 4);
821/// # Ok::<(), Box<dyn std::error::Error>>(())
822/// ```
823#[derive(Clone, Default)]
824pub struct ExecutionOptions {
825    /// Initial patch for a new run; must be empty on an existing run.
826    pub state: WorkflowState,
827    /// Transient inputs reused by successive actions.
828    pub inputs: Map<String, Value>,
829    /// Node or tag selectors to stop before invocation.
830    pub halt_before: Vec<String>,
831    /// Node or tag selectors to stop after a committed boundary.
832    pub halt_after: Vec<String>,
833    /// Optional maximum number of committed action boundaries.
834    pub max_steps: Option<NonZeroUsize>,
835}
836
837impl ExecutionOptions {
838    pub(crate) fn resolve_selectors(&mut self, graph: &GraphDescription) -> Result<(), AppError> {
839        for selectors in [&mut self.halt_before, &mut self.halt_after] {
840            let mut resolved = Vec::new();
841            for selector in selectors.iter() {
842                let mut matches = graph
843                    .nodes
844                    .iter()
845                    .filter(|node| {
846                        selector
847                            .strip_prefix('@')
848                            .map_or(node.id == *selector, |tag| {
849                                node.tags.iter().any(|candidate| candidate == tag)
850                            })
851                    })
852                    .peekable();
853                if matches.peek().is_none() {
854                    return Err(AppError::InvalidInput(
855                        "execution selector has no matching node".into(),
856                    ));
857                }
858                for node in matches {
859                    if !resolved.contains(&node.id) {
860                        resolved.push(node.id.clone());
861                    }
862                }
863            }
864            *selectors = resolved;
865        }
866        Ok(())
867    }
868}
869
870/// One transient result or committed execution boundary.
871#[non_exhaustive]
872pub enum ExecutionEvent {
873    /// A source result which has not changed workflow state.
874    Partial {
875        /// Node whose source emitted this item.
876        node: String,
877        /// Transient output independent of workflow state.
878        result: Map<String, Value>,
879    },
880    /// A durable action boundary with its complete ordered outcomes.
881    Committed {
882        /// Exact durable boundary sequence.
883        checkpoint: u64,
884        /// Complete results in canonical node order.
885        outcomes: Vec<ActionOutcome>,
886    },
887    /// A selected boundary parked for a later execution.
888    Halted {
889        /// Selected next node or last committed node.
890        node: String,
891        /// Present only for a committed after-boundary, including an empty map.
892        result: Option<Map<String, Value>>,
893    },
894}
895
896/// The current caller-visible run truth.
897///
898/// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
899#[non_exhaustive]
900pub struct RunSnapshot {
901    /// Identifies the run.
902    pub id: RunId,
903    /// Exact admitted execution occurrence of this logical run.
904    pub occurrence: String,
905    /// Fences explicit controls.
906    pub checkpoint: u64,
907    /// Projects lifecycle; actionable targets remain in `interactions`.
908    pub status: RunStatus,
909    /// Contains the current committed workflow state.
910    pub state: WorkflowState,
911    /// Canonical currently actionable controls.
912    pub interactions: Vec<Interaction>,
913    /// Last successfully committed action ordinal; controls do not advance it.
914    pub last_action_ordinal: Option<u64>,
915    /// Results from the latest committed wave, retained through later controls.
916    pub outcomes: Vec<ActionOutcome>,
917}
918
919/// The current stable run status.
920///
921/// Inspect [`crate::workflow::RunSnapshot::interactions`] alongside this lifecycle projection.
922/// Halted retains the selected before/after boundary and exact optional result;
923/// Paused denotes an ordinary parked control or prepared run. Neither status
924/// alone means that every external effect is safe to retry.
925#[non_exhaustive]
926pub enum RunStatus {
927    /// Execution can still advance without caller input.
928    Running,
929    /// A committed boundary is parked for explicit execution.
930    Paused,
931    /// An explicitly selected node boundary is retained.
932    Halted {
933        /// Selected next node or last committed node.
934        node: String,
935        /// Present only for a committed after-boundary, including an empty map.
936        result: Option<Map<String, Value>>,
937    },
938    /// Explicit cancellation has committed and owned cleanup is still running.
939    Cancelling,
940    /// The terminal frontier succeeded.
941    Succeeded,
942    /// The run terminated with a durable failure.
943    Failed(WorkflowFailure),
944    /// Explicit cancellation converged.
945    Cancelled,
946}
947
948/// A caller-actionable workflow interaction.
949#[derive(Clone)]
950#[non_exhaustive]
951pub enum Interaction {
952    /// Requests one schema-checked caller value.
953    Input {
954        /// Identifies this run-scoped control.
955        sequence: u64,
956        /// Fences this input reservation independently of sibling checkpoints.
957        generation: u64,
958        /// Describes the requested value.
959        message: String,
960        /// Optionally constrains the response.
961        schema: Option<Value>,
962    },
963    /// Exposes one exact lower-agent notification.
964    Notification {
965        /// Identifies this run-scoped control.
966        sequence: u64,
967        /// Fences this exact notification target independently of sibling checkpoints.
968        generation: u64,
969        /// Carries the exact portable lower notification.
970        notification: Arc<ConversationNotification>,
971    },
972    /// Retains an external effect whose outcome has not been established.
973    Unknown {
974        /// Identifies this run-scoped interaction target.
975        sequence: u64,
976        /// Fences this target independently of sibling checkpoints.
977        generation: u64,
978        /// Reports whether authoritative evidence permits another attempt.
979        retry_safe: bool,
980    },
981    /// Requests transient inputs for an action that has not started.
982    Inputs {
983        /// Identifies this run-scoped interaction target.
984        sequence: u64,
985        /// Fences this binding independently of sibling checkpoints.
986        generation: u64,
987        /// Names the exact missing input keys in canonical order.
988        required: Vec<String>,
989    },
990}
991
992/// Settles one current interaction or unknown effect.
993///
994/// Use the interaction sequence and its target generation from the same observed
995/// item. Sibling checkpoints do not invalidate that reservation. Unknown effects
996/// are never automatically retried; Retry requires authoritative retry-safe
997/// evidence, and Accept is an explicit caller settlement.
998pub enum Resolution {
999    /// Supplies one caller-input value.
1000    Input(Value),
1001    /// Binds exact missing inputs in the retained execution, without persisting values.
1002    Inputs(Map<String, Value>),
1003    /// Resolves one exact lower notification.
1004    Notification(NotificationResolution),
1005    /// Accepts an unknown effect and stages ordinary state contribution.
1006    Accept(Map<String, Value>),
1007    /// Fails an unknown effect without claiming its external outcome.
1008    Fail,
1009    /// Retries only after authoritative evidence proves retry safe.
1010    Retry,
1011}
1012
1013/// A stable terminal workflow-failure category.
1014#[non_exhaustive]
1015pub enum WorkflowFailure {
1016    /// Expression or routing evaluation failed.
1017    Evaluation(String),
1018    /// State validation or deterministic reduction failed.
1019    State(String),
1020    /// A lower agent operation failed safely.
1021    Agent(String),
1022    /// A caller-owned action source returned an error.
1023    Action(String),
1024    /// An opt-in lifecycle hook failed, retaining any prior execution failure.
1025    Hook {
1026        /// The caller-supplied hook error context.
1027        message: String,
1028        /// The execution failure observed before the hook failed.
1029        cause: Option<Box<WorkflowFailure>>,
1030    },
1031    /// A child or map relationship failed, or a child executor returned an error.
1032    ///
1033    /// An error returned by [`ChildExecutor::execute`]'s future has this category
1034    /// regardless of its [`AppError`] variant. Partial child results do not reduce
1035    /// the parent, and descendant cleanup retains ownership before publication.
1036    Child(String),
1037    /// Execution could not continue, including a contained child-executor panic.
1038    ///
1039    /// A panic while constructing or polling [`ChildExecutor::execute`]'s future
1040    /// has this category. It does not reduce the parent from partial child results;
1041    /// descendant cleanup retains ownership before terminal publication.
1042    Execution(String),
1043    /// A durable deadline expired.
1044    Timeout(String),
1045    /// A fixed private safety bound was exhausted.
1046    Limit(String),
1047}
1048
1049impl WorkflowDefinition {
1050    /// Parses and validates one strict versioned workflow document.
1051    ///
1052    /// # Errors
1053    ///
1054    /// Returns [`AppError::InvalidInput`] for malformed JSON, unsupported schema,
1055    /// unknown or duplicate members, invalid identities or expressions, inconsistent
1056    /// state/reducers, graph errors, or exceeded source/collection/depth limits.
1057    ///
1058    /// # Example
1059    ///
1060    /// ```
1061    /// use app_kit::workflow::WorkflowDefinition;
1062    ///
1063    /// let definition = WorkflowDefinition::parse(r#"{
1064    ///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1065    ///     "state":{"count":0}, "required":["count"],
1066    ///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1067    ///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1068    ///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1069    ///     ]}}], "routes":{}
1070    /// }"#)?;
1071    /// assert_eq!(definition.graph().id.to_string(), "counter");
1072    /// assert!(WorkflowDefinition::parse(r#"{"schema":"app-kit.workflow/v1"}"#).is_err());
1073    /// # Ok::<(), app_kit::AppError>(())
1074    /// ```
1075    pub fn parse(source: &str) -> Result<Self, AppError> {
1076        Ok(Self {
1077            compiled: definition::CompiledDefinition::parse(source)?,
1078        })
1079    }
1080
1081    pub(crate) fn into_compiled(self) -> definition::CompiledDefinition {
1082        self.compiled
1083    }
1084}
1085
1086impl<
1087    CS: StateStoreBackend,
1088    A: AgentBackend,
1089    X: ActionBackend,
1090    C: ConditionBackend,
1091    T: StateTyping,
1092    H: LifecycleHooks,
1093    E: ChildExecutor,
1094> Workflow<CS, A, X, C, T, H, E>
1095{
1096    pub(crate) fn from_runtime(runtime: run::WorkflowRuntime<CS, A, X, C, T, H, E>) -> Self {
1097        Self { runtime }
1098    }
1099
1100    /// Returns this workflow's stable identity.
1101    ///
1102    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1103    pub fn id(&self) -> &WorkflowId {
1104        self.runtime.id()
1105    }
1106
1107    /// Atomically admits one run at checkpoint zero.
1108    ///
1109    /// Schedules background advancement after admission. Dropping the returned run
1110    /// observer does not stop it. [`WorkflowRun::wait`] returns at an actionable or
1111    /// terminal boundary, so inspect status and interactions before assuming success.
1112    ///
1113    /// # Errors
1114    ///
1115    /// Returns `InvalidInput` for invalid initial state/options or missing bindings,
1116    /// `Busy` for exhausted admission/capacity, `Closed` for sealed ownership, or
1117    /// `Conflict` for stale workspace/revision evidence. Lower/storage failures use
1118    /// `Integration`; accepted execution failures appear in run status.
1119    ///
1120    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1121    pub async fn start(
1122        &self,
1123        options: ExecutionOptions,
1124    ) -> Result<WorkflowRun<CS, A, X, C, T, H, E>, AppError> {
1125        Ok(WorkflowRun {
1126            runtime: self.runtime.start(options).await?,
1127        })
1128    }
1129
1130    /// Admits checkpoint zero without executing the entry node.
1131    ///
1132    /// The initial patch is validated and atomically stored using the same
1133    /// workspace and App admission fences as background execution. Observers
1134    /// can read the parked run, and cancellation uses its checkpoint fence.
1135    ///
1136    /// # Errors
1137    ///
1138    /// Returns `InvalidInput` for invalid initial state or missing bindings,
1139    /// `Busy` for exhausted admission/capacity, `Closed` for sealed ownership, or
1140    /// `Conflict` for stale workspace/revision evidence or exhausted bounded
1141    /// transaction-contention retries. Concurrent preparations are not guaranteed
1142    /// to all succeed. Lower/storage failures use `Integration`; accepted
1143    /// execution failures appear in run status.
1144    ///
1145    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1146    pub async fn prepare(
1147        &self,
1148        state: WorkflowState,
1149    ) -> Result<WorkflowRun<CS, A, X, C, T, H, E>, AppError> {
1150        Ok(WorkflowRun {
1151            runtime: self.runtime.prepare(state.values).await?,
1152        })
1153    }
1154
1155    /// Copies a clean retained boundary into a fresh, paused run.
1156    ///
1157    /// Source selection and new admission commit atomically. The source remains
1158    /// unchanged; actions run only after explicit execution of the returned run.
1159    ///
1160    /// # Errors
1161    ///
1162    /// Returns `NotFound` for absent source/checkpoint without fallback, `InvalidInput`
1163    /// for invalid overrides or selectors, `Busy` for exhausted capacity, and
1164    /// `Conflict` when the selected boundary is unsafe to copy. Sealing/revocation
1165    /// returns `Closed`; inconsistent history or storage failures return `Integration`.
1166    ///
1167    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1168    pub async fn restore(
1169        &self,
1170        options: RestoreOptions,
1171    ) -> Result<WorkflowRun<CS, A, X, C, T, H, E>, AppError> {
1172        Ok(WorkflowRun {
1173            runtime: self.runtime.restore(options).await?,
1174        })
1175    }
1176
1177    /// Reopens one run by typed identity.
1178    ///
1179    /// # Errors
1180    ///
1181    /// Returns `InvalidInput` for a malformed exact occurrence, `NotFound` when the
1182    /// selected run/occurrence is absent, or `Closed` for sealed scope. Exact selection
1183    /// never falls back to Latest; inconsistent retained evidence is `Integration`.
1184    ///
1185    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1186    pub async fn open_run(
1187        &self,
1188        selector: RunSelector,
1189    ) -> Result<WorkflowRun<CS, A, X, C, T, H, E>, AppError> {
1190        Ok(WorkflowRun {
1191            runtime: self.runtime.open_run(selector).await?,
1192        })
1193    }
1194
1195    /// Lists runs by logical identity, then exact occurrence token.
1196    ///
1197    /// An Exact cursor continues after that occurrence; a Latest cursor skips
1198    /// every occurrence of its logical identity. This inventory order does not
1199    /// represent the native save order used when opening Latest.
1200    ///
1201    /// # Errors
1202    ///
1203    /// Returns `InvalidInput` for a limit above 1,000 or malformed cursor, `NotFound`
1204    /// for an unavailable cursor, and `Closed` or `Integration` for revoked or
1205    /// unreadable scope.
1206    ///
1207    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1208    pub async fn list_runs(
1209        &self,
1210        after: Option<RunSelector>,
1211        limit: NonZeroUsize,
1212    ) -> Result<Vec<RunSnapshot>, AppError> {
1213        self.runtime.list_runs(after, limit).await
1214    }
1215}
1216
1217impl<
1218    CS: StateStoreBackend,
1219    A: AgentBackend,
1220    X: ActionBackend,
1221    C: ConditionBackend,
1222    T: StateTyping,
1223    H: LifecycleHooks,
1224    E: ChildExecutor,
1225> WorkflowRun<CS, A, X, C, T, H, E>
1226{
1227    /// Returns this run's stable identity.
1228    ///
1229    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1230    pub fn id(&self) -> &RunId {
1231        self.runtime.id()
1232    }
1233
1234    /// Acquires exclusive caller-local advancement without constructing a source.
1235    ///
1236    /// A live execution keeps that borrow until its last use:
1237    ///
1238    /// ```compile_fail
1239    /// use agents_kit::AgentBackend;
1240    /// use app_kit::{AppError, workflow::{ExecutionOptions, RunSnapshot, WorkflowRun}};
1241    /// use storage_kit::StateStoreBackend;
1242    /// async fn overlapping<CS: StateStoreBackend, A: AgentBackend>(
1243    /// run: &mut WorkflowRun<CS, A>,
1244    /// ) -> Result<RunSnapshot, AppError> {
1245    /// let execution = run.execution(ExecutionOptions::default()).await?;
1246    /// run.snapshot().await?;
1247    /// execution.finish().await
1248    /// }
1249    /// ```
1250    ///
1251    /// # Errors
1252    ///
1253    /// Returns `InvalidInput` for a nonempty options state or invalid inputs/selectors,
1254    /// `Busy` if advancement or admission is already owned, `Conflict` for stale
1255    /// authority, and `Closed` after revocation. Storage failures use `Integration`.
1256    ///
1257    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1258    pub async fn execution(
1259        &mut self,
1260        options: ExecutionOptions,
1261    ) -> Result<Execution<'_, CS, A, X, C, T, H, E>, AppError> {
1262        Execution::open(self, options, ActionMode::Asynchronous).await
1263    }
1264
1265    /// Loads the current run snapshot.
1266    ///
1267    /// # Errors
1268    ///
1269    /// Returns `Closed` for revoked App ownership or sealed/deleted scope, `NotFound`
1270    /// for absent retained evidence, or `Integration` for inconsistent or unreadable
1271    /// records. A newer App generation can invalidate this handle.
1272    ///
1273    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1274    pub async fn snapshot(&self) -> Result<RunSnapshot, AppError> {
1275        self.runtime.snapshot().await
1276    }
1277
1278    /// Reads one immutable checkpoint without advancing the workflow.
1279    ///
1280    /// An action selector returns the latest successful or failed observation
1281    /// of that ordinal. Missing or explicitly deleted history returns NotFound.
1282    ///
1283    /// # Errors
1284    ///
1285    /// Returns `Closed` for revoked App ownership or sealed/deleted scope, `NotFound`
1286    /// for absent retained evidence, or `Integration` for inconsistent or unreadable
1287    /// records. A newer App generation can invalidate this handle.
1288    /// An explicit missing sequence/action is `NotFound`; action ordinals are distinct
1289    /// from checkpoint sequences and select the latest matching saved observation.
1290    ///
1291    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1292    pub async fn checkpoint(&self, selector: CheckpointSelector) -> Result<Checkpoint, AppError> {
1293        self.runtime.checkpoint(selector).await
1294    }
1295
1296    /// Reads a bounded page from a fixed retained-history cutoff.
1297    ///
1298    /// Limits are 1 through 1,000. A returned cursor preserves its cutoff and
1299    /// is valid only for this run creation and App generation. Reading a page
1300    /// never invokes actions, state typing, codecs or lower-agent recovery.
1301    ///
1302    /// # Errors
1303    ///
1304    /// Returns `Closed` for revoked App ownership or sealed/deleted scope, `NotFound`
1305    /// for absent retained evidence, or `Integration` for inconsistent or unreadable
1306    /// records. A newer App generation can invalidate this handle.
1307    /// An excessive page limit or a cursor from another run/occurrence/generation is
1308    /// `InvalidInput`. A captured cutoff never includes later appended checkpoints.
1309    ///
1310    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1311    pub async fn history(
1312        &self,
1313        after: Option<HistoryCursor>,
1314        limit: NonZeroUsize,
1315    ) -> Result<HistoryPage, AppError> {
1316        self.runtime.history(after, limit).await
1317    }
1318
1319    /// Deletes terminal history through bounded, resumable pages.
1320    ///
1321    /// The expected checkpoint fences the terminal boundary. Dropping this
1322    /// operation's observer preserves admitted deletion and native commits.
1323    ///
1324    /// # Errors
1325    ///
1326    /// Returns `Conflict` for a stale checkpoint, `Busy` for nonterminal/unknown work
1327    /// or a child still referenced by an active parent, and `Closed` after sealing or
1328    /// revocation. Storage failures use `Integration`. A successful seal survives
1329    /// observer loss and deletion resumes in bounded pages after reopen.
1330    ///
1331    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1332    pub async fn delete(&mut self, expected_checkpoint: u64) -> Result<(), AppError> {
1333        self.runtime.delete(expected_checkpoint).await
1334    }
1335
1336    /// Waits for the next actionable or terminal snapshot.
1337    ///
1338    /// This observes accepted work; it does not acquire execution ownership or resume
1339    /// a prepared run. A caller interaction or unknown effect can return before a
1340    /// terminal result. Dropping the observation future never requests cancellation.
1341    ///
1342    /// # Errors
1343    ///
1344    /// Returns `Closed` for revoked App ownership or sealed/deleted scope, `NotFound`
1345    /// for absent retained evidence, or `Integration` for inconsistent or unreadable
1346    /// records. A newer App generation can invalidate this handle.
1347    ///
1348    /// See [execution and retained history](crate::workflow#execute-observe-restore-and-delete).
1349    pub async fn wait(&self) -> Result<RunSnapshot, AppError> {
1350        self.runtime.wait().await
1351    }
1352
1353    /// Commits explicit checkpoint-fenced cancellation.
1354    ///
1355    /// # Errors
1356    ///
1357    /// Returns `Conflict` for a stale checkpoint, `Busy` for unavailable admission,
1358    /// `Closed` after revocation, or `Integration` for unreadable/failed storage.
1359    /// Cancellation admission is durable; observer loss does not reverse it.
1360    ///
1361    /// # Example
1362    ///
1363    /// ```
1364    /// # use agents_kit::Agent;
1365    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1366    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1367    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1368    /// # use storage_kit::StateStore;
1369    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1370    /// #
1371    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1372    /// # runtime.block_on(async {
1373    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1374    /// # let definition = WorkflowDefinition::parse(r#"{
1375    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1376    /// #     "state":{"count":0}, "required":["count"],
1377    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1378    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1379    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1380    /// #     ]}}], "routes":{}
1381    /// # }"#)?;
1382    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1383    /// # let mut app = App::open(config).await?;
1384    /// # let outcome = async {
1385    /// #     let mut workspace = app.create_workspace(WorkspaceConfig {
1386    /// #         name: Some("local counter".into()), root: std::env::current_dir()?,
1387    /// #     }).await?;
1388    /// #     let workflow = workspace.workflow("counter".parse()?).await?;
1389    /// #     let mut run = workflow.prepare(WorkflowState::default()).await?;
1390    ///     let before = run.snapshot().await?;
1391    ///     run.cancel(before.checkpoint).await?;
1392    ///     let cancelled = run.wait().await?;
1393    ///     assert!(matches!(cancelled.status, RunStatus::Cancelled));
1394    ///     #
1395    /// #     Ok::<(), Box<dyn std::error::Error>>(())
1396    /// # }.await;
1397    /// # let cleanup = app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await;
1398    /// # if let Err(error) = &cleanup { eprintln!("App shutdown: {error}"); }
1399    /// # outcome?;
1400    /// # cleanup?;
1401    /// # Ok::<(), Box<dyn std::error::Error>>(())
1402    /// # })?;
1403    /// # Ok::<(), Box<dyn std::error::Error>>(())
1404    /// ```
1405    pub async fn cancel(&self, expected_checkpoint: u64) -> Result<RunSnapshot, AppError> {
1406        self.runtime.cancel(expected_checkpoint).await
1407    }
1408
1409    /// Settles one exact interaction using its original target generation.
1410    ///
1411    /// Sibling checkpoints do not invalidate that generation. An identical
1412    /// decision replays its retained receipt; a different decision conflicts.
1413    /// Transient inputs replay only while their execution retains the values.
1414    ///
1415    /// # Errors
1416    ///
1417    /// Returns `InvalidInput` for a mismatched resolution or invalid schema/input,
1418    /// `Conflict` for a stale target generation, a different repeated decision, or an
1419    /// unsafe retry. Capacity, revocation, lower/storage failure and observation
1420    /// deadlines report `Busy`, `Closed`, `Integration`, or `Timeout`.
1421    ///
1422    /// # Example
1423    ///
1424    /// ```
1425    /// # use agents_kit::Agent;
1426    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1427    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1428    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1429    /// # use storage_kit::StateStore;
1430    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1431    /// #
1432    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1433    /// # runtime.block_on(async {
1434    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1435    /// # let definition = WorkflowDefinition::parse(r#"{
1436    /// #  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
1437    /// #  "state":{"answer":""},"required":["answer"],"reducers":{"answer":"reject_concurrent"},
1438    /// #  "entry":"ask","nodes":[{"id":"ask","step":{"kind":"input",
1439    /// #  "message":[{"kind":"text","text":"Review topic?"}],"target":"answer"}}],"routes":{}
1440    /// # }"#)?;
1441    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1442    /// # let mut app = App::open(config).await?;
1443    /// # let outcome = async {
1444    /// #     let mut workspace = app.create_workspace(WorkspaceConfig {
1445    /// #         name: Some("local counter".into()), root: std::env::current_dir()?,
1446    /// #     }).await?;
1447    /// #     let workflow = workspace.workflow("counter".parse()?).await?;
1448    ///     use app_kit::workflow::{Interaction, Resolution};
1449    ///     let run = workflow.start(ExecutionOptions::default()).await?;
1450    ///     let waiting = run.wait().await?;
1451    ///     let Interaction::Input { sequence, generation, .. } = waiting.interactions[0] else {
1452    ///         panic!("the input workflow must request a value");
1453    ///     };
1454    ///     run.resolve(sequence, generation, Resolution::Input(serde_json::json!("ownership"))).await?;
1455    ///     assert_eq!(run.wait().await?.state.values()["answer"], "ownership");
1456    ///     #
1457    /// #     Ok::<(), Box<dyn std::error::Error>>(())
1458    /// # }.await;
1459    /// # let cleanup = app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await;
1460    /// # if let Err(error) = &cleanup { eprintln!("App shutdown: {error}"); }
1461    /// # outcome?;
1462    /// # cleanup?;
1463    /// # Ok::<(), Box<dyn std::error::Error>>(())
1464    /// # })?;
1465    /// # Ok::<(), Box<dyn std::error::Error>>(())
1466    /// ```
1467    pub async fn resolve(
1468        &self,
1469        sequence: u64,
1470        generation: u64,
1471        resolution: Resolution,
1472    ) -> Result<RunSnapshot, AppError> {
1473        self.runtime.resolve(sequence, generation, resolution).await
1474    }
1475}
1476
1477impl WorkflowId {
1478    pub(crate) fn as_str(&self) -> &str {
1479        &self.0
1480    }
1481}
1482
1483impl RunId {
1484    pub(crate) fn validate_occurrence(occurrence: &str) -> Result<(), AppError> {
1485        if occurrence.len() == 32
1486            && occurrence
1487                .bytes()
1488                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1489        {
1490            Ok(())
1491        } else {
1492            Err(AppError::InvalidInput(
1493                "run occurrence must contain 32 lowercase hexadecimal digits".into(),
1494            ))
1495        }
1496    }
1497
1498    pub(crate) fn generate() -> Self {
1499        Self(format!("run_{}", Uuid::new_v4().hyphenated()))
1500    }
1501
1502    fn child(&self, task_key: &str) -> Self {
1503        let mut digest = Sha256::new();
1504        digest.update(self.as_str().as_bytes());
1505        digest.update(b":");
1506        digest.update(task_key.as_bytes());
1507        Self(format!("{:x}", digest.finalize()))
1508    }
1509
1510    pub(crate) fn as_str(&self) -> &str {
1511        &self.0
1512    }
1513}
1514
1515impl fmt::Display for WorkflowId {
1516    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1517        formatter.write_str(&self.0)
1518    }
1519}
1520
1521impl FromStr for WorkflowId {
1522    type Err = AppError;
1523
1524    fn from_str(value: &str) -> Result<Self, Self::Err> {
1525        IdentitySyntax::Named("WorkflowId").parse(value).map(Self)
1526    }
1527}
1528
1529impl Serialize for WorkflowId {
1530    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1531    where
1532        S: Serializer,
1533    {
1534        serializer.serialize_str(&self.0)
1535    }
1536}
1537
1538impl<'de> Deserialize<'de> for WorkflowId {
1539    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1540    where
1541        D: Deserializer<'de>,
1542    {
1543        String::deserialize(deserializer)?
1544            .parse()
1545            .map_err(serde::de::Error::custom)
1546    }
1547}
1548
1549impl fmt::Display for RunId {
1550    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1551        formatter.write_str(&self.0)
1552    }
1553}
1554
1555impl FromStr for RunId {
1556    type Err = AppError;
1557
1558    fn from_str(value: &str) -> Result<Self, Self::Err> {
1559        if value.len() == 64
1560            && value
1561                .bytes()
1562                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1563        {
1564            return Ok(Self(value.to_owned()));
1565        }
1566        IdentitySyntax::Uuid {
1567            name: "RunId",
1568            prefix: "run_",
1569        }
1570        .parse(value)
1571        .map(Self)
1572    }
1573}
1574
1575impl Serialize for RunId {
1576    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1577    where
1578        S: Serializer,
1579    {
1580        serializer.serialize_str(&self.0)
1581    }
1582}
1583
1584impl<'de> Deserialize<'de> for RunId {
1585    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1586    where
1587        D: Deserializer<'de>,
1588    {
1589        String::deserialize(deserializer)?
1590            .parse()
1591            .map_err(serde::de::Error::custom)
1592    }
1593}