1#![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};
59pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct RuntimeConfig {
73 pub max_in_flight: usize,
75 pub max_queued: usize,
77 pub max_parallel_requests: usize,
79 pub sink: SinkPolicy,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum ConfigError {
84 Zero(&'static str),
86 DuplicateBackend(String),
87 InvalidSource(String),
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum PrepareError {
93 SourceBinding(String),
94 MissingBackend(String),
96 DescriptorMismatch {
98 backend_id: String,
99 },
100 HardBudgetUnenforceable {
102 backend_id: String,
103 model: CostModel,
104 },
105 SinkVersionUnsupported {
108 schema: &'static str,
109 },
110}
111
112#[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 GenerationFailed(String),
126 Rejected(Rejection),
127 EvidenceNotDelivered {
130 record: Arc<RunRecord>,
131 failure: DeliveryFailure,
132 },
133}
134
135#[derive(Debug, Clone)]
137pub struct DecisionRequest {
138 pub decision_id: String,
140 pub snapshot: Snapshot,
141 pub evaluation_time: Timestamp,
143 pub deadline_ms: Option<u64>,
145 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#[derive(Debug, Clone, PartialEq)]
174pub enum CaptureOutcome {
175 NotAdmitted,
178 Captured(Capture),
179}
180
181#[derive(Debug)]
184pub struct CapturedDecision {
185 pub result: Result<Decided, DecideError>,
186 pub capture: CaptureOutcome,
187}
188
189#[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 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 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 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 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 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 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 pub fn shared_ledger(mut self, ledger: Arc<Ledger>) -> Self {
288 self.shared = Some(ledger);
289 self
290 }
291
292 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#[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
408pub 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 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 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 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 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 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 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 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 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 pub fn observer_panics(&self) -> u64 {
761 self.inner.observer_panics.load(Ordering::Relaxed)
762 }
763
764 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 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 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 let cancel = &cancel.child();
856 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 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 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 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 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 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
1142fn 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
1172fn 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};