Skip to main content

agent_sdk_core/application/
recovery.rs

1//! Application-layer coordination over core primitives. Use these services to lower
2//! helpers, drive runs, validate output, coordinate tools, approvals, delivery,
3//! isolation, telemetry, and feature layers. Methods in this layer may call
4//! configured ports, mutate in-memory stores, append journals, or publish events as
5//! documented. This file contains the recovery portion of that contract.
6//!
7use serde::{Deserialize, Serialize};
8
9use crate::error::RetryClassification;
10
11#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
12#[serde(rename_all = "snake_case")]
13/// Enumerates the finite recovery failure kind cases.
14/// Serialized names are part of the SDK contract; update fixtures when variants change.
15pub enum RecoveryFailureKind {
16    /// Use this variant when the contract needs to represent provider failure; selecting it has no side effect by itself.
17    ProviderFailure,
18    /// Use this variant when the contract needs to represent tool interrupted; selecting it has no side effect by itself.
19    ToolInterrupted,
20    /// Use this variant when the contract needs to represent tool failure; selecting it has no side effect by itself.
21    ToolFailure,
22    /// Use this variant when the contract needs to represent approval transport unknown; selecting it has no side effect by itself.
23    ApprovalTransportUnknown,
24    /// Use this variant when the contract needs to represent journal append after effect; selecting it has no side effect by itself.
25    JournalAppendAfterEffect,
26    /// Use this variant when the contract needs to represent missing content ref; selecting it has no side effect by itself.
27    MissingContentRef,
28    /// Use this variant when the contract needs to represent package fingerprint mismatch; selecting it has no side effect by itself.
29    PackageFingerprintMismatch,
30    /// Use this variant when the contract needs to represent invariant failed; selecting it has no side effect by itself.
31    InvariantFailed,
32    /// Use this variant when the contract needs to represent unsafe side effect; selecting it has no side effect by itself.
33    UnsafeSideEffect,
34    /// Use this variant when the contract needs to represent host policy required; selecting it has no side effect by itself.
35    HostPolicyRequired,
36}
37
38#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
39#[serde(rename_all = "snake_case")]
40/// Enumerates the finite recovery classification cases.
41/// Serialized names are part of the SDK contract; update fixtures when variants change.
42pub enum RecoveryClassification {
43    /// Use this variant when the contract needs to represent retryable safe step; selecting it has no side effect by itself.
44    RetryableSafeStep,
45    /// Use this variant when the contract needs to represent reconcile required; selecting it has no side effect by itself.
46    ReconcileRequired,
47    /// Use this variant when the contract needs to represent repair required; selecting it has no side effect by itself.
48    RepairRequired,
49    /// Use this variant when the contract needs to represent user action required; selecting it has no side effect by itself.
50    UserActionRequired,
51    /// Use this variant when the contract needs to represent host configuration required; selecting it has no side effect by itself.
52    HostConfigurationRequired,
53    /// Use this variant when the contract needs to represent irrecoverable; selecting it has no side effect by itself.
54    Irrecoverable,
55}
56
57impl RecoveryClassification {
58    /// Computes or returns retry classification for the application::recovery
59    /// contract without external I/O or side effects.
60    pub fn retry_classification(self) -> RetryClassification {
61        match self {
62            Self::RetryableSafeStep => RetryClassification::Retryable,
63            Self::ReconcileRequired | Self::RepairRequired => RetryClassification::RepairNeeded,
64            Self::UserActionRequired => RetryClassification::UserActionNeeded,
65            Self::HostConfigurationRequired => RetryClassification::HostConfigurationNeeded,
66            Self::Irrecoverable => RetryClassification::NotRetryable,
67        }
68    }
69
70    /// Returns whether requires repair plan applies for this contract.
71    /// This derives recovery or repair data from the supplied failure state and does not
72    /// perform the repair by itself.
73    pub fn requires_repair_plan(self) -> bool {
74        matches!(
75            self,
76            Self::ReconcileRequired | Self::RepairRequired | Self::HostConfigurationRequired
77        )
78    }
79}
80
81#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
82#[serde(rename_all = "snake_case")]
83/// Enumerates the finite recovery action cases.
84/// Serialized names are part of the SDK contract; update fixtures when variants change.
85pub enum RecoveryAction {
86    /// Use this variant when the contract needs to represent retry safe step; selecting it has no side effect by itself.
87    RetrySafeStep,
88    /// Use this variant when the contract needs to represent reconcile pending side effect; selecting it has no side effect by itself.
89    ReconcilePendingSideEffect,
90    /// Use this variant when the contract needs to represent restore from journal; selecting it has no side effect by itself.
91    RestoreFromJournal,
92    /// Use this variant when the contract needs to represent request host repair; selecting it has no side effect by itself.
93    RequestHostRepair,
94    /// Use this variant when the contract needs to represent surface repair needed; selecting it has no side effect by itself.
95    SurfaceRepairNeeded,
96    /// Use this variant when the contract needs to represent fail closed; selecting it has no side effect by itself.
97    FailClosed,
98}
99
100#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
101/// Holds recovery decision application-layer state or configuration.
102/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
103pub struct RecoveryDecision {
104    /// Kind discriminator for failure kind.
105    /// Use it to route finite match arms without parsing display text.
106    pub failure_kind: RecoveryFailureKind,
107    /// Classification used by this record or request.
108    pub classification: RecoveryClassification,
109    /// Action used by this record or request.
110    pub action: RecoveryAction,
111    /// Retry used by this record or request.
112    pub retry: RetryClassification,
113    /// Whether the recovery path requires an explicit repair plan before mutation.
114    /// Use it to fail closed when retrying would be unsafe or insufficiently audited.
115    pub repair_plan_required: bool,
116    /// Allowlist for this policy or contract.
117    /// Validation uses it to reject undeclared or policy-denied values.
118    pub idempotent_retry_allowed: bool,
119}
120
121impl RecoveryDecision {
122    /// Builds the classify value.
123    /// This is data construction and performs no I/O, journal append, event publication, or
124    /// process work.
125    pub fn classify(failure_kind: RecoveryFailureKind) -> Self {
126        classify_recovery(failure_kind)
127    }
128}
129
130/// Classify recovery.
131/// This derives recovery or repair data from the supplied failure state and does not perform
132/// the repair by itself.
133pub fn classify_recovery(failure_kind: RecoveryFailureKind) -> RecoveryDecision {
134    let (classification, action, idempotent_retry_allowed) = match failure_kind {
135        RecoveryFailureKind::ProviderFailure => (
136            RecoveryClassification::RetryableSafeStep,
137            RecoveryAction::RetrySafeStep,
138            true,
139        ),
140        RecoveryFailureKind::ToolInterrupted => (
141            RecoveryClassification::RetryableSafeStep,
142            RecoveryAction::RestoreFromJournal,
143            true,
144        ),
145        RecoveryFailureKind::ToolFailure | RecoveryFailureKind::InvariantFailed => (
146            RecoveryClassification::RepairRequired,
147            RecoveryAction::SurfaceRepairNeeded,
148            false,
149        ),
150        RecoveryFailureKind::ApprovalTransportUnknown
151        | RecoveryFailureKind::JournalAppendAfterEffect
152        | RecoveryFailureKind::UnsafeSideEffect => (
153            RecoveryClassification::ReconcileRequired,
154            RecoveryAction::ReconcilePendingSideEffect,
155            false,
156        ),
157        RecoveryFailureKind::MissingContentRef => (
158            RecoveryClassification::UserActionRequired,
159            RecoveryAction::SurfaceRepairNeeded,
160            false,
161        ),
162        RecoveryFailureKind::PackageFingerprintMismatch
163        | RecoveryFailureKind::HostPolicyRequired => (
164            RecoveryClassification::HostConfigurationRequired,
165            RecoveryAction::RequestHostRepair,
166            false,
167        ),
168    };
169
170    RecoveryDecision {
171        failure_kind,
172        classification,
173        action,
174        retry: classification.retry_classification(),
175        repair_plan_required: classification.requires_repair_plan(),
176        idempotent_retry_allowed,
177    }
178}