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}