Skip to main content

kiromi_ai_memory/
error.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2//! Public error type. `#[non_exhaustive]` so adding variants is non-breaking.
3
4use std::io;
5
6use crate::partition::{InvalidScheme, PartitionResolveError};
7use crate::tenant::InvalidTenantId;
8
9/// Crate result alias.
10pub type Result<T, E = Error> = std::result::Result<T, E>;
11
12/// Public error type. Carries structured context where useful.
13#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum Error {
16    /// Storage backend failure.
17    #[error("storage backend error: {message}")]
18    Storage {
19        /// Human-readable detail.
20        message: String,
21        #[source]
22        /// Underlying error.
23        source: Box<dyn std::error::Error + Send + Sync + 'static>,
24    },
25    /// Metadata backend failure (SQL, sqlite, migrations).
26    #[error("metadata backend error: {message}")]
27    Metadata {
28        /// Human-readable detail.
29        message: String,
30        #[source]
31        /// Underlying error.
32        source: Box<dyn std::error::Error + Send + Sync + 'static>,
33    },
34    /// Embedder failure.
35    #[error("embedder error: {message}")]
36    Embedder {
37        /// Human-readable detail.
38        message: String,
39        #[source]
40        /// Underlying error.
41        source: Box<dyn std::error::Error + Send + Sync + 'static>,
42    },
43    /// `embedder.id()` does not match the value persisted in `schema_meta`.
44    #[error(
45        "embedder mismatch: store has {expected:?} ({expected_dims}d), got {got:?} ({got_dims}d)"
46    )]
47    EmbedderMismatch {
48        /// Embedder id stored in `schema_meta`.
49        expected: String,
50        /// Stored embedder dims.
51        expected_dims: usize,
52        /// Embedder id passed to `open()`.
53        got: String,
54        /// Open-time embedder dims.
55        got_dims: usize,
56    },
57    /// Persisted partition scheme differs from the value passed to the builder.
58    #[error("partition scheme mismatch: store has {expected:?}, got {got:?}")]
59    PartitionSchemeMismatch {
60        /// Stored scheme.
61        expected: String,
62        /// Builder-supplied scheme.
63        got: String,
64    },
65    /// Partition path was malformed or did not match the scheme.
66    #[error("invalid partition: {0}")]
67    PartitionInvalid(#[from] PartitionResolveError),
68    /// Partition scheme template was malformed.
69    #[error("invalid partition scheme: {0}")]
70    PartitionSchemeInvalid(#[from] InvalidScheme),
71    /// Tenant id was invalid.
72    #[error("invalid tenant id: {0}")]
73    TenantInvalid(#[from] InvalidTenantId),
74    /// Lookup miss.
75    #[error("memory not found: {0}")]
76    MemoryNotFound(String),
77    /// Summary lookup miss (Plan 9).
78    #[error("summary not found: {0}")]
79    SummaryNotFound(String),
80    /// Operation rejected because the memory is soft-tombstoned.
81    #[error("memory is tombstoned: {0}")]
82    Tombstoned(String),
83    /// Link request invalid (self-link, missing endpoint, etc.).
84    #[error("invalid link request: {0}")]
85    LinkInvalid(String),
86    /// On-disk index detected as corrupt (Plan 3+).
87    #[error("index corrupt: {0}")]
88    IndexCorrupt(String),
89    /// Recovery walker failed to replay an audit-log entry (Plan 3+).
90    #[error("recovery error: {0}")]
91    Recovery(String),
92    /// Builder / config validation error.
93    #[error("config error: {0}")]
94    Config(String),
95    /// A configured plugin reported it does not support a capability the engine
96    /// requires.
97    #[error("capability missing on {plugin}: required={required:?}, got={got:?}")]
98    CapabilityMissing {
99        /// Which plugin (Storage / Metadata / Embedder).
100        plugin: &'static str,
101        /// The capability flag the engine required.
102        required: &'static str,
103        /// What the plugin reported.
104        got: bool,
105    },
106    /// I/O error (filesystem-side mostly).
107    #[error("io error: {0}")]
108    Io(#[from] io::Error),
109    /// Caller passed an [`crate::attribute::AttributeValue`] that violates a
110    /// constraint — kind mismatch on a range query, non-orderable kind on a
111    /// range query, or a decoding failure on read.
112    #[error("invalid attribute: {reason}")]
113    InvalidAttribute {
114        /// Human-readable reason.
115        reason: String,
116    },
117    /// Anchor / citation URI failed to parse. See [`crate::Memory::resolve_anchor`].
118    #[error("invalid anchor: {0}")]
119    InvalidAnchor(String),
120    /// Snapshot id was not found in `snapshot`. Reserved for the
121    /// deferred Plan 12 `Memory::at` / `Memory::restore` paths;
122    /// [`crate::Memory::delete_snapshot`] is currently idempotent on
123    /// missing rows and does not return this variant.
124    #[error("snapshot not found: {id}")]
125    SnapshotNotFound {
126        /// Snapshot id (ULID).
127        id: String,
128    },
129    /// `regenerate_embeddings` rejected an embedder whose dim differs
130    /// from the persisted store dim.
131    #[error("embedder dim mismatch: store has {old}d, got {new}d")]
132    EmbedderDimMismatch {
133        /// Stored dim.
134        old: usize,
135        /// New embedder's dim.
136        new: usize,
137    },
138    /// `migrate_scheme` could not proceed (e.g. previous run is still
139    /// `in_progress` with a different mapper).
140    #[error("migration conflict: {reason}")]
141    MigrationConflict {
142        /// Human-readable reason.
143        reason: String,
144    },
145}
146
147impl Error {
148    /// Convenience constructor.
149    pub fn storage<E>(message: impl Into<String>, source: E) -> Self
150    where
151        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
152    {
153        Error::Storage {
154            message: message.into(),
155            source: source.into(),
156        }
157    }
158
159    /// Convenience constructor.
160    pub fn metadata<E>(message: impl Into<String>, source: E) -> Self
161    where
162        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
163    {
164        Error::Metadata {
165            message: message.into(),
166            source: source.into(),
167        }
168    }
169
170    /// Convenience constructor.
171    pub fn embedder<E>(message: impl Into<String>, source: E) -> Self
172    where
173        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
174    {
175        Error::Embedder {
176            message: message.into(),
177            source: source.into(),
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn display_carries_context() {
188        let e = Error::EmbedderMismatch {
189            expected: "onnx:e5-small:v1".into(),
190            expected_dims: 384,
191            got: "onnx:e5-large:v1".into(),
192            got_dims: 1024,
193        };
194        let s = format!("{e}");
195        assert!(s.contains("e5-small"));
196        assert!(s.contains("384"));
197        assert!(s.contains("e5-large"));
198    }
199
200    #[test]
201    fn from_partition_resolve_error() {
202        let e: Error = PartitionResolveError::MissingKey { key: "user".into() }.into();
203        assert!(matches!(e, Error::PartitionInvalid(_)));
204    }
205}