Skip to main content

rustev_contract/
run.rs

1//! Run records, `rustev.run/1` (spec 003, 3.9): what the runtime delivers for
2//! one admitted decision. The core's evidence record is carried unchanged in
3//! its own member; everything else is a runtime observation, recorded and
4//! never promised to repeat (principle XIII).
5//!
6//! Also the cost and cancellation vocabulary the backend seam reports in
7//! (spec 003, 3.5 and 3.7), so an outside reader needs this crate alone.
8
9use serde::{Deserialize, Serialize};
10
11use crate::evidence::EvidenceRecord;
12use crate::execution::FailureClass;
13use crate::ids::{ArtifactId, ExecutionPolicyId, PlanId};
14use crate::judgment::Unresolved;
15use crate::limits::RECORD_V1;
16use crate::optimization::{OptimizationOutcome, OptimizationRecord};
17use crate::remote::{MAX_TEXT_BYTES, RemoteCode, ServedIdentity};
18use crate::schema;
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
21#[serde(deny_unknown_fields)]
22pub struct RunRecord {
23    pub schema: String,
24    /// Supplied by the caller; the delivery key sinks deduplicate by.
25    pub decision_id: String,
26    pub plan_id: PlanId,
27    pub execution: RecordedExecution,
28    pub termination: Termination,
29    pub core: CoreEvidence,
30    pub timing: Timing,
31    /// In the order the core listed them.
32    pub requests: Vec<RequestRecord>,
33    pub cost: CostSummary,
34    /// `rustev.run/2` only (spec 020, 3.2): at most one entry per remote
35    /// attempt, in attempt order, at most [`MAX_REMOTE_EVIDENCE`]. Absent in
36    /// every other version, so their bytes are unchanged.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub remote_evidence: Option<Vec<RemoteEvidence>>,
39    /// `rustev.run/2` only: remote attempts beyond the bound have no entry.
40    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
41    pub remote_evidence_truncated: bool,
42    /// Spec 036: omitted when the plan has no acquisition step.
43    #[serde(
44        default,
45        skip_serializing_if = "Option::is_none",
46        deserialize_with = "crate::acquisition_record::list"
47    )]
48    #[schemars(
49        with = "Vec<crate::acquisition_record::AcquisitionRecord>",
50        length(min = 1, max = 4096)
51    )]
52    pub acquisitions: Option<Vec<crate::acquisition_record::AcquisitionRecord>>,
53    /// `rustev.run/3` only (spec 044, 3.6.6): omitted when the plan has no
54    /// decision-result cache.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub decision_cache: Option<crate::optimization::DecisionCacheRecord>,
57}
58
59// `rustev.run/3` is `rustev.run/1` with its core member holding
60// `rustev.evidence/2` (spec 032, 3.10.3); `rustev.run/2` is `rustev.run/1`
61// with a remote evidence list (spec 020, 3.2).
62crate::document::versioned_document!(
63    RunRecord,
64    [schema::RUN, schema::RUN_V2, schema::RUN_V3],
65    "rustev.run/1 or rustev.run/2 or rustev.run/3",
66    RECORD_V1,
67    check = RunRecord::parsed_members_fit
68);
69
70/// The most remote evidence entries one `rustev.run/2` record holds.
71pub const MAX_REMOTE_EVIDENCE: usize = 256;
72/// The most canonical bytes of one remote evidence entry, so the list never
73/// exceeds 2 MiB.
74pub const MAX_REMOTE_EVIDENCE_ENTRY_BYTES: usize = 8 * 1024;
75/// The most instance ids an entry names.
76pub const MAX_REMOTE_EVIDENCE_INSTANCE: usize = 8;
77
78/// Why a record has no version 1 projection, or does not fit its version.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum RunVersionError {
81    /// Only a `rustev.run/2` record projects to version 1.
82    NotVersion2,
83    /// A version member is present where the schema has none, absent where
84    /// it is required, or beyond its bounds.
85    Members(&'static str),
86}
87
88impl std::fmt::Display for RunVersionError {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            RunVersionError::NotVersion2 => f.write_str("only rustev.run/2 projects to version 1"),
92            RunVersionError::Members(why) => write!(f, "run record members: {why}"),
93        }
94    }
95}
96
97impl std::error::Error for RunVersionError {}
98
99impl RunRecord {
100    /// Whether the version-specific members fit the schema: the remote
101    /// evidence list exactly in `rustev.run/2`, within its bounds.
102    pub fn check_version_members(&self) -> Result<(), RunVersionError> {
103        if self.schema != schema::RUN_V3
104            && self
105                .requests
106                .iter()
107                .any(|r| r.screen.is_some() || r.result == RequestResult::Withdrawn)
108        {
109            return Err(RunVersionError::Members("screening requires rustev.run/3"));
110        }
111        if let Some(pages) = &self.acquisitions {
112            if self.schema != schema::RUN_V3 {
113                return Err(RunVersionError::Members(
114                    "acquisitions require rustev.run/3",
115                ));
116            }
117            if pages.iter().map(|p| p.attempts.len()).sum::<usize>() > 4096 {
118                return Err(RunVersionError::Members("too many acquisition attempts"));
119            }
120        }
121        if let Some(d) = &self.decision_cache {
122            if self.schema != schema::RUN_V3 {
123                return Err(RunVersionError::Members(
124                    "decision_cache requires rustev.run/3",
125                ));
126            }
127            if d.diagnostics.len() > crate::optimization::MAX_OPTIMIZATION_DIAGNOSTICS {
128                return Err(RunVersionError::Members(
129                    "too many decision cache diagnostics",
130                ));
131            }
132            if let Some(a) = &d.abandoned {
133                if a.requests.is_empty() && a.acquisitions.is_empty() {
134                    return Err(RunVersionError::Members(
135                        "an abandoned run is omitted when empty",
136                    ));
137                }
138                if a.acquisitions
139                    .iter()
140                    .map(|p| p.attempts.len())
141                    .sum::<usize>()
142                    > 4096
143                {
144                    return Err(RunVersionError::Members(
145                        "too many abandoned acquisition attempts",
146                    ));
147                }
148            }
149        }
150        let v2 = self.schema == schema::RUN_V2;
151        match &self.remote_evidence {
152            None if v2 => Err(RunVersionError::Members(
153                "rustev.run/2 needs remote_evidence",
154            )),
155            Some(_) if !v2 => Err(RunVersionError::Members(
156                "remote_evidence is a rustev.run/2 member",
157            )),
158            _ if self.remote_evidence_truncated && !v2 => Err(RunVersionError::Members(
159                "remote_evidence_truncated is a rustev.run/2 member",
160            )),
161            Some(list) if list.len() > MAX_REMOTE_EVIDENCE => {
162                Err(RunVersionError::Members("too many remote evidence entries"))
163            }
164            Some(list) if !list.iter().all(RemoteEvidence::is_bounded) => Err(
165                RunVersionError::Members("a remote evidence entry beyond its bounds"),
166            ),
167            _ => Ok(()),
168        }
169    }
170
171    /// The parser's version rule: a record carries the remote evidence
172    /// members exactly in `rustev.run/2`, within their bounds, so a version
173    /// 1 or 3 record with them, or a version 2 record without the list, is
174    /// refused when read, not only when projected.
175    fn parsed_members_fit(&self) -> Result<(), String> {
176        if let CoreEvidence::Recorded(e) = &self.core {
177            e.screening_version()?;
178            if self.schema != schema::RUN_V3
179                && (e.extraction.is_some()
180                    || e.acceptance.is_some()
181                    || e.inputs.iter().any(|i| i.producer.is_some())
182                    || e.judgment.lineage.producers.is_some())
183            {
184                return Err("extraction evidence requires rustev.run/3".into());
185            }
186        }
187        if let CoreEvidence::Recorded(e) = &self.core
188            && e.generation.is_some()
189            && (self.schema != schema::RUN_V3 || e.schema != schema::EVIDENCE_V2)
190        {
191            return Err("generation evidence requires rustev.run/3 and rustev.evidence/2".into());
192        }
193        if let CoreEvidence::Recorded(e) = &self.core
194            && (self.schema != schema::RUN_V3 || e.schema != schema::EVIDENCE_V2)
195            && (e.acquisitions.is_some() || e.steps.iter().any(|s| s.constructed))
196        {
197            return Err("acquisition evidence requires rustev.evidence/2".into());
198        }
199
200        self.check_version_members().map_err(|e| e.to_string())
201    }
202
203    /// The deterministic version 1 projection of a `rustev.run/2` record
204    /// (spec 020, 3.2): the remote evidence removed, the schema tag
205    /// changed, every other member exactly as it is, so the canonical bytes
206    /// are the version 1 representation. Refused, never partial, for any
207    /// other record.
208    pub fn project_v1(&self) -> Result<RunRecord, RunVersionError> {
209        if self.schema != schema::RUN_V2 {
210            return Err(RunVersionError::NotVersion2);
211        }
212        self.check_version_members()?;
213        Ok(RunRecord {
214            schema: schema::RUN.into(),
215            remote_evidence: None,
216            remote_evidence_truncated: false,
217            ..self.clone()
218        })
219    }
220}
221
222/// One remote attempt's evidence reference in `rustev.run/2` (spec 020,
223/// 3.2, I-4). It names the exchange record, never embeds it: no request or
224/// response body, credential, secret address, unredacted principal
225/// reference, provider reasoning or header.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
227#[serde(deny_unknown_fields)]
228pub struct RemoteEvidence {
229    pub attempt_id: String,
230    /// The decision step and logical request (instance) the attempt served.
231    pub step: String,
232    pub instance: Vec<String>,
233    pub exchange: ExchangeReference,
234    /// What the adapter asked for; `None` when it names no model.
235    pub requested_identity: Option<String>,
236    pub served: ServedIdentity,
237    /// The adapter binding (its artifact), the protocol or foreign mapping
238    /// version, and the endpoint class (never a secret URL).
239    pub binding: ArtifactId,
240    pub protocol: String,
241    pub endpoint_class: String,
242    /// The remote error code of a failed attempt.
243    pub code: Option<RemoteCode>,
244    /// The attempt's final remote state, as the run record has it.
245    pub remote_state: RemoteState,
246    pub charge_provenance: ChargeProvenance,
247    /// Present when the attempt was a member of a batched exchange.
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub batch: Option<BatchMembership>,
250    /// The principal reference's evidence digest, only when one was sent.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub principal_digest: Option<String>,
253}
254
255impl RemoteEvidence {
256    /// Every string within [`MAX_TEXT_BYTES`], the instance within
257    /// [`MAX_REMOTE_EVIDENCE_INSTANCE`] ids, and the canonical bytes within
258    /// [`MAX_REMOTE_EVIDENCE_ENTRY_BYTES`].
259    pub fn is_bounded(&self) -> bool {
260        self.strings_within(MAX_TEXT_BYTES)
261            && crate::canonical::canonical_bytes(self)
262                .is_ok_and(|b| b.len() <= MAX_REMOTE_EVIDENCE_ENTRY_BYTES)
263    }
264
265    fn strings_within(&self, max: usize) -> bool {
266        let t = |s: &str| s.len() <= max;
267        let o = |s: &Option<String>| s.as_deref().is_none_or(t);
268        t(&self.attempt_id)
269            && t(&self.step)
270            && self.instance.len() <= MAX_REMOTE_EVIDENCE_INSTANCE
271            && self.instance.iter().all(|i| t(i))
272            && self.exchange.is_bounded(max)
273            && o(&self.requested_identity)
274            && o(&self.served.identity)
275            && t(&self.protocol)
276            && t(&self.endpoint_class)
277            && self.batch.as_ref().is_none_or(|b| t(&b.batch_id))
278            && o(&self.principal_digest)
279    }
280
281    /// The entry cut to its bounds: strings to [`MAX_TEXT_BYTES`], and when
282    /// the canonical bytes are still beyond the entry bound (strings that
283    /// escape to several bytes each), to 32 bytes. Deterministic.
284    pub fn bounded(self) -> Self {
285        let e = self.cut(MAX_TEXT_BYTES);
286        if e.is_bounded() { e } else { e.cut(32) }
287    }
288
289    fn cut(mut self, max: usize) -> Self {
290        let c = |s: &mut String| {
291            if s.len() > max {
292                *s = crate::remote::cut_to(s, max);
293            }
294        };
295        c(&mut self.attempt_id);
296        c(&mut self.step);
297        self.instance.truncate(MAX_REMOTE_EVIDENCE_INSTANCE);
298        self.instance.iter_mut().for_each(c);
299        if let ExchangeReference::Retained {
300            digest,
301            schema,
302            locator,
303            ..
304        } = &mut self.exchange
305        {
306            c(digest);
307            c(schema);
308            c(locator);
309        }
310        if let Some(r) = &mut self.requested_identity {
311            c(r);
312        }
313        if let Some(s) = &mut self.served.identity {
314            c(s);
315        }
316        c(&mut self.protocol);
317        c(&mut self.endpoint_class);
318        if let Some(b) = &mut self.batch {
319            c(&mut b.batch_id);
320        }
321        if let Some(p) = &mut self.principal_digest {
322            c(p);
323        }
324        self
325    }
326}
327
328/// Where the exchange record of an attempt is, or why it cannot be named.
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
330#[serde(rename_all = "snake_case", deny_unknown_fields)]
331pub enum ExchangeReference {
332    Retained {
333        /// The exchange record's record digest.
334        digest: String,
335        /// The exchange record's schema.
336        schema: String,
337        retention: RetentionClass,
338        /// An opaque host reference, never a URL with credentials.
339        locator: String,
340    },
341    Unavailable {
342        reason: EvidenceUnavailable,
343    },
344}
345
346impl ExchangeReference {
347    fn is_bounded(&self, max: usize) -> bool {
348        match self {
349            ExchangeReference::Retained {
350                digest,
351                schema,
352                locator,
353                ..
354            } => [digest, schema, locator].iter().all(|s| s.len() <= max),
355            ExchangeReference::Unavailable { .. } => true,
356        }
357    }
358}
359
360/// What the host retained of an exchange.
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
362#[serde(rename_all = "snake_case")]
363pub enum RetentionClass {
364    /// The record with digests only (spec 009, 3.9.3).
365    DigestOnly,
366    /// The record and the request and response bytes.
367    BytesRetained,
368}
369
370/// Why an attempt's exchange record cannot be named (spec 020, 3.2): stated,
371/// never reconstructed.
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
373#[serde(rename_all = "snake_case")]
374pub enum EvidenceUnavailable {
375    /// Nothing was exchanged: the attempt was refused before any request.
376    NoExchange,
377    /// The exchange had not ended when the record was built.
378    Pending,
379    /// The exchange sink refused or failed the record.
380    NotDelivered,
381    /// The exchange sink accepted the record but named no locator.
382    NoLocator,
383}
384
385/// How the attempt's charge was established.
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
387#[serde(rename_all = "snake_case")]
388pub enum ChargeProvenance {
389    /// Reported by the remote side for this attempt's own exchange.
390    Reported,
391    /// A share of a batched exchange's reported charge.
392    Attributed,
393    /// No request byte left the process: nothing was charged.
394    NothingSent,
395    /// Not known; the reservation stays liability.
396    Unknown,
397}
398
399/// An attempt's place in a batched exchange.
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
401#[serde(deny_unknown_fields)]
402pub struct BatchMembership {
403    pub batch_id: String,
404    pub index: u32,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
408#[serde(rename_all = "snake_case", deny_unknown_fields)]
409pub enum RecordedExecution {
410    None,
411    Declared(ExecutionPolicyId),
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
415#[serde(rename_all = "snake_case")]
416pub enum Termination {
417    /// Every request resolved and the core finished a judgment.
418    Judged,
419    /// The caller cancelled; no judgment was finished.
420    Cancelled,
421}
422
423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
424#[serde(rename_all = "snake_case", deny_unknown_fields)]
425pub enum CoreEvidence {
426    /// Exactly what the core's `finish` returned.
427    Recorded(Box<EvidenceRecord>),
428    NotProduced,
429}
430
431/// Monotonic milliseconds from submission; never wall-clock time.
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
433#[serde(deny_unknown_fields)]
434pub struct Timing {
435    pub queued_ms: u64,
436    pub elapsed_ms: u64,
437    pub deadline_ms: u64,
438    pub deadline_expired: bool,
439}
440
441#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
442#[serde(deny_unknown_fields)]
443pub struct RequestRecord {
444    pub step: String,
445    pub instance: Vec<String>,
446    pub result: RequestResult,
447    pub attempts: Vec<AttemptRecord>,
448    pub transitions: Vec<Transition>,
449    /// Absent for unoptimized execution, preserving existing record bytes.
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub optimization: Option<OptimizationRecord>,
452    /// Screening cache and text-unit observations (spec 035, 3.13.2).
453    #[serde(
454        default,
455        skip_serializing_if = "Option::is_none",
456        deserialize_with = "crate::screening::present"
457    )]
458    #[schemars(with = "crate::screening::ScreenRequest")]
459    pub screen: Option<crate::screening::ScreenRequest>,
460}
461
462/// Where a supplied value came from (spec 004, 3.2).
463#[derive(Debug, Clone, Copy, PartialEq)]
464pub struct SuppliedFrom<'r> {
465    /// The producing target of an output; for a failure, the `to` of the
466    /// last `fallback` transition, else 0.
467    pub target: u32,
468    /// The producing attempt: an output's last attempt; a failure's last
469    /// attempt only when no `retry` or `fallback` transition has an `after`
470    /// equal to its ordinal.
471    pub attempt: Option<&'r AttemptRecord>,
472    /// For an output reused from a cache entry (spec 017, 3.8): the attempt
473    /// of an earlier decision that produced it. Set only when the record has
474    /// no attempt of its own and its optimization record names a hit.
475    pub reused_from: Option<&'r str>,
476}
477
478impl RequestRecord {
479    /// Where this request's supplied value came from; `None` when nothing was
480    /// supplied. The one derivation runtime capture and replay both use.
481    pub fn supplied_from(&self) -> Option<SuppliedFrom<'_>> {
482        match &self.result {
483            RequestResult::NotSupplied | RequestResult::Withdrawn => None,
484            RequestResult::Output { target } => Some(SuppliedFrom {
485                target: *target,
486                attempt: self.attempts.last(),
487                reused_from: self
488                    .optimization
489                    .as_ref()
490                    .filter(|o| self.attempts.is_empty() && o.outcome == OptimizationOutcome::Hit)
491                    .and_then(|o| o.source_attempt_id.as_deref()),
492            }),
493            RequestResult::Failed(_) => {
494                let target = self
495                    .transitions
496                    .iter()
497                    .rev()
498                    .find_map(|t| match t {
499                        Transition::Fallback { to, .. } => Some(*to),
500                        _ => None,
501                    })
502                    .unwrap_or(0);
503                let n = self.attempts.len() as u32;
504                let moved_on = self.transitions.iter().any(|t| {
505                    matches!(t, Transition::Retry { after, .. } | Transition::Fallback { after, .. } if *after == n)
506                });
507                Some(SuppliedFrom {
508                    target,
509                    attempt: if moved_on { None } else { self.attempts.last() },
510                    reused_from: None,
511                })
512            }
513        }
514    }
515}
516
517#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
518#[serde(rename_all = "snake_case", deny_unknown_fields)]
519pub enum RequestResult {
520    /// An output was supplied, from this target (0 is the bound backend).
521    Output { target: u32 },
522    /// This runtime reason was supplied.
523    Failed(Unresolved),
524    /// Nothing was supplied: the decision was cancelled first.
525    NotSupplied,
526    /// Not dispatched because a screening refusal decided the outcome.
527    Withdrawn,
528}
529
530#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
531#[serde(deny_unknown_fields)]
532pub struct AttemptRecord {
533    /// `<decision id>/<step>/<instance ids joined by ','>/<n>`, each
534    /// component with `%`, `/` and `,` percent-escaped.
535    pub attempt_id: String,
536    pub target: u32,
537    pub backend_id: String,
538    pub artifact: ArtifactId,
539    pub dispatched_ms: u64,
540    pub ended_ms: u64,
541    pub end: AttemptEnd,
542    pub cancellation: Cancellation,
543    pub remote: RemoteState,
544    pub cost: AttemptCost,
545}
546
547#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
548#[serde(rename_all = "snake_case", deny_unknown_fields)]
549pub enum AttemptEnd {
550    /// A valid output.
551    Output,
552    Failed {
553        class: FailureClass,
554        detail: String,
555    },
556    /// The decision deadline passed while it was in flight.
557    Deadline,
558    /// The caller cancelled while it was in flight.
559    Cancelled,
560}
561
562/// What is known about cancelling an attempt (spec 003, 3.7.3).
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
564#[serde(rename_all = "snake_case", deny_unknown_fields)]
565pub enum Cancellation {
566    NotRequested,
567    Requested { answer: CancelAnswer },
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
571#[serde(rename_all = "snake_case")]
572pub enum CancelAnswer {
573    /// The backend confirmed that its work and charges stopped.
574    Stopped,
575    /// The backend answered without confirming a remote stop.
576    Unconfirmed,
577    /// The runtime stopped waiting before any answer.
578    NotObserved,
579}
580
581#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
582#[serde(rename_all = "snake_case")]
583pub enum RemoteState {
584    /// The backend returned a result of its own accord.
585    Finished,
586    /// The backend acknowledged a cancellation as stopped.
587    Stopped,
588    /// Nothing establishes that remote work or charges stopped.
589    PossiblyContinuing,
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
593#[serde(deny_unknown_fields)]
594pub struct AttemptCost {
595    pub bound: CostBound,
596    pub reserved: u64,
597    pub charge: Charge,
598}
599
600/// A backend's static cost disclosure.
601#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
602#[serde(rename_all = "snake_case")]
603pub enum CostModel {
604    /// Every call has an enforceable upper bound.
605    Bounded,
606    /// Calls have estimates, not bounds.
607    Estimated,
608    Unknown,
609}
610
611/// A backend's per-call cost disclosure, before dispatch.
612#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
613#[serde(rename_all = "snake_case", deny_unknown_fields)]
614pub enum CostBound {
615    /// The call cannot cost more than this.
616    Bounded {
617        max_units: u64,
618    },
619    Estimated {
620        units: u64,
621    },
622    Unknown,
623}
624
625/// What an attempt cost, as far as is known when it ends.
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
627#[serde(rename_all = "snake_case", deny_unknown_fields)]
628pub enum Charge {
629    Observed { units: u64 },
630    Estimated { units: u64 },
631    Unknown,
632}
633
634#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
635#[serde(rename_all = "snake_case", deny_unknown_fields)]
636pub enum Transition {
637    /// Retry the same target after attempt `after`.
638    Retry {
639        after: u32,
640        class: FailureClass,
641        delay_ms: u64,
642    },
643    /// Move to fallback target `to` after attempt `after`.
644    Fallback {
645        after: u32,
646        class: FailureClass,
647        to: u32,
648    },
649    /// Stop after attempt `after` (0 when nothing was dispatched).
650    Stop { after: u32, reason: String },
651}
652
653#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
654#[serde(rename_all = "snake_case")]
655pub enum CostMode {
656    Unlimited,
657    Hard,
658    Estimated,
659}
660
661/// A decision's cost at the moment its record was built (spec 003, 3.5.6).
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
663#[serde(deny_unknown_fields)]
664pub struct CostSummary {
665    pub mode: CostMode,
666    /// 0 under `unlimited`.
667    pub limit: u64,
668    pub observed: u64,
669    pub estimated: u64,
670    /// Reservations kept because the final cost is unknown.
671    pub liability: u64,
672    /// Observed charges above a `bounded` reservation.
673    pub bound_violations: u64,
674    pub final_cost: FinalCost,
675    /// True only under `hard` with no liability and no bound violation.
676    pub within_guaranteed_cap: bool,
677}
678
679#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
680#[serde(rename_all = "snake_case")]
681pub enum FinalCost {
682    Known,
683    IncludesEstimates,
684    Unknown,
685}