Skip to main content

rustev_contract/
execution.rs

1//! Execution policies, `rustev.execution/1` (spec 003, 3.2.2): how the
2//! runtime may retry, time out and fall back per semantic step, and the cost
3//! budget one decision may draw on. A policy is declared, validated by the
4//! compiler and embedded in the plan, so it is part of the `PlanId`; it is
5//! never inferred from whichever backends are installed (R-10).
6
7use serde::{Deserialize, Serialize};
8
9use crate::ids::ExecutionPolicyId;
10use crate::limits::DESCRIPTOR_V1;
11use crate::optimization::OptimizationPolicy;
12use crate::schema;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
15#[serde(deny_unknown_fields)]
16pub struct ExecutionPolicy {
17    pub schema: String,
18    /// Worst-case backend attempts of one decision, over every request,
19    /// retry and fallback.
20    pub max_attempts_per_decision: u64,
21    pub cost: CostPolicy,
22    /// Optional reusable-work policy (spec 017). Its absence preserves the
23    /// version 1 policy bytes and unoptimized behavior.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub optimization: Option<OptimizationPolicy>,
26    /// At most one entry per semantic step; an unlisted step gets one
27    /// attempt, no timeout and no fallback.
28    pub steps: Vec<StepExecution>,
29}
30
31crate::document::document!(
32    ExecutionPolicy,
33    schema::EXECUTION,
34    DESCRIPTOR_V1,
35    ExecutionPolicyId
36);
37
38/// The cost budget of one decision, in integer units the deployment chooses
39/// (spec 003, 3.5).
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
41#[serde(rename_all = "snake_case", deny_unknown_fields)]
42pub enum CostPolicy {
43    /// Nothing is refused; charges are still recorded.
44    Unlimited,
45    /// A guaranteed cap: every reachable backend must disclose an enforceable
46    /// per-call upper bound, which is reserved before dispatch.
47    Hard { max_units: u64 },
48    /// The weaker policy: estimates are reserved, and the result is never
49    /// labeled a cap.
50    Estimated { max_units: u64 },
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
54#[serde(deny_unknown_fields)]
55pub struct StepExecution {
56    pub step: String,
57    pub retry: RetryPolicy,
58    pub attempt_timeout: AttemptTimeout,
59    pub fallback: FallbackPolicy,
60}
61
62/// Retries on the same target (spec 003, 3.6.1 and 3.6.3).
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
64#[serde(deny_unknown_fields)]
65pub struct RetryPolicy {
66    /// Attempts per target, including the first; `1..=8`.
67    pub max_attempts: u32,
68    /// Retryable classes that are retried; empty exactly when
69    /// `max_attempts` is 1.
70    pub on: Vec<FailureClass>,
71    pub delay: Delay,
72}
73
74impl RetryPolicy {
75    /// One attempt, nothing retried.
76    pub fn none() -> Self {
77        RetryPolicy {
78            max_attempts: 1,
79            on: vec![],
80            delay: Delay::None,
81        }
82    }
83}
84
85/// The delay before retry `n` (1-based). No jitter.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
87#[serde(rename_all = "snake_case", deny_unknown_fields)]
88pub enum Delay {
89    None,
90    Fixed {
91        ms: u64,
92    },
93    /// `initial_ms`, doubling per retry, capped at `max_ms`.
94    Exponential {
95        initial_ms: u64,
96        max_ms: u64,
97    },
98}
99
100impl Delay {
101    /// The delay before retry number `n` (1 for the first retry).
102    pub fn before_retry(self, n: u32) -> u64 {
103        match self {
104            Delay::None => 0,
105            Delay::Fixed { ms } => ms,
106            Delay::Exponential { initial_ms, max_ms } => {
107                let shift = n.saturating_sub(1).min(63);
108                initial_ms.saturating_mul(1u64 << shift).min(max_ms)
109            }
110        }
111    }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
115#[serde(rename_all = "snake_case", deny_unknown_fields)]
116pub enum AttemptTimeout {
117    None,
118    Ms(u64),
119}
120
121/// Runtime failure fallback (spec 003, 3.6.2), distinct from the
122/// definition's compile-time capability fallback.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
124#[serde(deny_unknown_fields)]
125pub struct FallbackPolicy {
126    /// Backend ids tried in order after the bound backend; at most 4, none
127    /// repeated, never the bound backend.
128    pub backends: Vec<String>,
129    /// Classes that move the request to the next backend.
130    pub on: Vec<FailureClass>,
131}
132
133impl FallbackPolicy {
134    pub fn none() -> Self {
135        FallbackPolicy {
136            backends: vec![],
137            on: vec![],
138        }
139    }
140}
141
142/// How a dispatched attempt failed (spec 003, 3.6.1). Deadline and
143/// cancellation are not classes: they end a request, and are never retried
144/// or a fallback trigger.
145#[derive(
146    Debug,
147    Clone,
148    Copy,
149    PartialEq,
150    Eq,
151    PartialOrd,
152    Ord,
153    Hash,
154    Serialize,
155    Deserialize,
156    schemars::JsonSchema,
157)]
158#[serde(rename_all = "snake_case")]
159pub enum FailureClass {
160    /// The output failed the core's validation.
161    InvalidOutput,
162    /// The adapter says the failure may not recur.
163    Transient,
164    /// The adapter says the backend refused for load.
165    Overloaded,
166    /// The adapter says the failure will recur for this input.
167    Permanent,
168    /// The attempt timeout passed.
169    TimedOut,
170    /// The adapter panicked.
171    AdapterFault,
172}
173
174impl FailureClass {
175    /// Classes a retry policy may name.
176    pub fn retryable(self) -> bool {
177        matches!(
178            self,
179            FailureClass::Transient | FailureClass::Overloaded | FailureClass::TimedOut
180        )
181    }
182
183    pub fn name(self) -> &'static str {
184        match self {
185            FailureClass::InvalidOutput => "invalid_output",
186            FailureClass::Transient => "transient",
187            FailureClass::Overloaded => "overloaded",
188            FailureClass::Permanent => "permanent",
189            FailureClass::TimedOut => "timed_out",
190            FailureClass::AdapterFault => "adapter_fault",
191        }
192    }
193}
194
195/// Upper bounds of an execution policy (spec 003, 3.2.3).
196pub const MAX_ATTEMPTS_PER_TARGET: u32 = 8;
197pub const MAX_FALLBACKS: usize = 4;
198pub const MAX_DELAY_MS: u64 = 60_000;