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/// Receives complete candidate fields during a generation attempt. The runtime
155/// validates each field before exposing it. Core never invokes this seam.
156pub trait GenerationFields: Send + Sync {
157    fn field(&self, name: &str, value: rustev_contract::output::TextValue);
158}
159
160/// Answers semantic requests. States its capabilities and its cost; never
161/// manufactures a distribution or a calibration claim.
162pub trait DecisionBackend: Send + Sync {
163    fn descriptor(&self) -> &BackendDescriptor;
164    /// Static disclosure: whether every call has an enforceable upper bound.
165    fn cost_model(&self) -> CostModel;
166    /// Per-call disclosure before dispatch. A `bounded` model returns
167    /// `bounded`; fractional costs are rounded up.
168    fn cost_bound(&self, projection: &[u8]) -> CostBound;
169    fn infer<'a>(&'a self, call: AttemptCall<'a>) -> BoxFuture<'a, AttemptReport>;
170
171    /// Optional generation streaming. Existing backends return their ordinary
172    /// complete report without emitting provisional fields.
173    fn infer_generation<'a>(
174        &'a self,
175        call: AttemptCall<'a>,
176        _fields: &'a dyn GenerationFields,
177    ) -> BoxFuture<'a, AttemptReport> {
178        self.infer(call)
179    }
180
181    /// Opt-in general batch declaration. `None` preserves unbatched behavior.
182    fn batch_capability(&self, _projection: &[u8]) -> Option<BatchCapability> {
183        None
184    }
185
186    /// The remote evidence of an attempt this backend ran, for a
187    /// `rustev.run/2` record (spec 020, 3.2), taken once: the adapter's
188    /// part of the entry (exchange reference, identities, binding, code,
189    /// charge provenance); the runtime sets the step, instance and final
190    /// remote state. `None`, the default, for an in-process backend.
191    fn take_remote_evidence(
192        &self,
193        _attempt_id: &str,
194    ) -> Option<rustev_contract::run::RemoteEvidence> {
195        None
196    }
197
198    /// Dispatch a declared-compatible batch. The default is unreachable for
199    /// a backend that keeps the default `batch_capability` implementation.
200    fn infer_batch<'a>(&'a self, _call: BatchAttemptCall) -> BoxFuture<'a, BatchAttemptReport> {
201        Box::pin(async {
202            BatchAttemptReport {
203                result: Err((
204                    AdapterFailure::Permanent,
205                    "backend did not implement declared batch semantics".into(),
206                )),
207                charge: Charge::Unknown,
208                cancel: CancelAck::NotRequested,
209                remote: RemoteEnd::PossiblyContinuing,
210            }
211        })
212    }
213}
214
215/// One dynamic question's single attempt (spec 019, 3.5).
216pub struct DynamicCall<'a> {
217    /// The canonical `DynamicProjection` bytes: data only.
218    pub projection: &'a [u8],
219    /// The template the capability declared; the adapter maps the
220    /// projection through it and nothing else.
221    pub template: &'a TemplateRef,
222    pub attempt_id: &'a str,
223    /// Raised on caller cancellation or the deadline.
224    pub cancel: CancelSignal,
225    pub cx: &'a CallContext,
226}
227
228/// What a dynamic attempt produced, who served it, what it cost and how
229/// cancellation went. Invalid answers are judged by the core.
230#[derive(Debug, Clone, PartialEq)]
231pub struct DynamicReport {
232    pub result: Result<DynamicRawOutput, (AdapterFailure, String)>,
233    /// The served identity as the adapter can state it; `unknown` is a
234    /// value, never replaced by the requested artifact.
235    pub served: ServedIdentity,
236    pub charge: Charge,
237    pub cancel: CancelAck,
238    pub remote: RemoteEnd,
239}
240
241/// Answers dynamic questions (spec 019, 3.4). Separate from
242/// [`DecisionBackend`]: implementing that trait never makes a backend
243/// dynamic, and a backend answers only when it returns a dynamic
244/// capability. It is offered no tool, function or retrieval surface.
245pub trait DynamicBackend: Send + Sync {
246    fn backend_id(&self) -> &str;
247    /// The opt-in declaration, or `None`: then every request is refused
248    /// before dispatch.
249    fn dynamic_capability(&self) -> Option<&DynamicCapability>;
250    /// Per-call disclosure before dispatch; only `bounded` is admitted.
251    fn cost_bound(&self, projection: &[u8]) -> CostBound;
252    fn ask<'a>(&'a self, call: DynamicCall<'a>) -> BoxFuture<'a, DynamicReport>;
253}
254
255/// Receives dynamic outcomes (spec 019, 3.6). A receipt establishes that
256/// the sink accepted the outcome under its own durability terms, nothing
257/// more.
258pub trait DynamicEvidenceSink: Send + Sync {
259    fn deliver<'a>(&'a self, outcome: &'a DynamicOutcome) -> BoxFuture<'a, Result<String, String>>;
260}
261
262/// A task adapter producing an evaluation report envelope (R-06).
263pub trait Evaluator: Send + Sync {
264    fn evaluate<'a>(&'a self, cx: &'a CallContext) -> BoxFuture<'a, EvalReport>;
265}
266
267/// Receives run records (spec 003, 3.9). A receipt establishes that the sink
268/// accepted the record for its decision id under the sink's own durability
269/// terms, nothing more.
270pub trait EvidenceSink: Send + Sync {
271    fn deliver<'a>(&'a self, record: &'a RunRecord) -> BoxFuture<'a, Result<String, String>>;
272
273    /// Whether the sink accepts run records of `schema` (spec 032, 3.11.2).
274    /// A sink accepts only `rustev.run/1` unless it says otherwise.
275    fn accepts(&self, schema: &str) -> bool {
276        schema == rustev_contract::schema::RUN
277    }
278
279    /// The one run-record schema the sink selected at construction for a
280    /// plan 2 decision (spec 020, 2 and 3.2): `rustev.run/1`, the default,
281    /// or `rustev.run/2`. A sink that selects version 2 is never given
282    /// version 1 bytes in its place, and a rejected version 2 record is
283    /// never retried as version 1.
284    fn run_schema(&self) -> &'static str {
285        rustev_contract::schema::RUN
286    }
287}
288
289/// A one-way, clonable cancellation signal with children, using `std` only.
290/// Raising a signal raises every child created from it; a child can be
291/// raised alone.
292#[derive(Clone, Default)]
293pub struct CancelSignal(Arc<Inner>);
294
295#[derive(Default)]
296struct Inner {
297    raised: AtomicBool,
298    state: Mutex<State>,
299}
300
301#[derive(Default)]
302struct State {
303    wakers: Vec<Waker>,
304    children: Vec<Weak<Inner>>,
305}
306
307impl std::fmt::Debug for CancelSignal {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        f.debug_tuple("CancelSignal")
310            .field(&self.is_raised())
311            .finish()
312    }
313}
314
315fn lock(m: &Mutex<State>) -> std::sync::MutexGuard<'_, State> {
316    m.lock().unwrap_or_else(|e| e.into_inner())
317}
318
319impl Inner {
320    fn raise(&self) {
321        if self.raised.swap(true, Ordering::SeqCst) {
322            return;
323        }
324        let (wakers, children) = {
325            let mut s = lock(&self.state);
326            (
327                std::mem::take(&mut s.wakers),
328                std::mem::take(&mut s.children),
329            )
330        };
331        for w in wakers {
332            w.wake();
333        }
334        for c in children.iter().filter_map(Weak::upgrade) {
335            c.raise();
336        }
337    }
338}
339
340impl CancelSignal {
341    pub fn new() -> Self {
342        Self::default()
343    }
344
345    pub fn raise(&self) {
346        self.0.raise();
347    }
348
349    pub fn is_raised(&self) -> bool {
350        self.0.raised.load(Ordering::SeqCst)
351    }
352
353    /// A signal raised when this one is, or on its own.
354    pub fn child(&self) -> CancelSignal {
355        let child = CancelSignal::new();
356        {
357            let mut s = lock(&self.0.state);
358            if !self.is_raised() {
359                s.children.retain(|w| w.strong_count() > 0);
360                s.children.push(Arc::downgrade(&child.0));
361                return child;
362            }
363        }
364        child.raise();
365        child
366    }
367
368    /// Ready once raised; registers the task's waker otherwise.
369    pub fn poll_raised(&self, cx: &mut Context<'_>) -> Poll<()> {
370        if self.is_raised() {
371            return Poll::Ready(());
372        }
373        let mut s = lock(&self.0.state);
374        if self.is_raised() {
375            return Poll::Ready(());
376        }
377        if !s.wakers.iter().any(|w| w.will_wake(cx.waker())) {
378            s.wakers.push(cx.waker().clone());
379        }
380        Poll::Pending
381    }
382
383    /// Wakers and live children registered on this signal, for diagnostics:
384    /// a signal shared across many waiters must not accumulate them.
385    pub fn registered(&self) -> usize {
386        let s = lock(&self.0.state);
387        s.wakers.len() + s.children.iter().filter(|w| w.strong_count() > 0).count()
388    }
389
390    /// Resolves once raised.
391    pub fn raised(&self) -> impl Future<Output = ()> + Send + '_ {
392        std::future::poll_fn(move |cx| self.poll_raised(cx))
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    #[test]
401    fn raising_a_parent_raises_its_children_and_not_the_reverse() {
402        let parent = CancelSignal::new();
403        let a = parent.child();
404        let b = parent.child();
405        b.raise();
406        assert!(b.is_raised() && !a.is_raised() && !parent.is_raised());
407        parent.raise();
408        assert!(a.is_raised());
409        let late = parent.child();
410        assert!(late.is_raised(), "a child of a raised signal starts raised");
411    }
412}
413
414/// An acquisition adapter. Dispatch belongs to the caller, never to the core.
415/// Its future uses only std and has no executor or runtime dependency.
416pub trait AcquisitionSource: Send + Sync {
417    fn descriptor(&self) -> &SourceDescriptor;
418    fn cost_bound(&self, request: &AcquisitionRequest) -> CostBound;
419    fn acquire<'a>(&'a self, call: AcquireCall<'a>) -> BoxFuture<'a, AcquireReport>;
420}
421
422/// One acquisition attempt, with the core's canonical request and identity.
423pub struct AcquireCall<'a> {
424    pub operation: &'a str,
425    pub request: &'a rustev_contract::acquisition::AcquisitionRequest,
426    pub request_id: &'a rustev_contract::ids::AcquisitionRequestId,
427    pub attempt_id: &'a str,
428    pub cancel: CancelSignal,
429    pub cx: &'a CallContext,
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub enum AcquisitionFailure {
434    Transient,
435    Overloaded,
436    Permanent,
437    Denied,
438}
439
440/// Raw page bytes are always validated by the core before becoming values.
441#[derive(Debug, Clone, PartialEq)]
442pub struct AcquireReport {
443    pub result: Result<Vec<u8>, (AcquisitionFailure, String)>,
444    pub charge: Charge,
445    pub cancel: CancelAck,
446}