Skip to main content

oris_kernel/kernel/
execution_step.rs

1//! Execution step contract: canonical interface for one step of execution.
2//!
3//! This module defines the **frozen** execution contract so that adapters (graph, agent, etc.)
4//! interact with the kernel only through explicit inputs and outputs. No hidden runtime
5//! mutations or async side effects inside the boundary; all effects are captured in
6//! [StepResult].
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::kernel::state::KernelState;
12use crate::kernel::step::Next;
13use crate::kernel::KernelError;
14
15/// Explicit input for one execution step.
16///
17/// All inputs to a step are declared here so the boundary is pure and replayable.
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub enum ExecutionStepInput {
20    /// Initial step (no prior resume or signal).
21    Initial,
22    /// Resuming after an interrupt; payload is the resolved value.
23    Resume(Value),
24    /// External signal (e.g. named channel).
25    Signal { name: String, value: Value },
26}
27
28impl ExecutionStepInput {
29    /// Validates that the input is well-formed (e.g. Resume has a value).
30    pub fn validate(&self) -> Result<(), KernelError> {
31        match self {
32            ExecutionStepInput::Initial => Ok(()),
33            ExecutionStepInput::Resume(_) => Ok(()),
34            ExecutionStepInput::Signal { name, .. } => {
35                if name.is_empty() {
36                    return Err(KernelError::Driver(
37                        "ExecutionStepInput::Signal name must be non-empty".into(),
38                    ));
39                }
40                Ok(())
41            }
42        }
43    }
44}
45
46/// Result of one execution step: the only way a step can affect the run.
47///
48/// This is the canonical output type of the execution contract. Adapters must not
49/// mutate state or trigger side effects except by returning a [StepResult] that
50/// the driver then applies (emit events, do action, interrupt, complete).
51pub type StepResult = Next;
52
53/// Canonical execution step trait: pure boundary between kernel and adapters.
54///
55/// Implementations must:
56/// - Take only `state` and `input` as inputs (no hidden mutable state, no I/O).
57/// - Return only a [StepResult] (or error); all effects go through the result.
58///
59/// The driver calls `execute(state, input)` and applies the returned [StepResult];
60/// adapters (e.g. graph, agent) implement this trait and interact solely through it.
61pub trait ExecutionStep<S: KernelState>: Send + Sync {
62    /// Performs one step: given current state and explicit input, returns the next outcome.
63    ///
64    /// Pure boundary: no async, no hidden mutations. Inputs and outputs are explicit.
65    fn execute(&self, state: &S, input: &ExecutionStepInput) -> Result<StepResult, KernelError>;
66}
67
68/// Blanket impl: any [crate::kernel::step::StepFn] is an [ExecutionStep] with input ignored.
69///
70/// This keeps existing step functions (e.g. [crate::graph::step_adapter::GraphStepFnAdapter])
71/// valid under the frozen contract; they receive [ExecutionStepInput::Initial] each time.
72impl<S, F> ExecutionStep<S> for F
73where
74    S: KernelState,
75    F: crate::kernel::step::StepFn<S>,
76{
77    fn execute(&self, state: &S, _input: &ExecutionStepInput) -> Result<StepResult, KernelError> {
78        self.next(state)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::kernel::event::Event;
86    use crate::kernel::state::KernelState;
87    use crate::kernel::step::StepFn;
88
89    #[derive(Clone, Debug)]
90    struct TestState(u32);
91    impl KernelState for TestState {
92        fn version(&self) -> u32 {
93            1
94        }
95    }
96
97    #[test]
98    fn execution_step_input_initial_validates() {
99        let input = ExecutionStepInput::Initial;
100        assert!(input.validate().is_ok());
101    }
102
103    #[test]
104    fn execution_step_input_resume_validates() {
105        let input = ExecutionStepInput::Resume(serde_json::json!({"ok": true}));
106        assert!(input.validate().is_ok());
107    }
108
109    #[test]
110    fn execution_step_input_signal_empty_name_invalid() {
111        let input = ExecutionStepInput::Signal {
112            name: String::new(),
113            value: serde_json::json!(null),
114        };
115        assert!(input.validate().is_err());
116    }
117
118    #[test]
119    fn execution_step_input_signal_non_empty_name_validates() {
120        let input = ExecutionStepInput::Signal {
121            name: "evt".to_string(),
122            value: serde_json::json!(1),
123        };
124        assert!(input.validate().is_ok());
125    }
126
127    struct StepThatEmits;
128    impl StepFn<TestState> for StepThatEmits {
129        fn next(&self, state: &TestState) -> Result<Next, KernelError> {
130            Ok(Next::Emit(vec![Event::StateUpdated {
131                step_id: None,
132                payload: serde_json::json!({ "n": state.0 }),
133            }]))
134        }
135    }
136
137    #[test]
138    fn step_fn_impls_execution_step() {
139        let step = StepThatEmits;
140        let state = TestState(42);
141        let input = ExecutionStepInput::Initial;
142        let result = step.execute(&state, &input).unwrap();
143        match result {
144            Next::Emit(evs) => {
145                assert_eq!(evs.len(), 1);
146            }
147            _ => panic!("expected Emit"),
148        }
149    }
150}