1use std::collections::BTreeSet;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 error::{AgentError, AgentErrorKind, RetryClassification},
13 journal::JournalRecordKind,
14 recovery::RecoveryClassification,
15};
16
17#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum LoopState {
24 Starting,
26 ContextAssembly,
28 ProviderProjection,
30 ModelStreaming,
32 StreamIntervention,
34 ToolPlanning,
36 Approval,
38 ToolDenied,
40 ToolExecution,
42 Interrupted,
44 WaitingForResume,
46 Compaction,
48 Continue,
50 Recovery,
52 Completed,
54 Cancelled,
56 Failed,
58}
59
60impl LoopState {
61 pub const ALL: [Self; 17] = [
64 Self::Starting,
65 Self::ContextAssembly,
66 Self::ProviderProjection,
67 Self::ModelStreaming,
68 Self::StreamIntervention,
69 Self::ToolPlanning,
70 Self::Approval,
71 Self::ToolDenied,
72 Self::ToolExecution,
73 Self::Interrupted,
74 Self::WaitingForResume,
75 Self::Compaction,
76 Self::Continue,
77 Self::Recovery,
78 Self::Completed,
79 Self::Cancelled,
80 Self::Failed,
81 ];
82
83 pub fn all() -> &'static [Self] {
86 &Self::ALL
87 }
88
89 pub fn contract_name(self) -> &'static str {
92 match self {
93 Self::Starting => "Starting",
94 Self::ContextAssembly => "ContextAssembly",
95 Self::ProviderProjection => "ProviderProjection",
96 Self::ModelStreaming => "ModelStreaming",
97 Self::StreamIntervention => "StreamIntervention",
98 Self::ToolPlanning => "ToolPlanning",
99 Self::Approval => "Approval",
100 Self::ToolDenied => "ToolDenied",
101 Self::ToolExecution => "ToolExecution",
102 Self::Interrupted => "Interrupted",
103 Self::WaitingForResume => "WaitingForResume",
104 Self::Compaction => "Compaction",
105 Self::Continue => "Continue",
106 Self::Recovery => "Recovery",
107 Self::Completed => "Completed",
108 Self::Cancelled => "Cancelled",
109 Self::Failed => "Failed",
110 }
111 }
112
113 pub fn is_terminal(self) -> bool {
116 matches!(self, Self::Completed | Self::Cancelled)
117 }
118
119 pub fn can_carry_terminal_result(self) -> bool {
122 matches!(self, Self::Completed | Self::Cancelled | Self::Failed)
123 }
124
125 pub fn requires_cancel_transition(self) -> bool {
128 !self.is_terminal()
129 }
130}
131
132#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
133#[serde(rename_all = "snake_case")]
134pub enum LoopTrigger {
137 StartRun,
139 ContextReady,
141 ProjectionReady,
143 ToolUse,
145 StreamRuleMatch,
147 EndTurn,
149 ProviderFailure,
151 MaxIterationsReached {
153 outcome: MaxIterationOutcome,
155 },
156 CompactionNeeded,
158 StreamStopRun,
160 StreamAbortAndRetry,
162 StreamPauseForApproval,
164 StreamUnsafeIntervention,
166 PolicyAllow,
168 PolicyAsk,
170 PolicyDeny,
172 Approved,
174 ApprovalDenied,
176 ApprovalTimeout,
178 ApprovalTransportFatal,
180 StreamApprovalResumed,
182 ContinueWithDeniedResult,
184 FailOnDenied,
186 ToolComplete,
188 ToolInterrupt,
190 ToolFailure,
192 WaitForResume,
194 ResumeAllowed,
196 ResumeDenied,
198 CompactionComplete,
200 ContinueLoop,
202 FailureClassified {
204 classification: RecoveryClassification,
206 },
207 RepairApplied,
209 RepairCompletedTerminal,
211 RecoveryIrrecoverable,
213 CancelRequested,
215}
216
217#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
218#[serde(rename_all = "snake_case")]
219pub enum MaxIterationOutcome {
222 Complete,
224 Fail,
226}
227
228#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
229#[serde(rename_all = "snake_case")]
230pub enum TransitionGuard {
233 None,
235 PackageValid,
237 BudgetValid,
239 PackageHashesMatch,
241 ModelMessageHasToolCalls,
243 RuleActionAllowed,
245 FinalMessageComplete,
247 ProviderFailureClassified,
249 PermissionsPass,
251 DispatcherAvailableOrEscalationConfigured,
253 DeniedResultAllowed,
255 DecisionValid,
257 TimeoutElapsed,
259 ApprovalTransportFatal,
261 TerminalStatusAppended,
263 InterruptResumable,
265 ResumeTokenRequired,
267 CheckpointAndPackageValid,
269 ProtectedContextPreserved,
271 RepairPlanSafe,
273 InvariantRestored,
275 MaxIterationBudgetExhausted,
277 CancellationRequested,
279 UnsafeIntervention,
281}
282
283#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
284pub struct TransitionGuardSet {
287 #[serde(default)]
288 satisfied: BTreeSet<TransitionGuard>,
289}
290
291impl TransitionGuardSet {
292 pub fn new() -> Self {
296 Self::default()
297 }
298
299 pub fn with(mut self, guard: TransitionGuard) -> Self {
303 self.satisfied.insert(guard);
304 self
305 }
306
307 pub fn contains(&self, guard: TransitionGuard) -> bool {
311 guard == TransitionGuard::None || self.satisfied.contains(&guard)
312 }
313
314 pub fn for_rule(rule: &TransitionRule) -> Self {
317 if rule.guard == TransitionGuard::None {
318 Self::default()
319 } else {
320 Self::default().with(rule.guard)
321 }
322 }
323}
324
325impl<const N: usize> From<[TransitionGuard; N]> for TransitionGuardSet {
326 fn from(guards: [TransitionGuard; N]) -> Self {
327 let mut set = Self::default();
328 for guard in guards {
329 set.satisfied.insert(guard);
330 }
331 set
332 }
333}
334
335#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
336#[serde(rename_all = "snake_case")]
337pub enum CheckpointPolicy {
340 None,
342 Before,
344 After,
346 BeforeAndAfter,
348 Terminal,
350}
351
352#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
353#[serde(rename_all = "snake_case")]
354pub enum SideEffectPolicy {
357 None,
359 IntentBeforeEffect,
361 IdempotentRetryAllowed,
363 NonIdempotentFailClosed,
365 ReconcileRequired,
367}
368
369#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
370#[serde(rename_all = "snake_case")]
371pub enum LoopEventKind {
374 RunStarted,
376 RunCompleted,
378 RunFailed,
380 RunCancelled,
382 RunCancelRequested,
384 RunCheckpointed,
386 RunResumeRequested,
388 RunResumeFailed,
390 ContextAssembled,
392 ProviderRequestProjected,
394 ModelAttemptStarted,
396 ModelAttemptFailed,
398 ModelMessageCompleted,
400 ToolRequested,
402 ToolApprovalRequired,
404 ToolStarted,
406 ToolCompleted,
408 ToolFailed,
410 ToolInterrupted,
412 ApprovalRequested,
414 ApprovalResponded,
416 ApprovalTimedOut,
418 ApprovalDenied,
420 StreamRuleMatched,
422 StreamInterventionApplied,
424 ContextCompactionCompleted,
426 RecoveryPlanned,
428 ReplayCompleted,
430}
431
432#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
433#[serde(rename_all = "snake_case")]
434pub enum LoopTerminalStatus {
437 Completed,
439 Cancelled,
441 Failed,
443}
444
445#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
446#[serde(rename_all = "snake_case")]
447pub enum LoopStopReason {
450 EndTurn,
452 StreamRuleStop,
454 MaxIterations {
456 outcome: MaxIterationOutcome,
458 },
459 Cancelled,
461 ProviderFailure,
463 UnsafeStreamIntervention,
465 ToolDenied,
467 ApprovalDenied,
469 ApprovalTimeout,
471 ApprovalTransportFatal,
473 ToolFailure,
475 ResumeDenied,
477 RecoveryCompleted,
479 RecoveryIrrecoverable,
481}
482
483#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
484pub struct LoopTerminalResult {
487 pub status: LoopTerminalStatus,
489 pub stop_reason: LoopStopReason,
491}
492
493#[derive(Clone, Debug, Eq, PartialEq)]
494pub struct TransitionRule {
497 pub from_state: LoopState,
499 pub trigger: LoopTrigger,
501 pub guard: TransitionGuard,
503 pub events: &'static [LoopEventKind],
506 pub journal_records: &'static [JournalRecordKind],
509 pub checkpoint_policy: CheckpointPolicy,
511 pub side_effect_policy: SideEffectPolicy,
513 pub next_state: LoopState,
515 pub terminal_result: Option<LoopTerminalResult>,
518 pub recovery_classification: Option<RecoveryClassification>,
521}
522
523impl TransitionRule {
524 pub fn output(&self) -> TransitionOutput {
527 TransitionOutput {
528 from_state: self.from_state,
529 trigger: self.trigger,
530 guard: self.guard,
531 events: self.events.to_vec(),
532 journal_records: self.journal_records.to_vec(),
533 checkpoint_policy: self.checkpoint_policy,
534 side_effect_policy: self.side_effect_policy,
535 next_state: self.next_state,
536 terminal_result: self.terminal_result,
537 recovery_classification: self.recovery_classification,
538 }
539 }
540}
541
542#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
543pub struct TransitionInput {
546 pub state: LoopState,
548 pub trigger: LoopTrigger,
550 #[serde(default)]
551 pub guards: TransitionGuardSet,
553}
554
555impl TransitionInput {
556 pub fn new(state: LoopState, trigger: LoopTrigger) -> Self {
560 Self {
561 state,
562 trigger,
563 guards: TransitionGuardSet::default(),
564 }
565 }
566
567 pub fn with_guard(mut self, guard: TransitionGuard) -> Self {
571 self.guards = self.guards.with(guard);
572 self
573 }
574
575 pub fn with_guards(mut self, guards: TransitionGuardSet) -> Self {
579 self.guards = guards;
580 self
581 }
582}
583
584#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
585pub struct TransitionOutput {
588 pub from_state: LoopState,
590 pub trigger: LoopTrigger,
592 pub guard: TransitionGuard,
594 pub events: Vec<LoopEventKind>,
597 pub journal_records: Vec<JournalRecordKind>,
600 pub checkpoint_policy: CheckpointPolicy,
602 pub side_effect_policy: SideEffectPolicy,
604 pub next_state: LoopState,
606 #[serde(skip_serializing_if = "Option::is_none")]
607 pub terminal_result: Option<LoopTerminalResult>,
610 #[serde(skip_serializing_if = "Option::is_none")]
611 pub recovery_classification: Option<RecoveryClassification>,
614}
615
616#[derive(Clone, Copy, Debug, Default)]
617pub struct AgentStateMachine;
620
621impl AgentStateMachine {
622 pub fn transition_table(&self) -> &'static [TransitionRule] {
625 transition_table()
626 }
627
628 pub fn validate_transition(
632 &self,
633 input: TransitionInput,
634 ) -> Result<TransitionOutput, AgentError> {
635 validate_transition(input)
636 }
637}
638
639pub fn validate_transition(input: TransitionInput) -> Result<TransitionOutput, AgentError> {
643 let rule = transition_table()
644 .iter()
645 .find(|rule| rule.from_state == input.state && rule.trigger == input.trigger)
646 .ok_or_else(|| invalid_transition(input.state, input.trigger, None))?;
647
648 if !input.guards.contains(rule.guard) {
649 return Err(invalid_transition(
650 input.state,
651 input.trigger,
652 Some(rule.guard),
653 ));
654 }
655
656 Ok(rule.output())
657}
658
659pub fn transition_table() -> &'static [TransitionRule] {
662 TRANSITION_TABLE
663}
664
665pub fn contract_state_names() -> &'static [&'static str] {
668 CONTRACT_STATE_NAMES
669}
670
671fn invalid_transition(
672 state: LoopState,
673 trigger: LoopTrigger,
674 missing_guard: Option<TransitionGuard>,
675) -> AgentError {
676 let guard_message = missing_guard
677 .map(|guard| format!("; missing guard {guard:?}"))
678 .unwrap_or_default();
679 AgentError::new(
680 AgentErrorKind::InvalidStateTransition,
681 RetryClassification::RepairNeeded,
682 format!(
683 "loop transition from {:?} with trigger {:?} is not allowed{}",
684 state, trigger, guard_message
685 ),
686 )
687}
688
689const CONTRACT_STATE_NAMES: &[&str] = &[
690 "Starting",
691 "ContextAssembly",
692 "ProviderProjection",
693 "ModelStreaming",
694 "StreamIntervention",
695 "ToolPlanning",
696 "Approval",
697 "ToolDenied",
698 "ToolExecution",
699 "Interrupted",
700 "WaitingForResume",
701 "Compaction",
702 "Continue",
703 "Recovery",
704 "Completed",
705 "Cancelled",
706 "Failed",
707];
708
709const TRANSITION_TABLE: &[TransitionRule] = &[
710 TransitionRule {
711 from_state: LoopState::Starting,
712 trigger: LoopTrigger::StartRun,
713 guard: TransitionGuard::PackageValid,
714 events: &[LoopEventKind::RunStarted],
715 journal_records: &[JournalRecordKind::Run],
716 checkpoint_policy: CheckpointPolicy::After,
717 side_effect_policy: SideEffectPolicy::None,
718 next_state: LoopState::ContextAssembly,
719 terminal_result: None,
720 recovery_classification: None,
721 },
722 TransitionRule {
723 from_state: LoopState::ContextAssembly,
724 trigger: LoopTrigger::ContextReady,
725 guard: TransitionGuard::BudgetValid,
726 events: &[LoopEventKind::ContextAssembled],
727 journal_records: &[JournalRecordKind::Context],
728 checkpoint_policy: CheckpointPolicy::After,
729 side_effect_policy: SideEffectPolicy::None,
730 next_state: LoopState::ProviderProjection,
731 terminal_result: None,
732 recovery_classification: None,
733 },
734 TransitionRule {
735 from_state: LoopState::ProviderProjection,
736 trigger: LoopTrigger::ProjectionReady,
737 guard: TransitionGuard::PackageHashesMatch,
738 events: &[
739 LoopEventKind::ProviderRequestProjected,
740 LoopEventKind::ModelAttemptStarted,
741 ],
742 journal_records: &[JournalRecordKind::Context, JournalRecordKind::ModelAttempt],
743 checkpoint_policy: CheckpointPolicy::Before,
744 side_effect_policy: SideEffectPolicy::IntentBeforeEffect,
745 next_state: LoopState::ModelStreaming,
746 terminal_result: None,
747 recovery_classification: None,
748 },
749 TransitionRule {
750 from_state: LoopState::ModelStreaming,
751 trigger: LoopTrigger::ToolUse,
752 guard: TransitionGuard::ModelMessageHasToolCalls,
753 events: &[
754 LoopEventKind::ModelMessageCompleted,
755 LoopEventKind::ToolRequested,
756 ],
757 journal_records: &[JournalRecordKind::ModelAttempt, JournalRecordKind::Tool],
758 checkpoint_policy: CheckpointPolicy::After,
759 side_effect_policy: SideEffectPolicy::None,
760 next_state: LoopState::ToolPlanning,
761 terminal_result: None,
762 recovery_classification: None,
763 },
764 TransitionRule {
765 from_state: LoopState::ModelStreaming,
766 trigger: LoopTrigger::StreamRuleMatch,
767 guard: TransitionGuard::RuleActionAllowed,
768 events: &[LoopEventKind::StreamRuleMatched],
769 journal_records: &[JournalRecordKind::StreamRule],
770 checkpoint_policy: CheckpointPolicy::None,
771 side_effect_policy: SideEffectPolicy::None,
772 next_state: LoopState::StreamIntervention,
773 terminal_result: None,
774 recovery_classification: None,
775 },
776 TransitionRule {
777 from_state: LoopState::ModelStreaming,
778 trigger: LoopTrigger::CompactionNeeded,
779 guard: TransitionGuard::BudgetValid,
780 events: &[LoopEventKind::RunCheckpointed],
781 journal_records: &[JournalRecordKind::Context],
782 checkpoint_policy: CheckpointPolicy::Before,
783 side_effect_policy: SideEffectPolicy::None,
784 next_state: LoopState::Compaction,
785 terminal_result: None,
786 recovery_classification: None,
787 },
788 TransitionRule {
789 from_state: LoopState::ModelStreaming,
790 trigger: LoopTrigger::EndTurn,
791 guard: TransitionGuard::FinalMessageComplete,
792 events: &[
793 LoopEventKind::ModelMessageCompleted,
794 LoopEventKind::RunCompleted,
795 ],
796 journal_records: &[
797 JournalRecordKind::ModelAttempt,
798 JournalRecordKind::Message,
799 JournalRecordKind::Run,
800 ],
801 checkpoint_policy: CheckpointPolicy::Terminal,
802 side_effect_policy: SideEffectPolicy::None,
803 next_state: LoopState::Completed,
804 terminal_result: Some(LoopTerminalResult {
805 status: LoopTerminalStatus::Completed,
806 stop_reason: LoopStopReason::EndTurn,
807 }),
808 recovery_classification: None,
809 },
810 TransitionRule {
811 from_state: LoopState::ModelStreaming,
812 trigger: LoopTrigger::ProviderFailure,
813 guard: TransitionGuard::ProviderFailureClassified,
814 events: &[LoopEventKind::ModelAttemptFailed, LoopEventKind::RunFailed],
815 journal_records: &[
816 JournalRecordKind::ModelAttempt,
817 JournalRecordKind::Recovery,
818 JournalRecordKind::Run,
819 ],
820 checkpoint_policy: CheckpointPolicy::Terminal,
821 side_effect_policy: SideEffectPolicy::ReconcileRequired,
822 next_state: LoopState::Failed,
823 terminal_result: Some(LoopTerminalResult {
824 status: LoopTerminalStatus::Failed,
825 stop_reason: LoopStopReason::ProviderFailure,
826 }),
827 recovery_classification: None,
828 },
829 TransitionRule {
830 from_state: LoopState::StreamIntervention,
831 trigger: LoopTrigger::StreamStopRun,
832 guard: TransitionGuard::RuleActionAllowed,
833 events: &[
834 LoopEventKind::StreamInterventionApplied,
835 LoopEventKind::RunCompleted,
836 ],
837 journal_records: &[JournalRecordKind::StreamRule, JournalRecordKind::Run],
838 checkpoint_policy: CheckpointPolicy::Terminal,
839 side_effect_policy: SideEffectPolicy::None,
840 next_state: LoopState::Completed,
841 terminal_result: Some(LoopTerminalResult {
842 status: LoopTerminalStatus::Completed,
843 stop_reason: LoopStopReason::StreamRuleStop,
844 }),
845 recovery_classification: None,
846 },
847 TransitionRule {
848 from_state: LoopState::StreamIntervention,
849 trigger: LoopTrigger::StreamAbortAndRetry,
850 guard: TransitionGuard::RuleActionAllowed,
851 events: &[LoopEventKind::StreamInterventionApplied],
852 journal_records: &[
853 JournalRecordKind::StreamRule,
854 JournalRecordKind::ModelAttempt,
855 ],
856 checkpoint_policy: CheckpointPolicy::After,
857 side_effect_policy: SideEffectPolicy::IdempotentRetryAllowed,
858 next_state: LoopState::ProviderProjection,
859 terminal_result: None,
860 recovery_classification: None,
861 },
862 TransitionRule {
863 from_state: LoopState::StreamIntervention,
864 trigger: LoopTrigger::StreamPauseForApproval,
865 guard: TransitionGuard::RuleActionAllowed,
866 events: &[
867 LoopEventKind::StreamInterventionApplied,
868 LoopEventKind::ApprovalRequested,
869 ],
870 journal_records: &[JournalRecordKind::StreamRule, JournalRecordKind::Approval],
871 checkpoint_policy: CheckpointPolicy::After,
872 side_effect_policy: SideEffectPolicy::None,
873 next_state: LoopState::Approval,
874 terminal_result: None,
875 recovery_classification: None,
876 },
877 TransitionRule {
878 from_state: LoopState::StreamIntervention,
879 trigger: LoopTrigger::StreamUnsafeIntervention,
880 guard: TransitionGuard::UnsafeIntervention,
881 events: &[LoopEventKind::RunFailed],
882 journal_records: &[JournalRecordKind::StreamRule, JournalRecordKind::Run],
883 checkpoint_policy: CheckpointPolicy::Terminal,
884 side_effect_policy: SideEffectPolicy::None,
885 next_state: LoopState::Failed,
886 terminal_result: Some(LoopTerminalResult {
887 status: LoopTerminalStatus::Failed,
888 stop_reason: LoopStopReason::UnsafeStreamIntervention,
889 }),
890 recovery_classification: None,
891 },
892 TransitionRule {
893 from_state: LoopState::ToolPlanning,
894 trigger: LoopTrigger::PolicyAllow,
895 guard: TransitionGuard::PermissionsPass,
896 events: &[LoopEventKind::ToolStarted],
897 journal_records: &[JournalRecordKind::Tool],
898 checkpoint_policy: CheckpointPolicy::Before,
899 side_effect_policy: SideEffectPolicy::IntentBeforeEffect,
900 next_state: LoopState::ToolExecution,
901 terminal_result: None,
902 recovery_classification: None,
903 },
904 TransitionRule {
905 from_state: LoopState::ToolPlanning,
906 trigger: LoopTrigger::PolicyAsk,
907 guard: TransitionGuard::DispatcherAvailableOrEscalationConfigured,
908 events: &[LoopEventKind::ApprovalRequested],
909 journal_records: &[JournalRecordKind::Approval],
910 checkpoint_policy: CheckpointPolicy::After,
911 side_effect_policy: SideEffectPolicy::None,
912 next_state: LoopState::Approval,
913 terminal_result: None,
914 recovery_classification: None,
915 },
916 TransitionRule {
917 from_state: LoopState::ToolPlanning,
918 trigger: LoopTrigger::PolicyDeny,
919 guard: TransitionGuard::DeniedResultAllowed,
920 events: &[
921 LoopEventKind::ToolApprovalRequired,
922 LoopEventKind::ApprovalDenied,
923 ],
924 journal_records: &[JournalRecordKind::Approval, JournalRecordKind::Tool],
925 checkpoint_policy: CheckpointPolicy::After,
926 side_effect_policy: SideEffectPolicy::None,
927 next_state: LoopState::ToolDenied,
928 terminal_result: None,
929 recovery_classification: None,
930 },
931 TransitionRule {
932 from_state: LoopState::Approval,
933 trigger: LoopTrigger::Approved,
934 guard: TransitionGuard::DecisionValid,
935 events: &[LoopEventKind::ApprovalResponded, LoopEventKind::ToolStarted],
936 journal_records: &[JournalRecordKind::Approval, JournalRecordKind::Tool],
937 checkpoint_policy: CheckpointPolicy::Before,
938 side_effect_policy: SideEffectPolicy::IntentBeforeEffect,
939 next_state: LoopState::ToolExecution,
940 terminal_result: None,
941 recovery_classification: None,
942 },
943 TransitionRule {
944 from_state: LoopState::Approval,
945 trigger: LoopTrigger::ApprovalDenied,
946 guard: TransitionGuard::DecisionValid,
947 events: &[
948 LoopEventKind::ApprovalResponded,
949 LoopEventKind::ApprovalDenied,
950 ],
951 journal_records: &[JournalRecordKind::Approval],
952 checkpoint_policy: CheckpointPolicy::After,
953 side_effect_policy: SideEffectPolicy::None,
954 next_state: LoopState::ToolDenied,
955 terminal_result: None,
956 recovery_classification: None,
957 },
958 TransitionRule {
959 from_state: LoopState::Approval,
960 trigger: LoopTrigger::ApprovalTimeout,
961 guard: TransitionGuard::TimeoutElapsed,
962 events: &[
963 LoopEventKind::ApprovalTimedOut,
964 LoopEventKind::ApprovalDenied,
965 ],
966 journal_records: &[JournalRecordKind::Approval],
967 checkpoint_policy: CheckpointPolicy::After,
968 side_effect_policy: SideEffectPolicy::None,
969 next_state: LoopState::ToolDenied,
970 terminal_result: None,
971 recovery_classification: None,
972 },
973 TransitionRule {
974 from_state: LoopState::Approval,
975 trigger: LoopTrigger::ApprovalTransportFatal,
976 guard: TransitionGuard::ApprovalTransportFatal,
977 events: &[LoopEventKind::RunFailed],
978 journal_records: &[JournalRecordKind::Approval, JournalRecordKind::Recovery],
979 checkpoint_policy: CheckpointPolicy::After,
980 side_effect_policy: SideEffectPolicy::ReconcileRequired,
981 next_state: LoopState::Failed,
982 terminal_result: Some(LoopTerminalResult {
983 status: LoopTerminalStatus::Failed,
984 stop_reason: LoopStopReason::ApprovalTransportFatal,
985 }),
986 recovery_classification: None,
987 },
988 TransitionRule {
989 from_state: LoopState::Approval,
990 trigger: LoopTrigger::StreamApprovalResumed,
991 guard: TransitionGuard::DecisionValid,
992 events: &[LoopEventKind::ApprovalResponded],
993 journal_records: &[JournalRecordKind::Approval],
994 checkpoint_policy: CheckpointPolicy::After,
995 side_effect_policy: SideEffectPolicy::None,
996 next_state: LoopState::ProviderProjection,
997 terminal_result: None,
998 recovery_classification: None,
999 },
1000 TransitionRule {
1001 from_state: LoopState::ToolDenied,
1002 trigger: LoopTrigger::ContinueWithDeniedResult,
1003 guard: TransitionGuard::DeniedResultAllowed,
1004 events: &[LoopEventKind::ToolCompleted],
1005 journal_records: &[JournalRecordKind::Tool],
1006 checkpoint_policy: CheckpointPolicy::After,
1007 side_effect_policy: SideEffectPolicy::None,
1008 next_state: LoopState::Continue,
1009 terminal_result: None,
1010 recovery_classification: None,
1011 },
1012 TransitionRule {
1013 from_state: LoopState::ToolDenied,
1014 trigger: LoopTrigger::FailOnDenied,
1015 guard: TransitionGuard::DecisionValid,
1016 events: &[LoopEventKind::RunFailed],
1017 journal_records: &[JournalRecordKind::Recovery, JournalRecordKind::Run],
1018 checkpoint_policy: CheckpointPolicy::Terminal,
1019 side_effect_policy: SideEffectPolicy::None,
1020 next_state: LoopState::Failed,
1021 terminal_result: Some(LoopTerminalResult {
1022 status: LoopTerminalStatus::Failed,
1023 stop_reason: LoopStopReason::ToolDenied,
1024 }),
1025 recovery_classification: None,
1026 },
1027 TransitionRule {
1028 from_state: LoopState::ToolExecution,
1029 trigger: LoopTrigger::ToolComplete,
1030 guard: TransitionGuard::TerminalStatusAppended,
1031 events: &[LoopEventKind::ToolCompleted],
1032 journal_records: &[JournalRecordKind::Tool],
1033 checkpoint_policy: CheckpointPolicy::After,
1034 side_effect_policy: SideEffectPolicy::None,
1035 next_state: LoopState::Continue,
1036 terminal_result: None,
1037 recovery_classification: None,
1038 },
1039 TransitionRule {
1040 from_state: LoopState::ToolExecution,
1041 trigger: LoopTrigger::ToolInterrupt,
1042 guard: TransitionGuard::InterruptResumable,
1043 events: &[LoopEventKind::ToolInterrupted],
1044 journal_records: &[JournalRecordKind::Tool],
1045 checkpoint_policy: CheckpointPolicy::After,
1046 side_effect_policy: SideEffectPolicy::ReconcileRequired,
1047 next_state: LoopState::Interrupted,
1048 terminal_result: None,
1049 recovery_classification: None,
1050 },
1051 TransitionRule {
1052 from_state: LoopState::ToolExecution,
1053 trigger: LoopTrigger::ToolFailure,
1054 guard: TransitionGuard::TerminalStatusAppended,
1055 events: &[LoopEventKind::ToolFailed, LoopEventKind::RunFailed],
1056 journal_records: &[
1057 JournalRecordKind::Tool,
1058 JournalRecordKind::Recovery,
1059 JournalRecordKind::Run,
1060 ],
1061 checkpoint_policy: CheckpointPolicy::Terminal,
1062 side_effect_policy: SideEffectPolicy::ReconcileRequired,
1063 next_state: LoopState::Failed,
1064 terminal_result: Some(LoopTerminalResult {
1065 status: LoopTerminalStatus::Failed,
1066 stop_reason: LoopStopReason::ToolFailure,
1067 }),
1068 recovery_classification: None,
1069 },
1070 TransitionRule {
1071 from_state: LoopState::Interrupted,
1072 trigger: LoopTrigger::WaitForResume,
1073 guard: TransitionGuard::ResumeTokenRequired,
1074 events: &[LoopEventKind::RunCheckpointed],
1075 journal_records: &[JournalRecordKind::Recovery],
1076 checkpoint_policy: CheckpointPolicy::After,
1077 side_effect_policy: SideEffectPolicy::None,
1078 next_state: LoopState::WaitingForResume,
1079 terminal_result: None,
1080 recovery_classification: None,
1081 },
1082 TransitionRule {
1083 from_state: LoopState::WaitingForResume,
1084 trigger: LoopTrigger::ResumeAllowed,
1085 guard: TransitionGuard::CheckpointAndPackageValid,
1086 events: &[
1087 LoopEventKind::RunResumeRequested,
1088 LoopEventKind::ReplayCompleted,
1089 ],
1090 journal_records: &[JournalRecordKind::Recovery],
1091 checkpoint_policy: CheckpointPolicy::After,
1092 side_effect_policy: SideEffectPolicy::IdempotentRetryAllowed,
1093 next_state: LoopState::ToolExecution,
1094 terminal_result: None,
1095 recovery_classification: None,
1096 },
1097 TransitionRule {
1098 from_state: LoopState::WaitingForResume,
1099 trigger: LoopTrigger::ResumeDenied,
1100 guard: TransitionGuard::CheckpointAndPackageValid,
1101 events: &[LoopEventKind::RunResumeFailed, LoopEventKind::RunFailed],
1102 journal_records: &[JournalRecordKind::Recovery, JournalRecordKind::Run],
1103 checkpoint_policy: CheckpointPolicy::Terminal,
1104 side_effect_policy: SideEffectPolicy::None,
1105 next_state: LoopState::Failed,
1106 terminal_result: Some(LoopTerminalResult {
1107 status: LoopTerminalStatus::Failed,
1108 stop_reason: LoopStopReason::ResumeDenied,
1109 }),
1110 recovery_classification: None,
1111 },
1112 TransitionRule {
1113 from_state: LoopState::Compaction,
1114 trigger: LoopTrigger::CompactionComplete,
1115 guard: TransitionGuard::ProtectedContextPreserved,
1116 events: &[LoopEventKind::ContextCompactionCompleted],
1117 journal_records: &[JournalRecordKind::Context],
1118 checkpoint_policy: CheckpointPolicy::After,
1119 side_effect_policy: SideEffectPolicy::None,
1120 next_state: LoopState::ContextAssembly,
1121 terminal_result: None,
1122 recovery_classification: None,
1123 },
1124 TransitionRule {
1125 from_state: LoopState::Continue,
1126 trigger: LoopTrigger::ContinueLoop,
1127 guard: TransitionGuard::BudgetValid,
1128 events: &[],
1129 journal_records: &[],
1130 checkpoint_policy: CheckpointPolicy::None,
1131 side_effect_policy: SideEffectPolicy::None,
1132 next_state: LoopState::ContextAssembly,
1133 terminal_result: None,
1134 recovery_classification: None,
1135 },
1136 TransitionRule {
1137 from_state: LoopState::Continue,
1138 trigger: LoopTrigger::MaxIterationsReached {
1139 outcome: MaxIterationOutcome::Complete,
1140 },
1141 guard: TransitionGuard::MaxIterationBudgetExhausted,
1142 events: &[LoopEventKind::RunCompleted],
1143 journal_records: &[JournalRecordKind::Run],
1144 checkpoint_policy: CheckpointPolicy::Terminal,
1145 side_effect_policy: SideEffectPolicy::None,
1146 next_state: LoopState::Completed,
1147 terminal_result: Some(LoopTerminalResult {
1148 status: LoopTerminalStatus::Completed,
1149 stop_reason: LoopStopReason::MaxIterations {
1150 outcome: MaxIterationOutcome::Complete,
1151 },
1152 }),
1153 recovery_classification: None,
1154 },
1155 TransitionRule {
1156 from_state: LoopState::Continue,
1157 trigger: LoopTrigger::MaxIterationsReached {
1158 outcome: MaxIterationOutcome::Fail,
1159 },
1160 guard: TransitionGuard::MaxIterationBudgetExhausted,
1161 events: &[LoopEventKind::RunFailed],
1162 journal_records: &[JournalRecordKind::Run],
1163 checkpoint_policy: CheckpointPolicy::Terminal,
1164 side_effect_policy: SideEffectPolicy::None,
1165 next_state: LoopState::Failed,
1166 terminal_result: Some(LoopTerminalResult {
1167 status: LoopTerminalStatus::Failed,
1168 stop_reason: LoopStopReason::MaxIterations {
1169 outcome: MaxIterationOutcome::Fail,
1170 },
1171 }),
1172 recovery_classification: None,
1173 },
1174 TransitionRule {
1175 from_state: LoopState::Failed,
1176 trigger: LoopTrigger::FailureClassified {
1177 classification: RecoveryClassification::RetryableSafeStep,
1178 },
1179 guard: TransitionGuard::RepairPlanSafe,
1180 events: &[LoopEventKind::RecoveryPlanned],
1181 journal_records: &[JournalRecordKind::Recovery],
1182 checkpoint_policy: CheckpointPolicy::After,
1183 side_effect_policy: SideEffectPolicy::IdempotentRetryAllowed,
1184 next_state: LoopState::Recovery,
1185 terminal_result: None,
1186 recovery_classification: Some(RecoveryClassification::RetryableSafeStep),
1187 },
1188 TransitionRule {
1189 from_state: LoopState::Failed,
1190 trigger: LoopTrigger::FailureClassified {
1191 classification: RecoveryClassification::ReconcileRequired,
1192 },
1193 guard: TransitionGuard::RepairPlanSafe,
1194 events: &[LoopEventKind::RecoveryPlanned],
1195 journal_records: &[JournalRecordKind::Recovery],
1196 checkpoint_policy: CheckpointPolicy::After,
1197 side_effect_policy: SideEffectPolicy::ReconcileRequired,
1198 next_state: LoopState::Recovery,
1199 terminal_result: None,
1200 recovery_classification: Some(RecoveryClassification::ReconcileRequired),
1201 },
1202 TransitionRule {
1203 from_state: LoopState::Failed,
1204 trigger: LoopTrigger::FailureClassified {
1205 classification: RecoveryClassification::RepairRequired,
1206 },
1207 guard: TransitionGuard::RepairPlanSafe,
1208 events: &[LoopEventKind::RecoveryPlanned],
1209 journal_records: &[JournalRecordKind::Recovery],
1210 checkpoint_policy: CheckpointPolicy::After,
1211 side_effect_policy: SideEffectPolicy::ReconcileRequired,
1212 next_state: LoopState::Recovery,
1213 terminal_result: None,
1214 recovery_classification: Some(RecoveryClassification::RepairRequired),
1215 },
1216 TransitionRule {
1217 from_state: LoopState::Recovery,
1218 trigger: LoopTrigger::RepairApplied,
1219 guard: TransitionGuard::InvariantRestored,
1220 events: &[LoopEventKind::ReplayCompleted],
1221 journal_records: &[JournalRecordKind::Recovery],
1222 checkpoint_policy: CheckpointPolicy::After,
1223 side_effect_policy: SideEffectPolicy::None,
1224 next_state: LoopState::ContextAssembly,
1225 terminal_result: None,
1226 recovery_classification: None,
1227 },
1228 TransitionRule {
1229 from_state: LoopState::Recovery,
1230 trigger: LoopTrigger::RepairCompletedTerminal,
1231 guard: TransitionGuard::InvariantRestored,
1232 events: &[LoopEventKind::ReplayCompleted, LoopEventKind::RunCompleted],
1233 journal_records: &[JournalRecordKind::Recovery, JournalRecordKind::Run],
1234 checkpoint_policy: CheckpointPolicy::Terminal,
1235 side_effect_policy: SideEffectPolicy::None,
1236 next_state: LoopState::Completed,
1237 terminal_result: Some(LoopTerminalResult {
1238 status: LoopTerminalStatus::Completed,
1239 stop_reason: LoopStopReason::RecoveryCompleted,
1240 }),
1241 recovery_classification: None,
1242 },
1243 TransitionRule {
1244 from_state: LoopState::Recovery,
1245 trigger: LoopTrigger::RecoveryIrrecoverable,
1246 guard: TransitionGuard::RepairPlanSafe,
1247 events: &[LoopEventKind::RunFailed],
1248 journal_records: &[JournalRecordKind::Recovery, JournalRecordKind::Run],
1249 checkpoint_policy: CheckpointPolicy::Terminal,
1250 side_effect_policy: SideEffectPolicy::None,
1251 next_state: LoopState::Failed,
1252 terminal_result: Some(LoopTerminalResult {
1253 status: LoopTerminalStatus::Failed,
1254 stop_reason: LoopStopReason::RecoveryIrrecoverable,
1255 }),
1256 recovery_classification: Some(RecoveryClassification::Irrecoverable),
1257 },
1258 cancel_rule(LoopState::Starting),
1259 cancel_rule(LoopState::ContextAssembly),
1260 cancel_rule(LoopState::ProviderProjection),
1261 cancel_rule(LoopState::ModelStreaming),
1262 cancel_rule(LoopState::StreamIntervention),
1263 cancel_rule(LoopState::ToolPlanning),
1264 cancel_rule(LoopState::Approval),
1265 cancel_rule(LoopState::ToolDenied),
1266 cancel_rule(LoopState::ToolExecution),
1267 cancel_rule(LoopState::Interrupted),
1268 cancel_rule(LoopState::WaitingForResume),
1269 cancel_rule(LoopState::Compaction),
1270 cancel_rule(LoopState::Continue),
1271 cancel_rule(LoopState::Recovery),
1272 cancel_rule(LoopState::Failed),
1273];
1274
1275const fn cancel_rule(from_state: LoopState) -> TransitionRule {
1276 TransitionRule {
1277 from_state,
1278 trigger: LoopTrigger::CancelRequested,
1279 guard: TransitionGuard::CancellationRequested,
1280 events: &[
1281 LoopEventKind::RunCancelRequested,
1282 LoopEventKind::RunCancelled,
1283 ],
1284 journal_records: &[JournalRecordKind::Recovery, JournalRecordKind::Run],
1285 checkpoint_policy: CheckpointPolicy::Terminal,
1286 side_effect_policy: SideEffectPolicy::ReconcileRequired,
1287 next_state: LoopState::Cancelled,
1288 terminal_result: Some(LoopTerminalResult {
1289 status: LoopTerminalStatus::Cancelled,
1290 stop_reason: LoopStopReason::Cancelled,
1291 }),
1292 recovery_classification: None,
1293 }
1294}