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("invalid process execution configuration: {0}")]
42 ProcessExecutionConcurrency(#[from] lash_core::ProcessExecutionConcurrencyError),
43 #[error("session deletion requires a LashCore store factory")]
44 MissingSessionStoreFactory,
45 #[error("failed to delete process state for session `{session_id}`: {message}")]
46 SessionDeleteProcess { session_id: String, message: String },
47 #[error("missing required turn input for plugin `{plugin_id}`")]
48 MissingPluginTurnInput { plugin_id: &'static str },
49 #[error(
50 "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"
51 )]
52 SessionStillInUse,
53 #[error("failed to flush trace sink: {0}")]
54 TraceFlush(#[from] lash_trace::TraceSinkError),
55 #[error(
56 "configured effect host for {operation} is durable and requires a handler context; use .effects(&controller) and provide .turn_id(...) for replayable foreground requests"
57 )]
58 DurableEffectHostRequiresHandlerContext { operation: &'static str },
59 #[error(
60 "pull-style turn streams require an effect host that can create a static scoped controller; use stream_to(...) inside the handler context"
61 )]
62 StaticTurnStreamRequiresStaticEffectHost,
63 #[error("runtime session error: {0}")]
64 Session(#[from] SessionError),
65 #[error("runtime turn error: {0}")]
66 Runtime(#[from] lash_core::RuntimeError),
67 #[error("runtime plugin/control error: {0}")]
68 Plugin(#[from] lash_core::PluginError),
69 #[error("remote protocol error: {0}")]
70 RemoteProtocol(#[from] lash_remote_protocol::RemoteProtocolError),
71 #[error("failed to encode protocol turn options: {0}")]
72 ProtocolTurnOptions(#[from] serde_json::Error),
73 #[error("failed to decode protocol turn options: {0}")]
74 DecodeProtocolTurnOptions(#[from] lash_core::ProtocolTurnOptionsError),
75 #[error("runtime control unavailable: {0}")]
76 Control(#[from] lash_core::PluginOperationInvokeError),
77}
78
79impl EmbedError {
80 pub fn is_retryable(&self) -> bool {
107 use lash_core::RuntimeErrorCode;
108 match self {
109 Self::Runtime(err) => matches!(
110 err.code,
111 RuntimeErrorCode::SessionExecutionBusy
112 | RuntimeErrorCode::SessionExecutionLeaseLost
113 ),
114 _ => false,
115 }
116 }
117
118 pub fn is_terminal(&self) -> bool {
138 use lash_core::RuntimeErrorCode;
139 match self {
140 Self::MissingProtocolPlugin
141 | Self::MissingModelSpec
142 | Self::MissingEffectHost
143 | Self::MissingAttachmentStore
144 | Self::MissingProcessEnvStore
145 | Self::StoreSessionMismatch { .. }
146 | Self::MissingProcessWorkerStoreFactory
147 | Self::DurableStorePeerRequired { .. }
148 | Self::DurableProcessRegistryRequiresStoreFactory
149 | Self::ProcessRegistryRequiresStoreFactory
150 | Self::MissingProcessRegistry
151 | Self::ProcessExecutionConcurrency(_)
152 | Self::MissingSessionStoreFactory
153 | Self::MissingPluginTurnInput { .. }
154 | Self::DurableEffectHostRequiresHandlerContext { .. }
155 | Self::StaticTurnStreamRequiresStaticEffectHost => true,
156 Self::Runtime(err) => matches!(
157 err.code,
158 RuntimeErrorCode::MissingExecutionScopeId
159 | RuntimeErrorCode::ExecutionScopeTurnIdMismatch
160 | RuntimeErrorCode::MissingProcessExecutionId
161 | RuntimeErrorCode::DurableStoreRequired { .. }
162 | RuntimeErrorCode::DurableEffectLiveProtocolExtension
163 | RuntimeErrorCode::DurableEffectLivePluginInput
164 ),
165 Self::Session(err) => matches!(
166 err,
167 SessionError::ProviderMismatch { .. }
168 | SessionError::ProviderUnconfigured { .. }
169 | SessionError::ProviderUnavailable { .. }
170 | SessionError::CodeExecutionUnavailable
171 ),
172 _ => false,
173 }
174 }
175}
176
177pub type Result<T> = std::result::Result<T, EmbedError>;
178
179#[cfg(test)]
180mod tests {
181 use super::EmbedError;
182 use lash_core::{RuntimeError, RuntimeErrorCode};
183
184 fn runtime_error(code: RuntimeErrorCode) -> EmbedError {
185 EmbedError::Runtime(RuntimeError::new(code, "test"))
186 }
187
188 #[test]
189 fn lease_contention_codes_are_retryable_and_not_terminal() {
190 for code in [
191 RuntimeErrorCode::SessionExecutionBusy,
192 RuntimeErrorCode::SessionExecutionLeaseLost,
193 ] {
194 let err = runtime_error(code);
195 assert!(err.is_retryable(), "{err}");
196 assert!(!err.is_terminal(), "{err}");
197 }
198 }
199
200 #[test]
201 fn untyped_failures_are_neither_retryable_nor_terminal() {
202 for err in [
203 runtime_error(RuntimeErrorCode::StoreCommitFailed),
204 runtime_error(RuntimeErrorCode::Other("plugin_defined_abort".into())),
205 ] {
206 assert!(!err.is_retryable(), "{err}");
207 assert!(!err.is_terminal(), "{err}");
208 }
209 }
210
211 #[test]
212 fn wiring_errors_are_terminal_and_not_retryable() {
213 for err in [
214 EmbedError::MissingProtocolPlugin,
215 EmbedError::MissingEffectHost,
216 runtime_error(RuntimeErrorCode::DurableStoreRequired {
217 facet: lash_core::DurableStoreFacet::SessionStore,
218 }),
219 runtime_error(RuntimeErrorCode::MissingExecutionScopeId),
220 ] {
221 assert!(err.is_terminal(), "{err}");
222 assert!(!err.is_retryable(), "{err}");
223 }
224 }
225}