1use serde::{Deserialize, Serialize};
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum LifecyclePhase {
9 Submitted,
11 Runnable,
13 Active,
15 Suspended,
17 Terminal,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum OwnershipState {
27 Unowned,
29 Leased,
31 LeaseExpiredReclaimable,
33 LeaseRevoked,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum EligibilityState {
43 EligibleNow,
45 NotEligibleUntilTime,
47 BlockedByDependencies,
49 BlockedByBudget,
51 BlockedByQuota,
53 BlockedByRoute,
55 BlockedByLaneState,
57 BlockedByOperator,
59 NotApplicable,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum BlockingReason {
69 None,
71 WaitingForWorker,
73 WaitingForRetryBackoff,
75 WaitingForResumeDelay,
77 WaitingForDelay,
79 WaitingForSignal,
81 WaitingForApproval,
83 WaitingForCallback,
85 WaitingForToolResult,
87 WaitingForChildren,
89 WaitingForBudget,
91 WaitingForQuota,
93 WaitingForCapableWorker,
95 WaitingForLocalityMatch,
97 PausedByOperator,
99 PausedByPolicy,
101 PausedByFlowCancel,
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum TerminalOutcome {
111 None,
113 Success,
115 Failed,
117 Cancelled,
119 Expired,
121 Skipped,
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
129#[serde(rename_all = "snake_case")]
130pub enum AttemptState {
131 None,
133 PendingFirstAttempt,
135 RunningAttempt,
137 AttemptInterrupted,
139 PendingRetryAttempt,
141 PendingReplayAttempt,
143 AttemptTerminal,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub enum PublicState {
153 Waiting,
155 Delayed,
157 RateLimited,
159 WaitingChildren,
161 Active,
163 Suspended,
165 Completed,
167 Failed,
169 Cancelled,
171 Expired,
173 Skipped,
175}
176
177#[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 pub public_state: PublicState,
190}
191
192impl StateVector {
193 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 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 PublicState::Waiting
230 }
231 },
232 LifecyclePhase::Submitted => PublicState::Waiting,
233 }
234 }
235
236 pub fn is_consistent(&self) -> bool {
238 self.public_state == self.derive_public_state()
239 }
240}
241
242#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
246#[serde(rename_all = "snake_case")]
247pub enum AttemptLifecycle {
248 Created,
250 Started,
252 Suspended,
254 EndedSuccess,
256 EndedFailure,
258 EndedCancelled,
260 InterruptedReclaimed,
262}
263
264impl AttemptLifecycle {
265 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum AttemptType {
283 Initial,
285 Retry,
287 Reclaim,
289 Replay,
291 Fallback,
293}
294
295#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
299#[serde(rename_all = "snake_case")]
300pub enum LaneState {
301 Active,
303 Paused,
305 Draining,
307 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 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}