1use crate::support::*;
2
3#[derive(Debug, thiserror::Error)]
4pub enum EmbedError {
5 #[error(
6 "protocol plugin is required; call .protocol_plugin(...) or use LashCore::standard_builder()/LashCore::rlm_builder(...)"
7 )]
8 MissingProtocolPlugin,
9 #[error("model spec is required; hosts must supply explicit model metadata")]
10 MissingModelSpec,
11 #[error("effect host is required; provide an explicit effect host with .effect_host(...)")]
12 MissingEffectHost,
13 #[error(
14 "attachment store is required; provide an explicit attachment store with .attachment_store(...)"
15 )]
16 MissingAttachmentStore,
17 #[error(
18 "process execution environment store is required; provide an explicit process env store with .process_env_store(...)"
19 )]
20 MissingProcessEnvStore,
21 #[error("failed to create store for session `{session_id}`: {message}")]
22 StoreFactory { session_id: String, message: String },
23 #[error("store is bound to session `{loaded}` but builder requested `{requested}`")]
24 StoreSessionMismatch { loaded: String, requested: String },
25 #[error("durable process worker requires a LashCore store factory")]
26 MissingProcessWorkerStoreFactory,
27 #[error(
28 "durable session store requires a durable {facet}; an ephemeral {facet} cannot back a durable session store"
29 )]
30 DurableStorePeerRequired { facet: &'static str },
31 #[error(
32 "durable process registry requires a durable session store factory; call .store_factory(...) with a durable store"
33 )]
34 DurableProcessRegistryRequiresStoreFactory,
35 #[error(
36 "a process registry is configured for the default inline process work runner but no session store factory is wired; the runner rebuilds a session runtime per process and cannot do so without one. Wire .store_factory(...) - InMemorySessionStoreFactory::new() for ephemeral process execution, or a durable factory - or use .process_work_driver(...) for an externally driven durable runner."
37 )]
38 ProcessRegistryRequiresStoreFactory,
39 #[error("durable process worker config requires a LashCore process registry")]
40 MissingProcessRegistry,
41 #[error("session deletion requires a LashCore store factory")]
42 MissingSessionStoreFactory,
43 #[error("failed to delete process state for session `{session_id}`: {message}")]
44 SessionDeleteProcess { session_id: String, message: String },
45 #[error("missing required turn input for plugin `{plugin_id}`")]
46 MissingPluginTurnInput { plugin_id: &'static str },
47 #[error(
48 "session is still in use: park()/close() consume the session and require exclusive ownership; drop any cloned handles and finish or cancel in-flight turns first"
49 )]
50 SessionStillInUse,
51 #[error("failed to flush trace sink: {0}")]
52 TraceFlush(#[from] lash_trace::TraceSinkError),
53 #[error(
54 "configured effect host for {operation} is durable and requires a handler context; use .effects(&controller) and provide .turn_id(...) for replayable foreground requests"
55 )]
56 DurableEffectHostRequiresHandlerContext { operation: &'static str },
57 #[error(
58 "pull-style turn streams require an effect host that can create a static scoped controller; use stream_to(...) inside the handler context"
59 )]
60 StaticTurnStreamRequiresStaticEffectHost,
61 #[error("runtime session error: {0}")]
62 Session(#[from] SessionError),
63 #[error("runtime turn error: {0}")]
64 Runtime(#[from] lash_core::RuntimeError),
65 #[error("runtime plugin/control error: {0}")]
66 Plugin(#[from] lash_core::PluginError),
67 #[error("remote protocol error: {0}")]
68 RemoteProtocol(#[from] lash_remote_protocol::RemoteProtocolError),
69 #[error("failed to encode protocol turn options: {0}")]
70 ProtocolTurnOptions(#[from] serde_json::Error),
71 #[error("failed to decode protocol turn options: {0}")]
72 DecodeProtocolTurnOptions(#[from] lash_core::ProtocolTurnOptionsError),
73 #[error("runtime control unavailable: {0}")]
74 Control(#[from] lash_core::PluginOperationInvokeError),
75}
76
77impl EmbedError {
78 pub fn is_retryable(&self) -> bool {
105 use lash_core::RuntimeErrorCode;
106 match self {
107 Self::Runtime(err) => matches!(
108 err.code,
109 RuntimeErrorCode::SessionExecutionBusy
110 | RuntimeErrorCode::SessionExecutionLeaseLost
111 ),
112 _ => false,
113 }
114 }
115
116 pub fn is_terminal(&self) -> bool {
136 use lash_core::RuntimeErrorCode;
137 match self {
138 Self::MissingProtocolPlugin
139 | Self::MissingModelSpec
140 | Self::MissingEffectHost
141 | Self::MissingAttachmentStore
142 | Self::MissingProcessEnvStore
143 | Self::StoreSessionMismatch { .. }
144 | Self::MissingProcessWorkerStoreFactory
145 | Self::DurableStorePeerRequired { .. }
146 | Self::DurableProcessRegistryRequiresStoreFactory
147 | Self::ProcessRegistryRequiresStoreFactory
148 | Self::MissingProcessRegistry
149 | Self::MissingSessionStoreFactory
150 | Self::MissingPluginTurnInput { .. }
151 | Self::DurableEffectHostRequiresHandlerContext { .. }
152 | Self::StaticTurnStreamRequiresStaticEffectHost => true,
153 Self::Runtime(err) => matches!(
154 err.code,
155 RuntimeErrorCode::MissingExecutionScopeId
156 | RuntimeErrorCode::ExecutionScopeTurnIdMismatch
157 | RuntimeErrorCode::MissingProcessExecutionId
158 | RuntimeErrorCode::DurableStoreRequired { .. }
159 | RuntimeErrorCode::DurableEffectLiveProtocolExtension
160 | RuntimeErrorCode::DurableEffectLivePluginInput
161 ),
162 Self::Session(err) => matches!(
163 err,
164 SessionError::ProviderMismatch { .. }
165 | SessionError::ProviderUnconfigured { .. }
166 | SessionError::ProviderUnavailable { .. }
167 | SessionError::CodeExecutionUnavailable
168 ),
169 _ => false,
170 }
171 }
172}
173
174pub type Result<T> = std::result::Result<T, EmbedError>;
175
176#[cfg(test)]
177mod tests {
178 use super::EmbedError;
179 use lash_core::{RuntimeError, RuntimeErrorCode};
180
181 fn runtime_error(code: RuntimeErrorCode) -> EmbedError {
182 EmbedError::Runtime(RuntimeError::new(code, "test"))
183 }
184
185 #[test]
186 fn lease_contention_codes_are_retryable_and_not_terminal() {
187 for code in [
188 RuntimeErrorCode::SessionExecutionBusy,
189 RuntimeErrorCode::SessionExecutionLeaseLost,
190 ] {
191 let err = runtime_error(code);
192 assert!(err.is_retryable(), "{err}");
193 assert!(!err.is_terminal(), "{err}");
194 }
195 }
196
197 #[test]
198 fn untyped_failures_are_neither_retryable_nor_terminal() {
199 for err in [
200 runtime_error(RuntimeErrorCode::StoreCommitFailed),
201 runtime_error(RuntimeErrorCode::Other("plugin_defined_abort".into())),
202 ] {
203 assert!(!err.is_retryable(), "{err}");
204 assert!(!err.is_terminal(), "{err}");
205 }
206 }
207
208 #[test]
209 fn wiring_errors_are_terminal_and_not_retryable() {
210 for err in [
211 EmbedError::MissingProtocolPlugin,
212 EmbedError::MissingEffectHost,
213 runtime_error(RuntimeErrorCode::DurableStoreRequired {
214 facet: lash_core::DurableStoreFacet::SessionStore,
215 }),
216 runtime_error(RuntimeErrorCode::MissingExecutionScopeId),
217 ] {
218 assert!(err.is_terminal(), "{err}");
219 assert!(!err.is_retryable(), "{err}");
220 }
221 }
222}