Skip to main content

leviath_core/
error.rs

1//! Error types for Leviath Core.
2
3use thiserror::Error;
4
5/// Result type alias using Leviath's Error type.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors from blueprint, stage, region, and layout validation.
9#[derive(Debug, Clone, Error, PartialEq)]
10pub enum ValidationError {
11    /// Blueprint-level validation failure
12    #[error("Invalid blueprint: {0}")]
13    Blueprint(String),
14
15    /// Stage-level validation failure
16    #[error("Invalid stage '{stage}': {message}")]
17    Stage {
18        /// The offending stage's name.
19        stage: String,
20        /// What is wrong with it.
21        message: String,
22    },
23
24    /// Region-level validation failure
25    #[error("Invalid region '{region}': {message}")]
26    Region {
27        /// The offending region's name.
28        region: String,
29        /// What is wrong with it.
30        message: String,
31    },
32
33    /// Layout-level validation failure
34    #[error("Invalid layout: {0}")]
35    Layout(String),
36
37    /// Graph structure validation failure
38    #[error("Invalid graph: {0}")]
39    Graph(String),
40
41    /// Transition validation failure
42    #[error("Invalid transition from '{from}' to '{to}': {message}")]
43    Transition {
44        /// The stage the edge leaves.
45        from: String,
46        /// The stage the edge names as its target, which may not exist.
47        to: String,
48        /// What is wrong with it.
49        message: String,
50    },
51}
52
53/// Core error types for Leviath.
54#[derive(Error, Debug)]
55pub enum Error {
56    /// Region with the specified name was not found
57    #[error("Region not found: {0}")]
58    RegionNotFound(String),
59
60    /// Region validation failed
61    #[error("Region validation failed: {0}")]
62    ValidationFailed(String),
63
64    /// Content exceeds region's token budget
65    #[error("Content exceeds token budget: {used} > {max}")]
66    TokenBudgetExceeded {
67        /// Tokens the write would have brought the region to.
68        used: usize,
69        /// The region's ceiling.
70        max: usize,
71    },
72
73    /// A region under `admission = "reject"` is full, and the write was
74    /// refused rather than something else being dropped to fit it.
75    ///
76    /// Distinct from [`Error::TokenBudgetExceeded`] because the remedy is
77    /// different: that one says this single write is too big for the region,
78    /// this one says the region is full and the agent has to decide what it is
79    /// finished with.
80    #[error(
81        "Region '{region}' is full ({used}/{max} tokens) and does not evict automatically - \
82         release an entry before adding another"
83    )]
84    RegionFull {
85        /// The region that refused the write.
86        region: String,
87        /// Tokens the region currently holds.
88        used: usize,
89        /// The region's ceiling.
90        max: usize,
91    },
92
93    /// Pinned regions alone exceed total token budget
94    #[error("Pinned regions ({pinned_tokens}) exceed total budget ({total_budget})")]
95    PinnedRegionsOverBudget {
96        /// Tokens held by regions that can never be evicted, which is what makes
97        /// this unrecoverable rather than a matter of dropping something.
98        pinned_tokens: usize,
99        /// The whole window's budget.
100        total_budget: usize,
101    },
102
103    /// Blueprint validation failed
104    #[error("Blueprint validation failed: {0}")]
105    BlueprintInvalid(String),
106
107    /// Layout validation failed
108    #[error("Layout validation failed: {0}")]
109    LayoutInvalid(String),
110
111    /// Context transform failed
112    #[error("Context transform failed: {0}")]
113    TransformFailed(String),
114
115    /// Serialization error
116    #[error("Serialization error: {0}")]
117    SerializationError(#[from] serde_json::Error),
118
119    /// Generic error
120    #[error("{0}")]
121    Other(String),
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    // ─── ValidationError Display ────────────────────────────────────────────
129
130    #[test]
131    fn validation_error_blueprint() {
132        let e = ValidationError::Blueprint("missing name".into());
133        assert_eq!(e.to_string(), "Invalid blueprint: missing name");
134    }
135
136    #[test]
137    fn validation_error_stage() {
138        let e = ValidationError::Stage {
139            stage: "init".into(),
140            message: "no prompt".into(),
141        };
142        assert_eq!(e.to_string(), "Invalid stage 'init': no prompt");
143    }
144
145    #[test]
146    fn validation_error_region() {
147        let e = ValidationError::Region {
148            region: "context".into(),
149            message: "too large".into(),
150        };
151        assert_eq!(e.to_string(), "Invalid region 'context': too large");
152    }
153
154    #[test]
155    fn validation_error_layout() {
156        let e = ValidationError::Layout("overlapping regions".into());
157        assert_eq!(e.to_string(), "Invalid layout: overlapping regions");
158    }
159
160    #[test]
161    fn validation_error_graph() {
162        let e = ValidationError::Graph("cycle detected".into());
163        assert_eq!(e.to_string(), "Invalid graph: cycle detected");
164    }
165
166    #[test]
167    fn validation_error_transition() {
168        let e = ValidationError::Transition {
169            from: "A".into(),
170            to: "B".into(),
171            message: "missing condition".into(),
172        };
173        assert_eq!(
174            e.to_string(),
175            "Invalid transition from 'A' to 'B': missing condition"
176        );
177    }
178
179    // ─── Error Display ──────────────────────────────────────────────────────
180
181    #[test]
182    fn error_region_not_found() {
183        let e = Error::RegionNotFound("history".into());
184        assert_eq!(e.to_string(), "Region not found: history");
185    }
186
187    #[test]
188    fn error_validation_failed() {
189        let e = Error::ValidationFailed("bad input".into());
190        assert_eq!(e.to_string(), "Region validation failed: bad input");
191    }
192
193    #[test]
194    fn error_token_budget_exceeded() {
195        let e = Error::TokenBudgetExceeded {
196            used: 500,
197            max: 100,
198        };
199        assert_eq!(e.to_string(), "Content exceeds token budget: 500 > 100");
200    }
201
202    #[test]
203    fn error_pinned_regions_over_budget() {
204        let e = Error::PinnedRegionsOverBudget {
205            pinned_tokens: 2000,
206            total_budget: 1000,
207        };
208        assert_eq!(
209            e.to_string(),
210            "Pinned regions (2000) exceed total budget (1000)"
211        );
212    }
213
214    #[test]
215    fn error_blueprint_invalid() {
216        let e = Error::BlueprintInvalid("parse error".into());
217        assert_eq!(e.to_string(), "Blueprint validation failed: parse error");
218    }
219
220    #[test]
221    fn error_layout_invalid() {
222        let e = Error::LayoutInvalid("bad layout".into());
223        assert_eq!(e.to_string(), "Layout validation failed: bad layout");
224    }
225
226    #[test]
227    fn error_transform_failed() {
228        let e = Error::TransformFailed("script error".into());
229        assert_eq!(e.to_string(), "Context transform failed: script error");
230    }
231
232    #[test]
233    fn error_other() {
234        let e = Error::Other("misc".into());
235        assert_eq!(e.to_string(), "misc");
236    }
237
238    #[test]
239    fn error_from_serde_json() {
240        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
241        let e = Error::from(json_err);
242        assert!(e.to_string().contains("Serialization error"));
243    }
244
245    // ─── Clone for ValidationError ──────────────────────────────────────────
246
247    #[test]
248    fn validation_error_is_cloneable() {
249        let e = ValidationError::Graph("cycle".into());
250        let cloned = e.clone();
251        assert_eq!(e.to_string(), cloned.to_string());
252    }
253}