velesdb_memory/error.rs
1//! Error type for the memory layer.
2
3#[cfg(feature = "persistence")]
4use velesdb_core::agent::AgentMemoryError;
5use velesdb_core::Error as CoreError;
6
7use crate::embedder::EmbedError;
8use crate::extract::ExtractError;
9use crate::rerank::RerankError;
10
11/// The transport-neutral class of a [`MemoryError`] — the single source of
12/// truth every adapter maps onto its own error channel (JSON-RPC code, napi
13/// status, `PyO3` exception type), so the taxonomy can never drift between them.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ErrorCategory {
16 /// The caller supplied bad input (empty fact, reserved key, malformed
17 /// filter) — a 4xx-style fault.
18 InvalidInput,
19 /// A referenced memory id does not exist.
20 NotFound,
21 /// An internal storage / embedding / extraction failure — a 5xx-style fault.
22 Internal,
23}
24
25/// Errors returned by [`crate::service::MemoryService`].
26#[derive(Debug, thiserror::Error)]
27pub enum MemoryError {
28 /// Failure in the underlying `VelesDB` storage engine.
29 #[error("storage error: {0}")]
30 Storage(#[from] CoreError),
31
32 /// Failure in the Agent Memory SDK. Only constructible with the
33 /// `persistence` feature (the native, file-backed store) — a
34 /// `persistence`-free backend (e.g. `velesdb-wasm`'s in-memory one) never
35 /// touches `velesdb-core`'s `agent` module, so this variant can't arise.
36 #[cfg(feature = "persistence")]
37 #[error("memory error: {0}")]
38 Memory(#[from] AgentMemoryError),
39
40 /// A fact was empty or whitespace-only.
41 #[error("fact text must not be empty")]
42 EmptyFact,
43
44 /// A `remember` link or a `relate` endpoint referenced a memory id that
45 /// does not exist.
46 #[error("memory {0} does not exist")]
47 UnknownMemory(u64),
48
49 /// Caller metadata or a recall filter named a reserved key (`content` or a
50 /// `_veles_`-prefixed system key), which callers may not set or filter on.
51 #[error("metadata key '{0}' is reserved")]
52 ReservedKey(String),
53
54 /// Failure producing a text embedding.
55 #[error("embedding error: {0}")]
56 Embed(#[from] EmbedError),
57
58 /// Failure extracting facts from raw text in
59 /// [`crate::service::MemoryService::remember_extracted`].
60 #[error("extraction error: {0}")]
61 Extract(#[from] ExtractError),
62
63 /// Failure reranking a fused-recall candidate pool in
64 /// [`crate::service::MemoryService::recall_fused_reranked`].
65 #[error("rerank error: {0}")]
66 Rerank(#[from] RerankError),
67
68 /// A fused-recall filter referenced a field name that is not a plain
69 /// identifier, named a reserved key, or carried a non-scalar value.
70 #[error("invalid filter field: {0}")]
71 InvalidFilter(String),
72
73 /// A relation label supplied to [`crate::service::MemoryService::relate`] or
74 /// a [`crate::model::Link`] in
75 /// [`crate::service::MemoryService::remember`] was invalid — empty, too long,
76 /// or contained non-printable characters.
77 #[error("invalid relation label: {0}")]
78 InvalidRelation(String),
79
80 /// A `remember` link failed after the fact was stored AND the
81 /// compensating rollback delete also failed — unlike every other error
82 /// from `remember`, the fact **remains stored**. Both errors are
83 /// carried so the caller can see why the write failed and why the
84 /// cleanup couldn't undo it.
85 ///
86 /// Neither field is `#[source]` — deliberately: the `Display` message
87 /// already embeds both errors, and a source chain would double-print
88 /// them in chain-style reports (anyhow, miette). Match on the variant
89 /// to inspect the two errors programmatically.
90 #[error(
91 "link failed ({cause}); rollback delete also failed ({rollback}) — the fact remains stored"
92 )]
93 RollbackFailed {
94 /// The link failure that triggered the rollback.
95 cause: Box<MemoryError>,
96 /// The storage failure that prevented the rollback delete.
97 rollback: Box<MemoryError>,
98 },
99}
100
101impl MemoryError {
102 /// Classify this error into a transport-neutral [`ErrorCategory`]. Adapters
103 /// map the *category*, not the variant, so the client-facing taxonomy stays
104 /// identical across the MCP server and every binding.
105 #[must_use]
106 pub fn category(&self) -> ErrorCategory {
107 match self {
108 Self::EmptyFact
109 | Self::ReservedKey(_)
110 | Self::InvalidFilter(_)
111 | Self::InvalidRelation(_) => ErrorCategory::InvalidInput,
112 Self::UnknownMemory(_) => ErrorCategory::NotFound,
113 #[cfg(feature = "persistence")]
114 Self::Memory(_) => ErrorCategory::Internal,
115 Self::Storage(_) | Self::Embed(_) | Self::Extract(_) | Self::Rerank(_) => {
116 ErrorCategory::Internal
117 }
118 // The rollback failure is the storage-level fault that matters
119 // to a client: the write is in an unexpected state.
120 Self::RollbackFailed { .. } => ErrorCategory::Internal,
121 }
122 }
123}