Skip to main content

termivar_scanner/decision_loop/
state.rs

1//! Replayable session state and transition summaries.
2
3use super::{
4    AdaptationLedger, DecisionLoopError, DecisionStopReason, Deserialize, Deserializer, EntityId,
5    Serialize, VerificationCase,
6};
7
8/// Replayable state of one target-scoped decision session.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(tag = "state", rename_all = "snake_case")]
11#[non_exhaustive]
12pub enum DecisionLoopState {
13    /// Ready to run reasoning and planning.
14    Ready,
15    /// Waiting for evidence produced by an emitted action.
16    AwaitingPassive {
17        /// Case attached to the outstanding action.
18        case: VerificationCase,
19    },
20    /// Waiting for evidence from an explicit verification probe.
21    AwaitingActive {
22        /// Case attached to the outstanding probe.
23        case: VerificationCase,
24    },
25    /// The current objective was verified.
26    Completed,
27    /// The session stopped without a completed objective.
28    Halted {
29        /// Stable stop reason.
30        reason: DecisionStopReason,
31    },
32}
33
34impl DecisionLoopState {
35    pub(super) fn name(&self) -> &'static str {
36        match self {
37            Self::Ready => "ready",
38            Self::AwaitingPassive { .. } => "awaiting_passive",
39            Self::AwaitingActive { .. } => "awaiting_active",
40            Self::Completed => "completed",
41            Self::Halted { .. } => "halted",
42        }
43    }
44
45    pub(super) fn case(&self) -> Option<&VerificationCase> {
46        match self {
47            Self::AwaitingPassive { case } | Self::AwaitingActive { case } => Some(case),
48            Self::Ready | Self::Completed | Self::Halted { .. } => None,
49        }
50    }
51}
52
53/// Target-scoped counters and adaptive ledger for deterministic replay.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct DecisionSession {
56    pub(super) subject: EntityId,
57    pub(super) action_cycles: u32,
58    pub(super) state: DecisionLoopState,
59    pub(super) adaptation: AdaptationLedger,
60}
61
62impl DecisionSession {
63    /// Creates a ready session for one knowledge subject.
64    pub fn new(subject: EntityId) -> Self {
65        Self {
66            subject,
67            action_cycles: 0,
68            state: DecisionLoopState::Ready,
69            adaptation: AdaptationLedger::new(),
70        }
71    }
72
73    /// Returns the session subject.
74    pub fn subject(&self) -> &EntityId {
75        &self.subject
76    }
77
78    /// Returns the number of action executions issued so far.
79    pub fn action_cycles(&self) -> u32 {
80        self.action_cycles
81    }
82
83    /// Returns the current state.
84    pub fn state(&self) -> &DecisionLoopState {
85        &self.state
86    }
87
88    /// Stops an outstanding session when the host runtime refuses more work.
89    pub(crate) fn halt_for_runtime_budget(&mut self) {
90        self.state = DecisionLoopState::Halted {
91            reason: DecisionStopReason::RuntimeBudgetLimit,
92        };
93    }
94
95    /// Stops an outstanding session after an explicit host cancellation.
96    pub(crate) fn halt_for_host_cancellation(&mut self) {
97        self.state = DecisionLoopState::Halted {
98            reason: DecisionStopReason::CancelledByHost,
99        };
100    }
101
102    /// Returns an outstanding execution to the planning boundary after the
103    /// host's current defense authority suppresses it before dispatch.
104    ///
105    /// The issued-action counter remains monotonic: the command was already
106    /// authorized and emitted even though the runner correctly performed no
107    /// side effect. The next plan must use the same defense context and can
108    /// therefore only choose from the filtered baseline.
109    pub(crate) fn replan_after_defense_suppression(&mut self) -> Result<(), DecisionLoopError> {
110        match &self.state {
111            DecisionLoopState::AwaitingPassive { .. }
112            | DecisionLoopState::AwaitingActive { .. } => {
113                self.state = DecisionLoopState::Ready;
114                Ok(())
115            },
116            _ => Err(DecisionLoopError::InvalidTransition {
117                operation: "replan after defense suppression",
118                state: self.state.name(),
119            }),
120        }
121    }
122
123    /// Finalizes a multi-objective session as completed at the aggregate
124    /// boundary, once automated work is exhausted with at least one success and
125    /// no unresolved case. Narrow host helper — not a general session resume.
126    pub(crate) fn finalize_objective_complete(&mut self) {
127        self.state = DecisionLoopState::Completed;
128    }
129
130    /// Finalizes a multi-objective session as pending human review at the
131    /// aggregate boundary, once automated work is exhausted while at least one
132    /// unresolved (blocked / active-inconclusive) case remains. Narrow host
133    /// helper — not a general session resume.
134    pub(crate) fn finalize_human_review(&mut self) {
135        self.state = DecisionLoopState::Halted {
136            reason: DecisionStopReason::HumanReview,
137        };
138    }
139
140    /// Returns the adaptive transition ledger.
141    pub fn adaptation(&self) -> &AdaptationLedger {
142        &self.adaptation
143    }
144
145    /// Captures a lightweight state summary at one outcome boundary.
146    ///
147    /// This intentionally omits the subject and full adaptation ledger. It is
148    /// an audit summary for comparing one transition, not a persistence or
149    /// replay snapshot.
150    pub fn transition_summary(&self) -> DecisionSessionSummary {
151        DecisionSessionSummary {
152            state: self.state.clone(),
153            action_cycles: self.action_cycles,
154            adaptation_transitions: self.adaptation.transitions(),
155        }
156    }
157}
158
159impl<'de> Deserialize<'de> for DecisionSession {
160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161    where
162        D: Deserializer<'de>,
163    {
164        #[derive(Deserialize)]
165        struct WireSession {
166            subject: EntityId,
167            action_cycles: u32,
168            state: DecisionLoopState,
169            adaptation: AdaptationLedger,
170        }
171
172        let wire = WireSession::deserialize(deserializer)?;
173        if let Some(case) = wire.state.case() {
174            if case.subject() != &wire.subject {
175                return Err(serde::de::Error::custom(
176                    DecisionLoopError::CaseSubjectMismatch {
177                        expected: wire.subject,
178                        actual: case.subject().clone(),
179                    },
180                ));
181            }
182            if wire.action_cycles == 0 {
183                return Err(serde::de::Error::custom(
184                    DecisionLoopError::AwaitingWithoutAction,
185                ));
186            }
187        }
188        Ok(Self {
189            subject: wire.subject,
190            action_cycles: wire.action_cycles,
191            state: wire.state,
192            adaptation: wire.adaptation,
193        })
194    }
195}
196
197/// Lightweight audit summary captured around one outcome commit.
198///
199/// This is not a complete persistence or replay snapshot of a decision
200/// session. The owning [`DecisionSession`] remains the source of that state.
201#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
202pub struct DecisionSessionSummary {
203    pub(super) state: DecisionLoopState,
204    pub(super) action_cycles: u32,
205    pub(super) adaptation_transitions: u32,
206}
207
208impl DecisionSessionSummary {
209    /// Returns the state at this commit boundary.
210    pub fn state(&self) -> &DecisionLoopState {
211        &self.state
212    }
213
214    /// Returns the number of issued action executions at this boundary.
215    pub fn action_cycles(&self) -> u32 {
216        self.action_cycles
217    }
218
219    /// Returns the number of recorded adaptive directives at this boundary.
220    pub fn adaptation_transitions(&self) -> u32 {
221        self.adaptation_transitions
222    }
223}
224
225/// Before/after session summary produced by successful outcome processing.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
227pub struct DecisionSessionTransition {
228    pub(super) before: DecisionSessionSummary,
229    pub(super) after: DecisionSessionSummary,
230}
231
232impl DecisionSessionTransition {
233    pub(super) fn new(before: DecisionSessionSummary, after: DecisionSessionSummary) -> Self {
234        Self { before, after }
235    }
236
237    /// Returns the session summary before verification and adaptation.
238    pub fn before(&self) -> &DecisionSessionSummary {
239        &self.before
240    }
241
242    /// Returns the session summary after successful outcome processing.
243    pub fn after(&self) -> &DecisionSessionSummary {
244        &self.after
245    }
246}