Skip to main content

agent_first_data/
protocol.rs

1use serde::Serialize;
2use serde_json::Value;
3use std::fmt;
4
5// ═══════════════════════════════════════════
6// Event Type and Build Errors (0.16 API)
7// ═══════════════════════════════════════════
8
9/// An SDK-built AFDATA protocol v1 event envelope.
10///
11/// Wraps the complete JSON envelope and provides access to the underlying value.
12/// The non-error builders retain a caller-provided trace verbatim and cannot
13/// fail. Validate externally supplied trace values with
14/// [`validate_protocol_event`] before treating the envelope as protocol-valid.
15#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
16pub struct Event(Value);
17
18impl Event {
19    /// Access the underlying JSON value.
20    pub fn as_value(&self) -> &Value {
21        &self.0
22    }
23
24    /// Convert into the underlying JSON value.
25    pub fn into_value(self) -> Value {
26        self.0
27    }
28}
29
30impl From<Event> for Value {
31    fn from(event: Event) -> Self {
32        event.0
33    }
34}
35
36impl fmt::Display for Event {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "{}", self.0)
39    }
40}
41
42/// Error type for builder failures.
43///
44/// Errors occur when:
45/// - A reserved error field is overwritten (code, message, hint, retryable)
46/// - An object field (.fields or .extend) is not a JSON object
47/// - A required field (code/message for convenience functions) is empty
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum BuildError {
50    ReservedField(String),
51    NonObjectField(String),
52    EmptyRequiredField(String),
53}
54
55impl fmt::Display for BuildError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::ReservedField(msg) => write!(f, "reserved field: {msg}"),
59            Self::NonObjectField(msg) => write!(f, "non-object field: {msg}"),
60            Self::EmptyRequiredField(msg) => write!(f, "empty required field: {msg}"),
61        }
62    }
63}
64
65impl std::error::Error for BuildError {}
66
67/// Log level enumeration (serialized as lowercase).
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
69#[serde(rename_all = "lowercase")]
70pub enum LogLevel {
71    Debug,
72    Info,
73    Warn,
74    Error,
75}
76
77impl LogLevel {
78    /// CLI emission and the optional tracing adapter share the same canonical
79    /// level spelling. Keep the helper out of a core-only build so that
80    /// `--no-default-features` remains free of dead code.
81    #[cfg(any(feature = "cli", feature = "tracing"))]
82    pub(crate) fn as_str(&self) -> &'static str {
83        match self {
84            Self::Debug => "debug",
85            Self::Info => "info",
86            Self::Warn => "warn",
87            Self::Error => "error",
88        }
89    }
90}
91
92// ═══════════════════════════════════════════
93// Result Builder
94// ═══════════════════════════════════════════
95
96/// Builder for result events.
97pub struct ResultBuilder {
98    payload: Value,
99    trace: Option<Value>,
100}
101
102impl ResultBuilder {
103    /// Set the trace object.
104    pub fn trace(mut self, trace: Value) -> Self {
105        self.trace = Some(trace);
106        self
107    }
108
109    /// Build the event. This builder cannot fail.
110    pub fn build(self) -> Event {
111        let trace = self
112            .trace
113            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
114        let mut obj = serde_json::Map::new();
115        obj.insert("kind".to_string(), Value::String("result".to_string()));
116        obj.insert("result".to_string(), self.payload);
117        obj.insert("trace".to_string(), trace);
118        Event(Value::Object(obj))
119    }
120}
121
122/// Fluent builder: start building a result event.
123pub fn json_result(payload: Value) -> ResultBuilder {
124    ResultBuilder {
125        payload,
126        trace: None,
127    }
128}
129
130// ═══════════════════════════════════════════
131// Error Builder
132// ═══════════════════════════════════════════
133
134/// Builder for error events.
135pub struct ErrorBuilder {
136    code: String,
137    message: String,
138    retryable: bool,
139    hint: Option<String>,
140    fields: serde_json::Map<String, Value>,
141    trace: Option<Value>,
142    build_error: Option<BuildError>,
143}
144
145impl ErrorBuilder {
146    /// Mark the error as retryable.
147    pub fn retryable(mut self) -> Self {
148        self.retryable = true;
149        self
150    }
151
152    /// Set retryable based on a boolean condition.
153    pub fn retryable_if(mut self, should_retry: bool) -> Self {
154        self.retryable = should_retry;
155        self
156    }
157
158    /// Set the hint.
159    pub fn hint(mut self, hint: &str) -> Self {
160        if !hint.is_empty() {
161            self.hint = Some(hint.to_string());
162        }
163        self
164    }
165
166    /// Set the hint if present.
167    pub fn hint_if_some(mut self, hint: Option<&str>) -> Self {
168        if let Some(h) = hint.filter(|hint| !hint.is_empty()) {
169            self.hint = Some(h.to_string());
170        }
171        self
172    }
173
174    /// Add an extension field.
175    pub fn field(mut self, name: &str, value: Value) -> Self {
176        if self.build_error.is_none() {
177            match name {
178                "code" | "message" | "hint" | "retryable" => {
179                    self.build_error = Some(BuildError::ReservedField(format!(
180                        "cannot write reserved field {name:?} to error payload"
181                    )));
182                }
183                _ => {
184                    self.fields.insert(name.to_string(), value);
185                }
186            }
187        }
188        self
189    }
190
191    /// Add multiple extension fields from a JSON object.
192    pub fn fields(mut self, fields: Value) -> Self {
193        if self.build_error.is_none() {
194            match fields {
195                Value::Object(map) => {
196                    for (k, v) in map {
197                        match k.as_str() {
198                            "code" | "message" | "hint" | "retryable" => {
199                                self.build_error = Some(BuildError::ReservedField(format!(
200                                    "cannot write reserved field {k:?} to error payload"
201                                )));
202                                return self;
203                            }
204                            _ => {
205                                self.fields.insert(k, v);
206                            }
207                        }
208                    }
209                }
210                _ => {
211                    self.build_error = Some(BuildError::NonObjectField(
212                        "fields() argument must be a JSON object".to_string(),
213                    ));
214                }
215            }
216        }
217        self
218    }
219
220    /// Extend with a serializable value (must serialize to JSON object).
221    pub fn extend<T: Serialize>(mut self, value: T) -> Self {
222        if self.build_error.is_none() {
223            match serde_json::to_value(&value) {
224                Ok(Value::Object(map)) => {
225                    for (k, v) in map {
226                        match k.as_str() {
227                            "code" | "message" | "hint" | "retryable" => {
228                                self.build_error = Some(BuildError::ReservedField(format!(
229                                    "cannot write reserved field {k:?} to error payload"
230                                )));
231                                return self;
232                            }
233                            _ => {
234                                self.fields.insert(k, v);
235                            }
236                        }
237                    }
238                }
239                Ok(_) => {
240                    self.build_error = Some(BuildError::NonObjectField(
241                        "extend() argument must serialize to a JSON object".to_string(),
242                    ));
243                }
244                Err(_) => {
245                    self.build_error = Some(BuildError::NonObjectField(
246                        "extend() argument serialization failed".to_string(),
247                    ));
248                }
249            }
250        }
251        self
252    }
253
254    /// Set the trace object.
255    pub fn trace(mut self, trace: Value) -> Self {
256        self.trace = Some(trace);
257        self
258    }
259
260    /// Build the event.
261    pub fn build(self) -> Result<Event, BuildError> {
262        if let Some(err) = self.build_error {
263            return Err(err);
264        }
265        if self.trace.as_ref().is_some_and(|trace| !trace.is_object()) {
266            return Err(BuildError::NonObjectField(
267                "trace() argument must be a JSON object".to_string(),
268            ));
269        }
270
271        let mut error_obj = self.fields;
272        error_obj.insert("code".to_string(), Value::String(self.code));
273        error_obj.insert("message".to_string(), Value::String(self.message));
274        error_obj.insert("retryable".to_string(), Value::Bool(self.retryable));
275        if let Some(h) = self.hint {
276            error_obj.insert("hint".to_string(), Value::String(h));
277        }
278
279        let trace = self
280            .trace
281            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
282        let mut obj = serde_json::Map::new();
283        obj.insert("kind".to_string(), Value::String("error".to_string()));
284        obj.insert("error".to_string(), Value::Object(error_obj));
285        obj.insert("trace".to_string(), trace);
286
287        Ok(Event(Value::Object(obj)))
288    }
289}
290
291/// Fluent builder: start building an error event.
292///
293/// An empty `code` or `message` does not panic here; it seeds a deferred
294/// [`BuildError::EmptyRequiredField`] that is surfaced when [`ErrorBuilder::build`]
295/// is called.
296pub fn json_error(code: &str, message: &str) -> ErrorBuilder {
297    let build_error = if code.is_empty() {
298        Some(BuildError::EmptyRequiredField(
299            "error code must not be empty".to_string(),
300        ))
301    } else if message.is_empty() {
302        Some(BuildError::EmptyRequiredField(
303            "error message must not be empty".to_string(),
304        ))
305    } else {
306        None
307    };
308    ErrorBuilder {
309        code: code.to_string(),
310        message: message.to_string(),
311        retryable: false,
312        hint: None,
313        fields: serde_json::Map::new(),
314        trace: None,
315        build_error,
316    }
317}
318
319// ═══════════════════════════════════════════
320// Progress Builder
321// ═══════════════════════════════════════════
322
323/// Builder for progress events.
324pub struct ProgressBuilder {
325    payload: Value,
326    trace: Option<Value>,
327}
328
329impl ProgressBuilder {
330    /// Set the trace object.
331    pub fn trace(mut self, trace: Value) -> Self {
332        self.trace = Some(trace);
333        self
334    }
335
336    /// Build the event. This builder cannot fail.
337    pub fn build(self) -> Event {
338        let trace = self
339            .trace
340            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
341        let mut obj = serde_json::Map::new();
342        obj.insert("kind".to_string(), Value::String("progress".to_string()));
343        obj.insert("progress".to_string(), self.payload);
344        obj.insert("trace".to_string(), trace);
345
346        Event(Value::Object(obj))
347    }
348}
349
350/// Fluent builder: start building a progress event.
351///
352pub fn json_progress(payload: Value) -> ProgressBuilder {
353    ProgressBuilder {
354        payload,
355        trace: None,
356    }
357}
358
359// ═══════════════════════════════════════════
360// Log Builder
361// ═══════════════════════════════════════════
362
363/// Builder for log events.
364pub struct LogBuilder {
365    payload: Value,
366    trace: Option<Value>,
367}
368
369impl LogBuilder {
370    /// Set the trace object.
371    pub fn trace(mut self, trace: Value) -> Self {
372        self.trace = Some(trace);
373        self
374    }
375
376    /// Build the event. This builder cannot fail.
377    pub fn build(self) -> Event {
378        let trace = self
379            .trace
380            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
381        let mut obj = serde_json::Map::new();
382        obj.insert("kind".to_string(), Value::String("log".to_string()));
383        obj.insert("log".to_string(), self.payload);
384        obj.insert("trace".to_string(), trace);
385
386        Event(Value::Object(obj))
387    }
388}
389
390/// Fluent builder: start building a log event.
391///
392pub fn json_log(payload: Value) -> LogBuilder {
393    LogBuilder {
394        payload,
395        trace: None,
396    }
397}
398
399// ═══════════════════════════════════════════
400// CLI Helper
401// ═══════════════════════════════════════════
402
403/// Build a CLI error event with optional hint.
404///
405/// Always returns a strict-valid `kind:"error"` event with code `"cli_error"`,
406/// `retryable: false`, and an empty `trace`. An empty `message` is replaced with
407/// a generic placeholder so the returned event stays strict-valid without panicking.
408pub fn build_cli_error(message: &str, hint: Option<&str>) -> Event {
409    let message = if message.is_empty() {
410        "unspecified error"
411    } else {
412        message
413    };
414    match json_error("cli_error", message).hint_if_some(hint).build() {
415        Ok(event) => event,
416        // Unreachable in practice (non-empty code+message, no reserved fields);
417        // construct a minimal valid envelope directly rather than panic.
418        Err(_) => {
419            let mut error_obj = serde_json::Map::new();
420            error_obj.insert("code".to_string(), Value::String("cli_error".to_string()));
421            error_obj.insert(
422                "message".to_string(),
423                Value::String("unspecified error".to_string()),
424            );
425            error_obj.insert("retryable".to_string(), Value::Bool(false));
426            let mut obj = serde_json::Map::new();
427            obj.insert("kind".to_string(), Value::String("error".to_string()));
428            obj.insert("error".to_string(), Value::Object(error_obj));
429            obj.insert("trace".to_string(), Value::Object(serde_json::Map::new()));
430            Event(Value::Object(obj))
431        }
432    }
433}
434
435// ═══════════════════════════════════════════
436// Validation
437// ═══════════════════════════════════════════
438
439/// A single protocol-validation violation: a stable machine-readable `rule`
440/// slug, a JSON pointer to the offending location (`""` = the whole event),
441/// and a human-readable `message`.
442///
443/// The `rule` slugs are a stable contract (the CLI maps them 1:1 onto its
444/// `validate` finding `rule_id`s). The full set:
445/// `event_not_object`, `kind_invalid`, `kind_unsupported`, `payload_missing`,
446/// `unexpected_field`, `trace_not_object`, `trace_required`, `error_not_object`,
447/// `error_code_invalid`, `error_message_invalid`, `error_hint_invalid`,
448/// `error_retryable_invalid`, `stream_non_terminal_after_terminal`,
449/// `stream_duplicate_terminal`, `stream_missing_terminal`.
450#[derive(Clone, Debug, PartialEq, Eq)]
451pub struct ProtocolViolation {
452    pub rule: &'static str,
453    pub pointer: String,
454    pub message: String,
455}
456
457impl std::fmt::Display for ProtocolViolation {
458    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459        write!(f, "{}", self.message)
460    }
461}
462
463impl std::error::Error for ProtocolViolation {}
464
465fn violation(
466    rule: &'static str,
467    pointer: impl Into<String>,
468    message: impl Into<String>,
469) -> ProtocolViolation {
470    ProtocolViolation {
471        rule,
472        pointer: pointer.into(),
473        message: message.into(),
474    }
475}
476
477/// Validate one protocol v1 event envelope.
478///
479/// `strict` additionally enforces the recommended strict profile: `trace` is
480/// required, and kind-specific payload shapes are checked (see
481/// `validate_protocol_event_strict_payload`).
482///
483/// Returns the first [`ProtocolViolation`] found, or `Ok(())`.
484pub fn validate_protocol_event(event: &Value, strict: bool) -> Result<(), ProtocolViolation> {
485    validate_protocol_event_base(event)?;
486    if strict {
487        validate_protocol_event_strict_payload(event)?;
488    }
489    Ok(())
490}
491
492fn validate_protocol_event_base(event: &Value) -> Result<(), ProtocolViolation> {
493    let Some(obj) = event.as_object() else {
494        return Err(violation(
495            "event_not_object",
496            "",
497            "event must be a JSON object",
498        ));
499    };
500    let Some(kind) = obj.get("kind").and_then(Value::as_str) else {
501        return Err(violation(
502            "kind_invalid",
503            "/kind",
504            "event.kind must be one of result, error, progress, log",
505        ));
506    };
507    if !matches!(kind, "result" | "error" | "progress" | "log") {
508        return Err(violation(
509            "kind_unsupported",
510            "/kind",
511            format!("unsupported event kind {kind:?}"),
512        ));
513    }
514    if !obj.contains_key(kind) {
515        return Err(violation(
516            "payload_missing",
517            format!("/{kind}"),
518            format!("event payload field {kind:?} is required"),
519        ));
520    }
521    for key in obj.keys() {
522        if key != "kind" && key != kind && key != "trace" {
523            return Err(violation(
524                "unexpected_field",
525                format!("/{key}"),
526                format!("unexpected top-level field {key:?}"),
527            ));
528        }
529    }
530    if let Some(trace) = obj.get("trace")
531        && !trace.is_object()
532    {
533        return Err(violation(
534            "trace_not_object",
535            "/trace",
536            "event.trace must be a JSON object when present",
537        ));
538    }
539    if kind == "error" {
540        validate_error_payload(obj.get("error"))?;
541    }
542    Ok(())
543}
544
545fn validate_error_payload(error: Option<&Value>) -> Result<(), ProtocolViolation> {
546    let Some(error) = error.and_then(Value::as_object) else {
547        return Err(violation(
548            "error_not_object",
549            "/error",
550            "event.error must be a JSON object",
551        ));
552    };
553    match error.get("code").and_then(Value::as_str) {
554        Some(code) if !code.is_empty() => {}
555        _ => {
556            return Err(violation(
557                "error_code_invalid",
558                "/error/code",
559                "event.error.code must be a non-empty string",
560            ));
561        }
562    }
563    match error.get("message").and_then(Value::as_str) {
564        Some(message) if !message.is_empty() => {}
565        _ => {
566            return Err(violation(
567                "error_message_invalid",
568                "/error/message",
569                "event.error.message must be a non-empty string",
570            ));
571        }
572    }
573    if error.get("hint").is_some_and(|hint| !hint.is_string()) {
574        return Err(violation(
575            "error_hint_invalid",
576            "/error/hint",
577            "event.error.hint must be a string when present",
578        ));
579    }
580    // Absent is "unknown"; a value of the wrong type is not, and decoding it as
581    // absent would turn `"retryable":"false"` into a silent "maybe".
582    if error
583        .get("retryable")
584        .is_some_and(|retryable| !retryable.is_boolean())
585    {
586        return Err(violation(
587            "error_retryable_invalid",
588            "/error/retryable",
589            "event.error.retryable must be a boolean when present",
590        ));
591    }
592    Ok(())
593}
594
595/// Validate a finite structured CLI event stream:
596/// `(log | progress)* -> exactly one (result | error) -> end`.
597///
598/// `strict` is forwarded to [`validate_protocol_event`] for every event.
599///
600/// Unlike [`validate_protocol_event`], this collects every violation across
601/// the whole stream instead of failing fast on the first one; per-event
602/// violation pointers are prefixed with the event's index (`/{idx}{pointer}`).
603pub fn validate_protocol_stream(
604    events: &[Value],
605    strict: bool,
606) -> Result<(), Vec<ProtocolViolation>> {
607    let mut violations = Vec::new();
608    let mut terminal_seen = false;
609    for (idx, event) in events.iter().enumerate() {
610        if let Err(v) = validate_protocol_event(event, strict) {
611            violations.push(ProtocolViolation {
612                rule: v.rule,
613                pointer: format!("/{idx}{}", v.pointer),
614                message: v.message,
615            });
616        }
617        match event.get("kind").and_then(Value::as_str) {
618            Some("log") | Some("progress") => {
619                if terminal_seen {
620                    violations.push(violation(
621                        "stream_non_terminal_after_terminal",
622                        format!("/{idx}"),
623                        "non-terminal event after terminal",
624                    ));
625                }
626            }
627            Some("result") | Some("error") => {
628                if terminal_seen {
629                    violations.push(violation(
630                        "stream_duplicate_terminal",
631                        format!("/{idx}"),
632                        "duplicate terminal event",
633                    ));
634                } else {
635                    terminal_seen = true;
636                }
637            }
638            _ => {} // an invalid/absent kind is already recorded by validate_protocol_event above
639        }
640    }
641    if !terminal_seen {
642        violations.push(violation(
643            "stream_missing_terminal",
644            String::new(),
645            "event stream must contain exactly one terminal result or error",
646        ));
647    }
648    if violations.is_empty() {
649        Ok(())
650    } else {
651        Err(violations)
652    }
653}
654
655/// Validate one protocol v1 event's payload against the recommended strict profile.
656///
657/// Assumes the base envelope shape (see [`validate_protocol_event_base`]) already passed.
658fn validate_protocol_event_strict_payload(event: &Value) -> Result<(), ProtocolViolation> {
659    if !event.get("trace").is_some_and(Value::is_object) {
660        return Err(violation(
661            "trace_required",
662            "/trace",
663            "event.trace is required by the strict profile",
664        ));
665    }
666    match event.get("kind").and_then(Value::as_str) {
667        Some("error") => validate_strict_error_payload(event.get("error")),
668        _ => Ok(()),
669    }
670}
671
672fn validate_strict_error_payload(error: Option<&Value>) -> Result<(), ProtocolViolation> {
673    let Some(error) = error.and_then(Value::as_object) else {
674        return Err(violation(
675            "error_not_object",
676            "/error",
677            "event.error must be a JSON object in the strict profile",
678        ));
679    };
680    require_non_empty_string(
681        error,
682        "code",
683        "error_code_invalid",
684        "/error/code",
685        "event.error",
686    )?;
687    require_non_empty_string(
688        error,
689        "message",
690        "error_message_invalid",
691        "/error/message",
692        "event.error",
693    )?;
694    if error.get("retryable").and_then(Value::as_bool).is_none() {
695        return Err(violation(
696            "error_retryable_invalid",
697            "/error/retryable",
698            "event.error.retryable must be a boolean in the strict profile",
699        ));
700    }
701    if error.contains_key("hint") && !error.get("hint").is_some_and(Value::is_string) {
702        return Err(violation(
703            "error_hint_invalid",
704            "/error/hint",
705            "event.error.hint must be a string when present",
706        ));
707    }
708    Ok(())
709}
710
711fn require_non_empty_string(
712    payload: &serde_json::Map<String, Value>,
713    field: &str,
714    rule: &'static str,
715    pointer: &'static str,
716    path: &str,
717) -> Result<(), ProtocolViolation> {
718    if payload
719        .get(field)
720        .and_then(Value::as_str)
721        .is_some_and(|value| !value.is_empty())
722    {
723        return Ok(());
724    }
725    Err(violation(
726        rule,
727        pointer,
728        format!("{path}.{field} must be a non-empty string in the strict profile"),
729    ))
730}
731
732// ═══════════════════════════════════════════
733// Reader API: decode_protocol_event
734// ═══════════════════════════════════════════
735
736/// A decoded, base-valid AFDATA protocol v1 event, typed by kind.
737#[derive(Clone, Debug, PartialEq)]
738pub enum DecodedEvent {
739    Result(DecodedResult),
740    Error(DecodedError),
741    Progress(DecodedProgress),
742    Log(DecodedLog),
743}
744
745/// Decoded `kind:"result"` event.
746#[derive(Clone, Debug, PartialEq)]
747pub struct DecodedResult {
748    /// The raw `result` payload value.
749    pub result: Value,
750    /// The raw `trace` object, when present.
751    pub trace: Option<Value>,
752}
753
754/// Decoded `kind:"error"` event.
755#[derive(Clone, Debug, PartialEq)]
756pub struct DecodedError {
757    pub code: String,
758    pub message: String,
759    pub retryable: Option<bool>,
760    pub hint: Option<String>,
761    /// Payload keys beyond `code`, `message`, `retryable`, `hint`.
762    pub fields: serde_json::Map<String, Value>,
763    /// The raw `trace` object, when present.
764    pub trace: Option<Value>,
765}
766
767/// Decoded `kind:"progress"` event.
768#[derive(Clone, Debug, PartialEq)]
769pub struct DecodedProgress {
770    /// The raw `progress` payload value.
771    pub progress: Value,
772    /// The raw `trace` object, when present.
773    pub trace: Option<Value>,
774}
775
776/// Decoded `kind:"log"` event.
777#[derive(Clone, Debug, PartialEq)]
778pub struct DecodedLog {
779    /// The raw `log` payload value.
780    pub log: Value,
781    /// The raw `trace` object, when present.
782    pub trace: Option<Value>,
783}
784
785/// Error returned by [`decode_protocol_event`].
786#[derive(Clone, Debug, PartialEq, Eq)]
787pub enum EventDecodeError {
788    /// `text` is not valid JSON.
789    InvalidJson(String),
790    /// The parsed JSON value failed base protocol validation.
791    InvalidEvent(String),
792}
793
794impl fmt::Display for EventDecodeError {
795    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
796        match self {
797            Self::InvalidJson(err) => write!(f, "invalid JSON: {err}"),
798            Self::InvalidEvent(err) => write!(f, "invalid protocol event: {err}"),
799        }
800    }
801}
802
803impl std::error::Error for EventDecodeError {}
804
805/// Parse one protocol v1 line, base-validate it, and return a typed decoded event.
806///
807/// `text` is a single JSON text value (one protocol line), not a JSONL stream.
808///
809/// The decode rules every SDK shares come from `serde_json` here: a repeated
810/// key keeps its last value, a lone surrogate `\u` escape is invalid, and
811/// arrays and objects nest at most 127 deep counting the envelope (serde_json's
812/// recursion limit). The other SDKs enforce the same rules explicitly, and
813/// `spec/fixtures/protocol_decode.json` pins each one on both sides.
814pub fn decode_protocol_event(text: &str) -> Result<DecodedEvent, EventDecodeError> {
815    let value: Value =
816        serde_json::from_str(text).map_err(|err| EventDecodeError::InvalidJson(err.to_string()))?;
817    validate_protocol_event(&value, false)
818        .map_err(|v| EventDecodeError::InvalidEvent(v.to_string()))?;
819
820    // Base validation above guarantees the envelope is an object with a
821    // recognized `kind`; the `ok_or_else` fallbacks below are defensive only.
822    let malformed = || {
823        EventDecodeError::InvalidEvent(
824            "event passed base validation but has an unexpected shape".to_string(),
825        )
826    };
827    let obj = value.as_object().ok_or_else(malformed)?;
828    let trace = obj.get("trace").cloned();
829    match obj.get("kind").and_then(Value::as_str) {
830        Some("result") => Ok(DecodedEvent::Result(DecodedResult {
831            result: obj.get("result").cloned().unwrap_or(Value::Null),
832            trace,
833        })),
834        Some("error") => {
835            let mut fields = obj
836                .get("error")
837                .and_then(Value::as_object)
838                .ok_or_else(malformed)?
839                .clone();
840            let code = fields
841                .remove("code")
842                .and_then(|v| v.as_str().map(str::to_string))
843                .ok_or_else(malformed)?;
844            let message = fields
845                .remove("message")
846                .and_then(|v| v.as_str().map(str::to_string))
847                .ok_or_else(malformed)?;
848            let retryable = fields.remove("retryable").and_then(|v| v.as_bool());
849            let hint = fields
850                .remove("hint")
851                .and_then(|v| v.as_str().map(str::to_string));
852            Ok(DecodedEvent::Error(DecodedError {
853                code,
854                message,
855                retryable,
856                hint,
857                fields,
858                trace,
859            }))
860        }
861        Some("progress") => Ok(DecodedEvent::Progress(DecodedProgress {
862            progress: obj.get("progress").cloned().unwrap_or(Value::Null),
863            trace,
864        })),
865        Some("log") => Ok(DecodedEvent::Log(DecodedLog {
866            log: obj.get("log").cloned().unwrap_or(Value::Null),
867            trace,
868        })),
869        _ => Err(malformed()),
870    }
871}