gglib_core/normalize/error.rs
1//! Non-fatal error reporting from normalization parsers.
2//!
3//! Parsers in [`super::parsers`] never `Result`-fail at the trait level —
4//! a malformed dialect fragment is data, not an infrastructure problem.
5//! Instead, parsers attach a [`NormalizationError`] to their
6//! [`super::parser::ParserOutput`], and the surrounding stream wrapper
7//! surfaces those errors as
8//! `LlmStreamEvent::NormalizationError` events.
9//!
10//! Consumers are free to log, surface, or suppress these events. The proxy
11//! drops them on the wire (V1 contract); in-process consumers (Axum/Tauri)
12//! receive them for diagnostics.
13
14/// Discriminates the kind of malformation a parser detected.
15///
16/// Each variant carries enough context that a developer reading a log line
17/// can identify the offending bytes without rerunning the model.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum NormalizationErrorKind {
20 /// Found a complete `<tool_call>...</tool_call>` block but its body did
21 /// not parse as a JSON object with at least a `name` field.
22 ///
23 /// `raw` holds the body bytes between the open and close tags.
24 MalformedToolCallJson { raw: String },
25
26 /// The stream ended while we were still inside an open `<tool_call>`
27 /// tag. `partial` is the JSON body collected so far (which may be
28 /// empty if only the open tag was seen).
29 UnclosedToolCallTag { partial: String },
30
31 /// A dialect-specific marker was recognised but its surrounding shape
32 /// did not match any known schema. Reserved for future parsers.
33 UnknownMarkup { raw: String },
34}
35
36/// A non-fatal normalization issue surfaced from a parser.
37///
38/// `kind` carries the structured failure details; `raw` is a short snippet
39/// of the offending input suitable for log output.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct NormalizationError {
42 /// Structured detail about what went wrong.
43 pub kind: NormalizationErrorKind,
44 /// A short, human-readable excerpt of the offending input. Parsers
45 /// should keep this small (≲ 256 bytes) so it is safe to attach to a
46 /// stream event.
47 pub raw: String,
48}
49
50impl NormalizationError {
51 /// Construct a `MalformedToolCallJson` error with the body as `raw`.
52 #[must_use]
53 pub fn malformed_tool_call(body: impl Into<String>) -> Self {
54 let raw = body.into();
55 Self {
56 kind: NormalizationErrorKind::MalformedToolCallJson { raw: raw.clone() },
57 raw,
58 }
59 }
60
61 /// Construct an `UnclosedToolCallTag` error from a partial body.
62 #[must_use]
63 pub fn unclosed_tool_call(partial: impl Into<String>) -> Self {
64 let partial = partial.into();
65 Self {
66 kind: NormalizationErrorKind::UnclosedToolCallTag {
67 partial: partial.clone(),
68 },
69 raw: partial,
70 }
71 }
72}