Skip to main content

eredu_runtime/
execution_control.rs

1//! Shared completed-token lifecycle and non-rewindable snapshot reservations.
2
3use eredu_core::{execution_control::*, generation::FinishReason};
4use std::{cell::RefCell, rc::Rc};
5
6mod choice;
7mod sampling;
8mod snapshot;
9pub use choice::{TokenChoiceController, TokenChoiceError};
10pub use sampling::{
11    apply_prepared_sampling_override, apply_sampling_override, SamplingOverride,
12    SamplingOverrideError, SamplingStateFacts, TextSamplingControlBackend,
13    ValidatedSamplingOverride,
14};
15
16/// Logical owned storage of the audited serialized intervention request DTO.
17/// Native estimators and admitted handles are deliberately excluded.
18pub fn intervention_plan_storage_bytes(
19    plan: &eredu_core::intervention::InterventionPlan,
20) -> Option<u64> {
21    (std::mem::size_of_val(plan) as u64).checked_add(storage::heap_bytes(plan)?)
22}
23pub(crate) mod storage;
24pub use snapshot::{
25    ManagedTextContinuation, SnapshotTokenController, TextBranchRequest, TextContinuationBranch,
26    TextContinuationSnapshot, TextSnapshotBackend, TextSnapshotError,
27};
28
29/// Quiescent, rewindable lifecycle component of a complete generation snapshot.
30/// It is not native completion evidence or a substitute for a complete snapshot.
31#[derive(Debug, Clone)]
32pub struct GenerationBoundary {
33    status: GenerationStatus,
34    prediction: u64,
35    finish_reason: Option<FinishReason>,
36}
37
38/// One portable lifecycle shared by all native implementations. A Running state
39/// includes native completion and record delivery, not just host submission.
40#[derive(Debug)]
41pub struct GenerationLifecycle {
42    boundary: GenerationBoundary,
43    epoch: u64,
44}
45
46impl Default for GenerationLifecycle {
47    fn default() -> Self {
48        Self {
49            boundary: GenerationBoundary {
50                status: GenerationStatus::Prepared,
51                prediction: 0,
52                finish_reason: None,
53            },
54            epoch: 0,
55        }
56    }
57}
58
59impl GenerationLifecycle {
60    /// Current state. Paused means all work and delivery at the boundary completed.
61    pub fn status(&self) -> GenerationStatus {
62        self.boundary.status
63    }
64    /// Next absolute prediction: zero is prefill, later values are decode.
65    pub fn next_prediction(&self) -> u64 {
66        self.boundary.prediction
67    }
68    /// Monotone restore epoch for consumer reconciliation. Never rewound.
69    pub fn epoch(&self) -> u64 {
70        self.epoch
71    }
72    /// Retained terminal outcome, including cancellation.
73    pub fn finish_reason(&self) -> Option<FinishReason> {
74        self.boundary.finish_reason
75    }
76
77    fn invalid(&self, to: GenerationStatus) -> ExecutionControlError {
78        ExecutionControlError::Transition {
79            from: self.status(),
80            to,
81        }
82    }
83
84    /// Enters one prediction. Call only after observing remote pause/cancel requests.
85    pub fn begin_prediction(&mut self) -> Result<(), ExecutionControlError> {
86        if !matches!(
87            self.status(),
88            GenerationStatus::Prepared | GenerationStatus::Paused
89        ) {
90            return Err(self.invalid(GenerationStatus::Running));
91        }
92        self.boundary
93            .prediction
94            .checked_add(1)
95            .ok_or(ExecutionControlError::Overflow)?;
96        self.boundary.status = GenerationStatus::Running;
97        Ok(())
98    }
99
100    /// Commits one prediction only after exact native completion, semantic
101    /// commitment and associated capture/intervention record delivery succeed.
102    pub fn complete_prediction(
103        &mut self,
104        reason: Option<FinishReason>,
105    ) -> Result<(), ExecutionControlError> {
106        if self.status() != GenerationStatus::Running {
107            return Err(self.invalid(GenerationStatus::Paused));
108        }
109        self.boundary.prediction = self
110            .boundary
111            .prediction
112            .checked_add(1)
113            .ok_or(ExecutionControlError::Overflow)?;
114        self.boundary.finish_reason = reason;
115        self.boundary.status = match reason {
116            Some(FinishReason::Cancelled) => GenerationStatus::Cancelled,
117            Some(_) => GenerationStatus::Completed,
118            None => GenerationStatus::Paused,
119        };
120        Ok(())
121    }
122
123    /// Pauses an already quiescent session without advancing a prediction or RNG.
124    pub fn pause(&mut self) -> Result<(), ExecutionControlError> {
125        if !matches!(
126            self.status(),
127            GenerationStatus::Prepared | GenerationStatus::Paused
128        ) {
129            return Err(self.invalid(GenerationStatus::Paused));
130        }
131        self.boundary.status = GenerationStatus::Paused;
132        Ok(())
133    }
134
135    /// Cancels before the next prediction, after native work is known quiescent.
136    pub fn cancel(&mut self) -> Result<(), ExecutionControlError> {
137        if !matches!(
138            self.status(),
139            GenerationStatus::Prepared | GenerationStatus::Paused
140        ) {
141            return Err(self.invalid(GenerationStatus::Cancelled));
142        }
143        self.boundary.status = GenerationStatus::Cancelled;
144        self.boundary.finish_reason = Some(FinishReason::Cancelled);
145        Ok(())
146    }
147
148    /// Completes a cancellation observed after entering Running but before the
149    /// ordinary cursor submitted or committed any prediction. Call only after
150    /// verifying a quiescent native boundary; this never increments position.
151    pub fn cancel_without_prediction(&mut self) -> Result<(), ExecutionControlError> {
152        if self.status() != GenerationStatus::Running {
153            return Err(self.invalid(GenerationStatus::Cancelled));
154        }
155        self.boundary.status = GenerationStatus::Cancelled;
156        self.boundary.finish_reason = Some(FinishReason::Cancelled);
157        Ok(())
158    }
159
160    /// Fences generation after an unresolved native or semantic/delivery failure.
161    /// Failure does not imply native completion; the existing native owner survives.
162    pub fn fail(&mut self) {
163        self.boundary.status = GenerationStatus::Failed;
164    }
165
166    /// Saves a quiescent initial, paused or normally completed boundary.
167    pub fn checkpoint(&self) -> Result<GenerationBoundary, ExecutionControlError> {
168        if !matches!(
169            self.status(),
170            GenerationStatus::Prepared | GenerationStatus::Paused | GenerationStatus::Completed
171        ) {
172            return Err(self.invalid(GenerationStatus::Paused));
173        }
174        Ok(self.boundary.clone())
175    }
176
177    /// Validates the portable restore transition before native copying begins.
178    pub fn validate_restore(&self) -> Result<(), ExecutionControlError> {
179        self.checkpoint()?;
180        self.epoch
181            .checked_add(1)
182            .ok_or(ExecutionControlError::Overflow)?;
183        Ok(())
184    }
185
186    /// Installs the saved boundary after compatibility checks and atomic native
187    /// restoration. A completed snapshot remains terminal; an earlier snapshot
188    /// can resume an otherwise completed run. Cancellation and failure stay fenced.
189    pub fn restore(&mut self, saved: &GenerationBoundary) -> Result<(), ExecutionControlError> {
190        self.validate_restore()?;
191        self.epoch += 1;
192        self.boundary = saved.clone();
193        Ok(())
194    }
195
196    /// Creates child lifecycle state at the same absolute position with epoch zero.
197    /// Native state and immutable admissions must be independently forked/rebound.
198    pub fn fork(saved: &GenerationBoundary) -> Self {
199        Self {
200            boundary: saved.clone(),
201            epoch: 0,
202        }
203    }
204}
205
206struct BudgetState {
207    limits: SnapshotLimits,
208    usage: SnapshotUsage,
209}
210
211/// Shared logical reservation owner for retained snapshots, child states and
212/// provisional restore copies. It deliberately lives outside rewindable state.
213#[derive(Clone)]
214pub struct SnapshotBudget(Rc<RefCell<BudgetState>>);
215
216impl SnapshotBudget {
217    /// Creates an explicitly bounded resource owner; zero limits disable retention.
218    pub fn new(limits: SnapshotLimits) -> Self {
219        Self(Rc::new(RefCell::new(BudgetState {
220            limits,
221            usage: SnapshotUsage::default(),
222        })))
223    }
224    /// Observes retained counts and cumulative copying without native side effects.
225    pub fn usage(&self) -> SnapshotUsage {
226        self.0.borrow().usage
227    }
228    /// Reserves known costs before copying or retaining any state. Unknown costs,
229    /// overflows and limits fail without changing accounting. After admission,
230    /// dropping a failed attempt releases retention but never refunds copying.
231    pub fn reserve(
232        &self,
233        kind: SnapshotResourceKind,
234        estimate: Option<SnapshotEstimate>,
235    ) -> Result<SnapshotReservation, ExecutionControlError> {
236        let estimate = estimate.ok_or(ExecutionControlError::UnknownEstimate)?;
237        let mut state = self.0.borrow_mut();
238        let add = |a: u64, b: u64| a.checked_add(b).ok_or(ExecutionControlError::Overflow);
239        let next = SnapshotUsage {
240            snapshots: add(
241                state.usage.snapshots,
242                u64::from(kind == SnapshotResourceKind::Snapshot),
243            )?,
244            branches: add(
245                state.usage.branches,
246                u64::from(kind == SnapshotResourceKind::Branch),
247            )?,
248            retained_bytes: add(state.usage.retained_bytes, estimate.retained_bytes)?,
249            cumulative_copy_bytes: add(state.usage.cumulative_copy_bytes, estimate.copy_bytes)?,
250        };
251        for (exceeded, name) in [
252            (
253                next.snapshots > state.limits.max_snapshots,
254                "snapshot count",
255            ),
256            (next.branches > state.limits.max_branches, "branch count"),
257            (
258                next.retained_bytes > state.limits.retained_bytes,
259                "retained bytes",
260            ),
261            (
262                next.cumulative_copy_bytes > state.limits.cumulative_copy_bytes,
263                "cumulative copy bytes",
264            ),
265        ] {
266            if exceeded {
267                return Err(ExecutionControlError::Limit(name));
268            }
269        }
270        state.usage = next;
271        Ok(SnapshotReservation {
272            lease: Rc::new(ReservationLease {
273                budget: self.clone(),
274                kind,
275                retained_bytes: estimate.retained_bytes,
276            }),
277        })
278    }
279}
280
281/// Retention lease held alongside an opaque snapshot or child state. Cloning the
282/// lease is only for handles sharing that same object; copying native state needs
283/// another reservation. Final drop releases retained resources, not cumulative work.
284#[derive(Clone)]
285pub struct SnapshotReservation {
286    lease: Rc<ReservationLease>,
287}
288
289impl SnapshotReservation {
290    /// Logical storage charged while any handle to this exact object is retained.
291    pub fn retained_bytes(&self) -> u64 {
292        self.lease.retained_bytes
293    }
294}
295
296struct ReservationLease {
297    budget: SnapshotBudget,
298    kind: SnapshotResourceKind,
299    retained_bytes: u64,
300}
301
302impl Drop for ReservationLease {
303    fn drop(&mut self) {
304        let mut state = self.budget.0.borrow_mut();
305        state.usage.snapshots -= u64::from(self.kind == SnapshotResourceKind::Snapshot);
306        state.usage.branches -= u64::from(self.kind == SnapshotResourceKind::Branch);
307        state.usage.retained_bytes -= self.retained_bytes;
308    }
309}
310
311#[cfg(test)]
312mod tests;