Skip to main content

rustev_runtime/
lib.rs

1//! Rustev's runtime (spec 003).
2//!
3//! Drives the pure core's staged evaluation through [`DecisionBackend`]s
4//! under explicit bounds: a monotonic [`Clock`] and one end-to-end deadline,
5//! bounded admission and concurrency, cost [`Ledger`]s that reserve before
6//! dispatch, retries and fallback only as the plan's execution policy
7//! declares them, cancellation that records exactly what the backend
8//! acknowledged, and a run record delivered under a [`SinkPolicy`].
9//!
10//! The runtime spawns no task per decision, request or attempt: a decision
11//! runs on the caller's task. The one spawned task is the delivery worker of
12//! the queued sink policies (spec 003, 3.4.3).
13#![forbid(unsafe_code)]
14
15mod acquisition;
16mod capture;
17pub mod clock;
18mod driver;
19mod dynamic;
20mod generation;
21#[cfg(test)]
22mod generation_acceptance;
23pub use generation::{GenerationEvent, GenerationStream};
24pub mod ledger;
25mod live;
26mod optimization;
27mod progress;
28mod shared;
29mod sink;
30pub use progress::{ProgressObserver, StepProgress};
31mod wait;
32
33use std::collections::{BTreeMap, BTreeSet};
34use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
35use std::sync::{Arc, Mutex};
36
37use rustev_contract::execution::{
38    AttemptTimeout, CostPolicy, FailureClass, RetryPolicy, StepExecution,
39};
40use rustev_contract::ids::ArtifactId;
41use rustev_contract::judgment::Judgment;
42use rustev_contract::optimization::OptimizationPolicy;
43use rustev_contract::plan::{PlanExecution, PlanStepDetail};
44use rustev_contract::replay::Capture;
45use rustev_contract::run::{
46    CoreEvidence, CostMode, CostModel, MAX_REMOTE_EVIDENCE, RecordedExecution, RemoteEvidence,
47    RequestRecord, RunRecord, Termination, Timing,
48};
49use rustev_contract::snapshot::Snapshot;
50use rustev_contract::time::Timestamp;
51use rustev_contract::{Identified, schema};
52use rustev_core::Compiled;
53use rustev_core::evaluate::Evaluation;
54use rustev_core::seams::{CancelSignal, DecisionBackend, EvidenceSink};
55use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
56
57pub use capture::{CaptureConfig, CaptureConfigError, MAX_CAPTURE_BYTES};
58pub use clock::{Clock, Instant, ManualClock, TokioClock};
59/// Dynamic questions (spec 019): the only execution entry point.
60pub use dynamic::{DynamicAsked, DynamicBounds, DynamicDelivery, ask_dynamic};
61pub use ledger::Ledger;
62pub use live::{DispatchAnswer, LiveAuthorizer, LiveRefusal, LiveRequest, LiveResult};
63pub use optimization::InvalidationResult;
64pub use sink::{DeliveryFailure, DeliveryTicket, SinkPolicy, SinkStats};
65
66use crate::optimization::InvalidationSelector;
67use crate::sink::{Job, Stats};
68use crate::wait::{Raced, race};
69
70/// Bounds of one runtime (spec 003, 3.4).
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct RuntimeConfig {
73    /// Decisions admitted at once.
74    pub max_in_flight: usize,
75    /// Decisions waiting for admission.
76    pub max_queued: usize,
77    /// Requests in progress within one decision.
78    pub max_parallel_requests: usize,
79    pub sink: SinkPolicy,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum ConfigError {
84    /// A bound that must be positive is 0.
85    Zero(&'static str),
86    DuplicateBackend(String),
87    InvalidSource(String),
88}
89
90/// Why a plan cannot run on this runtime (spec 003, 3.5.3 and 3.6.5).
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum PrepareError {
93    SourceBinding(String),
94    /// The plan binds a backend id that is not registered.
95    MissingBackend(String),
96    /// The registered backend's descriptor is not the one the plan bound.
97    DescriptorMismatch {
98        backend_id: String,
99    },
100    /// A hard budget, and a reachable backend without enforceable bounds.
101    HardBudgetUnenforceable {
102        backend_id: String,
103        model: CostModel,
104    },
105    /// A plan 3 and an evidence sink that does not accept `rustev.run/3`
106    /// (spec 032, 3.11.2).
107    SinkVersionUnsupported {
108        schema: &'static str,
109    },
110}
111
112/// Why a decision was not admitted (spec 003, 3.4.1). Nothing was dispatched
113/// or spent, and there is no run record.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum Rejection {
116    Overloaded,
117    QueueDeadline,
118    Cancelled,
119    InvalidRequest(String),
120}
121
122#[derive(Debug)]
123pub enum DecideError {
124    /// A pure generation invariant failed; no text result is returned.
125    GenerationFailed(String),
126    Rejected(Rejection),
127    /// Work and spending happened; the record did not reach the sink. The
128    /// judgment inside `record` is evidence, not a result to act on.
129    EvidenceNotDelivered {
130        record: Arc<RunRecord>,
131        failure: DeliveryFailure,
132    },
133}
134
135/// One decision to run.
136#[derive(Debug, Clone)]
137pub struct DecisionRequest {
138    /// Non-empty, at most 256 bytes; the delivery key.
139    pub decision_id: String,
140    pub snapshot: Snapshot,
141    /// Domain time, supplied; never read from a clock.
142    pub evaluation_time: Timestamp,
143    /// Can only tighten the plan's `deadline_ms`.
144    pub deadline_ms: Option<u64>,
145    /// Opaque complete authorized-scope handle, including any tenant and
146    /// principal isolation the host requires; passed through to backends and
147    /// included in every reusable-work key.
148    pub principal_handle: Vec<u8>,
149}
150
151#[derive(Debug, Clone, PartialEq)]
152pub enum Completion {
153    Judged(Box<Judgment>),
154    Cancelled,
155}
156
157#[derive(Debug)]
158pub enum Delivery {
159    Acknowledged { receipt: String },
160    Queued(DeliveryTicket),
161    Dropped { dropped_total: u64 },
162}
163
164#[derive(Debug)]
165pub struct Decided {
166    pub text_result: Option<rustev_contract::generation_result::TextResult>,
167    pub completion: Completion,
168    pub record: Arc<RunRecord>,
169    pub delivery: Delivery,
170}
171
172/// What [`Runtime::decide_with_capture`] captured (spec 004, 5.3).
173#[derive(Debug, Clone, PartialEq)]
174pub enum CaptureOutcome {
175    /// The decision was rejected: nothing ran, and there is nothing to
176    /// retain or replay.
177    NotAdmitted,
178    Captured(Capture),
179}
180
181/// The normal decide result, and beside it the capture, also when evidence
182/// delivery failed.
183#[derive(Debug)]
184pub struct CapturedDecision {
185    pub result: Result<Decided, DecideError>,
186    pub capture: CaptureOutcome,
187}
188
189/// Live counts, for operators and tests. In memory only.
190#[derive(Debug, Clone, PartialEq, Eq, Default)]
191pub struct Gauges {
192    pub in_flight: usize,
193    pub queued: usize,
194    pub rejected: u64,
195    pub abandoned: u64,
196    /// Attempts in flight per backend.
197    pub backend_in_flight: BTreeMap<String, usize>,
198    pub sink: SinkStats,
199}
200
201#[derive(Clone)]
202pub(crate) struct Registered {
203    pub backend: Arc<dyn DecisionBackend>,
204    pub permits: Arc<Semaphore>,
205    pub max: usize,
206    /// Who implements it (spec 033, 3.9): `upstream` unless registered as
207    /// the host's own.
208    pub origin: rustev_contract::actionability::Origin,
209}
210
211pub(crate) struct Inner {
212    pub clock: Arc<dyn Clock>,
213    pub config: RuntimeConfig,
214    pub backends: BTreeMap<String, Registered>,
215    pub sources: BTreeMap<String, acquisition::RegisteredSource>,
216    pub sink: Arc<dyn EvidenceSink>,
217    pub queue: Option<mpsc::Sender<Job>>,
218    pub stats: Arc<Stats>,
219    pub shared: Option<Arc<Ledger>>,
220    optimizer: optimization::Optimizer,
221    admission: Arc<Semaphore>,
222    queued: AtomicUsize,
223    rejected: AtomicU64,
224    abandoned: AtomicU64,
225    observer_panics: AtomicU64,
226    /// Decision ids submitted and not yet finished, so no two live
227    /// decisions share attempt ids (spec 003, 3.9.2).
228    live: Mutex<BTreeSet<String>>,
229}
230
231pub struct Runtime {
232    inner: Arc<Inner>,
233}
234
235pub struct RuntimeBuilder {
236    clock: Arc<dyn Clock>,
237    sink: Arc<dyn EvidenceSink>,
238    config: RuntimeConfig,
239    backends: Vec<(
240        Arc<dyn DecisionBackend>,
241        usize,
242        rustev_contract::actionability::Origin,
243    )>,
244    shared: Option<Arc<Ledger>>,
245    sources: Vec<(Arc<dyn rustev_core::seams::AcquisitionSource>, usize)>,
246}
247
248impl RuntimeBuilder {
249    /// Install a source by its spec 032 descriptor with a per-source attempt bound.
250    pub fn acquisition_source(
251        mut self,
252        source: Arc<dyn rustev_core::seams::AcquisitionSource>,
253        max_concurrency: usize,
254    ) -> Self {
255        self.sources.push((source, max_concurrency));
256        self
257    }
258
259    /// Register a backend with its own attempt concurrency limit.
260    pub fn backend(mut self, backend: Arc<dyn DecisionBackend>, max_concurrency: usize) -> Self {
261        self.backends.push((
262            backend,
263            max_concurrency,
264            rustev_contract::actionability::Origin::Upstream,
265        ));
266        self
267    }
268
269    /// Register a backend the host implements itself (spec 033, 3.9.1): its
270    /// failures are caused by the host, not an upstream service. A host
271    /// relaying a failure from its own upstream may still report it as
272    /// `upstream`; nothing reports the caller.
273    pub fn host_backend(
274        mut self,
275        backend: Arc<dyn DecisionBackend>,
276        max_concurrency: usize,
277    ) -> Self {
278        self.backends.push((
279            backend,
280            max_concurrency,
281            rustev_contract::actionability::Origin::Host,
282        ));
283        self
284    }
285
286    /// A ledger shared by every decision, reserved after each decision's own.
287    pub fn shared_ledger(mut self, ledger: Arc<Ledger>) -> Self {
288        self.shared = Some(ledger);
289        self
290    }
291
292    /// Build the runtime. Under a queued sink policy this spawns the delivery
293    /// worker, so it must run inside a Tokio runtime.
294    pub fn build(self) -> Result<Runtime, ConfigError> {
295        let c = self.config;
296        for (name, v) in [
297            ("max_in_flight", c.max_in_flight),
298            ("max_parallel_requests", c.max_parallel_requests),
299        ] {
300            if v == 0 {
301                return Err(ConfigError::Zero(name));
302            }
303        }
304        let mut backends = BTreeMap::new();
305        for (b, max, origin) in self.backends {
306            if max == 0 {
307                return Err(ConfigError::Zero("backend max_concurrency"));
308            }
309            let id = b.descriptor().backend_id.clone();
310            let r = Registered {
311                backend: b,
312                permits: Arc::new(Semaphore::new(max)),
313                max,
314                origin,
315            };
316            if backends.insert(id.clone(), r).is_some() {
317                return Err(ConfigError::DuplicateBackend(id));
318            }
319        }
320        let mut sources = BTreeMap::new();
321        for (source, max) in self.sources {
322            if max == 0 {
323                return Err(ConfigError::Zero("source max_concurrency"));
324            }
325            let descriptor = source.descriptor().clone();
326            if descriptor.id().is_err() {
327                return Err(ConfigError::InvalidSource(descriptor.name));
328            }
329            let name = descriptor.name.clone();
330            if sources
331                .insert(
332                    name.clone(),
333                    acquisition::RegisteredSource {
334                        source,
335                        descriptor,
336                        permits: Arc::new(Semaphore::new(max)),
337                    },
338                )
339                .is_some()
340            {
341                return Err(ConfigError::InvalidSource(name));
342            }
343        }
344        let stats = Arc::new(Stats::default());
345        let queue = match c.sink {
346            SinkPolicy::FailDecision { .. } => None,
347            SinkPolicy::Backpressure {
348                max_pending,
349                delivery_timeout_ms,
350                ..
351            }
352            | SinkPolicy::DropCounted {
353                max_pending,
354                delivery_timeout_ms,
355            } => {
356                if max_pending == 0 {
357                    return Err(ConfigError::Zero("max_pending"));
358                }
359                let (tx, rx) = mpsc::channel(max_pending);
360                tokio::spawn(sink::worker(
361                    rx,
362                    self.sink.clone(),
363                    self.clock.clone(),
364                    delivery_timeout_ms,
365                    stats.clone(),
366                ));
367                Some(tx)
368            }
369        };
370        Ok(Runtime {
371            inner: Arc::new(Inner {
372                clock: self.clock,
373                config: c,
374                backends,
375                sources,
376                sink: self.sink,
377                queue,
378                stats,
379                shared: self.shared,
380                optimizer: optimization::Optimizer::default(),
381                admission: Arc::new(Semaphore::new(c.max_in_flight)),
382                queued: AtomicUsize::new(0),
383                rejected: AtomicU64::new(0),
384                abandoned: AtomicU64::new(0),
385                observer_panics: AtomicU64::new(0),
386                live: Mutex::new(BTreeSet::new()),
387            }),
388        })
389    }
390}
391
392/// A backend a request can be served by, in fallback order.
393#[derive(Clone)]
394pub(crate) struct Target {
395    pub backend_id: String,
396    pub artifact: ArtifactId,
397    pub registered: Registered,
398}
399
400pub(crate) struct StepRuntime {
401    pub targets: Vec<Target>,
402    pub extraction: Option<rustev_contract::extraction::ExtractionLimits>,
403    pub retry: RetryPolicy,
404    pub timeout: AttemptTimeout,
405    pub fallback_on: Vec<FailureClass>,
406}
407
408/// A compiled plan checked against this runtime's backends.
409pub struct PreparedPlan {
410    compiled: Arc<Compiled>,
411    pub(crate) steps: BTreeMap<String, StepRuntime>,
412    pub(crate) cost: CostPolicy,
413    execution: RecordedExecution,
414    pub(crate) optimization: Option<OptimizationPolicy>,
415}
416
417impl PreparedPlan {
418    pub fn compiled(&self) -> &Compiled {
419        &self.compiled
420    }
421
422    /// Identified cache namespace for host invalidation and audit.
423    pub fn optimization_namespace(&self) -> Option<String> {
424        self.optimization
425            .as_ref()
426            .map(|policy| optimization::namespace(&self.compiled.id, policy))
427    }
428}
429
430struct QueueSlot<'a>(&'a AtomicUsize);
431
432impl Drop for QueueSlot<'_> {
433    fn drop(&mut self) {
434        self.0.fetch_sub(1, Ordering::SeqCst);
435    }
436}
437
438struct Live<'a> {
439    set: &'a Mutex<BTreeSet<String>>,
440    id: String,
441}
442
443impl Drop for Live<'_> {
444    fn drop(&mut self) {
445        self.set
446            .lock()
447            .unwrap_or_else(|e| e.into_inner())
448            .remove(&self.id);
449    }
450}
451
452struct Abandon<'a> {
453    counter: &'a AtomicU64,
454    armed: bool,
455}
456
457impl Drop for Abandon<'_> {
458    fn drop(&mut self) {
459        if self.armed {
460            self.counter.fetch_add(1, Ordering::SeqCst);
461        }
462    }
463}
464
465impl Runtime {
466    pub fn builder(
467        clock: Arc<dyn Clock>,
468        sink: Arc<dyn EvidenceSink>,
469        config: RuntimeConfig,
470    ) -> RuntimeBuilder {
471        RuntimeBuilder {
472            clock,
473            sink,
474            config,
475            backends: vec![],
476            sources: vec![],
477            shared: None,
478        }
479    }
480
481    pub fn gauges(&self) -> Gauges {
482        let i = &self.inner;
483        Gauges {
484            in_flight: i.config.max_in_flight - i.admission.available_permits(),
485            queued: i.queued.load(Ordering::SeqCst),
486            rejected: i.rejected.load(Ordering::SeqCst),
487            abandoned: i.abandoned.load(Ordering::SeqCst),
488            backend_in_flight: i
489                .backends
490                .iter()
491                .map(|(k, r)| (k.clone(), r.max - r.permits.available_permits()))
492                .collect(),
493            sink: i.stats.snapshot(),
494        }
495    }
496
497    /// The registered backend's descriptor and static cost model, for a
498    /// host's checks before any dispatch (spec 043, 3.7.1).
499    pub fn backend_terms(
500        &self,
501        backend_id: &str,
502    ) -> Option<(&rustev_contract::descriptor::BackendDescriptor, CostModel)> {
503        let r = self.inner.backends.get(backend_id)?;
504        Some((r.backend.descriptor(), r.backend.cost_model()))
505    }
506
507    /// Check a compiled plan against the registered backends (spec 003,
508    /// 3.5.3 and 3.6.5).
509    pub fn prepare(&self, compiled: Compiled) -> Result<PreparedPlan, PrepareError> {
510        if compiled.plan.schema == schema::PLAN_V3 && !self.inner.sink.accepts(schema::RUN_V3) {
511            return Err(PrepareError::SinkVersionUnsupported {
512                schema: schema::RUN_V3,
513            });
514        }
515        let (policy, fallbacks, execution) = match &compiled.plan.execution {
516            PlanExecution::None => (None, vec![], RecordedExecution::None),
517            PlanExecution::Declared(d) => (
518                Some(d.policy.clone()),
519                d.fallbacks.clone(),
520                RecordedExecution::Declared(d.id.clone()),
521            ),
522        };
523        let cost = policy
524            .as_ref()
525            .map(|p| p.cost)
526            .unwrap_or(CostPolicy::Unlimited);
527        let optimization = policy.as_ref().and_then(|p| p.optimization.clone());
528        let target = |backend_id: &str, descriptor: &rustev_contract::ids::DescriptorId| {
529            let r = self
530                .inner
531                .backends
532                .get(backend_id)
533                .ok_or_else(|| PrepareError::MissingBackend(backend_id.to_string()))?;
534            if r.backend.descriptor().id().ok().as_ref() != Some(descriptor) {
535                return Err(PrepareError::DescriptorMismatch {
536                    backend_id: backend_id.to_string(),
537                });
538            }
539            let hard = matches!(cost, CostPolicy::Hard { .. })
540                || self
541                    .inner
542                    .shared
543                    .as_ref()
544                    .is_some_and(|l| l.mode() == CostMode::Hard);
545            let model = r.backend.cost_model();
546            if hard && model != CostModel::Bounded {
547                return Err(PrepareError::HardBudgetUnenforceable {
548                    backend_id: backend_id.to_string(),
549                    model,
550                });
551            }
552            Ok(r.clone())
553        };
554        // Fact-only sources describe snapshot evidence and need no runtime adapter.
555        for binding in compiled
556            .plan
557            .acquisitions
558            .iter()
559            .flatten()
560            .flat_map(|a| &a.sources)
561        {
562            let Some(source) = self.inner.sources.get(&binding.source.name) else {
563                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
564            };
565            if source.descriptor.id().ok().as_ref() != Some(&binding.source.id) {
566                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
567            }
568            let op = source
569                .descriptor
570                .operations
571                .iter()
572                .flatten()
573                .find(|o| o.name == binding.operation);
574            let hard = matches!(cost, CostPolicy::Hard { .. })
575                || self
576                    .inner
577                    .shared
578                    .as_ref()
579                    .is_some_and(|l| l.mode() == CostMode::Hard);
580            if op.is_none()
581                || (hard
582                    && !op.is_some_and(|o| {
583                        matches!(
584                            o.cost,
585                            rustev_contract::acquisition::OperationCost::Bounded { .. }
586                        )
587                    }))
588            {
589                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
590            }
591        }
592        let mut steps = BTreeMap::new();
593        for s in &compiled.plan.steps {
594            let (backend_id, artifact, descriptor, extraction) = match &s.detail {
595                PlanStepDetail::Semantic(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
596                PlanStepDetail::Generate(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
597                PlanStepDetail::Extract(b) => (
598                    &b.backend_id,
599                    &b.artifact,
600                    &b.descriptor,
601                    Some(b.limits.clone()),
602                ),
603                _ => continue,
604            };
605            let mut targets = vec![Target {
606                backend_id: backend_id.clone(),
607                artifact: artifact.clone(),
608                registered: target(backend_id, descriptor)?,
609            }];
610            let mut fb: Vec<_> = fallbacks.iter().filter(|f| f.step == s.id).collect();
611            fb.sort_by_key(|f| f.target);
612            for f in fb {
613                targets.push(Target {
614                    backend_id: f.backend_id.clone(),
615                    artifact: f.artifact.clone(),
616                    registered: target(&f.backend_id, &f.descriptor)?,
617                });
618            }
619            let declared: Option<&StepExecution> = policy
620                .as_ref()
621                .and_then(|p| p.steps.iter().find(|x| x.step == s.id));
622            steps.insert(
623                s.id.clone(),
624                StepRuntime {
625                    targets,
626                    extraction,
627                    retry: declared
628                        .map(|d| d.retry.clone())
629                        .unwrap_or_else(RetryPolicy::none),
630                    timeout: declared
631                        .map(|d| d.attempt_timeout)
632                        .unwrap_or(AttemptTimeout::None),
633                    fallback_on: declared.map(|d| d.fallback.on.clone()).unwrap_or_default(),
634                },
635            );
636        }
637        for s in &compiled.plan.steps {
638            if let PlanStepDetail::Generate(g) = &s.detail
639                && let Some(b) = &g.output_screener
640            {
641                for field in &g.output.fields {
642                    if matches!(
643                        field.ty,
644                        rustev_contract::generation::GeneratedType::Text { .. }
645                    ) {
646                        steps.insert(
647                            rustev_contract::generation::output_screen_step(&s.id, &field.name),
648                            StepRuntime {
649                                targets: vec![Target {
650                                    backend_id: b.backend_id.clone(),
651                                    artifact: b.artifact.clone(),
652                                    registered: target(&b.backend_id, &b.descriptor)?,
653                                }],
654                                extraction: None,
655                                retry: RetryPolicy::none(),
656                                timeout: AttemptTimeout::None,
657                                fallback_on: vec![],
658                            },
659                        );
660                    }
661                }
662            }
663        }
664        Ok(PreparedPlan {
665            compiled: Arc::new(compiled),
666            steps,
667            cost,
668            execution,
669            optimization,
670        })
671    }
672
673    /// Invalidate every reusable result in this plan's namespace, for any
674    /// correction, revocation, erasure, artifact withdrawal or policy
675    /// change the host observed. The generation advances before entries are
676    /// removed, so a result captured earlier is never stored or served.
677    pub fn invalidate_plan(&self, plan: &PreparedPlan) -> Option<InvalidationResult> {
678        let namespace = plan.optimization_namespace()?;
679        Some(
680            self.inner
681                .optimizer
682                .invalidate(&InvalidationSelector::Namespace { namespace }),
683        )
684    }
685
686    /// Invalidate reusable results for one opaque authorized scope, as for
687    /// an erasure or a scope revocation.
688    pub fn invalidate_scope(
689        &self,
690        plan: &PreparedPlan,
691        principal_handle: &[u8],
692    ) -> Option<InvalidationResult> {
693        let namespace = plan.optimization_namespace()?;
694        Some(
695            self.inner
696                .optimizer
697                .invalidate(&InvalidationSelector::Scope {
698                    namespace,
699                    scope_id: optimization::scope_id(principal_handle),
700                }),
701        )
702    }
703
704    async fn admit(
705        &self,
706        deadline: Instant,
707        cancel: &CancelSignal,
708    ) -> Result<OwnedSemaphorePermit, Rejection> {
709        let i = &self.inner;
710        if let Ok(p) = i.admission.clone().try_acquire_owned() {
711            return Ok(p);
712        }
713        let mut n = i.queued.load(Ordering::SeqCst);
714        loop {
715            if n >= i.config.max_queued {
716                return Err(Rejection::Overloaded);
717            }
718            match i
719                .queued
720                .compare_exchange(n, n + 1, Ordering::SeqCst, Ordering::SeqCst)
721            {
722                Ok(_) => break,
723                Err(now) => n = now,
724            }
725        }
726        let _slot = QueueSlot(&i.queued);
727        let mut acquire = Box::pin(i.admission.clone().acquire_owned());
728        let mut timer = i.clock.sleep_until(deadline);
729        match race(&mut acquire, cancel, &mut timer).await {
730            Raced::Done(Ok(p)) if i.clock.now() < deadline => Ok(p),
731            Raced::Done(Ok(_)) | Raced::Expired => Err(Rejection::QueueDeadline),
732            Raced::Done(Err(_)) => Err(Rejection::Overloaded),
733            Raced::Cancelled => Err(Rejection::Cancelled),
734        }
735    }
736
737    /// Run one decision (spec 003). `cancel` is the caller's cancellation.
738    pub async fn decide(
739        &self,
740        plan: &PreparedPlan,
741        req: DecisionRequest,
742        cancel: &CancelSignal,
743    ) -> Result<Decided, DecideError> {
744        self.run(plan, req, cancel, None, None, None, None).await
745    }
746
747    /// Run with a value-free observer. The ordinary entry point has no callbacks.
748    pub async fn decide_observed(
749        &self,
750        plan: &PreparedPlan,
751        req: DecisionRequest,
752        cancel: &CancelSignal,
753        observer: &dyn ProgressObserver,
754    ) -> Result<Decided, DecideError> {
755        self.run(plan, req, cancel, None, None, Some(observer), None)
756            .await
757    }
758
759    /// Contained progress callback panics across this runtime's decisions.
760    pub fn observer_panics(&self) -> u64 {
761        self.inner.observer_panics.load(Ordering::Relaxed)
762    }
763
764    /// [`Runtime::decide`] with opt-in bounded capture of every supplied
765    /// value (spec 004, 5.3). An invalid configuration is refused before
766    /// admission. Dropping the future returns nothing, as for `decide`.
767    pub async fn decide_with_capture(
768        &self,
769        plan: &PreparedPlan,
770        req: DecisionRequest,
771        cancel: &CancelSignal,
772        capture: &CaptureConfig,
773    ) -> Result<CapturedDecision, CaptureConfigError> {
774        self.decide_capturing(plan, req, cancel, capture, None)
775            .await
776    }
777
778    async fn decide_capturing(
779        &self,
780        plan: &PreparedPlan,
781        req: DecisionRequest,
782        cancel: &CancelSignal,
783        capture: &CaptureConfig,
784        streaming: Option<&generation::Streaming>,
785    ) -> Result<CapturedDecision, CaptureConfigError> {
786        // Spec 036, 3.15.1 admits every plan 3 decision; generation capture
787        // (spec 037, 3.15.3) shares the acquisition and semantic supply order.
788        capture.check()?;
789        let decision_id = req.decision_id.clone();
790        let mut capturing = capture::Capturing::new(capture);
791        let result = self
792            .run(
793                plan,
794                req,
795                cancel,
796                Some(&mut capturing),
797                None,
798                None,
799                streaming,
800            )
801            .await;
802        let capture = match &result {
803            Err(DecideError::Rejected(_) | DecideError::GenerationFailed(_)) => {
804                CaptureOutcome::NotAdmitted
805            }
806            Ok(Decided { record, .. }) | Err(DecideError::EvidenceNotDelivered { record, .. }) => {
807                CaptureOutcome::Captured(
808                    capturing.finish(&decision_id, record.termination == Termination::Cancelled),
809                )
810            }
811        };
812        Ok(CapturedDecision { result, capture })
813    }
814
815    /// Start an opt-in field stream. Poll the returned future to drive execution;
816    /// the receiver may be polled independently and never backpressures it.
817    pub fn decide_streaming<'a>(
818        &'a self,
819        plan: &'a PreparedPlan,
820        req: DecisionRequest,
821        cancel: &'a CancelSignal,
822        stream_buffer: usize,
823    ) -> Result<
824        (
825            impl std::future::Future<Output = Result<Decided, DecideError>> + Send + 'a,
826            GenerationStream,
827        ),
828        rustev_core::generation_eval::GenerationError,
829    > {
830        let (streaming, receiver) = generation::Streaming::new(plan, stream_buffer)?;
831        Ok((
832            async move {
833                self.run(plan, req, cancel, None, None, None, Some(&streaming))
834                    .await
835            },
836            receiver,
837        ))
838    }
839
840    #[allow(clippy::too_many_arguments)]
841    async fn run(
842        &self,
843        plan: &PreparedPlan,
844        req: DecisionRequest,
845        cancel: &CancelSignal,
846        mut capture: Option<&mut capture::Capturing<'_>>,
847        live: Option<&live::LiveCtx<'_>>,
848        observer: Option<&dyn ProgressObserver>,
849        streaming: Option<&generation::Streaming>,
850    ) -> Result<Decided, DecideError> {
851        let i = &*self.inner;
852        // The runtime waits on its own child of the caller's signal, so a
853        // long-lived caller signal holds no waker of this decision after it
854        // ends (spec 003, 3.4.3).
855        let cancel = &cancel.child();
856        // Dropping this future at any point after here, queued or admitted,
857        // counts as abandoned (spec 003, 3.4.5).
858        let mut abandon = Abandon {
859            counter: &i.abandoned,
860            armed: true,
861        };
862        let mut reject = |r: Rejection| {
863            abandon.armed = false;
864            i.rejected.fetch_add(1, Ordering::SeqCst);
865            DecideError::Rejected(r)
866        };
867        let submitted = i.clock.now();
868        if req.decision_id.is_empty() || req.decision_id.len() > 256 {
869            return Err(reject(Rejection::InvalidRequest(
870                "a decision id is 1 to 256 bytes".into(),
871            )));
872        }
873        let compiled: &Compiled = &plan.compiled;
874        let ev = match Evaluation::start(compiled, &req.snapshot, req.evaluation_time) {
875            Ok(ev) => ev,
876            Err(e) => return Err(reject(Rejection::InvalidRequest(e.to_string()))),
877        };
878        if !i
879            .live
880            .lock()
881            .unwrap_or_else(|e| e.into_inner())
882            .insert(req.decision_id.clone())
883        {
884            return Err(reject(Rejection::InvalidRequest(
885                "a decision with this id is already running".into(),
886            )));
887        }
888        let _live = Live {
889            set: &i.live,
890            id: req.decision_id.clone(),
891        };
892        let deadline_ms = compiled
893            .plan
894            .definition
895            .limits
896            .deadline_ms
897            .min(req.deadline_ms.unwrap_or(u64::MAX));
898        let deadline = submitted.saturating_add(deadline_ms);
899        let _permit = match self.admit(deadline, cancel).await {
900            Ok(p) => p,
901            Err(r) => return Err(reject(r)),
902        };
903        let queued_ms = i.clock.now().saturating_sub(submitted);
904        let observing = observer.map(|observer| progress::Observing {
905            observer,
906            panics: &i.observer_panics,
907        });
908        if let Some(o) = &observing {
909            o.call(|o| o.admitted());
910        }
911
912        let ledger = Ledger::new(plan.cost);
913        let ev = std::sync::Mutex::new(ev);
914        let deferred_cache = Mutex::new(vec![]);
915        let has_generation = compiled
916            .plan
917            .steps
918            .iter()
919            .any(|s| matches!(s.detail, PlanStepDetail::Generate(_)));
920        let (mut requests, acquisitions, mut cancelled) = {
921            let cx = driver::DecisionCtx {
922                rt: i,
923                plan,
924                deadline,
925                submitted,
926                cancel,
927                ledger: &ledger,
928                decision_id: &req.decision_id,
929                principal: &req.principal_handle,
930                evaluation_time: req.evaluation_time,
931                ev: Some(&ev),
932                generation: None,
933                step_ledger: None,
934                stream: None,
935                item_clock: None,
936                deferred_cache: has_generation.then_some(&deferred_cache),
937                live,
938                observing: observing.as_ref(),
939            };
940            driver::run_requests(&cx, capture.as_deref_mut()).await
941        };
942        let ev = ev.into_inner().unwrap_or_else(|e| e.into_inner());
943        let stage_cx = generation::StageContext {
944            rt: i,
945            plan,
946            deadline,
947            submitted,
948            cancel,
949            ledger: &ledger,
950            decision_id: &req.decision_id,
951            principal: &req.principal_handle,
952            evaluation_time: req.evaluation_time,
953            streaming,
954        };
955        let mut text_result = None;
956        let mut writes = vec![];
957        // Spec 037 3.13.1 permits Complete only after final verification and
958        // defines no failure event. Both cancellation paths close the stream
959        // without Complete: cancellation has no text result (3.12.6).
960        let (completion, core) = if cancelled {
961            (Completion::Cancelled, CoreEvidence::NotProduced)
962        } else {
963            let stage = ev
964                .begin_generation(&req.decision_id)
965                .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
966            let stage = Mutex::new(stage);
967            // Spec 003 3.9.1 permits only judged or cancelled records; spec 037
968            // 3.12.6 requires generation to complete before judged. An invariant
969            // failure has no specified run-record termination, so this path
970            // returns GenerationFailed without delivery or cache writes. Ordinary
971            // backend failures resolve through backstops and still deliver evidence
972            // (037 3.10.2, 3.12.7, 3.15.2); they do not take this error path.
973            let (generation_requests, pending_writes, was_cancelled) =
974                generation::run(&stage_cx, &stage, capture)
975                    .await
976                    .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
977            requests.extend(generation_requests);
978            cancelled = was_cancelled;
979            if cancelled {
980                (Completion::Cancelled, CoreEvidence::NotProduced)
981            } else {
982                let stage = stage.lock().unwrap_or_else(|e| e.into_inner());
983                let (mut judgment, evidence, mut text) = stage
984                    .finish()
985                    .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
986                if let Some(text) = &mut text {
987                    stage.verify_ownership(&mut judgment, text);
988                    if let Some(streaming) = streaming {
989                        streaming.finish(text);
990                    }
991                }
992                if let Some(o) = &observing {
993                    o.remaining(&evidence, &requests, &compiled.plan);
994                }
995                text_result = text;
996                writes = deferred_cache
997                    .into_inner()
998                    .unwrap_or_else(|e| e.into_inner());
999                writes.extend(pending_writes);
1000                (
1001                    Completion::Judged(Box::new(judgment)),
1002                    CoreEvidence::Recorded(Box::new(evidence)),
1003                )
1004            }
1005        };
1006        let elapsed_ms = i.clock.now().saturating_sub(submitted);
1007        let plan_3 = plan.compiled.plan.schema == schema::PLAN_V3;
1008        // A plan 2 decision's record is `rustev.run/2` exactly when the sink
1009        // selected it (spec 020, 3.2).
1010        let run_2 = !plan_3 && i.sink.run_schema() == schema::RUN_V2;
1011        let (remote_evidence, remote_evidence_truncated) = if run_2 {
1012            let (list, truncated) = remote_evidence(i, &requests);
1013            (Some(list), truncated)
1014        } else {
1015            (None, false)
1016        };
1017        let mut record = RunRecord {
1018            acquisitions: (!acquisitions.is_empty()).then_some(acquisitions),
1019            // A plan 3 decision's record holds evidence 2 (spec 032, 3.10.3).
1020            schema: if plan_3 {
1021                schema::RUN_V3
1022            } else if run_2 {
1023                schema::RUN_V2
1024            } else {
1025                schema::RUN
1026            }
1027            .into(),
1028            decision_id: req.decision_id.clone(),
1029            plan_id: compiled.id.clone(),
1030            execution: plan.execution.clone(),
1031            termination: match completion {
1032                Completion::Judged(_) => Termination::Judged,
1033                Completion::Cancelled => Termination::Cancelled,
1034            },
1035            core,
1036            timing: Timing {
1037                queued_ms,
1038                elapsed_ms,
1039                deadline_ms,
1040                deadline_expired: i.clock.now() >= deadline,
1041            },
1042            requests,
1043            cost: ledger.summary(),
1044            remote_evidence,
1045            remote_evidence_truncated,
1046        };
1047        fit_remote_evidence(&mut record);
1048        let record = Arc::new(record);
1049        let delivery = self.deliver(record.clone()).await;
1050        abandon.armed = false;
1051        match delivery {
1052            Ok(delivery) => {
1053                generation::write_cache(&stage_cx, writes);
1054                Ok(Decided {
1055                    text_result,
1056                    completion,
1057                    record,
1058                    delivery,
1059                })
1060            }
1061            Err(failure) => Err(DecideError::EvidenceNotDelivered { record, failure }),
1062        }
1063    }
1064
1065    async fn deliver(&self, record: Arc<RunRecord>) -> Result<Delivery, DeliveryFailure> {
1066        let i = &*self.inner;
1067        match i.config.sink {
1068            SinkPolicy::FailDecision { timeout_ms } => {
1069                sink::deliver_once(&*i.sink, &*i.clock, &record, timeout_ms, &i.stats)
1070                    .await
1071                    .map(|receipt| Delivery::Acknowledged { receipt })
1072            }
1073            SinkPolicy::Backpressure {
1074                enqueue_timeout_ms, ..
1075            } => {
1076                let Some(tx) = &i.queue else {
1077                    return Err(stopped());
1078                };
1079                let mut reserve = Box::pin(tx.reserve());
1080                let mut timer = i
1081                    .clock
1082                    .sleep_until(i.clock.now().saturating_add(enqueue_timeout_ms));
1083                match race(&mut reserve, &CancelSignal::new(), &mut timer).await {
1084                    Raced::Done(Ok(permit)) => {
1085                        let (done, rx) = oneshot::channel();
1086                        i.stats.queued.fetch_add(1, Ordering::SeqCst);
1087                        permit.send(Job { record, done });
1088                        Ok(Delivery::Queued(DeliveryTicket(rx)))
1089                    }
1090                    Raced::Done(Err(_)) => Err(stopped()),
1091                    Raced::Expired | Raced::Cancelled => {
1092                        i.stats.failed.fetch_add(1, Ordering::SeqCst);
1093                        Err(DeliveryFailure {
1094                            detail: format!("no delivery slot within {enqueue_timeout_ms} ms"),
1095                            uncertain: false,
1096                        })
1097                    }
1098                }
1099            }
1100            SinkPolicy::DropCounted { .. } => {
1101                let Some(tx) = &i.queue else {
1102                    return Err(stopped());
1103                };
1104                let (done, rx) = oneshot::channel();
1105                i.stats.queued.fetch_add(1, Ordering::SeqCst);
1106                match tx.try_send(Job { record, done }) {
1107                    Ok(()) => Ok(Delivery::Queued(DeliveryTicket(rx))),
1108                    Err(mpsc::error::TrySendError::Closed(_)) => {
1109                        i.stats.queued.fetch_sub(1, Ordering::SeqCst);
1110                        Err(stopped())
1111                    }
1112                    Err(mpsc::error::TrySendError::Full(_)) => {
1113                        i.stats.queued.fetch_sub(1, Ordering::SeqCst);
1114                        let total = i.stats.dropped.fetch_add(1, Ordering::SeqCst) + 1;
1115                        Ok(Delivery::Dropped {
1116                            dropped_total: total,
1117                        })
1118                    }
1119                }
1120            }
1121        }
1122    }
1123
1124    /// A later observed charge for an attempt whose cost was unknown, applied
1125    /// to the shared ledger (spec 003, 3.5.5). Delivered records are never
1126    /// rewritten.
1127    pub fn reconcile(&self, attempt_id: &str, observed_units: u64) -> bool {
1128        self.inner
1129            .shared
1130            .as_ref()
1131            .is_some_and(|l| l.reconcile(attempt_id, observed_units))
1132    }
1133}
1134
1135fn stopped() -> DeliveryFailure {
1136    DeliveryFailure {
1137        detail: "the delivery worker stopped".into(),
1138        uncertain: false,
1139    }
1140}
1141
1142/// The remote evidence list of a `rustev.run/2` record (spec 020, 3.2): at
1143/// most one entry per remote attempt, in attempt order, taken from the
1144/// backend that ran it, with the step, instance and final remote state the
1145/// record holds. Beyond [`MAX_REMOTE_EVIDENCE`] entries the list is cut and
1146/// says so.
1147fn remote_evidence(i: &Inner, requests: &[RequestRecord]) -> (Vec<RemoteEvidence>, bool) {
1148    let mut out = Vec::new();
1149    let mut truncated = false;
1150    for r in requests {
1151        for a in &r.attempts {
1152            let Some(reg) = i.backends.get(&a.backend_id) else {
1153                continue;
1154            };
1155            let Some(mut e) = reg.backend.take_remote_evidence(&a.attempt_id) else {
1156                continue;
1157            };
1158            if out.len() >= MAX_REMOTE_EVIDENCE {
1159                truncated = true;
1160                continue;
1161            }
1162            e.attempt_id = a.attempt_id.clone();
1163            e.step = r.step.clone();
1164            e.instance = r.instance.clone();
1165            e.remote_state = a.remote;
1166            out.push(e.bounded());
1167        }
1168    }
1169    (out, truncated)
1170}
1171
1172/// Keep a `rustev.run/2` record within its parse bounds: remote evidence
1173/// entries leave from the end, and the record says so, until it fits.
1174fn fit_remote_evidence(record: &mut RunRecord) {
1175    use rustev_contract::Document;
1176    let max = rustev_contract::limits::RECORD_V1.max_bytes;
1177    while record
1178        .remote_evidence
1179        .as_ref()
1180        .is_some_and(|l| !l.is_empty())
1181        && record.record_canonical().is_ok_and(|b| b.len() > max)
1182    {
1183        if let Some(l) = &mut record.remote_evidence {
1184            l.pop();
1185        }
1186        record.remote_evidence_truncated = true;
1187    }
1188}
1189
1190mod spend;
1191pub use spend::{
1192    LiveSpend, SpendAttempt, SpendBinding, SpendGate, SpendRefusal, SpendTicket, SpendUnit,
1193};