Skip to main content

WorkflowState

Struct WorkflowState 

Source
pub struct WorkflowState { /* private fields */ }
Expand description

Immutable, ordered workflow values validated at each operation boundary.

use app_kit::workflow::{JsonState, StateChange, WorkflowState};
use serde_json::{Map, Value};
let before = WorkflowState::new(Map::from_iter([("count".into(), Value::from(1))]))?;
let after = before.apply(&StateChange::Increment {
    values: vec![("count".into(), Value::from(1))],
}, &JsonState::default())?;
assert_eq!(before.values()["count"], 1);
assert_eq!(after.values()["count"], 2);

Changes go through the validated state operations:

ⓘ
use app_kit::workflow::WorkflowState;
let mut state = WorkflowState::default();
state.values.clear();

Implementations§

Source§

impl WorkflowState

Source

pub fn serialize(&self) -> Result<Map<String, Value>, AppError>

Encodes ordered state using one snapshot of the global field registry.

Unregistered fields keep their JSON values, including tagged objects. Field encoders must produce objects. The complete encoded state retains the ordinary JSON byte/depth bounds; failure leaves this state unchanged.

use app_kit::workflow::WorkflowState;
use serde_json::{Map, Value};
let values = Map::from_iter([("count".into(), Value::from(2))]);
let state = WorkflowState::new(values.clone())?;
assert_eq!(state.serialize()?, values);
§Errors

Returns InvalidInput for a codec error, a field encoder returning a nonobject, or exceeded JSON bounds; Integration reports registry lock poisoning.

§Panics

Panics if a registered field encoder panics. The registry lock is released before callbacks run.

Examples found in repository?
examples/state.rs (line 97)
55fn main() -> Result<(), AppError> {
56    let policy = CountPolicy { maximum: 100 };
57    let initial = WorkflowState::new(Map::from_iter([
58        ("count".into(), json!(1)),
59        ("labels".into(), json!([])),
60    ]))?;
61    let counted = initial.apply(
62        &StateChange::Increment {
63            values: vec![("count".into(), json!(3)), ("count".into(), json!(4))],
64        },
65        &policy,
66    )?;
67    let labeled = counted.apply(
68        &StateChange::Append {
69            values: vec![
70                ("labels".into(), json!("draft")),
71                ("labels".into(), json!("queued")),
72            ],
73        },
74        &policy,
75    )?;
76    assert_eq!(initial.values()["count"], 1);
77    assert_eq!(labeled.values()["count"], 8);
78    assert_eq!(labeled.values()["labels"], json!(["draft", "queued"]));
79
80    // A later invalid entry rejects the complete operation, including count + 1.
81    let rejected = labeled.apply(
82        &StateChange::Increment {
83            values: vec![("count".into(), json!(1)), ("labels".into(), json!(1))],
84        },
85        &policy,
86    );
87    assert!(matches!(rejected, Err(AppError::InvalidInput(_))));
88    assert_eq!(labeled.values()["count"], 8);
89
90    // This registration is process-global. This standalone example owns its
91    // process; an application should choose registrations during its setup.
92    StateSerialization::global().register_field(
93        "labels".into(),
94        |value| Ok::<_, &'static str>(json!({"version": 1, "items": value})),
95        |value| value.get("items").cloned().ok_or("missing labels.items"),
96    )?;
97    let encoded = labeled.serialize()?;
98    assert_eq!(
99        encoded["labels"],
100        json!({"version":1,"items":["draft","queued"]})
101    );
102    let decoded = WorkflowState::deserialize(encoded.clone())?;
103    assert_eq!(decoded, labeled);
104
105    StateSerialization::global().register_tag("example-money".into(), |value| {
106        value.get("minor").cloned().ok_or("missing money.minor")
107    })?;
108    let tagged = WorkflowState::deserialize(Map::from_iter([(
109        "amount".into(),
110        json!({"__burr_serde__":"example-money","minor":120}),
111    )]))?;
112    assert_eq!(tagged.values()["amount"], 120);
113    println!("{}", Value::Object(encoded));
114    Ok(())
115}
Source

pub fn deserialize(values: Map<String, Value>) -> Result<Self, AppError>

Decodes state with one snapshot of global field and tag registrations.

Top-level field decoders take precedence over recursive tagged decoding. Missing and malformed tags fail; handler outputs are not redispatched. Returned state preserves key order and passes the JSON boundary checks.

use app_kit::workflow::WorkflowState;
let values = serde_json::from_str(r#"{"count":2,"ready":true}"#)?;
let state = WorkflowState::deserialize(values)?;
assert_eq!(state.values()["count"], 2);
assert_eq!(state.values()["ready"], true);
§Errors

Returns InvalidInput for malformed state, a nonstring or unregistered tag, a decoder error, or invalid transformed values. Registry lock poisoning is Integration; field/tag decoder outputs are not recursively redispatched.

§Panics

Panics if a registered field or tag decoder panics. The registry lock is released before callbacks run.

Examples found in repository?
examples/state.rs (line 102)
55fn main() -> Result<(), AppError> {
56    let policy = CountPolicy { maximum: 100 };
57    let initial = WorkflowState::new(Map::from_iter([
58        ("count".into(), json!(1)),
59        ("labels".into(), json!([])),
60    ]))?;
61    let counted = initial.apply(
62        &StateChange::Increment {
63            values: vec![("count".into(), json!(3)), ("count".into(), json!(4))],
64        },
65        &policy,
66    )?;
67    let labeled = counted.apply(
68        &StateChange::Append {
69            values: vec![
70                ("labels".into(), json!("draft")),
71                ("labels".into(), json!("queued")),
72            ],
73        },
74        &policy,
75    )?;
76    assert_eq!(initial.values()["count"], 1);
77    assert_eq!(labeled.values()["count"], 8);
78    assert_eq!(labeled.values()["labels"], json!(["draft", "queued"]));
79
80    // A later invalid entry rejects the complete operation, including count + 1.
81    let rejected = labeled.apply(
82        &StateChange::Increment {
83            values: vec![("count".into(), json!(1)), ("labels".into(), json!(1))],
84        },
85        &policy,
86    );
87    assert!(matches!(rejected, Err(AppError::InvalidInput(_))));
88    assert_eq!(labeled.values()["count"], 8);
89
90    // This registration is process-global. This standalone example owns its
91    // process; an application should choose registrations during its setup.
92    StateSerialization::global().register_field(
93        "labels".into(),
94        |value| Ok::<_, &'static str>(json!({"version": 1, "items": value})),
95        |value| value.get("items").cloned().ok_or("missing labels.items"),
96    )?;
97    let encoded = labeled.serialize()?;
98    assert_eq!(
99        encoded["labels"],
100        json!({"version":1,"items":["draft","queued"]})
101    );
102    let decoded = WorkflowState::deserialize(encoded.clone())?;
103    assert_eq!(decoded, labeled);
104
105    StateSerialization::global().register_tag("example-money".into(), |value| {
106        value.get("minor").cloned().ok_or("missing money.minor")
107    })?;
108    let tagged = WorkflowState::deserialize(Map::from_iter([(
109        "amount".into(),
110        json!({"__burr_serde__":"example-money","minor":120}),
111    )]))?;
112    assert_eq!(tagged.values()["amount"], 120);
113    println!("{}", Value::Object(encoded));
114    Ok(())
115}
Source§

impl WorkflowState

Source

pub fn new(values: Map<String, Value>) -> Result<Self, AppError>

Validates ordered JSON values and takes ownership of them.

use app_kit::workflow::WorkflowState;
let values = serde_json::from_str(r#"{"first":1,"second":2}"#)?;
let state = WorkflowState::new(values)?;
assert_eq!(state.values().keys().collect::<Vec<_>>(), ["first", "second"]);
§Errors

Returns InvalidInput for invalid state keys or exceeded JSON byte, nesting, collection, or text bounds. Ownership is accepted only after full validation.

Examples found in repository?
examples/state.rs (lines 38-41)
37    fn construct_state(&self, data: &Self::Data) -> Result<WorkflowState, AppError> {
38        WorkflowState::new(Map::from_iter([
39            ("count".into(), json!(data.count)),
40            ("labels".into(), json!(data.labels)),
41        ]))
42    }
43
44    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
45        let data = self.construct_data(state)?;
46        if !(0..=self.maximum).contains(&data.count) {
47            return Err(AppError::InvalidInput(
48                "count is outside its allowed range".into(),
49            ));
50        }
51        Ok(())
52    }
53}
54
55fn main() -> Result<(), AppError> {
56    let policy = CountPolicy { maximum: 100 };
57    let initial = WorkflowState::new(Map::from_iter([
58        ("count".into(), json!(1)),
59        ("labels".into(), json!([])),
60    ]))?;
61    let counted = initial.apply(
62        &StateChange::Increment {
63            values: vec![("count".into(), json!(3)), ("count".into(), json!(4))],
64        },
65        &policy,
66    )?;
67    let labeled = counted.apply(
68        &StateChange::Append {
69            values: vec![
70                ("labels".into(), json!("draft")),
71                ("labels".into(), json!("queued")),
72            ],
73        },
74        &policy,
75    )?;
76    assert_eq!(initial.values()["count"], 1);
77    assert_eq!(labeled.values()["count"], 8);
78    assert_eq!(labeled.values()["labels"], json!(["draft", "queued"]));
79
80    // A later invalid entry rejects the complete operation, including count + 1.
81    let rejected = labeled.apply(
82        &StateChange::Increment {
83            values: vec![("count".into(), json!(1)), ("labels".into(), json!(1))],
84        },
85        &policy,
86    );
87    assert!(matches!(rejected, Err(AppError::InvalidInput(_))));
88    assert_eq!(labeled.values()["count"], 8);
89
90    // This registration is process-global. This standalone example owns its
91    // process; an application should choose registrations during its setup.
92    StateSerialization::global().register_field(
93        "labels".into(),
94        |value| Ok::<_, &'static str>(json!({"version": 1, "items": value})),
95        |value| value.get("items").cloned().ok_or("missing labels.items"),
96    )?;
97    let encoded = labeled.serialize()?;
98    assert_eq!(
99        encoded["labels"],
100        json!({"version":1,"items":["draft","queued"]})
101    );
102    let decoded = WorkflowState::deserialize(encoded.clone())?;
103    assert_eq!(decoded, labeled);
104
105    StateSerialization::global().register_tag("example-money".into(), |value| {
106        value.get("minor").cloned().ok_or("missing money.minor")
107    })?;
108    let tagged = WorkflowState::deserialize(Map::from_iter([(
109        "amount".into(),
110        json!({"__burr_serde__":"example-money","minor":120}),
111    )]))?;
112    assert_eq!(tagged.values()["amount"], 120);
113    println!("{}", Value::Object(encoded));
114    Ok(())
115}
More examples
Hide additional examples
examples/history_and_recovery.rs (line 107)
63    async fn create_fork(
64        &self,
65        app: &App<StateStore, Agent>,
66    ) -> Result<(WorkspaceId, RunSelector, RunSelector), Box<dyn Error + Send + Sync>> {
67        let workspace = app
68            .create_workspace(WorkspaceConfig {
69                name: Some("history example".into()),
70                root: std::env::current_dir()?,
71            })
72            .await?;
73        let workflow = workspace.workflow("history-counter".parse()?).await?;
74        let mut source = workflow.prepare(WorkflowState::default()).await?;
75        let completed = source
76            .execution(ExecutionOptions::default())
77            .await?
78            .finish()
79            .await?;
80        if !matches!(completed.status, RunStatus::Succeeded)
81            || completed.state.values()["count"] != 2
82        {
83            return Err(
84                AppError::Integration("source did not complete both increments".into()).into(),
85            );
86        }
87
88        // An action ordinal selects the whole committed boundary containing it;
89        // it is independent of the checkpoint sequence and storage revision.
90        let first = source.checkpoint(CheckpointSelector::Action(0)).await?;
91        if !matches!(first.kind, CheckpointKind::Action)
92            || first.position.as_deref() != Some("first")
93            || first.state.values()["count"] != 1
94        {
95            return Err(
96                AppError::Integration("first action history is inconsistent".into()).into(),
97            );
98        }
99        let source_selector = RunSelector::Exact {
100            id: source.id().clone(),
101            occurrence: first.occurrence.clone(),
102        };
103        let mut fork = workflow
104            .restore(RestoreOptions {
105                source: source_selector.clone(),
106                checkpoint: CheckpointSelector::Action(0),
107                overrides: WorkflowState::new(Map::from_iter([("count".into(), json!(40))]))?,
108                position: ResumePosition::AfterCompleted,
109                fallback: None,
110            })
111            .await?;
112        let parked = fork.snapshot().await?;
113        if !matches!(parked.status, RunStatus::Paused)
114            || parked.checkpoint != 0
115            || parked.state.values()["count"] != 40
116            || !parked.outcomes.is_empty()
117        {
118            return Err(
119                AppError::Integration("restore did not create a parked clean copy".into()).into(),
120            );
121        }
122
123        source.delete(completed.checkpoint).await?;
124        if !matches!(source.snapshot().await, Err(AppError::NotFound(_))) {
125            return Err(
126                AppError::Integration("explicit source deletion did not finish".into()).into(),
127            );
128        }
129        // Deletion does not invalidate the copied state. AfterCompleted skips
130        // the first increment, so only the second changes the override: 40 -> 41.
131        let completed = fork
132            .execution(ExecutionOptions::default())
133            .await?
134            .finish()
135            .await?;
136        if !matches!(completed.status, RunStatus::Succeeded)
137            || completed.state.values()["count"] != 41
138            || completed.last_action_ordinal != Some(1)
139        {
140            return Err(
141                AppError::Integration("fork resumed from the wrong boundary".into()).into(),
142            );
143        }
144        println!("Source count: 2; copied count: 40; completed fork count: 41");
145        Ok((
146            workspace.id().clone(),
147            source_selector,
148            RunSelector::Exact {
149                id: fork.id().clone(),
150                occurrence: completed.occurrence,
151            },
152        ))
153    }
examples/performance.rs (lines 802-805)
780    async fn repeated(
781        &self,
782        app: &App<StateStore, Agent, WorkloadAction>,
783        probe: &WorkloadProbe,
784    ) -> Result<Value, Box<dyn Error + Send + Sync>> {
785        let workspace = app
786            .create_workspace(WorkspaceConfig {
787                name: None,
788                root: self.root.join("workspace"),
789            })
790            .await?;
791        let workflow = workspace.workflow("performance-leaf".parse()?).await?;
792        let started = Instant::now();
793        // Admit one run at a time, then directly poll at most 16 original
794        // executions together. No consumer task is spawned or detached.
795        let mut runs = std::pin::pin!(
796            stream::iter(0..100_u64)
797                .then(|value| {
798                    let workflow = &workflow;
799                    async move {
800                        let started = Instant::now();
801                        let run = workflow
802                            .prepare(WorkflowState::new(Map::from_iter([(
803                                "value".into(),
804                                json!(value),
805                            )]))?)
806                            .await?;
807                        Ok::<_, AppError>((value, run, started))
808                    }
809                })
810                .map(|prepared| async move {
811                    let (value, mut run, started) = prepared?;
812                    let snapshot = run
813                        .execution(ExecutionOptions::default())
814                        .await?
815                        .finish()
816                        .await?;
817                    assert!(matches!(snapshot.status, RunStatus::Succeeded));
818                    assert!(snapshot.interactions.is_empty());
819                    assert_eq!(snapshot.state.values()["value"], value);
820                    assert_eq!(snapshot.state.values()["result"], value + 1);
821                    assert_eq!(snapshot.last_action_ordinal, Some(0));
822                    assert_eq!(snapshot.outcomes.len(), 1);
823                    Ok::<_, AppError>((run.id().clone(), started.elapsed()))
824                })
825                .buffer_unordered(16)
826        );
827        tokio::select! {
828            _ = probe.wait_for(&probe.constructed, 16) => {}
829            result = runs.next() => panic!(
830                "run finished before the first 16 blocked sources: {:?}",
831                result.map(|result| result.map(|_| ())),
832            ),
833        }
834        assert_eq!(probe.active.load(Ordering::SeqCst), 16);
835        assert_eq!(probe.constructed.load(Ordering::SeqCst), 16);
836        probe.release_all();
837        let mut ids = BTreeSet::new();
838        let mut samples = Vec::with_capacity(100);
839        while let Some(result) = runs.next().await {
840            let (id, elapsed) = result?;
841            assert!(ids.insert(id));
842            samples.push(elapsed);
843        }
844        assert_eq!(ids.len(), 100);
845        assert_eq!(probe.constructed.load(Ordering::SeqCst), 100);
846        assert_eq!(probe.completed.load(Ordering::SeqCst), 100);
847        assert_eq!(probe.peak.load(Ordering::SeqCst), 16);
848        probe.assert_idle();
849        let rows = workflow
850            .list_runs(None, NonZeroUsize::new(128).unwrap())
851            .await?;
852        assert_eq!(rows.len(), 100);
853        assert_eq!(
854            rows.iter()
855                .map(|row| row.id.clone())
856                .collect::<BTreeSet<_>>(),
857            ids
858        );
859        assert!(
860            rows.iter()
861                .all(|row| matches!(row.status, RunStatus::Succeeded))
862        );
863        let elapsed = started.elapsed();
864        Ok(
865            json!({"dataset":"repeated100","runs":100,"caller_in_flight_limit":16,"admission":"sequential",
866            "production_active_limit":"unchanged; this measures only the caller's 16-journey envelope",
867            "execution_ns":elapsed.as_nanos(),"runs_per_second":100.0 / elapsed.as_secs_f64(),
868            "run_latency_including_admission":Self::samples(samples),"indefinite_stability_proof":false}),
869        )
870    }
Source

pub fn values(&self) -> &Map<String, Value>

Borrows values in their insertion order.

use app_kit::workflow::WorkflowState;
let state = WorkflowState::default();
assert!(state.values().get("optional_input").is_none());
Examples found in repository?
examples/state.rs (line 24)
23    fn construct_data(&self, state: &WorkflowState) -> Result<Self::Data, AppError> {
24        let count = state.values().get("count").and_then(Value::as_i64);
25        let labels = state.values().get("labels").and_then(Value::as_array);
26        match (count, labels) {
27            (Some(count), Some(labels)) => Ok(Rc::new(CountData {
28                count,
29                labels: labels.clone(),
30            })),
31            _ => Err(AppError::InvalidInput(
32                "count and labels have invalid types".into(),
33            )),
34        }
35    }
36
37    fn construct_state(&self, data: &Self::Data) -> Result<WorkflowState, AppError> {
38        WorkflowState::new(Map::from_iter([
39            ("count".into(), json!(data.count)),
40            ("labels".into(), json!(data.labels)),
41        ]))
42    }
43
44    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
45        let data = self.construct_data(state)?;
46        if !(0..=self.maximum).contains(&data.count) {
47            return Err(AppError::InvalidInput(
48                "count is outside its allowed range".into(),
49            ));
50        }
51        Ok(())
52    }
53}
54
55fn main() -> Result<(), AppError> {
56    let policy = CountPolicy { maximum: 100 };
57    let initial = WorkflowState::new(Map::from_iter([
58        ("count".into(), json!(1)),
59        ("labels".into(), json!([])),
60    ]))?;
61    let counted = initial.apply(
62        &StateChange::Increment {
63            values: vec![("count".into(), json!(3)), ("count".into(), json!(4))],
64        },
65        &policy,
66    )?;
67    let labeled = counted.apply(
68        &StateChange::Append {
69            values: vec![
70                ("labels".into(), json!("draft")),
71                ("labels".into(), json!("queued")),
72            ],
73        },
74        &policy,
75    )?;
76    assert_eq!(initial.values()["count"], 1);
77    assert_eq!(labeled.values()["count"], 8);
78    assert_eq!(labeled.values()["labels"], json!(["draft", "queued"]));
79
80    // A later invalid entry rejects the complete operation, including count + 1.
81    let rejected = labeled.apply(
82        &StateChange::Increment {
83            values: vec![("count".into(), json!(1)), ("labels".into(), json!(1))],
84        },
85        &policy,
86    );
87    assert!(matches!(rejected, Err(AppError::InvalidInput(_))));
88    assert_eq!(labeled.values()["count"], 8);
89
90    // This registration is process-global. This standalone example owns its
91    // process; an application should choose registrations during its setup.
92    StateSerialization::global().register_field(
93        "labels".into(),
94        |value| Ok::<_, &'static str>(json!({"version": 1, "items": value})),
95        |value| value.get("items").cloned().ok_or("missing labels.items"),
96    )?;
97    let encoded = labeled.serialize()?;
98    assert_eq!(
99        encoded["labels"],
100        json!({"version":1,"items":["draft","queued"]})
101    );
102    let decoded = WorkflowState::deserialize(encoded.clone())?;
103    assert_eq!(decoded, labeled);
104
105    StateSerialization::global().register_tag("example-money".into(), |value| {
106        value.get("minor").cloned().ok_or("missing money.minor")
107    })?;
108    let tagged = WorkflowState::deserialize(Map::from_iter([(
109        "amount".into(),
110        json!({"__burr_serde__":"example-money","minor":120}),
111    )]))?;
112    assert_eq!(tagged.values()["amount"], 120);
113    println!("{}", Value::Object(encoded));
114    Ok(())
115}
More examples
Hide additional examples
examples/performance.rs (line 256)
252    fn verify_snapshot(snapshot: &RunSnapshot, actions: u64) {
253        assert!(matches!(snapshot.status, RunStatus::Succeeded));
254        assert!(snapshot.interactions.is_empty());
255        assert_eq!(
256            snapshot.state.values(),
257            &Map::from_iter([("count".into(), json!(actions))])
258        );
259        assert_eq!(snapshot.last_action_ordinal, Some(actions - 1));
260        assert_eq!(snapshot.outcomes.len(), 1);
261        assert_eq!(snapshot.outcomes[0].ordinal, actions - 1);
262        assert!(snapshot.outcomes[0].result.is_empty());
263    }
264
265    async fn history(
266        &self,
267        run: &WorkflowRun<StateStore, Agent, WorkloadAction>,
268        actions: u64,
269        linear: bool,
270        terminal_sequence: u64,
271    ) -> Result<Value, AppError> {
272        let ceiling = actions * 16 + 16;
273        let mut cursor = None;
274        let mut prior_sequence = None;
275        let mut rows = 0_u64;
276        let mut observed_actions = 0_u64;
277        let mut initial = 0;
278        let mut terminal = 0;
279        let mut controls = 0;
280        let mut halts = 0;
281        let mut samples = Vec::new();
282        let started = Instant::now();
283        loop {
284            assert!(
285                samples.len() < ceiling.div_ceil(PAGE_SIZE as u64) as usize + 1,
286                "history page ceiling exhausted without cursor completion"
287            );
288            let page_started = Instant::now();
289            let page = run
290                .history(cursor, NonZeroUsize::new(PAGE_SIZE).unwrap())
291                .await?;
292            samples.push(page_started.elapsed());
293            assert!(page.checkpoints.len() <= PAGE_SIZE);
294            assert!(
295                !page.checkpoints.is_empty() || page.next.is_none(),
296                "history cursor made no progress"
297            );
298            for row in &page.checkpoints {
299                assert_eq!(&row.run, run.id());
300                assert!(prior_sequence.is_none_or(|prior| row.sequence > prior));
301                prior_sequence = Some(row.sequence);
302                rows += 1;
303                assert!(
304                    rows <= ceiling,
305                    "declared retained-row workload ceiling exceeded"
306                );
307                match row.kind {
308                    CheckpointKind::Initial => {
309                        initial += 1;
310                        assert_eq!(observed_actions, 0);
311                        assert_eq!(row.sequence, 0);
312                    }
313                    CheckpointKind::Action => {
314                        assert_eq!(row.outcomes.len(), 1);
315                        let outcome = &row.outcomes[0];
316                        assert_eq!(outcome.ordinal, observed_actions);
317                        assert_eq!(
318                            outcome.node,
319                            if linear {
320                                format!("n{observed_actions:04}")
321                            } else {
322                                "count".into()
323                            }
324                        );
325                        assert!(outcome.result.is_empty());
326                        observed_actions += 1;
327                        assert_eq!(row.action_ordinal, Some(observed_actions - 1));
328                    }
329                    CheckpointKind::Terminal => {
330                        terminal += 1;
331                        assert!(matches!(row.status, RunStatus::Succeeded));
332                    }
333                    CheckpointKind::Control => {
334                        assert!(row.outcomes.is_empty());
335                        controls += 1;
336                        if let RunStatus::Halted { node, result } = &row.status {
337                            assert!(!linear);
338                            assert_eq!(row.sequence, 4);
339                            assert_eq!(node, "count");
340                            assert_eq!(result, &Some(Map::new()));
341                            halts += 1;
342                        }
343                    }
344                    CheckpointKind::Failed { .. } => {
345                        panic!("workload history contains a failed action")
346                    }
347                }
348                assert_eq!(
349                    row.state.values(),
350                    &Map::from_iter([("count".into(), json!(observed_actions))])
351                );
352                if !linear && matches!(row.sequence, 4 | 5) {
353                    assert!(matches!(row.kind, CheckpointKind::Control));
354                    assert_eq!(observed_actions, 1);
355                    assert_eq!(row.action_ordinal, Some(0));
356                    assert_eq!(row.position.as_deref(), Some("count"));
357                    if row.sequence == 5 {
358                        assert!(matches!(row.status, RunStatus::Paused));
359                    }
360                }
361            }
362            cursor = page.next;
363            // Each complete Checkpoint/WorkflowState is released with this page.
364            if cursor.is_none() {
365                break;
366            }
367        }
368        assert_eq!(initial, 1);
369        assert_eq!(terminal, 1);
370        assert_eq!(observed_actions, actions);
371        assert_eq!(prior_sequence, Some(terminal_sequence));
372        if !linear {
373            assert_eq!(rows, actions * 3 + 4);
374            assert_eq!(terminal_sequence, rows - 1);
375            assert_eq!(controls, actions * 2 + 2);
376            assert_eq!(halts, 1);
377            assert_eq!(samples.len(), (rows as usize).div_ceil(PAGE_SIZE));
378        }
379        let elapsed = started.elapsed();
380        let first = samples.first().copied().unwrap_or_default();
381        let subsequent: Duration = samples.iter().skip(1).copied().sum();
382        Ok(
383            json!({"rows":rows,"action_rows":observed_actions,"pages":samples.len(),
384            "elapsed_ns":elapsed.as_nanos(),"first_page_ns":first.as_nanos(),
385            "subsequent_pages_total_ns":subsequent.as_nanos(),
386            "rows_per_second":rows as f64 / elapsed.as_secs_f64(),"page_latency":Self::samples(samples)}),
387        )
388    }
389
390    async fn seed(
391        &self,
392        app: &App<StateStore, Agent, WorkloadAction>,
393        actions: u64,
394        linear: bool,
395    ) -> Result<Value, Box<dyn Error + Send + Sync>> {
396        let workspace = app
397            .create_workspace(WorkspaceConfig {
398                name: Some("performance consumer".into()),
399                root: self.root.join("workspace"),
400            })
401            .await?;
402        let workflow = workspace.workflow("performance".parse()?).await?;
403        let mut run = workflow.prepare(WorkflowState::default()).await?;
404        let mut execution = run
405            .execution(ExecutionOptions {
406                halt_after: if linear {
407                    Vec::new()
408                } else {
409                    vec!["count".into()]
410                },
411                ..ExecutionOptions::default()
412            })
413            .await?;
414        let mut samples = Vec::with_capacity(actions as usize);
415        let started = Instant::now();
416        for ordinal in 0..actions {
417            let step_started = Instant::now();
418            let event = execution.step().await?;
419            samples.push(step_started.elapsed());
420            let Some(ExecutionEvent::Committed { outcomes, .. }) = event else {
421                panic!("each serial workload step must commit one action");
422            };
423            assert_eq!(outcomes.len(), 1);
424            assert_eq!(outcomes[0].ordinal, ordinal);
425            assert_eq!(
426                outcomes[0].node,
427                if linear {
428                    format!("n{ordinal:04}")
429                } else {
430                    "count".into()
431                }
432            );
433            assert!(outcomes[0].result.is_empty());
434            if !linear && ordinal == 0 {
435                // One public halt and explicit resume add two control rows to
436                // Initial + three checkpoints per action + Terminal.
437                let halted = execution.finish().await?;
438                assert!(matches!(&halted.status, RunStatus::Halted { node, result }
439                    if node == "count" && result == &Some(Map::new())));
440                assert_eq!(halted.checkpoint, 4);
441                assert_eq!(
442                    halted.state.values(),
443                    &Map::from_iter([("count".into(), json!(1))])
444                );
445                assert_eq!(halted.last_action_ordinal, Some(0));
446                assert!(halted.outcomes == outcomes);
447                assert!(halted.interactions.is_empty());
448                execution = run.execution(ExecutionOptions::default()).await?;
449            }
450        }
451        let snapshot = execution.finish().await?;
452        Self::verify_snapshot(&snapshot, actions);
453        let execution_elapsed = started.elapsed();
454        let history = self
455            .history(&run, actions, linear, snapshot.checkpoint)
456            .await?;
457        if let Command::SeedHistory { rows } = self.command {
458            assert_eq!(history["rows"], rows);
459        }
460        Ok(
461            json!({"dataset":if linear {"linear"} else {"history"},"actions":actions,
462            "workspace":workspace.id().to_string(),"run":run.id().to_string(),
463            "occurrence":snapshot.occurrence,
464            "expected_state":snapshot.state.values(),"last_action_ordinal":snapshot.last_action_ordinal,
465            "terminal_checkpoint":snapshot.checkpoint,"retained_rows":history["rows"],
466            "execution_ns":execution_elapsed.as_nanos(),
467            "actions_per_second":actions as f64 / execution_elapsed.as_secs_f64(),
468            "step_latency":Self::samples(samples),"history":history}),
469        )
470    }
471
472    async fn reopen(
473        &self,
474        app: &App<StateStore, Agent, WorkloadAction>,
475        manifest: &Value,
476        actions: u64,
477        open_only: bool,
478    ) -> Result<Value, Box<dyn Error + Send + Sync>> {
479        let started = Instant::now();
480        let workspace: WorkspaceId = manifest["workspace"]
481            .as_str()
482            .ok_or("missing workspace ID")?
483            .parse()?;
484        let run_id: RunId = manifest["run"].as_str().ok_or("missing run ID")?.parse()?;
485        let occurrence = manifest["occurrence"]
486            .as_str()
487            .ok_or("missing run occurrence")?
488            .to_owned();
489        let workspace = app.workspace(workspace).await?;
490        let workflow = workspace.workflow("performance".parse()?).await?;
491        let run = workflow
492            .open_run(RunSelector::Exact {
493                id: run_id,
494                occurrence: occurrence.clone(),
495            })
496            .await?;
497        let snapshot = run.snapshot().await?;
498        Self::verify_snapshot(&snapshot, actions);
499        assert_eq!(snapshot.occurrence, occurrence);
500        assert_eq!(json!(snapshot.checkpoint), manifest["terminal_checkpoint"]);
501        assert_eq!(json!(snapshot.state.values()), manifest["expected_state"]);
502        assert_eq!(
503            json!(snapshot.last_action_ordinal),
504            manifest["last_action_ordinal"]
505        );
506        let lookup = started.elapsed();
507        if open_only {
508            return Ok(
509                json!({"dataset":"history","actions":actions,"mode":"open-only",
510                "lookup_and_snapshot_ns":lookup.as_nanos(),
511                "seed_retained_rows":manifest["retained_rows"],"observed_history_rows":null}),
512            );
513        }
514        let history = self
515            .history(&run, actions, false, snapshot.checkpoint)
516            .await?;
517        assert_eq!(history["rows"], manifest["retained_rows"]);
518        Ok(
519            json!({"dataset":"history","actions":actions,"mode":"full-history",
520            "lookup_and_snapshot_ns":lookup.as_nanos(),"history":history}),
521        )
522    }
523
524    fn definitions(&self, actions: u64) -> Result<(Vec<WorkflowDefinition>, usize), AppError> {
525        if matches!(
526            self.command,
527            Command::Linear | Command::SeedHistory { .. } | Command::ReopenHistory { .. }
528        ) {
529            let (definition, bytes) =
530                Self::definition(matches!(self.command, Command::Linear), actions)?;
531            return Ok((vec![definition], bytes));
532        }
533        let leaf = json!({
534            "schema":"app-kit.workflow/v2","id":"performance-leaf","revision":"r1",
535            "state":{"value":0,"result":0},"required":["value","result"],
536            "reducers":{"value":"reject_concurrent","result":"reject_concurrent"},
537            "entry":"work","nodes":[{"id":"work","deadline_ms":NODE_DEADLINE_MS,
538                "step":{"kind":"action","action":"performance-action","revision":"r1"}}],"routes":{}
539        });
540        let documents = match self.command {
541            Command::Ready => {
542                let mut nodes = vec![json!({"id":"fork","step":{"kind":"state","mutations":[]}})];
543                let mut branches = Vec::new();
544                for index in 0..32 {
545                    let node = format!("n{index:02}");
546                    nodes.push(json!({"id":node,"deadline_ms":NODE_DEADLINE_MS,
547                        "step":{"kind":"action","action":"performance-action","revision":"r1"}}));
548                    branches.push(json!({"target":{"kind":"node","id":node}}));
549                }
550                vec![json!({
551                    "schema":"app-kit.workflow/v2","id":"performance","revision":"r1",
552                    "state":{"value":0,"result":0},"required":["value","result"],
553                    "reducers":{"value":"reject_concurrent","result":"collect"},
554                    "entry":"fork","nodes":nodes,
555                    "routes":{"fork":{"mode":"broadcast","branches":branches}}
556                })]
557            }
558            Command::Map | Command::NestedCancel => {
559                let nested = matches!(self.command, Command::NestedCancel);
560                let items = if nested { 2 } else { 4_096 };
561                let mut documents = vec![
562                    json!({
563                        "schema":"app-kit.workflow/v2","id":"performance","revision":"r1",
564                        "state":{"value":0,"result":0},"required":["value","result"],
565                        "reducers":{"value":"reject_concurrent","result":"sum_integer"},
566                        "entry":"map","nodes":[{"id":"map","deadline_ms":if nested {NODE_DEADLINE_MS} else {WORKLOAD_BUDGET.as_millis() as u64},
567                            "step":{"kind":"map","workflow":if nested {"performance-mid"} else {"performance-leaf"},
568                            "items":{"kind":"literal","value":(0..items).collect::<Vec<_>>()},
569                            "input_key":"value","max_parallel":if nested {2} else {8},
570                            "reductions":[{"source":"result","target":"result","reducer":"sum_integer"}]}}],"routes":{}
571                    }),
572                    leaf,
573                ];
574                if nested {
575                    documents.push(json!({
576                        "schema":"app-kit.workflow/v2","id":"performance-mid","revision":"r1",
577                        "state":{"value":0,"result":0},"required":["value","result"],
578                        "reducers":{"value":"reject_concurrent","result":"sum_integer"},
579                        "entry":"map","nodes":[{"id":"map","deadline_ms":NODE_DEADLINE_MS,
580                            "step":{"kind":"map","workflow":"performance-leaf",
581                            "items":{"kind":"literal","value":(0..8).collect::<Vec<_>>()},
582                            "input_key":"value","max_parallel":8,
583                            "reductions":[{"source":"result","target":"result","reducer":"sum_integer"}]}}],"routes":{}
584                    }));
585                }
586                documents
587            }
588            Command::Repeated | Command::DelayedObserver => vec![leaf],
589            Command::Linear | Command::SeedHistory { .. } | Command::ReopenHistory { .. } => {
590                unreachable!()
591            }
592        };
593        let mut bytes = 0;
594        let mut definitions = Vec::with_capacity(documents.len());
595        for document in documents {
596            let source = document.to_string();
597            assert!(source.len() <= 1_048_576);
598            bytes += source.len();
599            definitions.push(WorkflowDefinition::parse(&source)?);
600        }
601        Ok((definitions, bytes))
602    }
603
604    async fn ready(
605        &self,
606        app: &App<StateStore, Agent, WorkloadAction>,
607        probe: &WorkloadProbe,
608    ) -> Result<Value, Box<dyn Error + Send + Sync>> {
609        let workspace = app
610            .create_workspace(WorkspaceConfig {
611                name: None,
612                root: self.root.join("workspace"),
613            })
614            .await?;
615        let workflow = workspace.workflow("performance".parse()?).await?;
616        let mut waves = Vec::with_capacity(32);
617        let mut releases = Vec::with_capacity(32 * 31);
618        let mut barriers = Vec::with_capacity(32);
619        let started = Instant::now();
620        for permutation in 0..32 {
621            probe.assert_idle();
622            let mut run = workflow.prepare(WorkflowState::default()).await?;
623            let observer = workflow
624                .open_run(RunSelector::Latest(run.id().clone()))
625                .await?;
626            let mut execution = run.execution(ExecutionOptions::default()).await?;
627            let Some(ExecutionEvent::Committed { outcomes, .. }) = execution.step().await? else {
628                panic!("the native fork must commit before the ready wave");
629            };
630            assert_eq!(outcomes.len(), 1);
631            assert_eq!(
632                (outcomes[0].node.as_str(), outcomes[0].ordinal),
633                ("fork", 0)
634            );
635            let wave = Instant::now();
636            let mut finish = Box::pin(execution.finish());
637            tokio::select! {
638                _ = probe.wait_for(&probe.constructed, (permutation + 1) * 32) => {}
639                result = &mut finish => panic!("gated ready wave finished before admission: {}", result.is_ok()),
640            }
641            assert_eq!(probe.active.load(Ordering::SeqCst), 32);
642            let order = (0..32)
643                .map(|offset| (permutation + 31 - offset) % 32)
644                .collect::<Vec<_>>();
645            for (released, index) in order.iter().take(31).enumerate() {
646                let releasing = Instant::now();
647                probe.release(*index);
648                tokio::select! {
649                    _ = probe.wait_for(&probe.completed, permutation * 32 + released + 1) => {}
650                    result = &mut finish => panic!("a partial ready wave committed: {}", result.is_ok()),
651                }
652                releases.push(releasing.elapsed());
653                let visible = observer.snapshot().await?;
654                assert_eq!(
655                    visible.state.values()["result"],
656                    0,
657                    "staged siblings cannot expose a partial barrier"
658                );
659                assert_eq!(visible.last_action_ordinal, Some(0));
660            }
661            let barrier = Instant::now();
662            probe.release(order[31]);
663            let snapshot = finish.await?;
664            barriers.push(barrier.elapsed());
665            waves.push(wave.elapsed());
666            assert!(matches!(snapshot.status, RunStatus::Succeeded));
667            assert!(snapshot.interactions.is_empty());
668            assert_eq!(
669                snapshot.state.values()["result"],
670                json!((1..=32).collect::<Vec<_>>())
671            );
672            assert_eq!(snapshot.last_action_ordinal, Some(32));
673            assert_eq!(snapshot.outcomes.len(), 32);
674            for (index, outcome) in snapshot.outcomes.iter().enumerate() {
675                assert_eq!(outcome.node, format!("n{index:02}"));
676                assert_eq!(outcome.ordinal, index as u64 + 1);
677                assert_eq!(
678                    outcome.result,
679                    Map::from_iter([("value".into(), json!(index + 1))])
680                );
681            }
682            assert_eq!(std::mem::take(&mut *probe.order.lock().unwrap()), order);
683            assert_eq!(
684                probe.completed.load(Ordering::SeqCst),
685                (permutation + 1) * 32
686            );
687            probe.assert_idle();
688        }
689        assert_eq!(probe.peak.load(Ordering::SeqCst), 32);
690        let elapsed = started.elapsed();
691        Ok(
692            json!({"dataset":"ready32","permutations":32,"ready_actions_per_wave":32,
693            "caller_action_commits":1024,"native_fork_commits":32,
694            "execution_ns":elapsed.as_nanos(),"caller_actions_per_second":1024.0 / elapsed.as_secs_f64(),
695            "wave_latency_including_snapshot_probes":Self::samples(waves),
696            "release_to_source_completion_latency":Self::samples(releases),
697            "last_release_to_committed_barrier_latency":Self::samples(barriers)}),
698        )
699    }
700
701    async fn mapped(
702        &self,
703        app: &App<StateStore, Agent, WorkloadAction>,
704        probe: &WorkloadProbe,
705    ) -> Result<Value, Box<dyn Error + Send + Sync>> {
706        let workspace = app
707            .create_workspace(WorkspaceConfig {
708                name: None,
709                root: self.root.join("workspace"),
710            })
711            .await?;
712        let workflow = workspace.workflow("performance".parse()?).await?;
713        let mut run = workflow.prepare(WorkflowState::default()).await?;
714        let observer = workflow
715            .open_run(RunSelector::Latest(run.id().clone()))
716            .await?;
717        let started = Instant::now();
718        let mut finish = Box::pin(run.execution(ExecutionOptions::default()).await?.finish());
719        tokio::select! {
720            _ = probe.wait_for(&probe.constructed, 8) => {}
721            result = &mut finish => panic!("map finished before its first eight blocked sources: {}", result.is_ok()),
722        }
723        assert_eq!(probe.active.load(Ordering::SeqCst), 8);
724        assert_eq!(probe.constructed.load(Ordering::SeqCst), 8);
725        let unchanged = observer.snapshot().await?;
726        assert_eq!(unchanged.state.values()["result"], 0);
727        assert!(unchanged.last_action_ordinal.is_none());
728        probe.release_all();
729        let snapshot = finish.await?;
730        let elapsed = started.elapsed();
731        assert!(matches!(snapshot.status, RunStatus::Succeeded));
732        assert!(snapshot.interactions.is_empty());
733        assert_eq!(snapshot.state.values()["result"], 4_096_u64 * 4_097 / 2);
734        assert_eq!(snapshot.last_action_ordinal, Some(0));
735        assert_eq!(snapshot.outcomes.len(), 1);
736        assert_eq!(snapshot.outcomes[0].node, "map");
737        assert_eq!(probe.constructed.load(Ordering::SeqCst), 4_096);
738        assert_eq!(probe.completed.load(Ordering::SeqCst), 4_096);
739        assert_eq!(probe.peak.load(Ordering::SeqCst), 8);
740        probe.assert_idle();
741        let child = workspace.workflow("performance-leaf".parse()?).await?;
742        let mut cursor = None;
743        let mut values = BTreeSet::new();
744        let verifying = Instant::now();
745        loop {
746            let page = child
747                .list_runs(cursor, NonZeroUsize::new(PAGE_SIZE).unwrap())
748                .await?;
749            assert!(page.len() <= PAGE_SIZE);
750            if page.is_empty() {
751                break;
752            }
753            for row in &page {
754                assert!(matches!(row.status, RunStatus::Succeeded));
755                let value = row.state.values()["value"].as_u64().unwrap();
756                assert!(
757                    value < 4_096 && values.insert(value),
758                    "every mapped input has exactly one retained child"
759                );
760                assert_eq!(row.state.values()["result"], value + 1);
761                assert_eq!(row.last_action_ordinal, Some(0));
762                assert_eq!(row.outcomes.len(), 1);
763                assert_eq!(row.outcomes[0].node, "work");
764            }
765            let last = page.last().unwrap();
766            cursor = Some(RunSelector::Exact {
767                id: last.id.clone(),
768                occurrence: last.occurrence.clone(),
769            });
770            assert!(values.len() <= 4_096);
771        }
772        assert_eq!(values.len(), 4_096);
773        Ok(json!({"dataset":"map4096","items":4096,"max_parallel":8,
774            "parent_node_deadline_ms":WORKLOAD_BUDGET.as_millis(),
775            "execution_ns":elapsed.as_nanos(),"child_actions_per_second":4096.0 / elapsed.as_secs_f64(),
776            "map_completion_latency":Self::samples(vec![elapsed]),
777            "child_inventory_verification_ns":verifying.elapsed().as_nanos(),"verified_children":values.len()}))
778    }
779
780    async fn repeated(
781        &self,
782        app: &App<StateStore, Agent, WorkloadAction>,
783        probe: &WorkloadProbe,
784    ) -> Result<Value, Box<dyn Error + Send + Sync>> {
785        let workspace = app
786            .create_workspace(WorkspaceConfig {
787                name: None,
788                root: self.root.join("workspace"),
789            })
790            .await?;
791        let workflow = workspace.workflow("performance-leaf".parse()?).await?;
792        let started = Instant::now();
793        // Admit one run at a time, then directly poll at most 16 original
794        // executions together. No consumer task is spawned or detached.
795        let mut runs = std::pin::pin!(
796            stream::iter(0..100_u64)
797                .then(|value| {
798                    let workflow = &workflow;
799                    async move {
800                        let started = Instant::now();
801                        let run = workflow
802                            .prepare(WorkflowState::new(Map::from_iter([(
803                                "value".into(),
804                                json!(value),
805                            )]))?)
806                            .await?;
807                        Ok::<_, AppError>((value, run, started))
808                    }
809                })
810                .map(|prepared| async move {
811                    let (value, mut run, started) = prepared?;
812                    let snapshot = run
813                        .execution(ExecutionOptions::default())
814                        .await?
815                        .finish()
816                        .await?;
817                    assert!(matches!(snapshot.status, RunStatus::Succeeded));
818                    assert!(snapshot.interactions.is_empty());
819                    assert_eq!(snapshot.state.values()["value"], value);
820                    assert_eq!(snapshot.state.values()["result"], value + 1);
821                    assert_eq!(snapshot.last_action_ordinal, Some(0));
822                    assert_eq!(snapshot.outcomes.len(), 1);
823                    Ok::<_, AppError>((run.id().clone(), started.elapsed()))
824                })
825                .buffer_unordered(16)
826        );
827        tokio::select! {
828            _ = probe.wait_for(&probe.constructed, 16) => {}
829            result = runs.next() => panic!(
830                "run finished before the first 16 blocked sources: {:?}",
831                result.map(|result| result.map(|_| ())),
832            ),
833        }
834        assert_eq!(probe.active.load(Ordering::SeqCst), 16);
835        assert_eq!(probe.constructed.load(Ordering::SeqCst), 16);
836        probe.release_all();
837        let mut ids = BTreeSet::new();
838        let mut samples = Vec::with_capacity(100);
839        while let Some(result) = runs.next().await {
840            let (id, elapsed) = result?;
841            assert!(ids.insert(id));
842            samples.push(elapsed);
843        }
844        assert_eq!(ids.len(), 100);
845        assert_eq!(probe.constructed.load(Ordering::SeqCst), 100);
846        assert_eq!(probe.completed.load(Ordering::SeqCst), 100);
847        assert_eq!(probe.peak.load(Ordering::SeqCst), 16);
848        probe.assert_idle();
849        let rows = workflow
850            .list_runs(None, NonZeroUsize::new(128).unwrap())
851            .await?;
852        assert_eq!(rows.len(), 100);
853        assert_eq!(
854            rows.iter()
855                .map(|row| row.id.clone())
856                .collect::<BTreeSet<_>>(),
857            ids
858        );
859        assert!(
860            rows.iter()
861                .all(|row| matches!(row.status, RunStatus::Succeeded))
862        );
863        let elapsed = started.elapsed();
864        Ok(
865            json!({"dataset":"repeated100","runs":100,"caller_in_flight_limit":16,"admission":"sequential",
866            "production_active_limit":"unchanged; this measures only the caller's 16-journey envelope",
867            "execution_ns":elapsed.as_nanos(),"runs_per_second":100.0 / elapsed.as_secs_f64(),
868            "run_latency_including_admission":Self::samples(samples),"indefinite_stability_proof":false}),
869        )
870    }
871
872    async fn nested_cancel(
873        &self,
874        app: &App<StateStore, Agent, WorkloadAction>,
875        probe: &WorkloadProbe,
876    ) -> Result<Value, Box<dyn Error + Send + Sync>> {
877        let workspace = app
878            .create_workspace(WorkspaceConfig {
879                name: None,
880                root: self.root.join("workspace"),
881            })
882            .await?;
883        let workflow = workspace.workflow("performance".parse()?).await?;
884        let run = workflow.start(ExecutionOptions::default()).await?;
885        tokio::select! {
886            _ = probe.wait_for(&probe.constructed, 16) => {}
887            result = run.wait() => panic!("nested run ended before its held leaf sources: {}", result.is_ok()),
888        }
889        assert_eq!(probe.active.load(Ordering::SeqCst), 16);
890        let cancelling = Instant::now();
891        let snapshot = tokio::time::timeout(Duration::from_secs(30), async {
892            loop {
893                let before = run.snapshot().await?;
894                assert_eq!(before.state.values()["result"], 0);
895                assert!(before.outcomes.is_empty() && before.last_action_ordinal.is_none());
896                match run.cancel(before.checkpoint).await {
897                    Ok(_) => break,
898                    Err(AppError::Conflict(_)) => tokio::task::yield_now().await,
899                    Err(error) => return Err(error),
900                }
901            }
902            run.wait().await
903        })
904        .await
905        .map_err(|_| {
906            AppError::Timeout("nested cancellation exceeded its declared 30-second grace".into())
907        })??;
908        let elapsed = cancelling.elapsed();
909        assert!(matches!(snapshot.status, RunStatus::Cancelled));
910        assert!(snapshot.interactions.is_empty() && snapshot.outcomes.is_empty());
911        assert!(snapshot.last_action_ordinal.is_none());
912        assert_eq!(snapshot.state.values()["result"], 0);
913        assert_eq!(probe.constructed.load(Ordering::SeqCst), 16);
914        assert_eq!(probe.completed.load(Ordering::SeqCst), 0);
915        probe.assert_idle();
916        for (id, count) in [
917            ("performance", 1),
918            ("performance-mid", 2),
919            ("performance-leaf", 16),
920        ] {
921            let child = workspace.workflow(id.parse()?).await?;
922            let rows = child
923                .list_runs(None, NonZeroUsize::new(128).unwrap())
924                .await?;
925            assert_eq!(rows.len(), count);
926            for row in rows {
927                assert!(matches!(row.status, RunStatus::Cancelled));
928                assert!(row.outcomes.is_empty() && row.last_action_ordinal.is_none());
929                assert_eq!(row.state.values()["result"], 0);
930            }
931        }
932        Ok(
933            json!({"dataset":"nested-cancel","depth":2,"intermediate_children":2,"leaf_children":16,
934            "cooperative_cancel_grace_ms":30_000,"cancellation_latency":Self::samples(vec![elapsed]),
935            "source_replays":0,"caller_sources_dropped":16,"agent_unknown_replay":"not applicable: no Agent attempt"}),
936        )
937    }
938
939    async fn delayed_observer(
940        &self,
941        app: &App<StateStore, Agent, WorkloadAction>,
942        probe: &WorkloadProbe,
943    ) -> Result<Value, Box<dyn Error + Send + Sync>> {
944        let workspace = app
945            .create_workspace(WorkspaceConfig {
946                name: None,
947                root: self.root.join("workspace"),
948            })
949            .await?;
950        let workflow = workspace.workflow("performance-leaf".parse()?).await?;
951        let run = workflow.start(ExecutionOptions::default()).await?;
952        let observer = workflow
953            .open_run(RunSelector::Latest(run.id().clone()))
954            .await?;
955        // This public observation is deliberately never polled while the source
956        // produces its 256 partials and commits. It owns no execution work.
957        let delayed = observer.wait();
958        let produced = run.wait().await?;
959        assert!(matches!(produced.status, RunStatus::Succeeded));
960        assert_eq!(probe.completed.load(Ordering::SeqCst), 1);
961        probe.assert_idle();
962        tokio::time::sleep(Duration::from_secs(2)).await;
963        let observing = Instant::now();
964        let snapshot = delayed.await?;
965        let elapsed = observing.elapsed();
966        assert!(matches!(snapshot.status, RunStatus::Succeeded));
967        assert_eq!(snapshot.checkpoint, produced.checkpoint);
968        assert_eq!(snapshot.state.values()["result"], 1);
969        assert_eq!(snapshot.last_action_ordinal, Some(0));
970        assert_eq!(snapshot.outcomes.len(), 1);
971        assert_eq!(
972            snapshot.outcomes[0].result,
973            Map::from_iter([("value".into(), json!(1))])
974        );
975        assert_eq!(probe.constructed.load(Ordering::SeqCst), 1);
976        assert_eq!(probe.partials.load(Ordering::SeqCst), 256);
977        probe.assert_idle();
978        let terminal = run.snapshot().await?;
979        assert_eq!(terminal.checkpoint, snapshot.checkpoint);
980        assert_eq!(terminal.state, snapshot.state);
981        let mut cursor = None;
982        let mut actions = 0;
983        let mut rows = 0;
984        loop {
985            let page = run
986                .history(cursor, NonZeroUsize::new(PAGE_SIZE).unwrap())
987                .await?;
988            for row in &page.checkpoints {
989                rows += 1;
990                assert!(rows <= 32);
991                if matches!(row.kind, CheckpointKind::Action) {
992                    actions += 1;
993                    assert_eq!(row.action_ordinal, Some(0));
994                    assert_eq!(row.outcomes.len(), 1);
995                }
996            }
997            cursor = page.next;
998            if cursor.is_none() {
999                break;
1000            }
1001        }
1002        assert_eq!(
1003            actions, 1,
1004            "partials cannot become extra durable action commits"
1005        );
1006        Ok(
1007            json!({"dataset":"delayed-observer","unobserved_partial_events":256,"delay_after_terminal_confirmation_ms":2_000,
1008            "observation_latency":Self::samples(vec![elapsed]),"retained_rows":rows,
1009            "memory_scope":"external process RSS; this consumer retains no partial payloads"}),
1010        )
1011    }
1012
1013    async fn close(
1014        app: &mut App<StateStore, Agent, WorkloadAction>,
1015        success: bool,
1016    ) -> Result<(), AppError> {
1017        let deadline = Instant::now() + CLEANUP_BUDGET;
1018        let mut first_failure = None;
1019        loop {
1020            let remaining = deadline.saturating_duration_since(Instant::now());
1021            if remaining.is_zero() {
1022                return Err(first_failure.unwrap_or_else(|| {
1023                    AppError::Timeout("performance cleanup budget exhausted".into())
1024                }));
1025            }
1026            let policy = if success {
1027                Shutdown::Drain {
1028                    timeout: CLEANUP_OBSERVATION,
1029                }
1030            } else {
1031                Shutdown::CancelAndDrain {
1032                    timeout: CLEANUP_OBSERVATION,
1033                }
1034            };
1035            let result =
1036                tokio::time::timeout(remaining.min(CLEANUP_OBSERVATION), app.shutdown(policy))
1037                    .await;
1038            match result {
1039                Ok(Ok(())) => return first_failure.map_or(Ok(()), Err),
1040                Ok(Err(error @ AppError::Timeout(_))) => {
1041                    first_failure.get_or_insert(error);
1042                }
1043                Err(_) => {
1044                    first_failure.get_or_insert_with(|| {
1045                        AppError::Timeout("performance cleanup observation timed out".into())
1046                    });
1047                }
1048                Ok(Err(error)) => return Err(first_failure.unwrap_or(error)),
1049            }
1050            // A timed-out observer reattaches to its original finalizer.
1051        }
1052    }
1053
1054    async fn run(mut self) -> Result<(), Box<dyn Error + Send + Sync>> {
1055        let manifest = if matches!(self.command, Command::ReopenHistory { .. }) {
1056            self.root = self.root.canonicalize()?;
1057            Some(self.read_manifest()?)
1058        } else {
1059            fs::create_dir(&self.root)?;
1060            self.root = self.root.canonicalize()?;
1061            fs::create_dir(self.root.join("workspace"))?;
1062            let mut marker = OpenOptions::new()
1063                .write(true)
1064                .create_new(true)
1065                .open(self.root.join(".performance-root"))?;
1066            marker.write_all(MANIFEST_SCHEMA.as_bytes())?;
1067            marker.sync_all()?;
1068            None
1069        };
1070        let actions = match self.command {
1071            Command::Linear => 1_000,
1072            Command::SeedHistory { rows } => (rows - 4) / 3,
1073            Command::Ready => 32 * 32,
1074            Command::Map => 4_096,
1075            Command::Repeated => 100,
1076            Command::NestedCancel => 16,
1077            Command::DelayedObserver => 1,
1078            Command::ReopenHistory { .. } => {
1079                manifest.as_ref().unwrap()["actions"].as_u64().unwrap()
1080            }
1081        };
1082        let linear = matches!(self.command, Command::Linear);
1083        let (definitions, definition_bytes) = self.definitions(actions)?;
1084        let probe = match self.command {
1085            Command::Ready => Some(WorkloadProbe::new(SourceMode::Permuted, 1_024)),
1086            Command::Map | Command::Repeated => {
1087                Some(WorkloadProbe::new(SourceMode::Parallel, actions as usize))
1088            }
1089            Command::NestedCancel => Some(WorkloadProbe::new(SourceMode::Held, 16)),
1090            Command::DelayedObserver => Some(WorkloadProbe::new(SourceMode::Partials, 1)),
1091            Command::Linear | Command::SeedHistory { .. } | Command::ReopenHistory { .. } => None,
1092        };
1093        if let Some(manifest) = &manifest {
1094            assert_eq!(manifest["definition_bytes"], json!(definition_bytes));
1095        }
1096        let mut app = None;
1097        let started = Instant::now();
1098        let observed = AssertUnwindSafe(tokio::time::timeout(WORKLOAD_BUDGET, async {
1099            let connecting = Instant::now();
1100            let config = LanceStateStoreConfig::directory(self.root.join("state"))
1101                .with_operation_timeout(Duration::from_secs(30))?;
1102            let store = StateStore::connect(StateStoreConfig::Lance(config)).await?;
1103            let connect = connecting.elapsed();
1104            let opening = Instant::now();
1105            app = Some(
1106                App::open(
1107                    AppConfig::<_, Agent>::new(store, Vec::new(), definitions).with_actions(
1108                        probe
1109                            .as_ref()
1110                            .map(WorkloadAction::new)
1111                            .into_iter()
1112                            .collect(),
1113                    ),
1114                )
1115                .await?,
1116            );
1117            let open = opening.elapsed();
1118            let mut result = match self.command {
1119                Command::Linear | Command::SeedHistory { .. } => {
1120                    self.seed(app.as_ref().unwrap(), actions, linear).await?
1121                }
1122                Command::Ready => {
1123                    self.ready(app.as_ref().unwrap(), probe.as_ref().unwrap())
1124                        .await?
1125                }
1126                Command::Map => {
1127                    self.mapped(app.as_ref().unwrap(), probe.as_ref().unwrap())
1128                        .await?
1129                }
1130                Command::Repeated => {
1131                    self.repeated(app.as_ref().unwrap(), probe.as_ref().unwrap())
1132                        .await?
1133                }
1134                Command::NestedCancel => {
1135                    self.nested_cancel(app.as_ref().unwrap(), probe.as_ref().unwrap())
1136                        .await?
1137                }
1138                Command::DelayedObserver => {
1139                    self.delayed_observer(app.as_ref().unwrap(), probe.as_ref().unwrap())
1140                        .await?
1141                }
1142                Command::ReopenHistory { open_only } => {
1143                    self.reopen(
1144                        app.as_ref().unwrap(),
1145                        manifest.as_ref().unwrap(),
1146                        actions,
1147                        open_only,
1148                    )
1149                    .await?
1150                }
1151            };
1152            result["store_connect_ns"] = json!(connect.as_nanos());
1153            result["app_open_ns"] = json!(open.as_nanos());
1154            result["workload_elapsed_ns"] = json!(started.elapsed().as_nanos());
1155            Ok::<_, Box<dyn Error + Send + Sync>>(result)
1156        }))
1157        .catch_unwind()
1158        .await;
1159        let success = matches!(&observed, Ok(Ok(Ok(_))));
1160        let closing = Instant::now();
1161        let cleanup = if let Some(mut app) = app.take() {
1162            let closed = AssertUnwindSafe(Self::close(&mut app, success))
1163                .catch_unwind()
1164                .await;
1165            drop(app);
1166            Some(closed)
1167        } else {
1168            None
1169        };
1170        if !success {
1171            eprintln!(
1172                "performance workload failed; cleanup completed={}",
1173                matches!(&cleanup, Some(Ok(Ok(()))))
1174            );
1175        }
1176        let mut result = match observed {
1177            Ok(Ok(result)) => result?,
1178            Ok(Err(_)) => {
1179                return Err(
1180                    AppError::Timeout("performance workload budget exhausted".into()).into(),
1181                );
1182            }
1183            Err(panic) => resume_unwind(panic),
1184        };
1185        match cleanup {
1186            Some(Ok(result)) => result?,
1187            Some(Err(panic)) => resume_unwind(panic),
1188            None => return Err("successful workload did not retain its App cleanup owner".into()),
1189        }
1190        // All run/workflow/App owners and the caller-created store have dropped.
1191        // A cleanup timeout remains a failure even if reattachment later joined.
1192        result["schema"] = json!(MANIFEST_SCHEMA);
1193        result["settings"] = Self::settings(actions);
1194        if let Some(probe) = &probe {
1195            probe.assert_idle();
1196            result["caller_sources"] = probe.report();
1197            result["settings"]
1198                .as_object_mut()
1199                .unwrap()
1200                .remove("history_row_ceiling");
1201            result["settings"]["private_task_counts"] =
1202                json!("not exposed; App shutdown proves joined ownership");
1203            result["settings"]["lower_agents_and_tool_handlers"] = json!("none constructed");
1204        }
1205        result["definition_bytes"] = json!(definition_bytes);
1206        result["process_id"] = json!(std::process::id());
1207        result["os"] = json!(std::env::consts::OS);
1208        result["architecture"] = json!(std::env::consts::ARCH);
1209        result["package_version"] = json!(env!("CARGO_PKG_VERSION"));
1210        result["cleanup_joined"] = json!(true);
1211        result["cleanup_ns"] = json!(closing.elapsed().as_nanos());
1212        let encoded = serde_json::to_vec(&result)?;
1213        assert!(
1214            encoded.len() <= 16_384,
1215            "result stays within the manifest bound"
1216        );
1217        if matches!(self.command, Command::SeedHistory { .. }) {
1218            let mut file = OpenOptions::new()
1219                .write(true)
1220                .create_new(true)
1221                .open(self.root.join("manifest.json"))?;
1222            file.write_all(&encoded)?;
1223            file.sync_all()?;
1224        }
1225        println!("{}", std::str::from_utf8(&encoded)?);
1226        Ok(())
1227    }
1228}
1229
1230// These counters observe only this consumer's concrete sources. They expose no
1231// runtime internals and do not claim to count App tasks or lower tool handlers.
1232#[derive(Clone, Copy)]
1233enum SourceMode {
1234    Permuted,
1235    Parallel,
1236    Held,
1237    Partials,
1238}
1239
1240struct WorkloadProbe {
1241    mode: SourceMode,
1242    expected: usize,
1243    open: AtomicBool,
1244    constructed: AtomicUsize,
1245    active: AtomicUsize,
1246    peak: AtomicUsize,
1247    completed: AtomicUsize,
1248    dropped: AtomicUsize,
1249    partials: AtomicUsize,
1250    permitted: Mutex<BTreeSet<usize>>,
1251    wakers: Mutex<BTreeMap<usize, Waker>>,
1252    order: Mutex<Vec<usize>>,
1253    lifetimes: Mutex<Vec<Duration>>,
1254    changed: Notify,
1255}
1256
1257impl WorkloadProbe {
1258    fn new(mode: SourceMode, expected: usize) -> Arc<Self> {
1259        Arc::new(Self {
1260            mode,
1261            expected,
1262            open: AtomicBool::new(false),
1263            constructed: AtomicUsize::new(0),
1264            active: AtomicUsize::new(0),
1265            peak: AtomicUsize::new(0),
1266            completed: AtomicUsize::new(0),
1267            dropped: AtomicUsize::new(0),
1268            partials: AtomicUsize::new(0),
1269            permitted: Mutex::new(BTreeSet::new()),
1270            wakers: Mutex::new(BTreeMap::new()),
1271            order: Mutex::new(Vec::new()),
1272            lifetimes: Mutex::new(Vec::new()),
1273            changed: Notify::new(),
1274        })
1275    }
1276
1277    async fn wait_for(&self, counter: &AtomicUsize, expected: usize) {
1278        loop {
1279            let changed = self.changed.notified();
1280            tokio::pin!(changed);
1281            changed.as_mut().enable();
1282            if counter.load(Ordering::SeqCst) >= expected {
1283                return;
1284            }
1285            changed.await;
1286        }
1287    }
1288
1289    fn release(&self, index: usize) {
1290        assert!(self.permitted.lock().unwrap().insert(index));
1291        let waker = self.wakers.lock().unwrap().remove(&index);
1292        if let Some(waker) = waker {
1293            waker.wake();
1294        }
1295    }
1296
1297    fn release_all(&self) {
1298        self.open.store(true, Ordering::SeqCst);
1299        let wakers = std::mem::take(&mut *self.wakers.lock().unwrap());
1300        for (_, waker) in wakers {
1301            waker.wake();
1302        }
1303    }
1304
1305    fn assert_idle(&self) {
1306        assert_eq!(
1307            self.active.load(Ordering::SeqCst),
1308            0,
1309            "original caller sources must be dropped"
1310        );
1311        assert_eq!(
1312            self.dropped.load(Ordering::SeqCst),
1313            self.constructed.load(Ordering::SeqCst)
1314        );
1315        assert!(self.wakers.lock().unwrap().is_empty());
1316        assert!(self.permitted.lock().unwrap().is_empty());
1317    }
1318
1319    fn report(&self) -> Value {
1320        json!({"constructed":self.constructed.load(Ordering::SeqCst),
1321            "completed":self.completed.load(Ordering::SeqCst),
1322            "dropped":self.dropped.load(Ordering::SeqCst),
1323            "active_after_cleanup":self.active.load(Ordering::SeqCst),
1324            "peak_live_sources":self.peak.load(Ordering::SeqCst),
1325            "partial_events":self.partials.load(Ordering::SeqCst),
1326            "construction_to_drop_latency":Performance::samples(std::mem::take(&mut *self.lifetimes.lock().unwrap()))})
1327    }
1328}
1329
1330struct WorkloadAction {
1331    declaration: ActionDeclaration,
1332    probe: Arc<WorkloadProbe>,
1333}
1334
1335impl WorkloadAction {
1336    fn new(probe: &Arc<WorkloadProbe>) -> Self {
1337        Self {
1338            declaration: ActionDeclaration {
1339                key: "performance-action".into(),
1340                revision: "r1".into(),
1341                reads: vec!["value".into()],
1342                writes: vec!["result".into()],
1343                required_inputs: Vec::new(),
1344                optional_inputs: Vec::new(),
1345                bound_inputs: Map::new(),
1346                default_inputs: Map::new(),
1347                tags: Vec::new(),
1348                source: None,
1349                mode: ActionMode::Asynchronous,
1350            },
1351            probe: Arc::clone(probe),
1352        }
1353    }
1354}
1355
1356impl ActionBackend for WorkloadAction {
1357    type Stream = WorkloadSource;
1358
1359    fn declaration(&self) -> &ActionDeclaration {
1360        &self.declaration
1361    }
1362
1363    fn start(&self, request: ActionRequest) -> Self::Stream {
1364        let value = match self.probe.mode {
1365            SourceMode::Permuted => request
1366                .context
1367                .node
1368                .strip_prefix('n')
1369                .unwrap()
1370                .parse()
1371                .unwrap(),
1372            SourceMode::Parallel | SourceMode::Held | SourceMode::Partials => {
1373                usize::try_from(request.state.values()["value"].as_u64().unwrap()).unwrap()
1374            }
1375        };
1376        assert!(
1377            self.probe.constructed.fetch_add(1, Ordering::SeqCst) < self.probe.expected,
1378            "a duplicate source construction exceeds this workload's exact bound"
1379        );
1380        let active = self.probe.active.fetch_add(1, Ordering::SeqCst) + 1;
1381        self.probe.peak.fetch_max(active, Ordering::SeqCst);
1382        self.probe.changed.notify_waiters();
1383        WorkloadSource {
1384            probe: Arc::clone(&self.probe),
1385            value,
1386            started: Instant::now(),
1387            state: SourceState::Polling(0),
1388        }
1389    }
examples/invoice/mod.rs (line 69)
67    fn calculate(state: WorkflowState, inputs: Map<String, Value>) -> Result<Self, AppError> {
68        let price = state
69            .values()
70            .get("unit_price")
71            .and_then(Value::as_u64)
72            .ok_or_else(|| {
73                AppError::InvalidInput("unit_price must be an unsigned integer".into())
74            })?;
75        let quantity = inputs
76            .get("quantity")
77            .and_then(Value::as_u64)
78            .ok_or_else(|| AppError::InvalidInput("quantity must be an unsigned integer".into()))?;
79        let total = price
80            .checked_mul(quantity)
81            .ok_or_else(|| AppError::InvalidInput("invoice total overflowed".into()))?;
82        let mut result = inputs;
83        result.insert("total".into(), json!(total));
84        Ok(Self::Completed { result, total })
85    }
examples/history_and_recovery.rs (line 81)
63    async fn create_fork(
64        &self,
65        app: &App<StateStore, Agent>,
66    ) -> Result<(WorkspaceId, RunSelector, RunSelector), Box<dyn Error + Send + Sync>> {
67        let workspace = app
68            .create_workspace(WorkspaceConfig {
69                name: Some("history example".into()),
70                root: std::env::current_dir()?,
71            })
72            .await?;
73        let workflow = workspace.workflow("history-counter".parse()?).await?;
74        let mut source = workflow.prepare(WorkflowState::default()).await?;
75        let completed = source
76            .execution(ExecutionOptions::default())
77            .await?
78            .finish()
79            .await?;
80        if !matches!(completed.status, RunStatus::Succeeded)
81            || completed.state.values()["count"] != 2
82        {
83            return Err(
84                AppError::Integration("source did not complete both increments".into()).into(),
85            );
86        }
87
88        // An action ordinal selects the whole committed boundary containing it;
89        // it is independent of the checkpoint sequence and storage revision.
90        let first = source.checkpoint(CheckpointSelector::Action(0)).await?;
91        if !matches!(first.kind, CheckpointKind::Action)
92            || first.position.as_deref() != Some("first")
93            || first.state.values()["count"] != 1
94        {
95            return Err(
96                AppError::Integration("first action history is inconsistent".into()).into(),
97            );
98        }
99        let source_selector = RunSelector::Exact {
100            id: source.id().clone(),
101            occurrence: first.occurrence.clone(),
102        };
103        let mut fork = workflow
104            .restore(RestoreOptions {
105                source: source_selector.clone(),
106                checkpoint: CheckpointSelector::Action(0),
107                overrides: WorkflowState::new(Map::from_iter([("count".into(), json!(40))]))?,
108                position: ResumePosition::AfterCompleted,
109                fallback: None,
110            })
111            .await?;
112        let parked = fork.snapshot().await?;
113        if !matches!(parked.status, RunStatus::Paused)
114            || parked.checkpoint != 0
115            || parked.state.values()["count"] != 40
116            || !parked.outcomes.is_empty()
117        {
118            return Err(
119                AppError::Integration("restore did not create a parked clean copy".into()).into(),
120            );
121        }
122
123        source.delete(completed.checkpoint).await?;
124        if !matches!(source.snapshot().await, Err(AppError::NotFound(_))) {
125            return Err(
126                AppError::Integration("explicit source deletion did not finish".into()).into(),
127            );
128        }
129        // Deletion does not invalidate the copied state. AfterCompleted skips
130        // the first increment, so only the second changes the override: 40 -> 41.
131        let completed = fork
132            .execution(ExecutionOptions::default())
133            .await?
134            .finish()
135            .await?;
136        if !matches!(completed.status, RunStatus::Succeeded)
137            || completed.state.values()["count"] != 41
138            || completed.last_action_ordinal != Some(1)
139        {
140            return Err(
141                AppError::Integration("fork resumed from the wrong boundary".into()).into(),
142            );
143        }
144        println!("Source count: 2; copied count: 40; completed fork count: 41");
145        Ok((
146            workspace.id().clone(),
147            source_selector,
148            RunSelector::Exact {
149                id: fork.id().clone(),
150                occurrence: completed.occurrence,
151            },
152        ))
153    }
154
155    async fn read_reopened(
156        &self,
157        app: &App<StateStore, Agent>,
158        workspace: WorkspaceId,
159        source: RunSelector,
160        fork: RunSelector,
161    ) -> Result<(), AppError> {
162        let workspace = app.workspace(workspace).await?;
163        let workflow = workspace.workflow("history-counter".parse()?).await?;
164        let fork = workflow.open_run(fork).await?;
165        let committed = fork.checkpoint(CheckpointSelector::Action(1)).await?;
166        let lineage = committed
167            .lineage
168            .as_ref()
169            .ok_or_else(|| AppError::Integration("reopened fork lost its source lineage".into()))?;
170        if committed.state.values()["count"] != 41
171            || !matches!(&source, RunSelector::Exact { id, occurrence }
172                if &lineage.source == id && &lineage.source_occurrence == occurrence)
173            || lineage.action_ordinal != Some(0)
174        {
175            return Err(AppError::Integration(
176                "reopened fork history changed".into(),
177            ));
178        }
179        println!(
180            "Reopened fork {} at count 41; source lineage remains readable",
181            fork.id()
182        );
183        Ok(())
184    }
examples/actions_and_inputs.rs (line 84)
23    async fn run() -> Result<(), Box<dyn Error + Send + Sync>> {
24        let definition = WorkflowDefinition::parse(
25            &json!({
26                "schema":"app-kit.workflow/v2", "id":"invoice", "revision":"r1",
27                "state":{"unit_price":7,"total":0}, "required":["unit_price","total"],
28                "reducers":{"unit_price":"reject_concurrent","total":"reject_concurrent"},
29                "entry":"calculate",
30                "nodes":[{"id":"calculate","step":{
31                    "kind":"action","action":"invoice","revision":"r1","project_result":false
32                }}], "routes":{}
33            })
34            .to_string(),
35        )?;
36        let store =
37            StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
38        // Native caller actions need no Agent value or provider service.
39        let mut app = App::open(
40            AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition])
41                .with_actions(vec![InvoiceAction::new()]),
42        )
43        .await?;
44        let observed = tokio::time::timeout(Duration::from_secs(15), async {
45            let workspace = app
46                .create_workspace(WorkspaceConfig {
47                    name: Some("invoice example".into()),
48                    root: PathBuf::from(env!("CARGO_MANIFEST_DIR")),
49                })
50                .await?;
51            let workflow = workspace.workflow("invoice".parse()?).await?;
52            let mut run = workflow.prepare(WorkflowState::default()).await?;
53            let mut execution = run
54                .execution(ExecutionOptions {
55                    inputs: Map::from_iter([
56                        ("quantity".into(), json!(6)),
57                        ("trace".into(), json!("sample")),
58                    ]),
59                    ..ExecutionOptions::default()
60                })
61                .await?;
62            let Some(ExecutionEvent::Partial { result, .. }) = execution.next().await? else {
63                return Err(AppError::Integration(
64                    "expected the real action partial".into(),
65                ));
66            };
67            if result.get("progress") != Some(&json!("calculated")) {
68                return Err(AppError::Integration("partial changed".into()));
69            }
70            let Some(ExecutionEvent::Committed { outcomes, .. }) = execution.step().await? else {
71                return Err(AppError::Integration(
72                    "expected one committed boundary".into(),
73                ));
74            };
75            let expected =
76                json!({"quantity":6,"label":"order","currency":"USD","trace":"sample","total":42});
77            if outcomes.len() != 1 || Value::Object(outcomes[0].result.clone()) != expected {
78                return Err(AppError::Integration(
79                    "resolved inputs or action result changed".into(),
80                ));
81            }
82            let terminal = execution.finish().await?;
83            if !matches!(terminal.status, RunStatus::Succeeded)
84                || terminal.state.values()
85                    != json!({"unit_price":7,"total":42}).as_object().unwrap()
86            {
87                return Err(AppError::Integration(
88                    "only the explicit total write should persist".into(),
89                ));
90            }
91            println!(
92                "Partial: calculated; committed total: 42; transient inputs stayed out of state"
93            );
94            Ok::<(), AppError>(())
95        })
96        .await;
97        let closed = tokio::time::timeout(
98            Duration::from_secs(15),
99            app.shutdown(Shutdown::CancelAndDrain {
100                timeout: Duration::from_secs(10),
101            }),
102        )
103        .await;
104        drop(app);
105        observed.map_err(|_| AppError::Timeout("invoice execution timed out".into()))??;
106        closed.map_err(|_| AppError::Timeout("invoice cleanup timed out".into()))??;
107        Ok(())
108    }
Source

pub fn apply<T: StateTyping>( &self, change: &StateChange, typing: &T, ) -> Result<Self, AppError>

Produces a new state with the pinned operation-specific validation order.

Primitive changes run pre/post transition validation, then full-state validation and roundtrip. Merge and subset construct new snapshots and run only full-state validation and roundtrip. Any failure leaves this state and its retained clones unchanged.

use app_kit::workflow::{JsonState, StateChange, WorkflowState};
use serde_json::{Map, Value};
let state = WorkflowState::new(Map::from_iter([
("count".into(), Value::from("unknown")),
]))?;
let change = StateChange::Increment {
values: vec![("count".into(), Value::from(1))],
};
assert!(state.apply(&change, &JsonState).is_err());
assert_eq!(state.values()["count"], "unknown");
§Errors

Returns InvalidInput for incompatible operand types, signed-integer delta or range errors, invalid state, or excessive operation payloads. Caller typing errors propagate unchanged; failure never mutates the original state.

§Panics

Panics if the caller-provided typing implementation panics. Direct state operations do not contain caller panics; App admission/execution has its own boundary for classifying them.

Examples found in repository?
examples/state.rs (lines 61-66)
55fn main() -> Result<(), AppError> {
56    let policy = CountPolicy { maximum: 100 };
57    let initial = WorkflowState::new(Map::from_iter([
58        ("count".into(), json!(1)),
59        ("labels".into(), json!([])),
60    ]))?;
61    let counted = initial.apply(
62        &StateChange::Increment {
63            values: vec![("count".into(), json!(3)), ("count".into(), json!(4))],
64        },
65        &policy,
66    )?;
67    let labeled = counted.apply(
68        &StateChange::Append {
69            values: vec![
70                ("labels".into(), json!("draft")),
71                ("labels".into(), json!("queued")),
72            ],
73        },
74        &policy,
75    )?;
76    assert_eq!(initial.values()["count"], 1);
77    assert_eq!(labeled.values()["count"], 8);
78    assert_eq!(labeled.values()["labels"], json!(["draft", "queued"]));
79
80    // A later invalid entry rejects the complete operation, including count + 1.
81    let rejected = labeled.apply(
82        &StateChange::Increment {
83            values: vec![("count".into(), json!(1)), ("labels".into(), json!(1))],
84        },
85        &policy,
86    );
87    assert!(matches!(rejected, Err(AppError::InvalidInput(_))));
88    assert_eq!(labeled.values()["count"], 8);
89
90    // This registration is process-global. This standalone example owns its
91    // process; an application should choose registrations during its setup.
92    StateSerialization::global().register_field(
93        "labels".into(),
94        |value| Ok::<_, &'static str>(json!({"version": 1, "items": value})),
95        |value| value.get("items").cloned().ok_or("missing labels.items"),
96    )?;
97    let encoded = labeled.serialize()?;
98    assert_eq!(
99        encoded["labels"],
100        json!({"version":1,"items":["draft","queued"]})
101    );
102    let decoded = WorkflowState::deserialize(encoded.clone())?;
103    assert_eq!(decoded, labeled);
104
105    StateSerialization::global().register_tag("example-money".into(), |value| {
106        value.get("minor").cloned().ok_or("missing money.minor")
107    })?;
108    let tagged = WorkflowState::deserialize(Map::from_iter([(
109        "amount".into(),
110        json!({"__burr_serde__":"example-money","minor":120}),
111    )]))?;
112    assert_eq!(tagged.values()["amount"], 120);
113    println!("{}", Value::Object(encoded));
114    Ok(())
115}

Trait Implementations§

Source§

impl Clone for WorkflowState

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for WorkflowState

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for WorkflowState

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl PartialEq for WorkflowState

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for WorkflowState

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Allocation for T
where T: RefUnwindSafe + Send + Sync,

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> ArchivePointee for T

Source§

type ArchivedMetadata = ()

The archived version of the pointer metadata for this type.
Source§

fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata

Converts some archived metadata to the pointer metadata for itself.
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Conv for T

Source§

fn conv<T>(self) -> T
where Self: Into<T>,

Converts self into T using Into<T>. Read more
Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> FmtForward for T

Source§

fn fmt_binary(self) -> FmtBinary<Self>
where Self: Binary,

Causes self to use its Binary implementation when Debug-formatted.
Source§

fn fmt_display(self) -> FmtDisplay<Self>
where Self: Display,

Causes self to use its Display implementation when Debug-formatted.
Source§

fn fmt_lower_exp(self) -> FmtLowerExp<Self>
where Self: LowerExp,

Causes self to use its LowerExp implementation when Debug-formatted.
Source§

fn fmt_lower_hex(self) -> FmtLowerHex<Self>
where Self: LowerHex,

Causes self to use its LowerHex implementation when Debug-formatted.
Source§

fn fmt_octal(self) -> FmtOctal<Self>
where Self: Octal,

Causes self to use its Octal implementation when Debug-formatted.
Source§

fn fmt_pointer(self) -> FmtPointer<Self>
where Self: Pointer,

Causes self to use its Pointer implementation when Debug-formatted.
Source§

fn fmt_upper_exp(self) -> FmtUpperExp<Self>
where Self: UpperExp,

Causes self to use its UpperExp implementation when Debug-formatted.
Source§

fn fmt_upper_hex(self) -> FmtUpperHex<Self>
where Self: UpperHex,

Causes self to use its UpperHex implementation when Debug-formatted.
Source§

fn fmt_list(self) -> FmtList<Self>
where &'a Self: for<'a> IntoIterator,

Formats each item in a sequence. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<L> LayerExt<L> for L

Source§

fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>
where L: Layer<S>,

Applies the layer to a service and wraps it in Layered.
Source§

impl<T> LayoutRaw for T

Source§

fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>

Returns the layout of the type.
Source§

impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
where T: SharedNiching<N1, N2>, N1: Niching<T>, N2: Niching<T>,

Source§

unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool

Returns whether the given value has been niched. Read more
Source§

fn resolve_niched(out: Place<NichedOption<T, N1>>)

Writes data to out indicating that a T is niched.
Source§

impl<T> Pipe for T
where T: ?Sized,

Source§

fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> R
where Self: Sized,

Pipes by value. This is generally the method you want to use. Read more
Source§

fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> R
where R: 'a,

Borrows self and passes that borrow into the pipe function. Read more
Source§

fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> R
where R: 'a,

Mutably borrows self and passes that borrow into the pipe function. Read more
Source§

fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
where Self: Borrow<B>, B: 'a + ?Sized, R: 'a,

Borrows self, then passes self.borrow() into the pipe function. Read more
Source§

fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
where Self: BorrowMut<B>, B: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.borrow_mut() into the pipe function. Read more
Source§

fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
where Self: AsRef<U>, U: 'a + ?Sized, R: 'a,

Borrows self, then passes self.as_ref() into the pipe function.
Source§

fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
where Self: AsMut<U>, U: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.as_mut() into the pipe function.
Source§

fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
where Self: Deref<Target = T>, T: 'a + ?Sized, R: 'a,

Borrows self, then passes self.deref() into the pipe function.
Source§

fn pipe_deref_mut<'a, T, R>( &'a mut self, func: impl FnOnce(&'a mut T) -> R, ) -> R
where Self: DerefMut<Target = T> + Deref, T: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.deref_mut() into the pipe function.
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Pointee for T

Source§

type Metadata = ()

The metadata type for pointers and references to this type.
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> Tap for T

Source§

fn tap(self, func: impl FnOnce(&Self)) -> Self

Immutable access to a value. Read more
Source§

fn tap_mut(self, func: impl FnOnce(&mut Self)) -> Self

Mutable access to a value. Read more
Source§

fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
where Self: Borrow<B>, B: ?Sized,

Immutable access to the Borrow<B> of a value. Read more
Source§

fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
where Self: BorrowMut<B>, B: ?Sized,

Mutable access to the BorrowMut<B> of a value. Read more
Source§

fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
where Self: AsRef<R>, R: ?Sized,

Immutable access to the AsRef<R> view of a value. Read more
Source§

fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
where Self: AsMut<R>, R: ?Sized,

Mutable access to the AsMut<R> view of a value. Read more
Source§

fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
where Self: Deref<Target = T>, T: ?Sized,

Immutable access to the Deref::Target of a value. Read more
Source§

fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
where Self: DerefMut<Target = T> + Deref, T: ?Sized,

Mutable access to the Deref::Target of a value. Read more
Source§

fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self

Calls .tap() only in debug builds, and is erased in release builds.
Source§

fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self

Calls .tap_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
where Self: Borrow<B>, B: ?Sized,

Calls .tap_borrow() only in debug builds, and is erased in release builds.
Source§

fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
where Self: BorrowMut<B>, B: ?Sized,

Calls .tap_borrow_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
where Self: AsRef<R>, R: ?Sized,

Calls .tap_ref() only in debug builds, and is erased in release builds.
Source§

fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
where Self: AsMut<R>, R: ?Sized,

Calls .tap_ref_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
where Self: Deref<Target = T>, T: ?Sized,

Calls .tap_deref() only in debug builds, and is erased in release builds.
Source§

fn tap_deref_mut_dbg<T>(self, func: impl FnOnce(&mut T)) -> Self
where Self: DerefMut<Target = T> + Deref, T: ?Sized,

Calls .tap_deref_mut() only in debug builds, and is erased in release builds.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> TryConv for T

Source§

fn try_conv<T>(self) -> Result<T, Self::Error>
where Self: TryInto<T>,

Attempts to convert self into T using TryInto<T>. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more