use crate::support::*;
#[derive(Debug, thiserror::Error)]
pub enum EmbedError {
#[error(
"protocol plugin is required; call .protocol_plugin(...) or use LashCore::standard_builder()/LashCore::rlm_builder(...)"
)]
MissingProtocolPlugin,
#[error("model spec is required; hosts must supply explicit model metadata")]
MissingModelSpec,
#[error("effect host is required; provide an explicit effect host with .effect_host(...)")]
MissingEffectHost,
#[error(
"attachment store is required; provide an explicit attachment store with .attachment_store(...)"
)]
MissingAttachmentStore,
#[error(
"process execution environment store is required; provide an explicit process env store with .process_env_store(...)"
)]
MissingProcessEnvStore,
#[error("failed to create store for session `{session_id}`: {message}")]
StoreFactory { session_id: String, message: String },
#[error("store is bound to session `{loaded}` but builder requested `{requested}`")]
StoreSessionMismatch { loaded: String, requested: String },
#[error("durable process worker requires a LashCore store factory")]
MissingProcessWorkerStoreFactory,
#[error(
"durable session store requires a durable {facet}; an ephemeral {facet} cannot back a durable session store"
)]
DurableStorePeerRequired { facet: &'static str },
#[error(
"durable process registry requires a durable session store factory; call .store_factory(...) with a durable store"
)]
DurableProcessRegistryRequiresStoreFactory,
#[error(
"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."
)]
ProcessRegistryRequiresStoreFactory,
#[error("durable process worker config requires a LashCore process registry")]
MissingProcessRegistry,
#[error("session deletion requires a LashCore store factory")]
MissingSessionStoreFactory,
#[error("failed to delete process state for session `{session_id}`: {message}")]
SessionDeleteProcess { session_id: String, message: String },
#[error("missing required turn input for plugin `{plugin_id}`")]
MissingPluginTurnInput { plugin_id: &'static str },
#[error(
"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"
)]
SessionStillInUse,
#[error("failed to flush trace sink: {0}")]
TraceFlush(#[from] lash_trace::TraceSinkError),
#[error(
"configured effect host for {operation} is durable and requires a handler context; use .effects(&controller) and provide .turn_id(...) for replayable foreground requests"
)]
DurableEffectHostRequiresHandlerContext { operation: &'static str },
#[error(
"pull-style turn streams require an effect host that can create a static scoped controller; use stream_to(...) inside the handler context"
)]
StaticTurnStreamRequiresStaticEffectHost,
#[error("runtime session error: {0}")]
Session(#[from] SessionError),
#[error("runtime turn error: {0}")]
Runtime(#[from] lash_core::RuntimeError),
#[error("runtime plugin/control error: {0}")]
Plugin(#[from] lash_core::PluginError),
#[error("remote protocol error: {0}")]
RemoteProtocol(#[from] lash_remote_protocol::RemoteProtocolError),
#[error("failed to encode protocol turn options: {0}")]
ProtocolTurnOptions(#[from] serde_json::Error),
#[error("failed to decode protocol turn options: {0}")]
DecodeProtocolTurnOptions(#[from] lash_core::ProtocolTurnOptionsError),
#[error("runtime control unavailable: {0}")]
Control(#[from] lash_core::PluginOperationInvokeError),
}
impl EmbedError {
pub fn is_retryable(&self) -> bool {
use lash_core::RuntimeErrorCode;
match self {
Self::Runtime(err) => matches!(
err.code,
RuntimeErrorCode::SessionExecutionBusy
| RuntimeErrorCode::SessionExecutionLeaseLost
),
_ => false,
}
}
pub fn is_terminal(&self) -> bool {
use lash_core::RuntimeErrorCode;
match self {
Self::MissingProtocolPlugin
| Self::MissingModelSpec
| Self::MissingEffectHost
| Self::MissingAttachmentStore
| Self::MissingProcessEnvStore
| Self::StoreSessionMismatch { .. }
| Self::MissingProcessWorkerStoreFactory
| Self::DurableStorePeerRequired { .. }
| Self::DurableProcessRegistryRequiresStoreFactory
| Self::ProcessRegistryRequiresStoreFactory
| Self::MissingProcessRegistry
| Self::MissingSessionStoreFactory
| Self::MissingPluginTurnInput { .. }
| Self::DurableEffectHostRequiresHandlerContext { .. }
| Self::StaticTurnStreamRequiresStaticEffectHost => true,
Self::Runtime(err) => matches!(
err.code,
RuntimeErrorCode::MissingExecutionScopeId
| RuntimeErrorCode::ExecutionScopeTurnIdMismatch
| RuntimeErrorCode::MissingProcessExecutionId
| RuntimeErrorCode::DurableStoreRequired { .. }
| RuntimeErrorCode::DurableEffectLiveProtocolExtension
| RuntimeErrorCode::DurableEffectLivePluginInput
),
Self::Session(err) => matches!(
err,
SessionError::ProviderMismatch { .. }
| SessionError::ProviderUnconfigured { .. }
| SessionError::ProviderUnavailable { .. }
| SessionError::CodeExecutionUnavailable
),
_ => false,
}
}
}
pub type Result<T> = std::result::Result<T, EmbedError>;
#[cfg(test)]
mod tests {
use super::EmbedError;
use lash_core::{RuntimeError, RuntimeErrorCode};
fn runtime_error(code: RuntimeErrorCode) -> EmbedError {
EmbedError::Runtime(RuntimeError::new(code, "test"))
}
#[test]
fn lease_contention_codes_are_retryable_and_not_terminal() {
for code in [
RuntimeErrorCode::SessionExecutionBusy,
RuntimeErrorCode::SessionExecutionLeaseLost,
] {
let err = runtime_error(code);
assert!(err.is_retryable(), "{err}");
assert!(!err.is_terminal(), "{err}");
}
}
#[test]
fn untyped_failures_are_neither_retryable_nor_terminal() {
for err in [
runtime_error(RuntimeErrorCode::StoreCommitFailed),
runtime_error(RuntimeErrorCode::Other("plugin_defined_abort".into())),
] {
assert!(!err.is_retryable(), "{err}");
assert!(!err.is_terminal(), "{err}");
}
}
#[test]
fn wiring_errors_are_terminal_and_not_retryable() {
for err in [
EmbedError::MissingProtocolPlugin,
EmbedError::MissingEffectHost,
runtime_error(RuntimeErrorCode::DurableStoreRequired {
facet: lash_core::DurableStoreFacet::SessionStore,
}),
runtime_error(RuntimeErrorCode::MissingExecutionScopeId),
] {
assert!(err.is_terminal(), "{err}");
assert!(!err.is_retryable(), "{err}");
}
}
}