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}