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 store;
30pub use store::{
31    Invalidated, PutAnswer, PutTerms, RuntimeStore, StoreCall, StoreFailure, StoreSelector, Stored,
32};
33mod sink;
34pub use progress::{ProgressObserver, SkippedStep, StepProgress};
35mod wait;
36
37use std::collections::{BTreeMap, BTreeSet};
38use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
39use std::sync::{Arc, Mutex};
40
41use rustev_contract::execution::{
42    AttemptTimeout, CostPolicy, FailureClass, RetryPolicy, StepExecution,
43};
44use rustev_contract::ids::ArtifactId;
45use rustev_contract::judgment::Judgment;
46use rustev_contract::optimization::{DecisionCacheTier, OptimizationPolicy};
47use rustev_contract::plan::{PlanExecution, PlanStepDetail};
48use rustev_contract::replay::Capture;
49use rustev_contract::run::{
50    CoreEvidence, CostMode, CostModel, MAX_REMOTE_EVIDENCE, RecordedExecution, RemoteEvidence,
51    RequestRecord, RunRecord, Termination, Timing,
52};
53use rustev_contract::snapshot::Snapshot;
54use rustev_contract::time::Timestamp;
55use rustev_contract::{Identified, schema};
56use rustev_core::Compiled;
57use rustev_core::evaluate::Evaluation;
58use rustev_core::seams::{CancelSignal, DecisionBackend, EvidenceSink};
59use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
60
61pub use capture::{CaptureConfig, CaptureConfigError, MAX_CAPTURE_BYTES};
62pub use clock::{Clock, Instant, ManualClock, TokioClock};
63/// Dynamic questions (spec 019): the only execution entry point.
64pub use dynamic::{DynamicAsked, DynamicBounds, DynamicDelivery, ask_dynamic};
65pub use ledger::Ledger;
66pub use live::{
67    AcquisitionAuthorizationRequest, DispatchAnswer, LiveAuthorizer, LiveRefusal, LiveRequest,
68    LiveResult, SourceOutcome,
69};
70pub use optimization::{InvalidationResult, PersistentInvalidation};
71pub use sink::{DeliveryFailure, DeliveryTicket, SinkPolicy, SinkStats};
72
73use crate::optimization::InvalidationSelector;
74use crate::sink::{Job, Stats};
75use crate::wait::{Raced, race};
76
77/// Bounds of one runtime (spec 003, 3.4).
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct RuntimeConfig {
80    /// Decisions admitted at once.
81    pub max_in_flight: usize,
82    /// Decisions waiting for admission.
83    pub max_queued: usize,
84    /// Requests in progress within one decision.
85    pub max_parallel_requests: usize,
86    pub sink: SinkPolicy,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum ConfigError {
91    /// A bound that must be positive is 0.
92    Zero(&'static str),
93    DuplicateBackend(String),
94    InvalidSource(String),
95}
96
97/// Why a plan cannot run on this runtime (spec 003, 3.5.3 and 3.6.5).
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum PrepareError {
100    SourceBinding(String),
101    /// The plan binds a backend id that is not registered.
102    MissingBackend(String),
103    /// The registered backend's descriptor is not the one the plan bound.
104    DescriptorMismatch {
105        backend_id: String,
106    },
107    /// A hard budget, and a reachable backend without enforceable bounds.
108    HardBudgetUnenforceable {
109        backend_id: String,
110        model: CostModel,
111    },
112    /// A plan 3 and an evidence sink that does not accept `rustev.run/3`
113    /// (spec 032, 3.11.2).
114    SinkVersionUnsupported {
115        schema: &'static str,
116    },
117    /// A persistent tier, and a runtime built without a store (spec 044,
118    /// 3.4.1).
119    StoreMissing,
120    /// The store's contract is not the one the plan states.
121    StoreContractMismatch,
122}
123
124/// Why a decision was not admitted (spec 003, 3.4.1). Nothing was dispatched
125/// or spent, and there is no run record.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum Rejection {
128    Overloaded,
129    QueueDeadline,
130    Cancelled,
131    InvalidRequest(String),
132}
133
134#[derive(Debug)]
135pub enum DecideError {
136    /// A pure generation invariant failed; no text result is returned.
137    GenerationFailed(String),
138    Rejected(Rejection),
139    /// Work and spending happened; the record did not reach the sink. The
140    /// judgment inside `record` is evidence, not a result to act on.
141    EvidenceNotDelivered {
142        record: Arc<RunRecord>,
143        failure: DeliveryFailure,
144    },
145}
146
147/// One decision to run.
148#[derive(Debug, Clone)]
149pub struct DecisionRequest {
150    /// Non-empty, at most 256 bytes; the delivery key.
151    pub decision_id: String,
152    pub snapshot: Snapshot,
153    /// Domain time, supplied; never read from a clock.
154    pub evaluation_time: Timestamp,
155    /// Can only tighten the plan's `deadline_ms`.
156    pub deadline_ms: Option<u64>,
157    /// Opaque complete authorized-scope handle, including any tenant and
158    /// principal isolation the host requires; passed through to backends and
159    /// included in every reusable-work key.
160    pub principal_handle: Vec<u8>,
161}
162
163#[derive(Debug, Clone, PartialEq)]
164pub enum Completion {
165    Judged(Box<Judgment>),
166    Cancelled,
167}
168
169#[derive(Debug)]
170pub enum Delivery {
171    Acknowledged { receipt: String },
172    Queued(DeliveryTicket),
173    Dropped { dropped_total: u64 },
174}
175
176#[derive(Debug)]
177pub struct Decided {
178    pub text_result: Option<rustev_contract::generation_result::TextResult>,
179    pub completion: Completion,
180    pub record: Arc<RunRecord>,
181    pub delivery: Delivery,
182    /// The decision-result cache entry the decision was served from, its
183    /// judgment recomputed by the core (spec 044, 3.6.6).
184    pub served_from_cache: Option<ServedFromCache>,
185    /// The persistent tier's answer to this decision's entry (spec 044,
186    /// 3.6.4): `Ok` when stored, otherwise the diagnostic of an admission
187    /// not made, a store failure under either `on_store_failure` included
188    /// (3.4.2). The entry names the delivered run record's digest, so it
189    /// is written after delivery and its answer is here, not in that
190    /// record. `None` when nothing was to be written.
191    pub decision_cache_write: Option<Result<(), String>>,
192}
193
194/// The entry a decision was served from (spec 044, 3.6.6), as its run
195/// record's `decision_cache` member names it.
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct ServedFromCache {
198    pub entry_id: String,
199    /// The digest of the run record of the decision that wrote the entry.
200    pub source_run_record: rustev_contract::ids::ContentDigest,
201}
202
203/// What [`Runtime::decide_with_capture`] captured (spec 004, 5.3).
204#[derive(Debug, Clone, PartialEq)]
205pub enum CaptureOutcome {
206    /// The decision was rejected: nothing ran, and there is nothing to
207    /// retain or replay.
208    NotAdmitted,
209    Captured(Capture),
210}
211
212/// The normal decide result, and beside it the capture, also when evidence
213/// delivery failed.
214#[derive(Debug)]
215pub struct CapturedDecision {
216    pub result: Result<Decided, DecideError>,
217    pub capture: CaptureOutcome,
218}
219
220/// Live counts, for operators and tests. In memory only.
221#[derive(Debug, Clone, PartialEq, Eq, Default)]
222pub struct Gauges {
223    pub in_flight: usize,
224    pub queued: usize,
225    pub rejected: u64,
226    pub abandoned: u64,
227    /// Attempts in flight per backend.
228    pub backend_in_flight: BTreeMap<String, usize>,
229    pub sink: SinkStats,
230}
231
232#[derive(Clone)]
233pub(crate) struct Registered {
234    pub backend: Arc<dyn DecisionBackend>,
235    pub permits: Arc<Semaphore>,
236    pub max: usize,
237    /// Who implements it (spec 033, 3.9): `upstream` unless registered as
238    /// the host's own.
239    pub origin: rustev_contract::actionability::Origin,
240}
241
242pub(crate) struct Inner {
243    pub clock: Arc<dyn Clock>,
244    pub config: RuntimeConfig,
245    pub backends: BTreeMap<String, Registered>,
246    pub sources: BTreeMap<String, acquisition::RegisteredSource>,
247    pub sink: Arc<dyn EvidenceSink>,
248    pub queue: Option<mpsc::Sender<Job>>,
249    pub stats: Arc<Stats>,
250    pub shared: Option<Arc<Ledger>>,
251    /// The host's store behind the persistent tier (spec 044, 3.4).
252    pub store: Option<Arc<dyn RuntimeStore>>,
253    /// Persistent selections invalidated in the memory tiers and not yet
254    /// in the store: a namespace, or a scope in it (spec 044, 3.10.3).
255    unavailable: Mutex<BTreeSet<(String, Option<String>)>>,
256    optimizer: optimization::Optimizer,
257    admission: Arc<Semaphore>,
258    queued: AtomicUsize,
259    rejected: AtomicU64,
260    abandoned: AtomicU64,
261    observer_panics: AtomicU64,
262    /// Decision ids submitted and not yet finished, so no two live
263    /// decisions share attempt ids (spec 003, 3.9.2).
264    live: Mutex<BTreeSet<String>>,
265}
266
267impl Inner {
268    /// Whether a persistent selection covering this key awaits its store
269    /// invalidation (spec 044, 3.10.3).
270    pub(crate) fn persistent_unavailable(&self, namespace: &str, scope_id: &str) -> bool {
271        let marks = self.unavailable.lock().unwrap_or_else(|e| e.into_inner());
272        marks.contains(&(namespace.to_string(), None))
273            || marks.contains(&(namespace.to_string(), Some(scope_id.to_string())))
274    }
275}
276
277pub struct Runtime {
278    inner: Arc<Inner>,
279}
280
281pub struct RuntimeBuilder {
282    clock: Arc<dyn Clock>,
283    sink: Arc<dyn EvidenceSink>,
284    config: RuntimeConfig,
285    backends: Vec<(
286        Arc<dyn DecisionBackend>,
287        usize,
288        rustev_contract::actionability::Origin,
289    )>,
290    shared: Option<Arc<Ledger>>,
291    sources: Vec<(Arc<dyn rustev_core::seams::AcquisitionSource>, usize)>,
292    store: Option<Arc<dyn RuntimeStore>>,
293}
294
295impl RuntimeBuilder {
296    /// Install a source by its spec 032 descriptor with a per-source attempt bound.
297    pub fn acquisition_source(
298        mut self,
299        source: Arc<dyn rustev_core::seams::AcquisitionSource>,
300        max_concurrency: usize,
301    ) -> Self {
302        self.sources.push((source, max_concurrency));
303        self
304    }
305
306    /// Register a backend with its own attempt concurrency limit.
307    pub fn backend(mut self, backend: Arc<dyn DecisionBackend>, max_concurrency: usize) -> Self {
308        self.backends.push((
309            backend,
310            max_concurrency,
311            rustev_contract::actionability::Origin::Upstream,
312        ));
313        self
314    }
315
316    /// Register a backend the host implements itself (spec 033, 3.9.1): its
317    /// failures are caused by the host, not an upstream service. A host
318    /// relaying a failure from its own upstream may still report it as
319    /// `upstream`; nothing reports the caller.
320    pub fn host_backend(
321        mut self,
322        backend: Arc<dyn DecisionBackend>,
323        max_concurrency: usize,
324    ) -> Self {
325        self.backends.push((
326            backend,
327            max_concurrency,
328            rustev_contract::actionability::Origin::Host,
329        ));
330        self
331    }
332
333    /// The host's store, for plans whose policy enables the persistent tier
334    /// (spec 044, 3.4.1).
335    pub fn store(mut self, store: Arc<dyn RuntimeStore>) -> Self {
336        self.store = Some(store);
337        self
338    }
339
340    /// A ledger shared by every decision, reserved after each decision's own.
341    pub fn shared_ledger(mut self, ledger: Arc<Ledger>) -> Self {
342        self.shared = Some(ledger);
343        self
344    }
345
346    /// Build the runtime. Under a queued sink policy this spawns the delivery
347    /// worker, so it must run inside a Tokio runtime.
348    pub fn build(self) -> Result<Runtime, ConfigError> {
349        let c = self.config;
350        for (name, v) in [
351            ("max_in_flight", c.max_in_flight),
352            ("max_parallel_requests", c.max_parallel_requests),
353        ] {
354            if v == 0 {
355                return Err(ConfigError::Zero(name));
356            }
357        }
358        let mut backends = BTreeMap::new();
359        for (b, max, origin) in self.backends {
360            if max == 0 {
361                return Err(ConfigError::Zero("backend max_concurrency"));
362            }
363            let id = b.descriptor().backend_id.clone();
364            let r = Registered {
365                backend: b,
366                permits: Arc::new(Semaphore::new(max)),
367                max,
368                origin,
369            };
370            if backends.insert(id.clone(), r).is_some() {
371                return Err(ConfigError::DuplicateBackend(id));
372            }
373        }
374        let mut sources = BTreeMap::new();
375        for (source, max) in self.sources {
376            if max == 0 {
377                return Err(ConfigError::Zero("source max_concurrency"));
378            }
379            let descriptor = source.descriptor().clone();
380            if descriptor.id().is_err() {
381                return Err(ConfigError::InvalidSource(descriptor.name));
382            }
383            let name = descriptor.name.clone();
384            if sources
385                .insert(
386                    name.clone(),
387                    acquisition::RegisteredSource {
388                        source,
389                        descriptor,
390                        permits: Arc::new(Semaphore::new(max)),
391                    },
392                )
393                .is_some()
394            {
395                return Err(ConfigError::InvalidSource(name));
396            }
397        }
398        let stats = Arc::new(Stats::default());
399        let queue = match c.sink {
400            SinkPolicy::FailDecision { .. } => None,
401            SinkPolicy::Backpressure {
402                max_pending,
403                delivery_timeout_ms,
404                ..
405            }
406            | SinkPolicy::DropCounted {
407                max_pending,
408                delivery_timeout_ms,
409            } => {
410                if max_pending == 0 {
411                    return Err(ConfigError::Zero("max_pending"));
412                }
413                let (tx, rx) = mpsc::channel(max_pending);
414                tokio::spawn(sink::worker(
415                    rx,
416                    self.sink.clone(),
417                    self.clock.clone(),
418                    delivery_timeout_ms,
419                    stats.clone(),
420                ));
421                Some(tx)
422            }
423        };
424        Ok(Runtime {
425            inner: Arc::new(Inner {
426                clock: self.clock,
427                config: c,
428                backends,
429                sources,
430                sink: self.sink,
431                queue,
432                stats,
433                shared: self.shared,
434                store: self.store,
435                unavailable: Mutex::new(BTreeSet::new()),
436                optimizer: optimization::Optimizer::default(),
437                admission: Arc::new(Semaphore::new(c.max_in_flight)),
438                queued: AtomicUsize::new(0),
439                rejected: AtomicU64::new(0),
440                abandoned: AtomicU64::new(0),
441                observer_panics: AtomicU64::new(0),
442                live: Mutex::new(BTreeSet::new()),
443            }),
444        })
445    }
446}
447
448/// A backend a request can be served by, in fallback order.
449#[derive(Clone)]
450pub(crate) struct Target {
451    pub backend_id: String,
452    pub artifact: ArtifactId,
453    pub registered: Registered,
454}
455
456pub(crate) struct StepRuntime {
457    pub targets: Vec<Target>,
458    pub extraction: Option<rustev_contract::extraction::ExtractionLimits>,
459    pub retry: RetryPolicy,
460    pub timeout: AttemptTimeout,
461    pub fallback_on: Vec<FailureClass>,
462}
463
464/// A compiled plan checked against this runtime's backends.
465pub struct PreparedPlan {
466    compiled: Arc<Compiled>,
467    pub(crate) steps: BTreeMap<String, StepRuntime>,
468    pub(crate) cost: CostPolicy,
469    execution: RecordedExecution,
470    pub(crate) optimization: Option<OptimizationPolicy>,
471}
472
473impl PreparedPlan {
474    pub fn compiled(&self) -> &Compiled {
475        &self.compiled
476    }
477
478    /// Identified cache namespace for host invalidation and audit.
479    pub fn optimization_namespace(&self) -> Option<String> {
480        self.optimization
481            .as_ref()
482            .map(|policy| optimization::namespace(&self.compiled.id, policy))
483    }
484}
485
486struct QueueSlot<'a>(&'a AtomicUsize);
487
488impl Drop for QueueSlot<'_> {
489    fn drop(&mut self) {
490        self.0.fetch_sub(1, Ordering::SeqCst);
491    }
492}
493
494struct Live<'a> {
495    set: &'a Mutex<BTreeSet<String>>,
496    id: String,
497}
498
499impl Drop for Live<'_> {
500    fn drop(&mut self) {
501        self.set
502            .lock()
503            .unwrap_or_else(|e| e.into_inner())
504            .remove(&self.id);
505    }
506}
507
508struct Abandon<'a> {
509    counter: &'a AtomicU64,
510    armed: bool,
511}
512
513impl Drop for Abandon<'_> {
514    fn drop(&mut self) {
515        if self.armed {
516            self.counter.fetch_add(1, Ordering::SeqCst);
517        }
518    }
519}
520
521impl Runtime {
522    pub fn builder(
523        clock: Arc<dyn Clock>,
524        sink: Arc<dyn EvidenceSink>,
525        config: RuntimeConfig,
526    ) -> RuntimeBuilder {
527        RuntimeBuilder {
528            clock,
529            sink,
530            config,
531            backends: vec![],
532            sources: vec![],
533            shared: None,
534            store: None,
535        }
536    }
537
538    pub fn gauges(&self) -> Gauges {
539        let i = &self.inner;
540        Gauges {
541            in_flight: i.config.max_in_flight - i.admission.available_permits(),
542            queued: i.queued.load(Ordering::SeqCst),
543            rejected: i.rejected.load(Ordering::SeqCst),
544            abandoned: i.abandoned.load(Ordering::SeqCst),
545            backend_in_flight: i
546                .backends
547                .iter()
548                .map(|(k, r)| (k.clone(), r.max - r.permits.available_permits()))
549                .collect(),
550            sink: i.stats.snapshot(),
551        }
552    }
553
554    /// The registered backend's descriptor and static cost model, for a
555    /// host's checks before any dispatch (spec 043, 3.7.1).
556    pub fn backend_terms(
557        &self,
558        backend_id: &str,
559    ) -> Option<(&rustev_contract::descriptor::BackendDescriptor, CostModel)> {
560        let r = self.inner.backends.get(backend_id)?;
561        Some((r.backend.descriptor(), r.backend.cost_model()))
562    }
563
564    /// Check a compiled plan against the registered backends (spec 003,
565    /// 3.5.3 and 3.6.5).
566    pub fn prepare(&self, compiled: Compiled) -> Result<PreparedPlan, PrepareError> {
567        if compiled.plan.schema == schema::PLAN_V3 && !self.inner.sink.accepts(schema::RUN_V3) {
568            return Err(PrepareError::SinkVersionUnsupported {
569                schema: schema::RUN_V3,
570            });
571        }
572        let (policy, fallbacks, execution) = match &compiled.plan.execution {
573            PlanExecution::None => (None, vec![], RecordedExecution::None),
574            PlanExecution::Declared(d) => (
575                Some(d.policy.clone()),
576                d.fallbacks.clone(),
577                RecordedExecution::Declared(d.id.clone()),
578            ),
579        };
580        let cost = policy
581            .as_ref()
582            .map(|p| p.cost)
583            .unwrap_or(CostPolicy::Unlimited);
584        let optimization = policy.as_ref().and_then(|p| p.optimization.clone());
585        if let Some(persistent) = optimization
586            .as_ref()
587            .and_then(|o| o.persistent_cache.as_ref())
588        {
589            let Some(store) = &self.inner.store else {
590                return Err(PrepareError::StoreMissing);
591            };
592            if *store.contract() != persistent.store_contract {
593                return Err(PrepareError::StoreContractMismatch);
594            }
595        }
596        let target = |backend_id: &str, descriptor: &rustev_contract::ids::DescriptorId| {
597            let r = self
598                .inner
599                .backends
600                .get(backend_id)
601                .ok_or_else(|| PrepareError::MissingBackend(backend_id.to_string()))?;
602            if r.backend.descriptor().id().ok().as_ref() != Some(descriptor) {
603                return Err(PrepareError::DescriptorMismatch {
604                    backend_id: backend_id.to_string(),
605                });
606            }
607            let hard = matches!(cost, CostPolicy::Hard { .. })
608                || self
609                    .inner
610                    .shared
611                    .as_ref()
612                    .is_some_and(|l| l.mode() == CostMode::Hard);
613            let model = r.backend.cost_model();
614            if hard && model != CostModel::Bounded {
615                return Err(PrepareError::HardBudgetUnenforceable {
616                    backend_id: backend_id.to_string(),
617                    model,
618                });
619            }
620            Ok(r.clone())
621        };
622        // Fact-only sources describe snapshot evidence and need no runtime adapter.
623        for binding in compiled
624            .plan
625            .acquisitions
626            .iter()
627            .flatten()
628            .flat_map(|a| &a.sources)
629        {
630            let Some(source) = self.inner.sources.get(&binding.source.name) else {
631                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
632            };
633            if source.descriptor.id().ok().as_ref() != Some(&binding.source.id) {
634                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
635            }
636            let op = source
637                .descriptor
638                .operations
639                .iter()
640                .flatten()
641                .find(|o| o.name == binding.operation);
642            let hard = matches!(cost, CostPolicy::Hard { .. })
643                || self
644                    .inner
645                    .shared
646                    .as_ref()
647                    .is_some_and(|l| l.mode() == CostMode::Hard);
648            if op.is_none()
649                || (hard
650                    && !op.is_some_and(|o| {
651                        matches!(
652                            o.cost,
653                            rustev_contract::acquisition::OperationCost::Bounded { .. }
654                        )
655                    }))
656            {
657                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
658            }
659        }
660        let mut steps = BTreeMap::new();
661        for s in &compiled.plan.steps {
662            let (backend_id, artifact, descriptor, extraction) = match &s.detail {
663                PlanStepDetail::Semantic(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
664                PlanStepDetail::Generate(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
665                PlanStepDetail::Extract(b) => (
666                    &b.backend_id,
667                    &b.artifact,
668                    &b.descriptor,
669                    Some(b.limits.clone()),
670                ),
671                PlanStepDetail::Rank(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
672                _ => continue,
673            };
674            let mut targets = vec![Target {
675                backend_id: backend_id.clone(),
676                artifact: artifact.clone(),
677                registered: target(backend_id, descriptor)?,
678            }];
679            let mut fb: Vec<_> = fallbacks.iter().filter(|f| f.step == s.id).collect();
680            fb.sort_by_key(|f| f.target);
681            for f in fb {
682                targets.push(Target {
683                    backend_id: f.backend_id.clone(),
684                    artifact: f.artifact.clone(),
685                    registered: target(&f.backend_id, &f.descriptor)?,
686                });
687            }
688            let declared: Option<&StepExecution> = policy
689                .as_ref()
690                .and_then(|p| p.steps.iter().find(|x| x.step == s.id));
691            steps.insert(
692                s.id.clone(),
693                StepRuntime {
694                    targets,
695                    extraction,
696                    retry: declared
697                        .map(|d| d.retry.clone())
698                        .unwrap_or_else(RetryPolicy::none),
699                    timeout: declared
700                        .map(|d| d.attempt_timeout)
701                        .unwrap_or(AttemptTimeout::None),
702                    fallback_on: declared.map(|d| d.fallback.on.clone()).unwrap_or_default(),
703                },
704            );
705        }
706        for s in &compiled.plan.steps {
707            if let PlanStepDetail::Generate(g) = &s.detail
708                && let Some(b) = &g.output_screener
709            {
710                for field in &g.output.fields {
711                    if matches!(
712                        field.ty,
713                        rustev_contract::generation::GeneratedType::Text { .. }
714                    ) {
715                        steps.insert(
716                            rustev_contract::generation::output_screen_step(&s.id, &field.name),
717                            StepRuntime {
718                                targets: vec![Target {
719                                    backend_id: b.backend_id.clone(),
720                                    artifact: b.artifact.clone(),
721                                    registered: target(&b.backend_id, &b.descriptor)?,
722                                }],
723                                extraction: None,
724                                retry: RetryPolicy::none(),
725                                timeout: AttemptTimeout::None,
726                                fallback_on: vec![],
727                            },
728                        );
729                    }
730                }
731            }
732        }
733        Ok(PreparedPlan {
734            compiled: Arc::new(compiled),
735            steps,
736            cost,
737            execution,
738            optimization,
739        })
740    }
741
742    /// Invalidate every reusable result in this plan's namespace, for any
743    /// correction, revocation, erasure, artifact withdrawal or policy
744    /// change the host observed. The generation advances before entries are
745    /// removed, so a result captured earlier is never stored or served. A
746    /// plan with a persistent tier also has its namespace marked
747    /// unavailable in this process until [`Runtime::invalidate`] completes
748    /// for it (spec 044, 3.10.3).
749    pub fn invalidate_plan(&self, plan: &PreparedPlan) -> Option<InvalidationResult> {
750        let namespace = plan.optimization_namespace()?;
751        Some(self.invalidate_memory(plan, namespace, None))
752    }
753
754    /// Invalidate reusable results for one opaque authorized scope, as for
755    /// an erasure or a scope revocation, as [`Runtime::invalidate_plan`]
756    /// does for a namespace.
757    pub fn invalidate_scope(
758        &self,
759        plan: &PreparedPlan,
760        principal_handle: &[u8],
761    ) -> Option<InvalidationResult> {
762        let namespace = plan.optimization_namespace()?;
763        Some(self.invalidate_memory(
764            plan,
765            namespace,
766            Some(optimization::scope_id(principal_handle)),
767        ))
768    }
769
770    /// Invalidate the plan's namespace, or one authorized scope of it, in
771    /// every tier (spec 044, 3.10.3): the memory tier at once, then the
772    /// store, whose generation advances. The selection is unavailable to
773    /// the persistent tier until the store answers; a failure leaves it
774    /// unavailable, and a later call reconciles it. Invalidation is not
775    /// carried to other processes (3.10.4).
776    pub async fn invalidate(
777        &self,
778        plan: &PreparedPlan,
779        principal_handle: Option<&[u8]>,
780    ) -> Option<InvalidationResult> {
781        let namespace = plan.optimization_namespace()?;
782        let scope = principal_handle.map(optimization::scope_id);
783        let mut result = self.invalidate_memory(plan, namespace.clone(), scope.clone());
784        let Some(store) = self
785            .inner
786            .store
787            .as_ref()
788            .filter(|_| result.persistent.is_some())
789        else {
790            return Some(result);
791        };
792        let selector = match &scope {
793            None => StoreSelector::Namespace,
794            Some(scope) => StoreSelector::Scope(scope.clone()),
795        };
796        let call = StoreCall {
797            remaining_ms: None,
798            cancel: CancelSignal::new(),
799        };
800        match store.invalidate(&namespace, selector, call).await {
801            Ok(done) => {
802                let mut marks = self.unavailable();
803                match &scope {
804                    // A namespace covers every scope in it.
805                    None => marks.retain(|(n, _)| *n != namespace),
806                    Some(scope) => {
807                        marks.remove(&(namespace.clone(), Some(scope.clone())));
808                    }
809                }
810                result.persistent = Some(PersistentInvalidation::Done {
811                    generation: done.generation,
812                    removed: done.removed,
813                });
814                // A scope stays unavailable while its namespace awaits its
815                // own store invalidation.
816                result.complete = !marks.contains(&(namespace.clone(), None));
817            }
818            Err(failure) => {
819                result.persistent = Some(PersistentInvalidation::Failed {
820                    detail: failure.detail,
821                });
822            }
823        }
824        Some(result)
825    }
826
827    fn unavailable(&self) -> std::sync::MutexGuard<'_, BTreeSet<(String, Option<String>)>> {
828        self.inner
829            .unavailable
830            .lock()
831            .unwrap_or_else(|e| e.into_inner())
832    }
833
834    /// The memory tier's invalidation, with the selection marked
835    /// unavailable to the persistent tier when the plan has one.
836    fn invalidate_memory(
837        &self,
838        plan: &PreparedPlan,
839        namespace: String,
840        scope: Option<String>,
841    ) -> InvalidationResult {
842        let selector = match &scope {
843            None => InvalidationSelector::Namespace {
844                namespace: namespace.clone(),
845            },
846            Some(scope_id) => InvalidationSelector::Scope {
847                namespace: namespace.clone(),
848                scope_id: scope_id.clone(),
849            },
850        };
851        let mut result = self.inner.optimizer.invalidate(&selector);
852        let persistent = plan
853            .optimization
854            .as_ref()
855            .is_some_and(|o| o.persistent_cache.is_some());
856        if persistent && self.inner.store.is_some() {
857            self.unavailable().insert((namespace, scope));
858            result.persistent = Some(PersistentInvalidation::Pending);
859            result.complete = false;
860        }
861        result
862    }
863
864    async fn admit(
865        &self,
866        deadline: Instant,
867        cancel: &CancelSignal,
868    ) -> Result<OwnedSemaphorePermit, Rejection> {
869        let i = &self.inner;
870        if let Ok(p) = i.admission.clone().try_acquire_owned() {
871            return Ok(p);
872        }
873        let mut n = i.queued.load(Ordering::SeqCst);
874        loop {
875            if n >= i.config.max_queued {
876                return Err(Rejection::Overloaded);
877            }
878            match i
879                .queued
880                .compare_exchange(n, n + 1, Ordering::SeqCst, Ordering::SeqCst)
881            {
882                Ok(_) => break,
883                Err(now) => n = now,
884            }
885        }
886        let _slot = QueueSlot(&i.queued);
887        let mut acquire = Box::pin(i.admission.clone().acquire_owned());
888        let mut timer = i.clock.sleep_until(deadline);
889        match race(&mut acquire, cancel, &mut timer).await {
890            Raced::Done(Ok(p)) if i.clock.now() < deadline => Ok(p),
891            Raced::Done(Ok(_)) | Raced::Expired => Err(Rejection::QueueDeadline),
892            Raced::Done(Err(_)) => Err(Rejection::Overloaded),
893            Raced::Cancelled => Err(Rejection::Cancelled),
894        }
895    }
896
897    /// Run one decision (spec 003). `cancel` is the caller's cancellation.
898    pub async fn decide(
899        &self,
900        plan: &PreparedPlan,
901        req: DecisionRequest,
902        cancel: &CancelSignal,
903    ) -> Result<Decided, DecideError> {
904        self.run(plan, req, cancel, None, None, None, None, &BTreeMap::new())
905            .await
906    }
907
908    /// [`Runtime::decide`] with the host's key dimensions (spec 044, 3.5
909    /// item 3): one scope handle per dimension the plan's
910    /// `decision_cache` declares. A decision missing a declared dimension,
911    /// or naming one the plan does not declare, is neither served from nor
912    /// admitted to the decision cache, and runs or is refused under the
913    /// policy's `admission`. A plan without the decision cache ignores
914    /// them.
915    pub async fn decide_keyed(
916        &self,
917        plan: &PreparedPlan,
918        req: DecisionRequest,
919        dimensions: &BTreeMap<String, rustev_contract::scope::Handle>,
920        cancel: &CancelSignal,
921    ) -> Result<Decided, DecideError> {
922        self.run(plan, req, cancel, None, None, None, None, dimensions)
923            .await
924    }
925
926    /// Run with a value-free observer. The ordinary entry point has no callbacks.
927    pub async fn decide_observed(
928        &self,
929        plan: &PreparedPlan,
930        req: DecisionRequest,
931        cancel: &CancelSignal,
932        observer: &dyn ProgressObserver,
933    ) -> Result<Decided, DecideError> {
934        self.run(
935            plan,
936            req,
937            cancel,
938            None,
939            None,
940            Some(observer),
941            None,
942            &BTreeMap::new(),
943        )
944        .await
945    }
946
947    /// Contained progress callback panics across this runtime's decisions.
948    pub fn observer_panics(&self) -> u64 {
949        self.inner.observer_panics.load(Ordering::Relaxed)
950    }
951
952    /// [`Runtime::decide`] with opt-in bounded capture of every supplied
953    /// value (spec 004, 5.3). An invalid configuration is refused before
954    /// admission. Dropping the future returns nothing, as for `decide`.
955    pub async fn decide_with_capture(
956        &self,
957        plan: &PreparedPlan,
958        req: DecisionRequest,
959        cancel: &CancelSignal,
960        capture: &CaptureConfig,
961    ) -> Result<CapturedDecision, CaptureConfigError> {
962        self.decide_capturing(plan, req, cancel, capture, None)
963            .await
964    }
965
966    async fn decide_capturing(
967        &self,
968        plan: &PreparedPlan,
969        req: DecisionRequest,
970        cancel: &CancelSignal,
971        capture: &CaptureConfig,
972        streaming: Option<&generation::Streaming>,
973    ) -> Result<CapturedDecision, CaptureConfigError> {
974        // Spec 036, 3.15.1 admits every plan 3 decision; generation capture
975        // (spec 037, 3.15.3) shares the acquisition and semantic supply order.
976        capture.check()?;
977        let decision_id = req.decision_id.clone();
978        let mut capturing = capture::Capturing::new(capture);
979        let result = self
980            .run(
981                plan,
982                req,
983                cancel,
984                Some(&mut capturing),
985                None,
986                None,
987                streaming,
988                &BTreeMap::new(),
989            )
990            .await;
991        let capture = match &result {
992            Err(DecideError::Rejected(_) | DecideError::GenerationFailed(_)) => {
993                CaptureOutcome::NotAdmitted
994            }
995            Ok(Decided { record, .. }) | Err(DecideError::EvidenceNotDelivered { record, .. }) => {
996                CaptureOutcome::Captured(
997                    capturing.finish(&decision_id, record.termination == Termination::Cancelled),
998                )
999            }
1000        };
1001        Ok(CapturedDecision { result, capture })
1002    }
1003
1004    /// Start an opt-in field stream. Poll the returned future to drive execution;
1005    /// the receiver may be polled independently and never backpressures it.
1006    pub fn decide_streaming<'a>(
1007        &'a self,
1008        plan: &'a PreparedPlan,
1009        req: DecisionRequest,
1010        cancel: &'a CancelSignal,
1011        stream_buffer: usize,
1012    ) -> Result<
1013        (
1014            impl std::future::Future<Output = Result<Decided, DecideError>> + Send + 'a,
1015            GenerationStream,
1016        ),
1017        rustev_core::generation_eval::GenerationError,
1018    > {
1019        let (streaming, receiver) = generation::Streaming::new(plan, stream_buffer)?;
1020        Ok((
1021            async move {
1022                self.run(
1023                    plan,
1024                    req,
1025                    cancel,
1026                    None,
1027                    None,
1028                    None,
1029                    Some(&streaming),
1030                    &BTreeMap::new(),
1031                )
1032                .await
1033            },
1034            receiver,
1035        ))
1036    }
1037
1038    #[allow(clippy::too_many_arguments)]
1039    async fn run(
1040        &self,
1041        plan: &PreparedPlan,
1042        req: DecisionRequest,
1043        cancel: &CancelSignal,
1044        mut capture: Option<&mut capture::Capturing<'_>>,
1045        live: Option<&live::LiveCtx<'_>>,
1046        observer: Option<&dyn ProgressObserver>,
1047        streaming: Option<&generation::Streaming>,
1048        dimensions: &BTreeMap<String, rustev_contract::scope::Handle>,
1049    ) -> Result<Decided, DecideError> {
1050        let i = &*self.inner;
1051        // The runtime waits on its own child of the caller's signal, so a
1052        // long-lived caller signal holds no waker of this decision after it
1053        // ends (spec 003, 3.4.3).
1054        let cancel = &cancel.child();
1055        // Dropping this future at any point after here, queued or admitted,
1056        // counts as abandoned (spec 003, 3.4.5).
1057        let mut abandon = Abandon {
1058            counter: &i.abandoned,
1059            armed: true,
1060        };
1061        let mut reject = |r: Rejection| {
1062            abandon.armed = false;
1063            i.rejected.fetch_add(1, Ordering::SeqCst);
1064            DecideError::Rejected(r)
1065        };
1066        let submitted = i.clock.now();
1067        if req.decision_id.is_empty() || req.decision_id.len() > 256 {
1068            return Err(reject(Rejection::InvalidRequest(
1069                "a decision id is 1 to 256 bytes".into(),
1070            )));
1071        }
1072        let compiled: &Compiled = &plan.compiled;
1073        let ev = match Evaluation::start(compiled, &req.snapshot, req.evaluation_time) {
1074            Ok(ev) => ev,
1075            Err(e) => return Err(reject(Rejection::InvalidRequest(e.to_string()))),
1076        };
1077        if !i
1078            .live
1079            .lock()
1080            .unwrap_or_else(|e| e.into_inner())
1081            .insert(req.decision_id.clone())
1082        {
1083            return Err(reject(Rejection::InvalidRequest(
1084                "a decision with this id is already running".into(),
1085            )));
1086        }
1087        let _live = Live {
1088            set: &i.live,
1089            id: req.decision_id.clone(),
1090        };
1091        let deadline_ms = compiled
1092            .plan
1093            .definition
1094            .limits
1095            .deadline_ms
1096            .min(req.deadline_ms.unwrap_or(u64::MAX));
1097        let deadline = submitted.saturating_add(deadline_ms);
1098        let _permit = match self.admit(deadline, cancel).await {
1099            Ok(p) => p,
1100            Err(r) => return Err(reject(r)),
1101        };
1102        let queued_ms = i.clock.now().saturating_sub(submitted);
1103        let observing = observer.map(|observer| progress::Observing {
1104            observer,
1105            panics: &i.observer_panics,
1106        });
1107        if let Some(o) = &observing {
1108            o.call(|o| o.admitted());
1109        }
1110
1111        let ledger = Ledger::new(plan.cost);
1112        let mut ev = std::sync::Mutex::new(ev);
1113        let deferred_cache = Mutex::new(vec![]);
1114        // The persistent tier (spec 044, 3.4); live re-execution observes
1115        // fresh backend work and reads no tier.
1116        let persistent = plan
1117            .optimization
1118            .as_ref()
1119            .and_then(|o| o.persistent_cache.as_ref())
1120            .filter(|_| i.store.is_some() && live.is_none());
1121        let mut pending = store::Pending::default();
1122        // The decision-result cache reads its tier after admission and
1123        // before the core starts (spec 044, 3.6.3).
1124        let mut decision = optimization::decision::DecisionRun::start(
1125            &i.optimizer,
1126            plan,
1127            &req,
1128            dimensions,
1129            live.is_some(),
1130        );
1131        let decision_policy = plan
1132            .optimization
1133            .as_ref()
1134            .and_then(|o| o.decision_cache.as_ref());
1135        // Then its persistent tier (spec 044, 3.3), on the decision's task.
1136        let decision_store = match (persistent, i.store.as_deref()) {
1137            (Some(persistent), Some(store)) => Some(store::StoreCtx {
1138                rt: i,
1139                store,
1140                persistent,
1141                cancel,
1142                deadline,
1143            }),
1144            _ => None,
1145        };
1146        if let (Some(d), Some(calls), Some(policy), Some(cache)) = (
1147            decision.as_mut(),
1148            decision_store.as_ref(),
1149            plan.optimization.as_ref(),
1150            decision_policy,
1151        ) {
1152            d.read_store(&i.optimizer, policy, cache, calls, req.evaluation_time)
1153                .await;
1154        }
1155        // A served decision's volatile inputs, as the core validated them
1156        // from the new snapshot; under `serve_cached_within_freshness` the
1157        // core starts again over the stored entries (spec 044, 3.6.5).
1158        if let (Some(d), Some(cache)) = (decision.as_mut(), decision_policy)
1159            && let Some(substituted) = d.refresh_inputs(
1160                &cache.volatile,
1161                ev.get_mut().unwrap_or_else(|e| e.into_inner()),
1162                &req.snapshot,
1163                capture.is_some(),
1164            )
1165        {
1166            match Evaluation::start(compiled, &substituted, req.evaluation_time) {
1167                Ok(fresh) => ev = std::sync::Mutex::new(fresh),
1168                Err(e) => d.unsubstitute(e.to_string()),
1169            }
1170        }
1171        let has_generation = compiled
1172            .plan
1173            .steps
1174            .iter()
1175            .any(|s| matches!(s.detail, PlanStepDetail::Generate(_)));
1176        // Under `recompute` a served decision whose refresh failed runs
1177        // again once every request of the served run has ended: a second
1178        // pass with nothing reused and `r<n>` attempt ids, within the same
1179        // deadline and ledger (spec 044, 3.6.5).
1180        let mut rerun = false;
1181        let (mut requests, acquisitions, cancelled, stage, pending_writes) = loop {
1182            let (mut requests, acquisitions, mut cancelled) = {
1183                let cx = driver::DecisionCtx {
1184                    rt: i,
1185                    plan,
1186                    deadline,
1187                    submitted,
1188                    cancel,
1189                    ledger: &ledger,
1190                    decision_id: &req.decision_id,
1191                    principal: &req.principal_handle,
1192                    evaluation_time: req.evaluation_time,
1193                    ev: Some(&ev),
1194                    generation: None,
1195                    step_ledger: None,
1196                    stream: None,
1197                    item_clock: None,
1198                    deferred_cache: has_generation.then_some(&deferred_cache),
1199                    persistent: persistent.map(|_| &pending),
1200                    decision: decision.as_ref(),
1201                    live,
1202                    observing: observing.as_ref(),
1203                    rerun,
1204                };
1205                driver::run_requests(&cx, capture.as_deref_mut()).await
1206            };
1207            let mut stage = None;
1208            let mut pending_writes = vec![];
1209            if !cancelled {
1210                let generating = ev
1211                    .into_inner()
1212                    .unwrap_or_else(|e| e.into_inner())
1213                    .begin_generation(&req.decision_id)
1214                    .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1215                let generating = Mutex::new(generating);
1216                let stage_cx = generation::StageContext {
1217                    rt: i,
1218                    plan,
1219                    deadline,
1220                    submitted,
1221                    cancel,
1222                    ledger: &ledger,
1223                    decision_id: &req.decision_id,
1224                    principal: &req.principal_handle,
1225                    evaluation_time: req.evaluation_time,
1226                    streaming,
1227                    decision: decision.as_ref(),
1228                    rerun,
1229                };
1230                // Spec 003 3.9.1 permits only judged or cancelled records; spec
1231                // 037 3.12.6 requires generation to complete before judged. An
1232                // invariant failure has no specified run-record termination, so
1233                // this path returns GenerationFailed without delivery or cache
1234                // writes. Ordinary backend failures resolve through backstops
1235                // and still deliver evidence (037 3.10.2, 3.12.7, 3.15.2); they
1236                // do not take this error path.
1237                let (generation_requests, writes, was_cancelled) =
1238                    generation::run(&stage_cx, &generating, capture.as_deref_mut())
1239                        .await
1240                        .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1241                requests.extend(generation_requests);
1242                cancelled = was_cancelled;
1243                pending_writes = writes;
1244                stage = Some(generating);
1245            }
1246            if !cancelled
1247                && let Some(d) = decision.as_mut()
1248                && d.recomputes(&requests, &acquisitions)
1249            {
1250                // The served run began over this snapshot, so the core
1251                // accepts it again.
1252                match Evaluation::start(compiled, &req.snapshot, req.evaluation_time) {
1253                    Ok(fresh) => {
1254                        d.abandon(requests, acquisitions);
1255                        if let Some(c) = capture.as_deref_mut() {
1256                            c.restart();
1257                        }
1258                        ev = std::sync::Mutex::new(fresh);
1259                        rerun = true;
1260                        continue;
1261                    }
1262                    Err(e) => d.diagnose(format!("recompute not started: {e}")),
1263                }
1264            }
1265            break (requests, acquisitions, cancelled, stage, pending_writes);
1266        };
1267        let stage_cx = generation::StageContext {
1268            rt: i,
1269            plan,
1270            deadline,
1271            submitted,
1272            cancel,
1273            ledger: &ledger,
1274            decision_id: &req.decision_id,
1275            principal: &req.principal_handle,
1276            evaluation_time: req.evaluation_time,
1277            streaming,
1278            decision: decision.as_ref(),
1279            rerun,
1280        };
1281        let mut text_result = None;
1282        let mut writes = vec![];
1283        let mut judged = false;
1284        // Spec 037 3.13.1 permits Complete only after final verification and
1285        // defines no failure event. Both cancellation paths close the stream
1286        // without Complete: cancellation has no text result (3.12.6).
1287        let (completion, core) = match stage {
1288            Some(stage) if !cancelled => {
1289                let (judgment, evidence) = {
1290                    let stage = stage.lock().unwrap_or_else(|e| e.into_inner());
1291                    let (mut judgment, evidence, mut text) = stage
1292                        .finish()
1293                        .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1294                    if let Some(text) = &mut text {
1295                        stage.verify_ownership(&mut judgment, text);
1296                        if let Some(streaming) = streaming {
1297                            streaming.finish(text);
1298                        }
1299                    }
1300                    if let Some(o) = &observing {
1301                        o.remaining(&evidence, &requests, &compiled.plan);
1302                    }
1303                    text_result = text;
1304                    (judgment, evidence)
1305                };
1306                // Persistent writes follow the evidence, whose digest each
1307                // entry binds, and precede the run record, which records
1308                // every admission not made (spec 044, 3.4).
1309                if let (Some(persistent), Some(store), Some(policy)) =
1310                    (persistent, i.store.as_deref(), plan.optimization.as_ref())
1311                {
1312                    let written = store::write_pending(
1313                        std::mem::take(&mut pending),
1314                        store::Writing {
1315                            rt: i,
1316                            store,
1317                            policy,
1318                            persistent,
1319                            cancel,
1320                            deadline,
1321                            created: req.evaluation_time,
1322                            evidence: rustev_contract::Document::record_digest(&evidence).ok(),
1323                        },
1324                    )
1325                    .await;
1326                    for ((step, instance), diagnostic) in written {
1327                        if let Some(o) = requests
1328                            .iter_mut()
1329                            .find(|r| r.step == step && r.instance == instance)
1330                            .and_then(|r| r.optimization.as_mut())
1331                        {
1332                            o.diagnose(diagnostic);
1333                        }
1334                    }
1335                }
1336                writes = deferred_cache
1337                    .into_inner()
1338                    .unwrap_or_else(|e| e.into_inner());
1339                writes.extend(pending_writes);
1340                judged = true;
1341                (
1342                    Completion::Judged(Box::new(judgment)),
1343                    CoreEvidence::Recorded(Box::new(evidence)),
1344                )
1345            }
1346            _ => (Completion::Cancelled, CoreEvidence::NotProduced),
1347        };
1348        let elapsed_ms = i.clock.now().saturating_sub(submitted);
1349        let plan_3 = plan.compiled.plan.schema == schema::PLAN_V3;
1350        // A plan 2 decision's record is `rustev.run/2` exactly when the sink
1351        // selected it (spec 020, 3.2).
1352        let run_2 = !plan_3 && i.sink.run_schema() == schema::RUN_V2;
1353        let (remote_evidence, remote_evidence_truncated) = if run_2 {
1354            let (list, truncated) = remote_evidence(i, &requests);
1355            (Some(list), truncated)
1356        } else {
1357            (None, false)
1358        };
1359        // A judged miss may be admitted once its record exists, since the
1360        // entry names the record's digest (spec 044, 3.6.4). It stores the
1361        // snapshot entries of its volatile inputs for a later failed refresh
1362        // (3.6.5).
1363        let volatile: Vec<_> = decision_policy
1364            .map(|p| {
1365                req.snapshot
1366                    .entries
1367                    .iter()
1368                    .filter(|e| p.volatile.contains(&e.field))
1369                    .cloned()
1370                    .collect()
1371            })
1372            .unwrap_or_default();
1373        let admission = match (&decision, decision_policy) {
1374            (Some(d), Some(policy)) if judged => match d.admissible(&requests, &volatile, policy) {
1375                Ok(admit) => admit,
1376                Err(diagnostic) => {
1377                    d.diagnose(diagnostic);
1378                    None
1379                }
1380            },
1381            _ => None,
1382        };
1383        let served_from_cache =
1384            decision
1385                .as_ref()
1386                .and_then(|d| d.hit.as_ref())
1387                .map(|hit| ServedFromCache {
1388                    entry_id: hit.entry_id.clone(),
1389                    source_run_record: hit.entry.source_run_record.clone(),
1390                });
1391        let decision_cache = decision
1392            .as_ref()
1393            .map(|d| d.record(&requests, &acquisitions));
1394        let mut record = RunRecord {
1395            acquisitions: (!acquisitions.is_empty()).then_some(acquisitions),
1396            decision_cache,
1397            // A plan 3 decision's record holds evidence 2 (spec 032, 3.10.3).
1398            schema: if plan_3 {
1399                schema::RUN_V3
1400            } else if run_2 {
1401                schema::RUN_V2
1402            } else {
1403                schema::RUN
1404            }
1405            .into(),
1406            decision_id: req.decision_id.clone(),
1407            plan_id: compiled.id.clone(),
1408            execution: plan.execution.clone(),
1409            termination: match completion {
1410                Completion::Judged(_) => Termination::Judged,
1411                Completion::Cancelled => Termination::Cancelled,
1412            },
1413            core,
1414            timing: Timing {
1415                queued_ms,
1416                elapsed_ms,
1417                deadline_ms,
1418                deadline_expired: i.clock.now() >= deadline,
1419            },
1420            requests,
1421            cost: ledger.summary(),
1422            remote_evidence,
1423            remote_evidence_truncated,
1424        };
1425        fit_remote_evidence(&mut record);
1426        let record = Arc::new(record);
1427        let delivery = self.deliver(record.clone()).await;
1428        abandon.armed = false;
1429        match delivery {
1430            Ok(delivery) => {
1431                generation::write_cache(&stage_cx, writes);
1432                let mut decision_cache_write = None;
1433                if let (Some(d), Some(policy), Some((key, requests)), Ok(source)) = (
1434                    &decision,
1435                    decision_policy,
1436                    admission,
1437                    rustev_contract::Document::record_digest(&*record),
1438                ) {
1439                    if let (Some(calls), Some(generations)) = (&decision_store, d.captured)
1440                        && policy.tiers.contains(&DecisionCacheTier::Persistent)
1441                    {
1442                        use rustev_contract::Document;
1443                        let digests = match (&completion, &record.core) {
1444                            (Completion::Judged(j), CoreEvidence::Recorded(e)) => {
1445                                j.record_digest().ok().zip(e.record_digest().ok())
1446                            }
1447                            _ => None,
1448                        };
1449                        decision_cache_write = Some(match (digests, plan.optimization.as_ref()) {
1450                            (Some((judgment, evidence)), Some(optimization)) => {
1451                                store::write_decision(
1452                                    calls,
1453                                    store::DecisionWrite {
1454                                        key: &key,
1455                                        policy: optimization,
1456                                        requests: &requests,
1457                                        volatile: volatile.clone(),
1458                                        judgment,
1459                                        run_record: source.clone(),
1460                                        evidence,
1461                                        generations,
1462                                        created: req.evaluation_time,
1463                                        ttl_ms: policy.ttl_ms,
1464                                    },
1465                                )
1466                                .await
1467                            }
1468                            _ => {
1469                                Err("persistent decision cache entry has no canonical form".into())
1470                            }
1471                        });
1472                    }
1473                    // A refused admission (a generation advanced, or an
1474                    // equal digest over other material) is a miss for the
1475                    // next decision; this record is already delivered.
1476                    if policy.tiers.contains(&DecisionCacheTier::Memory) {
1477                        let _ = i.optimizer.insert_decision(
1478                            policy,
1479                            key,
1480                            optimization::decision::DecisionEntry {
1481                                requests,
1482                                volatile,
1483                                source_run_record: source,
1484                                created: req.evaluation_time,
1485                            },
1486                            d.generation,
1487                        );
1488                    }
1489                }
1490                Ok(Decided {
1491                    text_result,
1492                    completion,
1493                    record,
1494                    delivery,
1495                    served_from_cache,
1496                    decision_cache_write,
1497                })
1498            }
1499            Err(failure) => Err(DecideError::EvidenceNotDelivered { record, failure }),
1500        }
1501    }
1502
1503    async fn deliver(&self, record: Arc<RunRecord>) -> Result<Delivery, DeliveryFailure> {
1504        let i = &*self.inner;
1505        match i.config.sink {
1506            SinkPolicy::FailDecision { timeout_ms } => {
1507                sink::deliver_once(&*i.sink, &*i.clock, &record, timeout_ms, &i.stats)
1508                    .await
1509                    .map(|receipt| Delivery::Acknowledged { receipt })
1510            }
1511            SinkPolicy::Backpressure {
1512                enqueue_timeout_ms, ..
1513            } => {
1514                let Some(tx) = &i.queue else {
1515                    return Err(stopped());
1516                };
1517                let mut reserve = Box::pin(tx.reserve());
1518                let mut timer = i
1519                    .clock
1520                    .sleep_until(i.clock.now().saturating_add(enqueue_timeout_ms));
1521                match race(&mut reserve, &CancelSignal::new(), &mut timer).await {
1522                    Raced::Done(Ok(permit)) => {
1523                        let (done, rx) = oneshot::channel();
1524                        i.stats.queued.fetch_add(1, Ordering::SeqCst);
1525                        permit.send(Job { record, done });
1526                        Ok(Delivery::Queued(DeliveryTicket(rx)))
1527                    }
1528                    Raced::Done(Err(_)) => Err(stopped()),
1529                    Raced::Expired | Raced::Cancelled => {
1530                        i.stats.failed.fetch_add(1, Ordering::SeqCst);
1531                        Err(DeliveryFailure {
1532                            detail: format!("no delivery slot within {enqueue_timeout_ms} ms"),
1533                            uncertain: false,
1534                        })
1535                    }
1536                }
1537            }
1538            SinkPolicy::DropCounted { .. } => {
1539                let Some(tx) = &i.queue else {
1540                    return Err(stopped());
1541                };
1542                let (done, rx) = oneshot::channel();
1543                i.stats.queued.fetch_add(1, Ordering::SeqCst);
1544                match tx.try_send(Job { record, done }) {
1545                    Ok(()) => Ok(Delivery::Queued(DeliveryTicket(rx))),
1546                    Err(mpsc::error::TrySendError::Closed(_)) => {
1547                        i.stats.queued.fetch_sub(1, Ordering::SeqCst);
1548                        Err(stopped())
1549                    }
1550                    Err(mpsc::error::TrySendError::Full(_)) => {
1551                        i.stats.queued.fetch_sub(1, Ordering::SeqCst);
1552                        let total = i.stats.dropped.fetch_add(1, Ordering::SeqCst) + 1;
1553                        Ok(Delivery::Dropped {
1554                            dropped_total: total,
1555                        })
1556                    }
1557                }
1558            }
1559        }
1560    }
1561
1562    /// A later observed charge for an attempt whose cost was unknown, applied
1563    /// to the shared ledger (spec 003, 3.5.5). Delivered records are never
1564    /// rewritten.
1565    pub fn reconcile(&self, attempt_id: &str, observed_units: u64) -> bool {
1566        self.inner
1567            .shared
1568            .as_ref()
1569            .is_some_and(|l| l.reconcile(attempt_id, observed_units))
1570    }
1571}
1572
1573fn stopped() -> DeliveryFailure {
1574    DeliveryFailure {
1575        detail: "the delivery worker stopped".into(),
1576        uncertain: false,
1577    }
1578}
1579
1580/// The remote evidence list of a `rustev.run/2` record (spec 020, 3.2): at
1581/// most one entry per remote attempt, in attempt order, taken from the
1582/// backend that ran it, with the step, instance and final remote state the
1583/// record holds. Beyond [`MAX_REMOTE_EVIDENCE`] entries the list is cut and
1584/// says so.
1585fn remote_evidence(i: &Inner, requests: &[RequestRecord]) -> (Vec<RemoteEvidence>, bool) {
1586    let mut out = Vec::new();
1587    let mut truncated = false;
1588    for r in requests {
1589        for a in &r.attempts {
1590            let Some(reg) = i.backends.get(&a.backend_id) else {
1591                continue;
1592            };
1593            let Some(mut e) = reg.backend.take_remote_evidence(&a.attempt_id) else {
1594                continue;
1595            };
1596            if out.len() >= MAX_REMOTE_EVIDENCE {
1597                truncated = true;
1598                continue;
1599            }
1600            e.attempt_id = a.attempt_id.clone();
1601            e.step = r.step.clone();
1602            e.instance = r.instance.clone();
1603            e.remote_state = a.remote;
1604            out.push(e.bounded());
1605        }
1606    }
1607    (out, truncated)
1608}
1609
1610/// Keep a `rustev.run/2` record within its parse bounds: remote evidence
1611/// entries leave from the end, and the record says so, until it fits.
1612fn fit_remote_evidence(record: &mut RunRecord) {
1613    use rustev_contract::Document;
1614    let max = rustev_contract::limits::RECORD_V1.max_bytes;
1615    while record
1616        .remote_evidence
1617        .as_ref()
1618        .is_some_and(|l| !l.is_empty())
1619        && record.record_canonical().is_ok_and(|b| b.len() > max)
1620    {
1621        if let Some(l) = &mut record.remote_evidence {
1622            l.pop();
1623        }
1624        record.remote_evidence_truncated = true;
1625    }
1626}
1627
1628mod spend;
1629pub use spend::{
1630    AcquisitionPosture, AcquisitionSpendAttempt, LiveAcquisition, LiveSources, LiveSpend,
1631    SpendAttempt, SpendBinding, SpendGate, SpendRefusal, SpendTicket, SpendUnit,
1632};