Skip to main content

rustev_core/
seams.rs

1//! Seam traits (spec 002, 3.1.2, amended by spec 003, 3.2.6; design section
2//! 8), declared as signatures over `std::future` only. The runtime (spec 003)
3//! composes them. They are transport-neutral; network implementations live
4//! under `integrations/` (spec 001, 3.4.3).
5//!
6//! Nothing here reads a clock, performs I/O or spawns: time reaches a seam as
7//! a remaining duration, and cancellation as a [`CancelSignal`].
8
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::{Arc, Mutex, Weak};
13use std::task::{Context, Poll, Waker};
14
15use rustev_contract::acquisition::AcquisitionRequest;
16use rustev_contract::descriptor::BackendDescriptor;
17use rustev_contract::dynamic::{DynamicCapability, DynamicOutcome, DynamicRawOutput, TemplateRef};
18use rustev_contract::eval_report::EvalReport;
19use rustev_contract::output::RawOutput;
20use rustev_contract::remote::ServedIdentity;
21use rustev_contract::run::{Charge, CostBound, CostModel, RunRecord};
22use rustev_contract::snapshot::Snapshot;
23use rustev_contract::source::SourceDescriptor;
24use rustev_contract::time::DurationMs;
25
26/// A boxed future, so the traits stay object-safe without an executor.
27pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
28
29/// Passed to every seam call. Time is a value, not a clock read.
30#[derive(Debug, Clone)]
31pub struct CallContext {
32    /// Time left before the decision's deadline when the call was made, by
33    /// the runtime's monotonic clock.
34    pub remaining_ms: DurationMs,
35    pub trace_id: String,
36    /// Opaque to Rustev: passed through for the host's authorization and
37    /// cache isolation, never interpreted as authority (spec 001, 3.2).
38    pub principal_handle: Vec<u8>,
39}
40
41/// Supplies an authorized context snapshot. Returns only what the caller may
42/// read; a missing field is reported missing, never filled.
43pub trait ContextSource: Send + Sync {
44    fn fetch<'a>(
45        &'a self,
46        fields: &'a [String],
47        cx: &'a CallContext,
48    ) -> BoxFuture<'a, Result<Snapshot, String>>;
49}
50
51/// One dispatched attempt (spec 003, 3.2.6).
52pub struct AttemptCall<'a> {
53    /// Canonical projection, exactly as the core listed it.
54    pub projection: &'a [u8],
55    /// Stable across timing; usable as an idempotency key.
56    pub attempt_id: &'a str,
57    /// Raised on caller cancellation, deadline or attempt timeout.
58    pub cancel: CancelSignal,
59    pub cx: &'a CallContext,
60}
61
62/// How an adapter says an attempt failed. Invalid outputs are judged by the
63/// core, not reported here.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum AdapterFailure {
66    /// May not recur.
67    Transient,
68    /// Refused for load.
69    Overloaded,
70    /// Will recur for this input.
71    Permanent,
72    /// Stopped because the attempt's cancellation signal was raised.
73    Cancelled,
74}
75
76/// The adapter's answer to a raised cancellation signal.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum CancelAck {
79    /// No cancellation was observed.
80    NotRequested,
81    /// Work and charges stopped; the adapter can establish this.
82    Stopped,
83    /// The adapter stopped waiting but cannot establish a remote stop.
84    Unconfirmed,
85}
86
87/// Whether remote work may outlive a report the adapter returned of its own
88/// accord (spec 013, 3.1).
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum RemoteEnd {
91    /// The remote work, if any, is over as far as the adapter can establish.
92    /// Every in-process backend reports this.
93    Finished,
94    /// Request bytes left the process and nothing establishes that the remote
95    /// side stopped.
96    PossiblyContinuing,
97}
98
99/// What one attempt produced, what it cost and how cancellation went.
100#[derive(Debug, Clone, PartialEq)]
101pub struct AttemptReport {
102    pub result: Result<RawOutput, (AdapterFailure, String)>,
103    pub charge: Charge,
104    pub cancel: CancelAck,
105    /// Read only for a report returned of the adapter's own accord; the
106    /// cancellation path decides from `cancel` (spec 013, 3.2).
107    pub remote: RemoteEnd,
108}
109
110/// A backend's explicit declaration that one projection may participate in a
111/// general runtime batch. Equal compatibility identifiers mean the backend
112/// promises compatible semantics. Work units are backend-defined and are
113/// used only against the plan's hard `max_backend_work` bound.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct BatchCapability {
116    pub compatibility: String,
117    pub work_units: u64,
118}
119
120/// One owned member of a general runtime batch.
121#[derive(Debug, Clone)]
122pub struct BatchMemberCall {
123    pub projection: Vec<u8>,
124    pub attempt_id: String,
125    pub cancel: CancelSignal,
126    pub cx: CallContext,
127}
128
129/// One general batch dispatch. Member order is stable and evidence-visible.
130pub struct BatchAttemptCall {
131    pub batch_id: String,
132    pub cancel: CancelSignal,
133    pub members: Vec<BatchMemberCall>,
134}
135
136/// A member result is keyed by attempt id. Duplicate, unknown, or missing ids
137/// are malformed for the affected member and never fill another member.
138#[derive(Debug, Clone, PartialEq)]
139pub struct BatchMemberReport {
140    pub attempt_id: String,
141    pub result: Result<RawOutput, (AdapterFailure, String)>,
142}
143
144/// One backend-wide batch report. A top-level failure is attributed to every
145/// dispatched member. The batch charge is allocated by the plan policy.
146#[derive(Debug, Clone, PartialEq)]
147pub struct BatchAttemptReport {
148    pub result: Result<Vec<BatchMemberReport>, (AdapterFailure, String)>,
149    pub charge: Charge,
150    pub cancel: CancelAck,
151    pub remote: RemoteEnd,
152}
153
154/// Answers semantic requests. States its capabilities and its cost; never
155/// manufactures a distribution or a calibration claim.
156pub trait DecisionBackend: Send + Sync {
157    fn descriptor(&self) -> &BackendDescriptor;
158    /// Static disclosure: whether every call has an enforceable upper bound.
159    fn cost_model(&self) -> CostModel;
160    /// Per-call disclosure before dispatch. A `bounded` model returns
161    /// `bounded`; fractional costs are rounded up.
162    fn cost_bound(&self, projection: &[u8]) -> CostBound;
163    fn infer<'a>(&'a self, call: AttemptCall<'a>) -> BoxFuture<'a, AttemptReport>;
164
165    /// Opt-in general batch declaration. `None` preserves unbatched behavior.
166    fn batch_capability(&self, _projection: &[u8]) -> Option<BatchCapability> {
167        None
168    }
169
170    /// The remote evidence of an attempt this backend ran, for a
171    /// `rustev.run/2` record (spec 020, 3.2), taken once: the adapter's
172    /// part of the entry (exchange reference, identities, binding, code,
173    /// charge provenance); the runtime sets the step, instance and final
174    /// remote state. `None`, the default, for an in-process backend.
175    fn take_remote_evidence(
176        &self,
177        _attempt_id: &str,
178    ) -> Option<rustev_contract::run::RemoteEvidence> {
179        None
180    }
181
182    /// Dispatch a declared-compatible batch. The default is unreachable for
183    /// a backend that keeps the default `batch_capability` implementation.
184    fn infer_batch<'a>(&'a self, _call: BatchAttemptCall) -> BoxFuture<'a, BatchAttemptReport> {
185        Box::pin(async {
186            BatchAttemptReport {
187                result: Err((
188                    AdapterFailure::Permanent,
189                    "backend did not implement declared batch semantics".into(),
190                )),
191                charge: Charge::Unknown,
192                cancel: CancelAck::NotRequested,
193                remote: RemoteEnd::PossiblyContinuing,
194            }
195        })
196    }
197}
198
199/// One dynamic question's single attempt (spec 019, 3.5).
200pub struct DynamicCall<'a> {
201    /// The canonical `DynamicProjection` bytes: data only.
202    pub projection: &'a [u8],
203    /// The template the capability declared; the adapter maps the
204    /// projection through it and nothing else.
205    pub template: &'a TemplateRef,
206    pub attempt_id: &'a str,
207    /// Raised on caller cancellation or the deadline.
208    pub cancel: CancelSignal,
209    pub cx: &'a CallContext,
210}
211
212/// What a dynamic attempt produced, who served it, what it cost and how
213/// cancellation went. Invalid answers are judged by the core.
214#[derive(Debug, Clone, PartialEq)]
215pub struct DynamicReport {
216    pub result: Result<DynamicRawOutput, (AdapterFailure, String)>,
217    /// The served identity as the adapter can state it; `unknown` is a
218    /// value, never replaced by the requested artifact.
219    pub served: ServedIdentity,
220    pub charge: Charge,
221    pub cancel: CancelAck,
222    pub remote: RemoteEnd,
223}
224
225/// Answers dynamic questions (spec 019, 3.4). Separate from
226/// [`DecisionBackend`]: implementing that trait never makes a backend
227/// dynamic, and a backend answers only when it returns a dynamic
228/// capability. It is offered no tool, function or retrieval surface.
229pub trait DynamicBackend: Send + Sync {
230    fn backend_id(&self) -> &str;
231    /// The opt-in declaration, or `None`: then every request is refused
232    /// before dispatch.
233    fn dynamic_capability(&self) -> Option<&DynamicCapability>;
234    /// Per-call disclosure before dispatch; only `bounded` is admitted.
235    fn cost_bound(&self, projection: &[u8]) -> CostBound;
236    fn ask<'a>(&'a self, call: DynamicCall<'a>) -> BoxFuture<'a, DynamicReport>;
237}
238
239/// Receives dynamic outcomes (spec 019, 3.6). A receipt establishes that
240/// the sink accepted the outcome under its own durability terms, nothing
241/// more.
242pub trait DynamicEvidenceSink: Send + Sync {
243    fn deliver<'a>(&'a self, outcome: &'a DynamicOutcome) -> BoxFuture<'a, Result<String, String>>;
244}
245
246/// A task adapter producing an evaluation report envelope (R-06).
247pub trait Evaluator: Send + Sync {
248    fn evaluate<'a>(&'a self, cx: &'a CallContext) -> BoxFuture<'a, EvalReport>;
249}
250
251/// Receives run records (spec 003, 3.9). A receipt establishes that the sink
252/// accepted the record for its decision id under the sink's own durability
253/// terms, nothing more.
254pub trait EvidenceSink: Send + Sync {
255    fn deliver<'a>(&'a self, record: &'a RunRecord) -> BoxFuture<'a, Result<String, String>>;
256
257    /// Whether the sink accepts run records of `schema` (spec 032, 3.11.2).
258    /// A sink accepts only `rustev.run/1` unless it says otherwise.
259    fn accepts(&self, schema: &str) -> bool {
260        schema == rustev_contract::schema::RUN
261    }
262
263    /// The one run-record schema the sink selected at construction for a
264    /// plan 2 decision (spec 020, 2 and 3.2): `rustev.run/1`, the default,
265    /// or `rustev.run/2`. A sink that selects version 2 is never given
266    /// version 1 bytes in its place, and a rejected version 2 record is
267    /// never retried as version 1.
268    fn run_schema(&self) -> &'static str {
269        rustev_contract::schema::RUN
270    }
271}
272
273/// A one-way, clonable cancellation signal with children, using `std` only.
274/// Raising a signal raises every child created from it; a child can be
275/// raised alone.
276#[derive(Clone, Default)]
277pub struct CancelSignal(Arc<Inner>);
278
279#[derive(Default)]
280struct Inner {
281    raised: AtomicBool,
282    state: Mutex<State>,
283}
284
285#[derive(Default)]
286struct State {
287    wakers: Vec<Waker>,
288    children: Vec<Weak<Inner>>,
289}
290
291impl std::fmt::Debug for CancelSignal {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        f.debug_tuple("CancelSignal")
294            .field(&self.is_raised())
295            .finish()
296    }
297}
298
299fn lock(m: &Mutex<State>) -> std::sync::MutexGuard<'_, State> {
300    m.lock().unwrap_or_else(|e| e.into_inner())
301}
302
303impl Inner {
304    fn raise(&self) {
305        if self.raised.swap(true, Ordering::SeqCst) {
306            return;
307        }
308        let (wakers, children) = {
309            let mut s = lock(&self.state);
310            (
311                std::mem::take(&mut s.wakers),
312                std::mem::take(&mut s.children),
313            )
314        };
315        for w in wakers {
316            w.wake();
317        }
318        for c in children.iter().filter_map(Weak::upgrade) {
319            c.raise();
320        }
321    }
322}
323
324impl CancelSignal {
325    pub fn new() -> Self {
326        Self::default()
327    }
328
329    pub fn raise(&self) {
330        self.0.raise();
331    }
332
333    pub fn is_raised(&self) -> bool {
334        self.0.raised.load(Ordering::SeqCst)
335    }
336
337    /// A signal raised when this one is, or on its own.
338    pub fn child(&self) -> CancelSignal {
339        let child = CancelSignal::new();
340        {
341            let mut s = lock(&self.0.state);
342            if !self.is_raised() {
343                s.children.retain(|w| w.strong_count() > 0);
344                s.children.push(Arc::downgrade(&child.0));
345                return child;
346            }
347        }
348        child.raise();
349        child
350    }
351
352    /// Ready once raised; registers the task's waker otherwise.
353    pub fn poll_raised(&self, cx: &mut Context<'_>) -> Poll<()> {
354        if self.is_raised() {
355            return Poll::Ready(());
356        }
357        let mut s = lock(&self.0.state);
358        if self.is_raised() {
359            return Poll::Ready(());
360        }
361        if !s.wakers.iter().any(|w| w.will_wake(cx.waker())) {
362            s.wakers.push(cx.waker().clone());
363        }
364        Poll::Pending
365    }
366
367    /// Wakers and live children registered on this signal, for diagnostics:
368    /// a signal shared across many waiters must not accumulate them.
369    pub fn registered(&self) -> usize {
370        let s = lock(&self.0.state);
371        s.wakers.len() + s.children.iter().filter(|w| w.strong_count() > 0).count()
372    }
373
374    /// Resolves once raised.
375    pub fn raised(&self) -> impl Future<Output = ()> + Send + '_ {
376        std::future::poll_fn(move |cx| self.poll_raised(cx))
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn raising_a_parent_raises_its_children_and_not_the_reverse() {
386        let parent = CancelSignal::new();
387        let a = parent.child();
388        let b = parent.child();
389        b.raise();
390        assert!(b.is_raised() && !a.is_raised() && !parent.is_raised());
391        parent.raise();
392        assert!(a.is_raised());
393        let late = parent.child();
394        assert!(late.is_raised(), "a child of a raised signal starts raised");
395    }
396}
397
398/// An acquisition adapter. Dispatch belongs to the caller, never to the core.
399/// Its future uses only std and has no executor or runtime dependency.
400pub trait AcquisitionSource: Send + Sync {
401    fn descriptor(&self) -> &SourceDescriptor;
402    fn cost_bound(&self, request: &AcquisitionRequest) -> CostBound;
403    fn acquire<'a>(&'a self, call: AcquireCall<'a>) -> BoxFuture<'a, AcquireReport>;
404}
405
406/// One acquisition attempt, with the core's canonical request and identity.
407pub struct AcquireCall<'a> {
408    pub operation: &'a str,
409    pub request: &'a rustev_contract::acquisition::AcquisitionRequest,
410    pub request_id: &'a rustev_contract::ids::AcquisitionRequestId,
411    pub attempt_id: &'a str,
412    pub cancel: CancelSignal,
413    pub cx: &'a CallContext,
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum AcquisitionFailure {
418    Transient,
419    Overloaded,
420    Permanent,
421    Denied,
422}
423
424/// Raw page bytes are always validated by the core before becoming values.
425#[derive(Debug, Clone, PartialEq)]
426pub struct AcquireReport {
427    pub result: Result<Vec<u8>, (AcquisitionFailure, String)>,
428    pub charge: Charge,
429    pub cancel: CancelAck,
430}