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