Skip to main content

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 context-compile request carried a token budget that cannot hold any
81    /// context: zero, or not larger than the response reserve the policy
82    /// keeps aside for the model's answer.
83    #[cfg(feature = "context")]
84    #[error("token budget {budget} cannot hold any context (reserve {reserve})")]
85    ContextBudget {
86        /// The caller-supplied token budget.
87        budget: u64,
88        /// The response reserve the policy subtracts from the budget.
89        reserve: u64,
90    },
91
92    /// A context-compile request exceeded a resource cap from
93    /// [`crate::limits`] — too many fragments, or one fragment larger than
94    /// the per-fragment byte ceiling.
95    #[cfg(feature = "context")]
96    #[error("context request over limit: {0}")]
97    ContextOverLimit(String),
98
99    /// A `ctx://source/<hash>` handle was malformed or nothing is stored
100    /// under it (the source was never stored, expired, or was forgotten).
101    #[cfg(feature = "context")]
102    #[error("unknown context source handle: {0}")]
103    UnknownHandle(String),
104
105    /// A persisted working context could not be (de)serialized — the stored
106    /// payload predates or postdates this crate's schema.
107    #[cfg(feature = "context")]
108    #[error("working context codec error: {0}")]
109    WorkingContextCodec(String),
110
111    /// A `remember` link failed after the fact was stored AND the
112    /// compensating rollback delete also failed — unlike every other error
113    /// from `remember`, the fact **remains stored**. Both errors are
114    /// carried so the caller can see why the write failed and why the
115    /// cleanup couldn't undo it.
116    ///
117    /// Neither field is `#[source]` — deliberately: the `Display` message
118    /// already embeds both errors, and a source chain would double-print
119    /// them in chain-style reports (anyhow, miette). Match on the variant
120    /// to inspect the two errors programmatically.
121    #[error(
122        "link failed ({cause}); rollback delete also failed ({rollback}) — the fact remains stored"
123    )]
124    RollbackFailed {
125        /// The link failure that triggered the rollback.
126        cause: Box<MemoryError>,
127        /// The storage failure that prevented the rollback delete.
128        rollback: Box<MemoryError>,
129    },
130}
131
132impl MemoryError {
133    /// Classify this error into a transport-neutral [`ErrorCategory`]. Adapters
134    /// map the *category*, not the variant, so the client-facing taxonomy stays
135    /// identical across the MCP server and every binding.
136    #[must_use]
137    pub fn category(&self) -> ErrorCategory {
138        match self {
139            Self::EmptyFact
140            | Self::ReservedKey(_)
141            | Self::InvalidFilter(_)
142            | Self::InvalidRelation(_) => ErrorCategory::InvalidInput,
143            #[cfg(feature = "context")]
144            Self::ContextBudget { .. } | Self::ContextOverLimit(_) => ErrorCategory::InvalidInput,
145            #[cfg(feature = "context")]
146            Self::UnknownHandle(_) => ErrorCategory::NotFound,
147            #[cfg(feature = "context")]
148            Self::WorkingContextCodec(_) => ErrorCategory::Internal,
149            Self::UnknownMemory(_) => ErrorCategory::NotFound,
150            #[cfg(feature = "persistence")]
151            Self::Memory(_) => ErrorCategory::Internal,
152            Self::Storage(_) | Self::Embed(_) | Self::Extract(_) | Self::Rerank(_) => {
153                ErrorCategory::Internal
154            }
155            // The rollback failure is the storage-level fault that matters
156            // to a client: the write is in an unexpected state.
157            Self::RollbackFailed { .. } => ErrorCategory::Internal,
158        }
159    }
160}