1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use thiserror::Error;
6
7#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
9#[non_exhaustive]
10pub enum RetrySafety {
11 Safe,
13 RequiresIdempotency,
15 UnsafeAfterVisibleOutput,
17 UnsafeAfterSideEffect,
19 Unknown,
21}
22
23#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[non_exhaustive]
26pub enum RunErrorKind {
27 InvalidInput,
29 CapabilityDenied,
31 BudgetExceeded,
33 DeadlineExceeded,
35 Cancelled,
37 Transport,
39 Protocol,
41 Invocation,
43 Extension(String),
45}
46
47impl RunErrorKind {
48 pub fn code(&self) -> &str {
50 match self {
51 Self::InvalidInput => "runifold.invalid_input",
52 Self::CapabilityDenied => "runifold.capability_denied",
53 Self::BudgetExceeded => "runifold.budget_exceeded",
54 Self::DeadlineExceeded => "runifold.deadline_exceeded",
55 Self::Cancelled => "runifold.cancelled",
56 Self::Transport => "runifold.transport",
57 Self::Protocol => "runifold.protocol",
58 Self::Invocation => "runifold.invocation",
59 Self::Extension(namespace) => namespace,
60 }
61 }
62
63 pub fn recommendation(&self) -> &'static str {
65 match self {
66 Self::InvalidInput => "Validate configuration and request data before retrying.",
67 Self::CapabilityDenied => {
68 "Grant only the required capability or remove the unauthorized operation."
69 }
70 Self::BudgetExceeded => "Increase the explicit budget or reduce bounded work.",
71 Self::DeadlineExceeded => {
72 "Review the deadline and upstream latency before deciding whether to retry."
73 }
74 Self::Cancelled => "Do not retry unless the caller starts a new operation.",
75 Self::Transport => "Inspect retry safety, endpoint health, and network diagnostics.",
76 Self::Protocol => {
77 "Inspect the Provider response and adapter compatibility before retrying."
78 }
79 Self::Invocation => "Inspect the invoked component's typed cause and metadata.",
80 Self::Extension(_) => "Inspect the namespaced extension metadata and documentation.",
81 }
82 }
83}
84
85#[derive(Clone, Debug, Deserialize, Error, PartialEq, Serialize)]
87#[error("{kind:?}: {message}")]
88pub struct RunError {
89 pub kind: RunErrorKind,
91 pub message: String,
93 pub retry_safety: RetrySafety,
95 pub metadata: BTreeMap<String, Value>,
97}
98
99impl RunError {
100 pub fn code(&self) -> &str {
102 self.kind.code()
103 }
104
105 pub fn recommendation(&self) -> &'static str {
107 self.kind.recommendation()
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::RunErrorKind;
114
115 #[test]
116 fn diagnostic_codes_are_stable_and_extension_aware() {
117 assert_eq!(RunErrorKind::Protocol.code(), "runifold.protocol");
118 assert_eq!(
119 RunErrorKind::Extension("acme.custom".into()).code(),
120 "acme.custom"
121 );
122 assert!(!RunErrorKind::Transport.recommendation().is_empty());
123 }
124}