Skip to main content

ff_core/
state.rs

1use serde::{Deserialize, Serialize};
2
3// ── Dimension A — Lifecycle Phase ──
4
5/// What major phase of existence is this execution in?
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum LifecyclePhase {
9    /// Accepted by engine, not yet resolved to runnable/delayed. Transient.
10    Submitted,
11    /// Eligible or potentially eligible for claiming.
12    Runnable,
13    /// Currently owned by a worker lease and in progress.
14    Active,
15    /// Intentionally paused, waiting for signal/approval/callback.
16    Suspended,
17    /// Execution is finished. No further state transitions except replay.
18    Terminal,
19}
20
21// ── Dimension B — Ownership State ──
22
23/// Who, if anyone, is currently allowed to mutate active execution state?
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum OwnershipState {
27    /// No current lease.
28    Unowned,
29    /// A worker holds a valid lease.
30    Leased,
31    /// Lease TTL passed without renewal. Execution awaits reclaim.
32    LeaseExpiredReclaimable,
33    /// Lease was explicitly revoked by operator or engine.
34    LeaseRevoked,
35}
36
37// ── Dimension C — Eligibility State ──
38
39/// Can the execution be claimed for work right now?
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum EligibilityState {
43    /// Ready for claiming.
44    EligibleNow,
45    /// Delayed until a future timestamp.
46    NotEligibleUntilTime,
47    /// Waiting on upstream executions in a flow/DAG.
48    BlockedByDependencies,
49    /// Budget limit reached.
50    BlockedByBudget,
51    /// Quota or rate-limit reached.
52    BlockedByQuota,
53    /// No capable/available worker matches requirements.
54    BlockedByRoute,
55    /// Lane is paused or draining.
56    BlockedByLaneState,
57    /// Operator hold.
58    BlockedByOperator,
59    /// Used when lifecycle_phase is active, suspended, or terminal.
60    NotApplicable,
61}
62
63// ── Dimension D — Blocking Reason ──
64
65/// What is the most specific explanation for lack of forward progress?
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum BlockingReason {
69    /// Not blocked.
70    None,
71    /// Eligible but no worker has claimed yet.
72    WaitingForWorker,
73    /// Delayed for retry backoff.
74    WaitingForRetryBackoff,
75    /// Delayed after suspension resume.
76    WaitingForResumeDelay,
77    /// Worker-initiated explicit delay.
78    WaitingForDelay,
79    /// Suspended, waiting for a generic signal.
80    WaitingForSignal,
81    /// Suspended, waiting for human approval.
82    WaitingForApproval,
83    /// Suspended, waiting for external callback.
84    WaitingForCallback,
85    /// Suspended, waiting for tool completion.
86    WaitingForToolResult,
87    /// Blocked on child/dependency executions.
88    WaitingForChildren,
89    /// Budget exhausted.
90    WaitingForBudget,
91    /// Quota/rate-limit window full.
92    WaitingForQuota,
93    /// No worker with required capabilities available.
94    WaitingForCapableWorker,
95    /// No worker in required region/locality.
96    WaitingForLocalityMatch,
97    /// Operator placed a hold.
98    PausedByOperator,
99    /// Policy rule prevents progress (e.g. lane pause).
100    PausedByPolicy,
101    /// Flow cancellation with let_active_finish blocked this unclaimed member.
102    PausedByFlowCancel,
103}
104
105// ── Dimension E — Terminal Outcome ──
106
107/// If the execution is terminal, how did it end?
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum TerminalOutcome {
111    /// Not terminal.
112    None,
113    /// Completed successfully with result.
114    Success,
115    /// Failed after exhausting retries or by explicit failure.
116    Failed,
117    /// Intentionally terminated by user, operator, or policy.
118    Cancelled,
119    /// Deadline, TTL, or suspension timeout elapsed.
120    Expired,
121    /// Required dependency failed, making this execution impossible.
122    Skipped,
123}
124
125// ── Dimension F — Attempt State ──
126
127/// What is happening at the concrete run-attempt layer?
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
129#[serde(rename_all = "snake_case")]
130pub enum AttemptState {
131    /// No attempt context (e.g. freshly submitted, or skipped before any attempt).
132    None,
133    /// Awaiting initial claim.
134    PendingFirstAttempt,
135    /// An attempt is actively executing.
136    RunningAttempt,
137    /// Current attempt was interrupted (crash, reclaim, suspension, delay, waiting children).
138    AttemptInterrupted,
139    /// Awaiting retry after failure.
140    PendingRetryAttempt,
141    /// Awaiting replay after terminal state.
142    PendingReplayAttempt,
143    /// The final attempt has concluded.
144    AttemptTerminal,
145}
146
147// ── Derived: Public State ──
148
149/// Engine-computed user-facing label. Derived from the state vector.
150#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub enum PublicState {
153    /// Eligible and waiting for a worker to claim.
154    Waiting,
155    /// Not yet eligible due to time-based delay.
156    Delayed,
157    /// Blocked by budget, quota, or rate-limit policy.
158    RateLimited,
159    /// Blocked on child/dependency executions.
160    WaitingChildren,
161    /// Currently being processed by a worker.
162    Active,
163    /// Intentionally paused, waiting for signal/approval/callback.
164    Suspended,
165    /// Terminal: finished successfully.
166    Completed,
167    /// Terminal: finished unsuccessfully.
168    Failed,
169    /// Terminal: intentionally terminated.
170    Cancelled,
171    /// Terminal: deadline/TTL elapsed.
172    Expired,
173    /// Terminal: impossible to run because required dependency failed.
174    Skipped,
175}
176
177// ── State Vector ──
178
179/// The full 6+1 dimension execution state vector.
180#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
181pub struct StateVector {
182    pub lifecycle_phase: LifecyclePhase,
183    pub ownership_state: OwnershipState,
184    pub eligibility_state: EligibilityState,
185    pub blocking_reason: BlockingReason,
186    pub terminal_outcome: TerminalOutcome,
187    pub attempt_state: AttemptState,
188    /// Engine-derived user-facing label.
189    pub public_state: PublicState,
190}
191
192impl StateVector {
193    /// Derive public_state from the other 6 dimensions per RFC-001 §2.4.
194    ///
195    /// Never panics. In distributed systems, constraint violations can occur
196    /// via partial writes or reconciler drift. Impossible combinations log a
197    /// warning and return a safe fallback instead of crashing.
198    pub fn derive_public_state(&self) -> PublicState {
199        match self.lifecycle_phase {
200            LifecyclePhase::Terminal => match self.terminal_outcome {
201                TerminalOutcome::Success => PublicState::Completed,
202                TerminalOutcome::Failed => PublicState::Failed,
203                TerminalOutcome::Cancelled => PublicState::Cancelled,
204                TerminalOutcome::Expired => PublicState::Expired,
205                TerminalOutcome::Skipped => PublicState::Skipped,
206                TerminalOutcome::None => {
207                    // V4 violation: terminal without outcome. Corrupt state —
208                    // surface as Failed so operators notice and investigate.
209                    // No logging here (ff-core has no tracing dep); callers
210                    // detect this via is_consistent() returning false.
211                    PublicState::Failed
212                }
213            },
214            LifecyclePhase::Suspended => PublicState::Suspended,
215            LifecyclePhase::Active => PublicState::Active,
216            LifecyclePhase::Runnable => match self.eligibility_state {
217                EligibilityState::EligibleNow => PublicState::Waiting,
218                EligibilityState::NotEligibleUntilTime => PublicState::Delayed,
219                EligibilityState::BlockedByDependencies => PublicState::WaitingChildren,
220                EligibilityState::BlockedByBudget | EligibilityState::BlockedByQuota => {
221                    PublicState::RateLimited
222                }
223                EligibilityState::BlockedByRoute
224                | EligibilityState::BlockedByLaneState
225                | EligibilityState::BlockedByOperator => PublicState::Waiting,
226                EligibilityState::NotApplicable => {
227                    // Constraint violation: runnable should not have not_applicable.
228                    // Surface as Waiting — the index reconciler will correct this.
229                    PublicState::Waiting
230                }
231            },
232            LifecyclePhase::Submitted => PublicState::Waiting,
233        }
234    }
235
236    /// Check if the stored public_state matches the derived value.
237    pub fn is_consistent(&self) -> bool {
238        self.public_state == self.derive_public_state()
239    }
240}
241
242// ── Attempt Lifecycle (RFC-002) ──
243
244/// Per-attempt lifecycle states.
245#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
246#[serde(rename_all = "snake_case")]
247pub enum AttemptLifecycle {
248    /// Attempt record exists; worker has not yet acquired a lease.
249    Created,
250    /// Worker has acquired a lease and is actively executing.
251    Started,
252    /// Attempt is paused because the execution intentionally suspended.
253    Suspended,
254    /// Attempt completed successfully.
255    EndedSuccess,
256    /// Attempt failed.
257    EndedFailure,
258    /// Attempt was cancelled.
259    EndedCancelled,
260    /// Attempt was interrupted by lease expiry/revocation and the execution was reclaimed.
261    InterruptedReclaimed,
262}
263
264impl AttemptLifecycle {
265    /// Whether this attempt lifecycle state is terminal.
266    pub fn is_terminal(self) -> bool {
267        matches!(
268            self,
269            Self::EndedSuccess
270                | Self::EndedFailure
271                | Self::EndedCancelled
272                | Self::InterruptedReclaimed
273        )
274    }
275}
276
277// ── Attempt Type (RFC-002) ──
278
279/// Why this attempt was created.
280#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum AttemptType {
283    /// First attempt for this execution.
284    Initial,
285    /// Retry after a failed attempt.
286    Retry,
287    /// Reclaim after lease expiry or revocation.
288    Reclaim,
289    /// Replay of a terminal execution.
290    Replay,
291    /// Fallback progression to next provider/model.
292    Fallback,
293}
294
295// ── Lane State (RFC-009) ──
296
297/// Lane operational state.
298#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
299#[serde(rename_all = "snake_case")]
300pub enum LaneState {
301    /// Lane is accepting and processing work.
302    Active,
303    /// Lane is accepting submissions but not claiming (paused).
304    Paused,
305    /// Lane is not accepting new submissions but processing existing.
306    Draining,
307    /// Lane is disabled.
308    Disabled,
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn derive_public_state_terminal_success() {
317        let sv = StateVector {
318            lifecycle_phase: LifecyclePhase::Terminal,
319            ownership_state: OwnershipState::Unowned,
320            eligibility_state: EligibilityState::NotApplicable,
321            blocking_reason: BlockingReason::None,
322            terminal_outcome: TerminalOutcome::Success,
323            attempt_state: AttemptState::AttemptTerminal,
324            public_state: PublicState::Completed,
325        };
326        assert_eq!(sv.derive_public_state(), PublicState::Completed);
327        assert!(sv.is_consistent());
328    }
329
330    #[test]
331    fn derive_public_state_active() {
332        let sv = StateVector {
333            lifecycle_phase: LifecyclePhase::Active,
334            ownership_state: OwnershipState::Leased,
335            eligibility_state: EligibilityState::NotApplicable,
336            blocking_reason: BlockingReason::None,
337            terminal_outcome: TerminalOutcome::None,
338            attempt_state: AttemptState::RunningAttempt,
339            public_state: PublicState::Active,
340        };
341        assert_eq!(sv.derive_public_state(), PublicState::Active);
342        assert!(sv.is_consistent());
343    }
344
345    #[test]
346    fn derive_public_state_active_lease_expired_still_active() {
347        // D3: Active dominates ownership nuance
348        let sv = StateVector {
349            lifecycle_phase: LifecyclePhase::Active,
350            ownership_state: OwnershipState::LeaseExpiredReclaimable,
351            eligibility_state: EligibilityState::NotApplicable,
352            blocking_reason: BlockingReason::None,
353            terminal_outcome: TerminalOutcome::None,
354            attempt_state: AttemptState::AttemptInterrupted,
355            public_state: PublicState::Active,
356        };
357        assert_eq!(sv.derive_public_state(), PublicState::Active);
358    }
359
360    #[test]
361    fn derive_public_state_runnable_eligible() {
362        let sv = StateVector {
363            lifecycle_phase: LifecyclePhase::Runnable,
364            ownership_state: OwnershipState::Unowned,
365            eligibility_state: EligibilityState::EligibleNow,
366            blocking_reason: BlockingReason::WaitingForWorker,
367            terminal_outcome: TerminalOutcome::None,
368            attempt_state: AttemptState::PendingFirstAttempt,
369            public_state: PublicState::Waiting,
370        };
371        assert_eq!(sv.derive_public_state(), PublicState::Waiting);
372    }
373
374    #[test]
375    fn derive_public_state_delayed() {
376        let sv = StateVector {
377            lifecycle_phase: LifecyclePhase::Runnable,
378            ownership_state: OwnershipState::Unowned,
379            eligibility_state: EligibilityState::NotEligibleUntilTime,
380            blocking_reason: BlockingReason::WaitingForRetryBackoff,
381            terminal_outcome: TerminalOutcome::None,
382            attempt_state: AttemptState::PendingRetryAttempt,
383            public_state: PublicState::Delayed,
384        };
385        assert_eq!(sv.derive_public_state(), PublicState::Delayed);
386    }
387
388    #[test]
389    fn derive_public_state_waiting_children() {
390        let sv = StateVector {
391            lifecycle_phase: LifecyclePhase::Runnable,
392            ownership_state: OwnershipState::Unowned,
393            eligibility_state: EligibilityState::BlockedByDependencies,
394            blocking_reason: BlockingReason::WaitingForChildren,
395            terminal_outcome: TerminalOutcome::None,
396            attempt_state: AttemptState::PendingFirstAttempt,
397            public_state: PublicState::WaitingChildren,
398        };
399        assert_eq!(sv.derive_public_state(), PublicState::WaitingChildren);
400    }
401
402    #[test]
403    fn derive_public_state_rate_limited() {
404        let sv = StateVector {
405            lifecycle_phase: LifecyclePhase::Runnable,
406            ownership_state: OwnershipState::Unowned,
407            eligibility_state: EligibilityState::BlockedByBudget,
408            blocking_reason: BlockingReason::WaitingForBudget,
409            terminal_outcome: TerminalOutcome::None,
410            attempt_state: AttemptState::PendingFirstAttempt,
411            public_state: PublicState::RateLimited,
412        };
413        assert_eq!(sv.derive_public_state(), PublicState::RateLimited);
414    }
415
416    #[test]
417    fn derive_public_state_suspended() {
418        let sv = StateVector {
419            lifecycle_phase: LifecyclePhase::Suspended,
420            ownership_state: OwnershipState::Unowned,
421            eligibility_state: EligibilityState::NotApplicable,
422            blocking_reason: BlockingReason::WaitingForApproval,
423            terminal_outcome: TerminalOutcome::None,
424            attempt_state: AttemptState::AttemptInterrupted,
425            public_state: PublicState::Suspended,
426        };
427        assert_eq!(sv.derive_public_state(), PublicState::Suspended);
428    }
429
430    #[test]
431    fn derive_public_state_submitted_collapses_to_waiting() {
432        let sv = StateVector {
433            lifecycle_phase: LifecyclePhase::Submitted,
434            ownership_state: OwnershipState::Unowned,
435            eligibility_state: EligibilityState::NotApplicable,
436            blocking_reason: BlockingReason::None,
437            terminal_outcome: TerminalOutcome::None,
438            attempt_state: AttemptState::None,
439            public_state: PublicState::Waiting,
440        };
441        assert_eq!(sv.derive_public_state(), PublicState::Waiting);
442    }
443
444    #[test]
445    fn derive_public_state_skipped() {
446        let sv = StateVector {
447            lifecycle_phase: LifecyclePhase::Terminal,
448            ownership_state: OwnershipState::Unowned,
449            eligibility_state: EligibilityState::NotApplicable,
450            blocking_reason: BlockingReason::None,
451            terminal_outcome: TerminalOutcome::Skipped,
452            attempt_state: AttemptState::None,
453            public_state: PublicState::Skipped,
454        };
455        assert_eq!(sv.derive_public_state(), PublicState::Skipped);
456    }
457
458    #[test]
459    fn attempt_lifecycle_terminal_check() {
460        assert!(AttemptLifecycle::EndedSuccess.is_terminal());
461        assert!(AttemptLifecycle::EndedFailure.is_terminal());
462        assert!(AttemptLifecycle::EndedCancelled.is_terminal());
463        assert!(AttemptLifecycle::InterruptedReclaimed.is_terminal());
464        assert!(!AttemptLifecycle::Created.is_terminal());
465        assert!(!AttemptLifecycle::Started.is_terminal());
466        assert!(!AttemptLifecycle::Suspended.is_terminal());
467    }
468
469    #[test]
470    fn serde_roundtrip_lifecycle_phase() {
471        let phase = LifecyclePhase::Active;
472        let json = serde_json::to_string(&phase).unwrap();
473        assert_eq!(json, "\"active\"");
474        let parsed: LifecyclePhase = serde_json::from_str(&json).unwrap();
475        assert_eq!(parsed, phase);
476    }
477
478    #[test]
479    fn serde_roundtrip_blocking_reason() {
480        let reason = BlockingReason::PausedByFlowCancel;
481        let json = serde_json::to_string(&reason).unwrap();
482        assert_eq!(json, "\"paused_by_flow_cancel\"");
483        let parsed: BlockingReason = serde_json::from_str(&json).unwrap();
484        assert_eq!(parsed, reason);
485    }
486
487    #[test]
488    fn serde_roundtrip_state_vector() {
489        let sv = StateVector {
490            lifecycle_phase: LifecyclePhase::Active,
491            ownership_state: OwnershipState::Leased,
492            eligibility_state: EligibilityState::NotApplicable,
493            blocking_reason: BlockingReason::None,
494            terminal_outcome: TerminalOutcome::None,
495            attempt_state: AttemptState::RunningAttempt,
496            public_state: PublicState::Active,
497        };
498        let json = serde_json::to_string(&sv).unwrap();
499        let parsed: StateVector = serde_json::from_str(&json).unwrap();
500        assert_eq!(sv, parsed);
501    }
502}