1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub enum DurableStoreFacet {
8 AttachmentStore,
9 ArtifactStore,
10 SessionStore,
11 ProcessRegistry,
12}
13
14impl DurableStoreFacet {
15 fn as_code(self) -> &'static str {
18 match self {
19 Self::AttachmentStore => "durable_store_required:attachment_store",
20 Self::ArtifactStore => "durable_store_required:artifact_store",
21 Self::SessionStore => "durable_store_required:session_store",
22 Self::ProcessRegistry => "durable_store_required:process_registry",
23 }
24 }
25}
26
27#[derive(Clone, Debug, PartialEq, Eq, Hash)]
32pub enum RuntimeErrorCode {
33 MissingEffectScopeId,
34 EffectScopeTurnIdMismatch,
35 MissingProcessExecutionId,
39 DurableStoreRequired {
42 facet: DurableStoreFacet,
43 },
44 StoreCommitFailed,
45 PluginSessionManager,
46 PluginFinalizeTurn,
47 PluginCheckpoint,
48 PluginPrepareTurn,
49 ContextPrepareTurn,
50 ProtocolTurnExtension,
51 ProtocolBeforeLlmCall,
52 TurnStreamJoin,
53 EmptyAgentFrameRun,
54 DurableEffectLiveProtocolExtension,
55 DurableEffectLivePluginInput,
56 Other(String),
57}
58
59impl RuntimeErrorCode {
60 pub fn as_str(&self) -> &str {
61 match self {
62 Self::MissingEffectScopeId => "missing_effect_scope_id",
63 Self::EffectScopeTurnIdMismatch => "effect_scope_turn_id_mismatch",
64 Self::MissingProcessExecutionId => "missing_process_execution_id",
65 Self::DurableStoreRequired { facet } => facet.as_code(),
66 Self::StoreCommitFailed => "store_commit_failed",
67 Self::PluginSessionManager => "plugin_session_manager",
68 Self::PluginFinalizeTurn => "plugin_finalize_turn",
69 Self::PluginCheckpoint => "plugin_checkpoint",
70 Self::PluginPrepareTurn => "plugin_prepare_turn",
71 Self::ContextPrepareTurn => "context_prepare_turn",
72 Self::ProtocolTurnExtension => "protocol_turn_extension",
73 Self::ProtocolBeforeLlmCall => "protocol_before_llm_call",
74 Self::TurnStreamJoin => "turn_stream_join",
75 Self::EmptyAgentFrameRun => "empty_agent_frame_run",
76 Self::DurableEffectLiveProtocolExtension => "durable_effect_live_protocol_extension",
77 Self::DurableEffectLivePluginInput => "durable_effect_live_plugin_input",
78 Self::Other(code) => code.as_str(),
79 }
80 }
81}
82
83impl std::fmt::Display for RuntimeErrorCode {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.write_str(self.as_str())
86 }
87}
88
89impl From<&str> for RuntimeErrorCode {
90 fn from(code: &str) -> Self {
91 match code {
92 "missing_effect_scope_id" => Self::MissingEffectScopeId,
93 "effect_scope_turn_id_mismatch" => Self::EffectScopeTurnIdMismatch,
94 "missing_process_execution_id" => Self::MissingProcessExecutionId,
95 "durable_store_required:attachment_store" => Self::DurableStoreRequired {
96 facet: DurableStoreFacet::AttachmentStore,
97 },
98 "durable_store_required:artifact_store" => Self::DurableStoreRequired {
99 facet: DurableStoreFacet::ArtifactStore,
100 },
101 "durable_store_required:session_store" => Self::DurableStoreRequired {
102 facet: DurableStoreFacet::SessionStore,
103 },
104 "durable_store_required:process_registry" => Self::DurableStoreRequired {
105 facet: DurableStoreFacet::ProcessRegistry,
106 },
107 "store_commit_failed" => Self::StoreCommitFailed,
108 "plugin_session_manager" => Self::PluginSessionManager,
109 "plugin_finalize_turn" => Self::PluginFinalizeTurn,
110 "plugin_checkpoint" => Self::PluginCheckpoint,
111 "plugin_prepare_turn" => Self::PluginPrepareTurn,
112 "context_prepare_turn" => Self::ContextPrepareTurn,
113 "protocol_turn_extension" => Self::ProtocolTurnExtension,
114 "protocol_before_llm_call" => Self::ProtocolBeforeLlmCall,
115 "turn_stream_join" => Self::TurnStreamJoin,
116 "empty_agent_frame_run" => Self::EmptyAgentFrameRun,
117 "durable_effect_live_protocol_extension" => Self::DurableEffectLiveProtocolExtension,
118 "durable_effect_live_plugin_input" => Self::DurableEffectLivePluginInput,
119 other => Self::Other(other.to_string()),
120 }
121 }
122}
123
124impl From<String> for RuntimeErrorCode {
125 fn from(code: String) -> Self {
126 Self::from(code.as_str())
127 }
128}
129
130impl serde::Serialize for RuntimeErrorCode {
131 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
132 where
133 S: serde::Serializer,
134 {
135 serializer.serialize_str(self.as_str())
136 }
137}
138
139impl<'de> serde::Deserialize<'de> for RuntimeErrorCode {
140 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
141 where
142 D: serde::Deserializer<'de>,
143 {
144 let code = <String as serde::Deserialize>::deserialize(deserializer)?;
145 Ok(Self::from(code))
146 }
147}
148
149#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
151pub struct RuntimeError {
152 pub code: RuntimeErrorCode,
153 pub message: String,
154}
155
156impl RuntimeError {
157 pub fn new(code: impl Into<RuntimeErrorCode>, message: impl Into<String>) -> Self {
158 Self {
159 code: code.into(),
160 message: message.into(),
161 }
162 }
163
164 pub fn is_code(&self, code: RuntimeErrorCode) -> bool {
165 self.code == code
166 }
167
168 pub fn durable_store_required(facet: DurableStoreFacet) -> Self {
171 let facet_label = match facet {
172 DurableStoreFacet::AttachmentStore => "attachment store",
173 DurableStoreFacet::ArtifactStore => "lashlang artifact store",
174 DurableStoreFacet::SessionStore => "session store",
175 DurableStoreFacet::ProcessRegistry => "process registry",
176 };
177 Self::new(
178 RuntimeErrorCode::DurableStoreRequired { facet },
179 format!("durable effect hosts require a durable {facet_label}"),
180 )
181 }
182
183 pub fn missing_process_execution_id() -> Self {
190 Self::new(
191 RuntimeErrorCode::MissingProcessExecutionId,
192 "process execution requires a non-empty persisted process id",
193 )
194 }
195}
196
197impl std::fmt::Display for RuntimeError {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 write!(f, "{}: {}", self.code, self.message)
200 }
201}
202
203impl std::error::Error for RuntimeError {}
204
205#[cfg(test)]
206mod tests {
207 use super::{DurableStoreFacet, RuntimeError, RuntimeErrorCode};
208
209 #[test]
210 fn durable_store_required_round_trips_per_facet() {
211 for facet in [
212 DurableStoreFacet::AttachmentStore,
213 DurableStoreFacet::ArtifactStore,
214 DurableStoreFacet::SessionStore,
215 DurableStoreFacet::ProcessRegistry,
216 ] {
217 let err = RuntimeError::durable_store_required(facet);
218 let json = serde_json::to_value(&err).expect("serialize runtime error");
219 let decoded: RuntimeError = serde_json::from_value(json).expect("decode runtime error");
220 assert_eq!(
221 decoded.code,
222 RuntimeErrorCode::DurableStoreRequired { facet }
223 );
224 }
225 }
226
227 #[test]
228 fn missing_process_execution_id_round_trips() {
229 let err = RuntimeError::missing_process_execution_id();
230 assert_eq!(err.code, RuntimeErrorCode::MissingProcessExecutionId);
231 let json = serde_json::to_value(&err).expect("serialize runtime error");
232 assert_eq!(json["code"], "missing_process_execution_id");
233 let decoded: RuntimeError = serde_json::from_value(json).expect("decode runtime error");
234 assert_eq!(decoded.code, RuntimeErrorCode::MissingProcessExecutionId);
235 }
236
237 #[test]
238 fn runtime_error_code_serializes_as_stable_string() {
239 let err = RuntimeError::new(RuntimeErrorCode::StoreCommitFailed, "commit failed");
240
241 let json = serde_json::to_value(&err).expect("serialize runtime error");
242 assert_eq!(json["code"], "store_commit_failed");
243
244 let decoded: RuntimeError = serde_json::from_value(json).expect("decode runtime error");
245 assert_eq!(decoded.code, RuntimeErrorCode::StoreCommitFailed);
246 }
247
248 #[test]
249 fn unknown_runtime_error_code_round_trips() {
250 let decoded: RuntimeError = serde_json::from_value(serde_json::json!({
251 "code": "plugin_defined_abort",
252 "message": "stopped by plugin"
253 }))
254 .expect("decode plugin runtime error");
255
256 assert_eq!(
257 decoded.code,
258 RuntimeErrorCode::Other("plugin_defined_abort".to_string())
259 );
260 assert_eq!(decoded.code.as_str(), "plugin_defined_abort");
261 }
262}