Skip to main content

agent_sdk_core/application/
loop_state.rs

1//! Application-layer coordination over core primitives. Use these services to lower
2//! helpers, drive runs, validate output, coordinate tools, approvals, delivery,
3//! isolation, telemetry, and feature layers. Methods in this layer may call
4//! configured ports, mutate in-memory stores, append journals, or publish events as
5//! documented. This file contains the loop state portion of that contract.
6//!
7use 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")]
19/// Names the finite phases of the SDK run loop.
20/// Use it in state tables, journal records, and tests to describe where a run is paused or
21/// executing; the enum is a marker only and side effects happen in the driver transition that
22/// enters the phase.
23pub enum LoopState {
24    /// The run has been accepted and initial package, policy, and registry checks are beginning.
25    Starting,
26    /// The runtime is collecting context contributions before anything is shown to a provider.
27    ContextAssembly,
28    /// Context has been admitted and is being projected into the provider-visible request shape.
29    ProviderProjection,
30    /// A provider call is active and model deltas may be emitted.
31    ModelStreaming,
32    /// Stream rules are inspecting, interrupting, or transforming an active model stream.
33    StreamIntervention,
34    /// The runtime is interpreting provider output into candidate tool calls or loop actions.
35    ToolPlanning,
36    /// A tool or extension action is waiting for an approval decision before execution.
37    Approval,
38    /// Policy or approval denied a requested tool before the executor was started.
39    ToolDenied,
40    /// A journal-backed tool intent has been recorded and the configured executor may be running.
41    ToolExecution,
42    /// Cancellation, timeout, stream policy, or host intervention interrupted active work.
43    Interrupted,
44    /// The run is suspended and requires a wake, resume signal, or durable replay input.
45    WaitingForResume,
46    /// The runtime is compacting context or summaries before continuing the loop.
47    Compaction,
48    /// The current iteration completed and the driver is preparing another loop iteration.
49    Continue,
50    /// Replay, repair, or reconciliation is resolving an incomplete or uncertain effect.
51    Recovery,
52    /// The run reached a successful terminal result.
53    Completed,
54    /// The run reached a cancellation terminal result.
55    Cancelled,
56    /// The run reached an unrecoverable failure terminal result.
57    Failed,
58}
59
60impl LoopState {
61    /// Constant value for the application::loop_state contract. Use it
62    /// to keep SDK records and tests aligned on the same stable value.
63    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    /// Returns the all currently held by this value.
84    /// This is state-table metadata only and does not advance a run or mutate loop state.
85    pub fn all() -> &'static [Self] {
86        &Self::ALL
87    }
88
89    /// Returns the stable contract spelling for this loop state.
90    /// This is a pure mapping used by fixtures and diagnostics; it does not advance the loop.
91    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    /// Reports whether this value is terminal. The check is pure and
114    /// does not mutate SDK or host state.
115    pub fn is_terminal(self) -> bool {
116        matches!(self, Self::Completed | Self::Cancelled)
117    }
118
119    /// Returns whether can carry terminal result applies for this state.
120    /// This is state-table metadata only and does not advance a run or mutate loop state.
121    pub fn can_carry_terminal_result(self) -> bool {
122        matches!(self, Self::Completed | Self::Cancelled | Self::Failed)
123    }
124
125    /// Returns whether requires cancel transition applies for this state.
126    /// This is state-table metadata only and does not advance a run or mutate loop state.
127    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")]
134/// Enumerates the finite loop trigger cases.
135/// Serialized names are part of the SDK contract; update fixtures when variants change.
136pub enum LoopTrigger {
137    /// Use this variant when the contract needs to represent start run; selecting it has no side effect by itself.
138    StartRun,
139    /// Use this variant when the contract needs to represent context ready; selecting it has no side effect by itself.
140    ContextReady,
141    /// Use this variant when the contract needs to represent projection ready; selecting it has no side effect by itself.
142    ProjectionReady,
143    /// Use this variant when the contract needs to represent tool use; selecting it has no side effect by itself.
144    ToolUse,
145    /// Use this variant when the contract needs to represent stream rule match; selecting it has no side effect by itself.
146    StreamRuleMatch,
147    /// Use this variant when the contract needs to represent end turn; selecting it has no side effect by itself.
148    EndTurn,
149    /// Use this variant when the contract needs to represent provider failure; selecting it has no side effect by itself.
150    ProviderFailure,
151    /// Use this variant when the contract needs to represent max iterations reached; selecting it has no side effect by itself.
152    MaxIterationsReached {
153        /// Outcome used by this record or request.
154        outcome: MaxIterationOutcome,
155    },
156    /// Use this variant when the contract needs to represent compaction needed; selecting it has no side effect by itself.
157    CompactionNeeded,
158    /// Use this variant when the contract needs to represent stream stop run; selecting it has no side effect by itself.
159    StreamStopRun,
160    /// Use this variant when the contract needs to represent stream abort and retry; selecting it has no side effect by itself.
161    StreamAbortAndRetry,
162    /// Use this variant when the contract needs to represent stream pause for approval; selecting it has no side effect by itself.
163    StreamPauseForApproval,
164    /// Use this variant when the contract needs to represent stream unsafe intervention; selecting it has no side effect by itself.
165    StreamUnsafeIntervention,
166    /// Use this variant when the contract needs to represent policy allow; selecting it has no side effect by itself.
167    PolicyAllow,
168    /// Use this variant when the contract needs to represent policy ask; selecting it has no side effect by itself.
169    PolicyAsk,
170    /// Use this variant when the contract needs to represent policy deny; selecting it has no side effect by itself.
171    PolicyDeny,
172    /// Use this variant when the contract needs to represent approved; selecting it has no side effect by itself.
173    Approved,
174    /// Use this variant when the contract needs to represent approval denied; selecting it has no side effect by itself.
175    ApprovalDenied,
176    /// Use this variant when the contract needs to represent approval timeout; selecting it has no side effect by itself.
177    ApprovalTimeout,
178    /// Use this variant when the contract needs to represent approval transport fatal; selecting it has no side effect by itself.
179    ApprovalTransportFatal,
180    /// Use this variant when the contract needs to represent stream approval resumed; selecting it has no side effect by itself.
181    StreamApprovalResumed,
182    /// Use this variant when the contract needs to represent continue with denied result; selecting it has no side effect by itself.
183    ContinueWithDeniedResult,
184    /// Use this variant when the contract needs to represent fail on denied; selecting it has no side effect by itself.
185    FailOnDenied,
186    /// Use this variant when the contract needs to represent tool complete; selecting it has no side effect by itself.
187    ToolComplete,
188    /// Use this variant when the contract needs to represent tool interrupt; selecting it has no side effect by itself.
189    ToolInterrupt,
190    /// Use this variant when the contract needs to represent tool failure; selecting it has no side effect by itself.
191    ToolFailure,
192    /// Use this variant when the contract needs to represent wait for resume; selecting it has no side effect by itself.
193    WaitForResume,
194    /// Use this variant when the contract needs to represent resume allowed; selecting it has no side effect by itself.
195    ResumeAllowed,
196    /// Use this variant when the contract needs to represent resume denied; selecting it has no side effect by itself.
197    ResumeDenied,
198    /// Use this variant when the contract needs to represent compaction complete; selecting it has no side effect by itself.
199    CompactionComplete,
200    /// Use this variant when the contract needs to represent continue loop; selecting it has no side effect by itself.
201    ContinueLoop,
202    /// Use this variant when the contract needs to represent failure classified; selecting it has no side effect by itself.
203    FailureClassified {
204        /// Classification used by this record or request.
205        classification: RecoveryClassification,
206    },
207    /// Use this variant when the contract needs to represent repair applied; selecting it has no side effect by itself.
208    RepairApplied,
209    /// Use this variant when the contract needs to represent repair completed terminal; selecting it has no side effect by itself.
210    RepairCompletedTerminal,
211    /// Use this variant when the contract needs to represent recovery irrecoverable; selecting it has no side effect by itself.
212    RecoveryIrrecoverable,
213    /// Use this variant when the contract needs to represent cancel requested; selecting it has no side effect by itself.
214    CancelRequested,
215}
216
217#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
218#[serde(rename_all = "snake_case")]
219/// Enumerates the finite max iteration outcome cases.
220/// Serialized names are part of the SDK contract; update fixtures when variants change.
221pub enum MaxIterationOutcome {
222    /// Use this variant when the contract needs to represent complete; selecting it has no side effect by itself.
223    Complete,
224    /// Use this variant when the contract needs to represent fail; selecting it has no side effect by itself.
225    Fail,
226}
227
228#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
229#[serde(rename_all = "snake_case")]
230/// Enumerates the finite transition guard cases.
231/// Serialized names are part of the SDK contract; update fixtures when variants change.
232pub enum TransitionGuard {
233    /// Use this variant when the contract needs to represent none; selecting it has no side effect by itself.
234    None,
235    /// Use this variant when the contract needs to represent package valid; selecting it has no side effect by itself.
236    PackageValid,
237    /// Use this variant when the contract needs to represent budget valid; selecting it has no side effect by itself.
238    BudgetValid,
239    /// Use this variant when the contract needs to represent package hashes match; selecting it has no side effect by itself.
240    PackageHashesMatch,
241    /// Use this variant when the contract needs to represent model message has tool calls; selecting it has no side effect by itself.
242    ModelMessageHasToolCalls,
243    /// Use this variant when the contract needs to represent rule action allowed; selecting it has no side effect by itself.
244    RuleActionAllowed,
245    /// Use this variant when the contract needs to represent final message complete; selecting it has no side effect by itself.
246    FinalMessageComplete,
247    /// Use this variant when the contract needs to represent provider failure classified; selecting it has no side effect by itself.
248    ProviderFailureClassified,
249    /// Use this variant when the contract needs to represent permissions pass; selecting it has no side effect by itself.
250    PermissionsPass,
251    /// Use this variant when the contract needs to represent dispatcher available or escalation configured; selecting it has no side effect by itself.
252    DispatcherAvailableOrEscalationConfigured,
253    /// Use this variant when the contract needs to represent denied result allowed; selecting it has no side effect by itself.
254    DeniedResultAllowed,
255    /// Use this variant when the contract needs to represent decision valid; selecting it has no side effect by itself.
256    DecisionValid,
257    /// Use this variant when the contract needs to represent timeout elapsed; selecting it has no side effect by itself.
258    TimeoutElapsed,
259    /// Use this variant when the contract needs to represent approval transport fatal; selecting it has no side effect by itself.
260    ApprovalTransportFatal,
261    /// Use this variant when the contract needs to represent terminal status appended; selecting it has no side effect by itself.
262    TerminalStatusAppended,
263    /// Use this variant when the contract needs to represent interrupt resumable; selecting it has no side effect by itself.
264    InterruptResumable,
265    /// Use this variant when the contract needs to represent resume token required; selecting it has no side effect by itself.
266    ResumeTokenRequired,
267    /// Use this variant when the contract needs to represent checkpoint and package valid; selecting it has no side effect by itself.
268    CheckpointAndPackageValid,
269    /// Use this variant when the contract needs to represent protected context preserved; selecting it has no side effect by itself.
270    ProtectedContextPreserved,
271    /// Use this variant when the contract needs to represent repair plan safe; selecting it has no side effect by itself.
272    RepairPlanSafe,
273    /// Use this variant when the contract needs to represent invariant restored; selecting it has no side effect by itself.
274    InvariantRestored,
275    /// Use this variant when the contract needs to represent max iteration budget exhausted; selecting it has no side effect by itself.
276    MaxIterationBudgetExhausted,
277    /// Use this variant when the contract needs to represent cancellation requested; selecting it has no side effect by itself.
278    CancellationRequested,
279    /// Use this variant when the contract needs to represent unsafe intervention; selecting it has no side effect by itself.
280    UnsafeIntervention,
281}
282
283#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
284/// Holds transition guard set application-layer state or configuration.
285/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
286pub struct TransitionGuardSet {
287    #[serde(default)]
288    satisfied: BTreeSet<TransitionGuard>,
289}
290
291impl TransitionGuardSet {
292    /// Creates a new application::loop_state value with explicit
293    /// caller-provided inputs. This constructor is data-only and
294    /// performs no I/O or external side effects.
295    pub fn new() -> Self {
296        Self::default()
297    }
298
299    /// Returns an updated value with with configured.
300    /// This is data-only and does not perform I/O, call host ports, append journals, publish
301    /// events, or start processes.
302    pub fn with(mut self, guard: TransitionGuard) -> Self {
303        self.satisfied.insert(guard);
304        self
305    }
306
307    /// Reads the stored contains without registry or runtime work.
308    /// This is data-only and does not perform I/O, call host ports, append journals, publish
309    /// events, or start processes.
310    pub fn contains(&self, guard: TransitionGuard) -> bool {
311        guard == TransitionGuard::None || self.satisfied.contains(&guard)
312    }
313
314    /// Returns for rule derived from the supplied state.
315    /// This uses only local coordinator state and performs no hidden host work.
316    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")]
337/// Enumerates the finite checkpoint policy cases.
338/// Serialized names are part of the SDK contract; update fixtures when variants change.
339pub enum CheckpointPolicy {
340    /// Use this variant when the contract needs to represent none; selecting it has no side effect by itself.
341    None,
342    /// Use this variant when the contract needs to represent before; selecting it has no side effect by itself.
343    Before,
344    /// Use this variant when the contract needs to represent after; selecting it has no side effect by itself.
345    After,
346    /// Use this variant when the contract needs to represent before and after; selecting it has no side effect by itself.
347    BeforeAndAfter,
348    /// Use this variant when the contract needs to represent terminal; selecting it has no side effect by itself.
349    Terminal,
350}
351
352#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
353#[serde(rename_all = "snake_case")]
354/// Enumerates the finite side effect policy cases.
355/// Serialized names are part of the SDK contract; update fixtures when variants change.
356pub enum SideEffectPolicy {
357    /// Use this variant when the contract needs to represent none; selecting it has no side effect by itself.
358    None,
359    /// Use this variant when the contract needs to represent intent before effect; selecting it has no side effect by itself.
360    IntentBeforeEffect,
361    /// Use this variant when the contract needs to represent idempotent retry allowed; selecting it has no side effect by itself.
362    IdempotentRetryAllowed,
363    /// Use this variant when the contract needs to represent non idempotent fail closed; selecting it has no side effect by itself.
364    NonIdempotentFailClosed,
365    /// Use this variant when the contract needs to represent reconcile required; selecting it has no side effect by itself.
366    ReconcileRequired,
367}
368
369#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
370#[serde(rename_all = "snake_case")]
371/// Enumerates the finite loop event kind cases.
372/// Serialized names are part of the SDK contract; update fixtures when variants change.
373pub enum LoopEventKind {
374    /// Use this variant when the contract needs to represent run started; selecting it has no side effect by itself.
375    RunStarted,
376    /// Use this variant when the contract needs to represent run completed; selecting it has no side effect by itself.
377    RunCompleted,
378    /// Use this variant when the contract needs to represent run failed; selecting it has no side effect by itself.
379    RunFailed,
380    /// Use this variant when the contract needs to represent run cancelled; selecting it has no side effect by itself.
381    RunCancelled,
382    /// Use this variant when the contract needs to represent run cancel requested; selecting it has no side effect by itself.
383    RunCancelRequested,
384    /// Use this variant when the contract needs to represent run checkpointed; selecting it has no side effect by itself.
385    RunCheckpointed,
386    /// Use this variant when the contract needs to represent run resume requested; selecting it has no side effect by itself.
387    RunResumeRequested,
388    /// Use this variant when the contract needs to represent run resume failed; selecting it has no side effect by itself.
389    RunResumeFailed,
390    /// Use this variant when the contract needs to represent context assembled; selecting it has no side effect by itself.
391    ContextAssembled,
392    /// Use this variant when the contract needs to represent provider request projected; selecting it has no side effect by itself.
393    ProviderRequestProjected,
394    /// Use this variant when the contract needs to represent model attempt started; selecting it has no side effect by itself.
395    ModelAttemptStarted,
396    /// Use this variant when the contract needs to represent model attempt failed; selecting it has no side effect by itself.
397    ModelAttemptFailed,
398    /// Use this variant when the contract needs to represent model message completed; selecting it has no side effect by itself.
399    ModelMessageCompleted,
400    /// Use this variant when the contract needs to represent tool requested; selecting it has no side effect by itself.
401    ToolRequested,
402    /// Use this variant when the contract needs to represent tool approval required; selecting it has no side effect by itself.
403    ToolApprovalRequired,
404    /// Use this variant when the contract needs to represent tool started; selecting it has no side effect by itself.
405    ToolStarted,
406    /// Use this variant when the contract needs to represent tool completed; selecting it has no side effect by itself.
407    ToolCompleted,
408    /// Use this variant when the contract needs to represent tool failed; selecting it has no side effect by itself.
409    ToolFailed,
410    /// Use this variant when the contract needs to represent tool interrupted; selecting it has no side effect by itself.
411    ToolInterrupted,
412    /// Use this variant when the contract needs to represent approval requested; selecting it has no side effect by itself.
413    ApprovalRequested,
414    /// Use this variant when the contract needs to represent approval responded; selecting it has no side effect by itself.
415    ApprovalResponded,
416    /// Use this variant when the contract needs to represent approval timed out; selecting it has no side effect by itself.
417    ApprovalTimedOut,
418    /// Use this variant when the contract needs to represent approval denied; selecting it has no side effect by itself.
419    ApprovalDenied,
420    /// Use this variant when the contract needs to represent stream rule matched; selecting it has no side effect by itself.
421    StreamRuleMatched,
422    /// Use this variant when the contract needs to represent stream intervention applied; selecting it has no side effect by itself.
423    StreamInterventionApplied,
424    /// Use this variant when the contract needs to represent context compaction completed; selecting it has no side effect by itself.
425    ContextCompactionCompleted,
426    /// Use this variant when the contract needs to represent recovery planned; selecting it has no side effect by itself.
427    RecoveryPlanned,
428    /// Use this variant when the contract needs to represent replay completed; selecting it has no side effect by itself.
429    ReplayCompleted,
430}
431
432#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
433#[serde(rename_all = "snake_case")]
434/// Enumerates the finite loop terminal status cases.
435/// Serialized names are part of the SDK contract; update fixtures when variants change.
436pub enum LoopTerminalStatus {
437    /// Use this variant when the contract needs to represent completed; selecting it has no side effect by itself.
438    Completed,
439    /// Use this variant when the contract needs to represent cancelled; selecting it has no side effect by itself.
440    Cancelled,
441    /// Use this variant when the contract needs to represent failed; selecting it has no side effect by itself.
442    Failed,
443}
444
445#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
446#[serde(rename_all = "snake_case")]
447/// Enumerates the finite loop stop reason cases.
448/// Serialized names are part of the SDK contract; update fixtures when variants change.
449pub enum LoopStopReason {
450    /// Use this variant when the contract needs to represent end turn; selecting it has no side effect by itself.
451    EndTurn,
452    /// Use this variant when the contract needs to represent stream rule stop; selecting it has no side effect by itself.
453    StreamRuleStop,
454    /// Use this variant when the contract needs to represent max iterations; selecting it has no side effect by itself.
455    MaxIterations {
456        /// Outcome used by this record or request.
457        outcome: MaxIterationOutcome,
458    },
459    /// Use this variant when the contract needs to represent cancelled; selecting it has no side effect by itself.
460    Cancelled,
461    /// Use this variant when the contract needs to represent provider failure; selecting it has no side effect by itself.
462    ProviderFailure,
463    /// Use this variant when the contract needs to represent unsafe stream intervention; selecting it has no side effect by itself.
464    UnsafeStreamIntervention,
465    /// Use this variant when the contract needs to represent tool denied; selecting it has no side effect by itself.
466    ToolDenied,
467    /// Use this variant when the contract needs to represent approval denied; selecting it has no side effect by itself.
468    ApprovalDenied,
469    /// Use this variant when the contract needs to represent approval timeout; selecting it has no side effect by itself.
470    ApprovalTimeout,
471    /// Use this variant when the contract needs to represent approval transport fatal; selecting it has no side effect by itself.
472    ApprovalTransportFatal,
473    /// Use this variant when the contract needs to represent tool failure; selecting it has no side effect by itself.
474    ToolFailure,
475    /// Use this variant when the contract needs to represent resume denied; selecting it has no side effect by itself.
476    ResumeDenied,
477    /// Use this variant when the contract needs to represent recovery completed; selecting it has no side effect by itself.
478    RecoveryCompleted,
479    /// Use this variant when the contract needs to represent recovery irrecoverable; selecting it has no side effect by itself.
480    RecoveryIrrecoverable,
481}
482
483#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
484/// Holds loop terminal result application-layer state or configuration.
485/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
486pub struct LoopTerminalResult {
487    /// Finite status for this record or lifecycle stage.
488    pub status: LoopTerminalStatus,
489    /// Stop reason used by this record or request.
490    pub stop_reason: LoopStopReason,
491}
492
493#[derive(Clone, Debug, Eq, PartialEq)]
494/// Holds transition rule application-layer state or configuration.
495/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
496pub struct TransitionRule {
497    /// From state used by this record or request.
498    pub from_state: LoopState,
499    /// Trigger used by this record or request.
500    pub trigger: LoopTrigger,
501    /// Guard used by this record or request.
502    pub guard: TransitionGuard,
503    /// Bounded events included in this record. Limits and truncation are
504    /// represented by companion metadata when applicable.
505    pub events: &'static [LoopEventKind],
506    /// Journal record kinds produced by this capability or feature.
507    /// Use them to keep replay and recovery fixtures aligned with the public contract.
508    pub journal_records: &'static [JournalRecordKind],
509    /// Checkpoint policy used by this record or request.
510    pub checkpoint_policy: CheckpointPolicy,
511    /// Side effect policy used by this record or request.
512    pub side_effect_policy: SideEffectPolicy,
513    /// Next state used by this record or request.
514    pub next_state: LoopState,
515    /// Optional terminal result value.
516    /// When absent, callers should use the documented default or skip that optional behavior.
517    pub terminal_result: Option<LoopTerminalResult>,
518    /// Optional recovery classification value.
519    /// When absent, callers should use the documented default or skip that optional behavior.
520    pub recovery_classification: Option<RecoveryClassification>,
521}
522
523impl TransitionRule {
524    /// Returns output derived from the supplied state.
525    /// This uses only local coordinator state and performs no hidden host work.
526    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)]
543/// Holds transition input application-layer state or configuration.
544/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
545pub struct TransitionInput {
546    /// State used by this record or request.
547    pub state: LoopState,
548    /// Trigger used by this record or request.
549    pub trigger: LoopTrigger,
550    #[serde(default)]
551    /// Guards used by this record or request.
552    pub guards: TransitionGuardSet,
553}
554
555impl TransitionInput {
556    /// Creates a new application::loop_state value with explicit
557    /// caller-provided inputs. This constructor is data-only and
558    /// performs no I/O or external side effects.
559    pub fn new(state: LoopState, trigger: LoopTrigger) -> Self {
560        Self {
561            state,
562            trigger,
563            guards: TransitionGuardSet::default(),
564        }
565    }
566
567    /// Returns this value with its guard setting replaced. The method
568    /// follows builder-style data construction and does not execute
569    /// external work.
570    pub fn with_guard(mut self, guard: TransitionGuard) -> Self {
571        self.guards = self.guards.with(guard);
572        self
573    }
574
575    /// Returns this value with its guards setting replaced. The method
576    /// follows builder-style data construction and does not execute
577    /// external work.
578    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)]
585/// Holds transition output application-layer state or configuration.
586/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
587pub struct TransitionOutput {
588    /// From state used by this record or request.
589    pub from_state: LoopState,
590    /// Trigger used by this record or request.
591    pub trigger: LoopTrigger,
592    /// Guard used by this record or request.
593    pub guard: TransitionGuard,
594    /// Bounded events included in this record. Limits and truncation are
595    /// represented by companion metadata when applicable.
596    pub events: Vec<LoopEventKind>,
597    /// Journal record kinds produced by this capability or feature.
598    /// Use them to keep replay and recovery fixtures aligned with the public contract.
599    pub journal_records: Vec<JournalRecordKind>,
600    /// Checkpoint policy used by this record or request.
601    pub checkpoint_policy: CheckpointPolicy,
602    /// Side effect policy used by this record or request.
603    pub side_effect_policy: SideEffectPolicy,
604    /// Next state used by this record or request.
605    pub next_state: LoopState,
606    #[serde(skip_serializing_if = "Option::is_none")]
607    /// Optional terminal result value.
608    /// When absent, callers should use the documented default or skip that optional behavior.
609    pub terminal_result: Option<LoopTerminalResult>,
610    #[serde(skip_serializing_if = "Option::is_none")]
611    /// Optional recovery classification value.
612    /// When absent, callers should use the documented default or skip that optional behavior.
613    pub recovery_classification: Option<RecoveryClassification>,
614}
615
616#[derive(Clone, Copy, Debug, Default)]
617/// Holds agent state machine application-layer state or configuration.
618/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
619pub struct AgentStateMachine;
620
621impl AgentStateMachine {
622    /// Returns the transition table derived from this value.
623    /// This uses only local coordinator state and performs no hidden host work.
624    pub fn transition_table(&self) -> &'static [TransitionRule] {
625        transition_table()
626    }
627
628    /// Validates the application::loop_state invariants and returns a
629    /// typed error on failure. Validation is pure and does not perform
630    /// I/O, dispatch, journal appends, or adapter calls.
631    pub fn validate_transition(
632        &self,
633        input: TransitionInput,
634    ) -> Result<TransitionOutput, AgentError> {
635        validate_transition(input)
636    }
637}
638
639/// Validates the application::loop_state invariants and returns a typed
640/// error on failure. Validation is pure and does not perform I/O,
641/// dispatch, journal appends, or adapter calls.
642pub 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
659/// Returns the transition table derived from this value.
660/// This derives SDK state locally and does not call host adapters.
661pub fn transition_table() -> &'static [TransitionRule] {
662    TRANSITION_TABLE
663}
664
665/// Returns the contract state names derived from this value.
666/// This derives SDK state locally and does not call host adapters.
667pub 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}