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