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}