Skip to main content

agent_first_data/
protocol.rs

1use serde::Serialize;
2use serde_json::Value;
3use std::fmt;
4use std::ops::Deref;
5
6// ═══════════════════════════════════════════
7// Event Type and Build Errors (0.16 API)
8// ═══════════════════════════════════════════
9
10/// A typed, strict-valid AFDATA protocol v1 event.
11///
12/// Wraps the complete JSON envelope and provides access to the underlying value.
13/// Events produced by builders are guaranteed to pass `validate_protocol_event(_, true)`.
14#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
15pub struct Event(Value);
16
17impl Event {
18    /// Access the underlying JSON value.
19    pub fn as_value(&self) -> &Value {
20        &self.0
21    }
22
23    /// Convert into the underlying JSON value.
24    pub fn into_value(self) -> Value {
25        self.0
26    }
27}
28
29impl From<Event> for Value {
30    fn from(event: Event) -> Self {
31        event.0
32    }
33}
34
35impl Deref for Event {
36    type Target = Value;
37
38    fn deref(&self) -> &Self::Target {
39        &self.0
40    }
41}
42
43impl fmt::Display for Event {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}", self.0)
46    }
47}
48
49/// Error type for builder failures.
50///
51/// Errors occur when:
52/// - A reserved error field is overwritten (code, message, hint, retryable)
53/// - An object field (.fields or .extend) is not a JSON object
54/// - A required field (code/message for convenience functions) is empty
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum BuildError {
57    ReservedField(String),
58    NonObjectField(String),
59    EmptyRequiredField(String),
60}
61
62impl fmt::Display for BuildError {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::ReservedField(msg) => write!(f, "reserved field: {msg}"),
66            Self::NonObjectField(msg) => write!(f, "non-object field: {msg}"),
67            Self::EmptyRequiredField(msg) => write!(f, "empty required field: {msg}"),
68        }
69    }
70}
71
72impl std::error::Error for BuildError {}
73
74/// Log level enumeration (serialized as lowercase).
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
76#[serde(rename_all = "lowercase")]
77pub enum LogLevel {
78    Debug,
79    Info,
80    Warn,
81    Error,
82}
83
84impl LogLevel {
85    pub(crate) fn as_str(&self) -> &'static str {
86        match self {
87            Self::Debug => "debug",
88            Self::Info => "info",
89            Self::Warn => "warn",
90            Self::Error => "error",
91        }
92    }
93}
94
95// ═══════════════════════════════════════════
96// Result Builder
97// ═══════════════════════════════════════════
98
99/// Builder for result events.
100pub struct ResultBuilder {
101    payload: Value,
102    trace: Option<Value>,
103}
104
105impl ResultBuilder {
106    /// Set the trace object.
107    pub fn trace(mut self, trace: Value) -> Self {
108        self.trace = Some(trace);
109        self
110    }
111
112    /// Build the event.
113    pub fn build(self) -> Result<Event, BuildError> {
114        let trace = self
115            .trace
116            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
117        let mut obj = serde_json::Map::new();
118        obj.insert("kind".to_string(), Value::String("result".to_string()));
119        obj.insert("result".to_string(), self.payload);
120        obj.insert("trace".to_string(), trace);
121        Ok(Event(Value::Object(obj)))
122    }
123}
124
125/// Fluent builder: start building a result event.
126pub fn json_result(payload: Value) -> ResultBuilder {
127    ResultBuilder {
128        payload,
129        trace: None,
130    }
131}
132
133// ═══════════════════════════════════════════
134// Error Builder
135// ═══════════════════════════════════════════
136
137/// Builder for error events.
138pub struct ErrorBuilder {
139    code: String,
140    message: String,
141    retryable: bool,
142    hint: Option<String>,
143    fields: serde_json::Map<String, Value>,
144    trace: Option<Value>,
145    build_error: Option<BuildError>,
146}
147
148impl ErrorBuilder {
149    /// Mark the error as retryable.
150    pub fn retryable(mut self) -> Self {
151        self.retryable = true;
152        self
153    }
154
155    /// Set retryable based on a boolean condition.
156    pub fn retryable_if(mut self, should_retry: bool) -> Self {
157        self.retryable = should_retry;
158        self
159    }
160
161    /// Set the hint.
162    pub fn hint(mut self, hint: &str) -> Self {
163        self.hint = Some(hint.to_string());
164        self
165    }
166
167    /// Set the hint if present.
168    pub fn hint_if_some(mut self, hint: Option<&str>) -> Self {
169        if let Some(h) = hint {
170            self.hint = Some(h.to_string());
171        }
172        self
173    }
174
175    /// Add an extension field.
176    pub fn field(mut self, name: &str, value: Value) -> Self {
177        if self.build_error.is_none() {
178            match name {
179                "code" | "message" | "hint" | "retryable" => {
180                    self.build_error = Some(BuildError::ReservedField(format!(
181                        "cannot write reserved field {name:?} to error payload"
182                    )));
183                }
184                _ => {
185                    self.fields.insert(name.to_string(), value);
186                }
187            }
188        }
189        self
190    }
191
192    /// Add multiple extension fields from a JSON object.
193    pub fn fields(mut self, fields: Value) -> Self {
194        if self.build_error.is_none() {
195            match fields {
196                Value::Object(map) => {
197                    for (k, v) in map {
198                        match k.as_str() {
199                            "code" | "message" | "hint" | "retryable" => {
200                                self.build_error = Some(BuildError::ReservedField(format!(
201                                    "cannot write reserved field {k:?} to error payload"
202                                )));
203                                return self;
204                            }
205                            _ => {
206                                self.fields.insert(k, v);
207                            }
208                        }
209                    }
210                }
211                _ => {
212                    self.build_error = Some(BuildError::NonObjectField(
213                        "fields() argument must be a JSON object".to_string(),
214                    ));
215                }
216            }
217        }
218        self
219    }
220
221    /// Extend with a serializable value (must serialize to JSON object).
222    pub fn extend<T: Serialize>(mut self, value: T) -> Self {
223        if self.build_error.is_none() {
224            match serde_json::to_value(&value) {
225                Ok(Value::Object(map)) => {
226                    for (k, v) in map {
227                        match k.as_str() {
228                            "code" | "message" | "hint" | "retryable" => {
229                                self.build_error = Some(BuildError::ReservedField(format!(
230                                    "cannot write reserved field {k:?} to error payload"
231                                )));
232                                return self;
233                            }
234                            _ => {
235                                self.fields.insert(k, v);
236                            }
237                        }
238                    }
239                }
240                Ok(_) => {
241                    self.build_error = Some(BuildError::NonObjectField(
242                        "extend() argument must serialize to a JSON object".to_string(),
243                    ));
244                }
245                Err(_) => {
246                    self.build_error = Some(BuildError::NonObjectField(
247                        "extend() argument serialization failed".to_string(),
248                    ));
249                }
250            }
251        }
252        self
253    }
254
255    /// Set the trace object.
256    pub fn trace(mut self, trace: Value) -> Self {
257        self.trace = Some(trace);
258        self
259    }
260
261    /// Build the event.
262    pub fn build(self) -> Result<Event, BuildError> {
263        if let Some(err) = self.build_error {
264            return Err(err);
265        }
266
267        let mut error_obj = self.fields;
268        error_obj.insert("code".to_string(), Value::String(self.code));
269        error_obj.insert("message".to_string(), Value::String(self.message));
270        error_obj.insert("retryable".to_string(), Value::Bool(self.retryable));
271        if let Some(h) = self.hint {
272            error_obj.insert("hint".to_string(), Value::String(h));
273        }
274
275        let trace = self
276            .trace
277            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
278        let mut obj = serde_json::Map::new();
279        obj.insert("kind".to_string(), Value::String("error".to_string()));
280        obj.insert("error".to_string(), Value::Object(error_obj));
281        obj.insert("trace".to_string(), trace);
282
283        Ok(Event(Value::Object(obj)))
284    }
285}
286
287/// Fluent builder: start building an error event.
288///
289/// Panics if `code` or `message` is empty (required by protocol contract).
290#[allow(clippy::panic)]
291pub fn json_error(code: &str, message: &str) -> ErrorBuilder {
292    if code.is_empty() {
293        panic!("json_error: code must not be empty");
294    }
295    if message.is_empty() {
296        panic!("json_error: message must not be empty");
297    }
298    ErrorBuilder {
299        code: code.to_string(),
300        message: message.to_string(),
301        retryable: false,
302        hint: None,
303        fields: serde_json::Map::new(),
304        trace: None,
305        build_error: None,
306    }
307}
308
309// ═══════════════════════════════════════════
310// Progress Builder
311// ═══════════════════════════════════════════
312
313/// Builder for progress events.
314pub struct ProgressBuilder {
315    payload: Value,
316    trace: Option<Value>,
317}
318
319impl ProgressBuilder {
320    /// Set the trace object.
321    pub fn trace(mut self, trace: Value) -> Self {
322        self.trace = Some(trace);
323        self
324    }
325
326    /// Build the event.
327    pub fn build(self) -> Result<Event, BuildError> {
328        let trace = self
329            .trace
330            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
331        let mut obj = serde_json::Map::new();
332        obj.insert("kind".to_string(), Value::String("progress".to_string()));
333        obj.insert("progress".to_string(), self.payload);
334        obj.insert("trace".to_string(), trace);
335
336        Ok(Event(Value::Object(obj)))
337    }
338}
339
340/// Fluent builder: start building a progress event.
341///
342pub fn json_progress(payload: Value) -> ProgressBuilder {
343    ProgressBuilder {
344        payload,
345        trace: None,
346    }
347}
348
349// ═══════════════════════════════════════════
350// Log Builder
351// ═══════════════════════════════════════════
352
353/// Builder for log events.
354pub struct LogBuilder {
355    payload: Value,
356    trace: Option<Value>,
357}
358
359impl LogBuilder {
360    /// Set the trace object.
361    pub fn trace(mut self, trace: Value) -> Self {
362        self.trace = Some(trace);
363        self
364    }
365
366    /// Build the event.
367    pub fn build(self) -> Result<Event, BuildError> {
368        let trace = self
369            .trace
370            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
371        let mut obj = serde_json::Map::new();
372        obj.insert("kind".to_string(), Value::String("log".to_string()));
373        obj.insert("log".to_string(), self.payload);
374        obj.insert("trace".to_string(), trace);
375
376        Ok(Event(Value::Object(obj)))
377    }
378}
379
380/// Fluent builder: start building a log event.
381///
382pub fn json_log(payload: Value) -> LogBuilder {
383    LogBuilder {
384        payload,
385        trace: None,
386    }
387}
388
389// ═══════════════════════════════════════════
390// CLI Helper
391// ═══════════════════════════════════════════
392
393/// Build a CLI error event with optional hint.
394///
395/// Equivalent to: `json_error("cli_error", message).hint_if_some(hint).build()`
396///
397/// Always returns an event with:
398/// - code: "cli_error"
399/// - retryable: false
400/// - trace: {}
401///
402/// Panics if `message` is empty (required by protocol contract).
403#[allow(clippy::panic, clippy::expect_used)]
404pub fn build_cli_error(message: &str, hint: Option<&str>) -> Event {
405    if message.is_empty() {
406        panic!("build_cli_error: message must not be empty");
407    }
408    json_error("cli_error", message)
409        .hint_if_some(hint)
410        .build()
411        .expect("build_cli_error: builder returned error unexpectedly")
412}
413
414// ═══════════════════════════════════════════
415// Validation
416// ═══════════════════════════════════════════
417
418/// Validate one protocol v1 event envelope.
419///
420/// `strict` additionally enforces the recommended strict profile: `trace` is
421/// required, and kind-specific payload shapes are checked (see
422/// [`validate_protocol_event_strict_payload`]).
423pub fn validate_protocol_event(event: &Value, strict: bool) -> Result<(), String> {
424    validate_protocol_event_base(event)?;
425    if strict {
426        validate_protocol_event_strict_payload(event)?;
427    }
428    Ok(())
429}
430
431fn validate_protocol_event_base(event: &Value) -> Result<(), String> {
432    let Some(obj) = event.as_object() else {
433        return Err("event must be a JSON object".to_string());
434    };
435    let Some(kind) = obj.get("kind").and_then(Value::as_str) else {
436        return Err("event.kind must be one of result, error, progress, log".to_string());
437    };
438    if !matches!(kind, "result" | "error" | "progress" | "log") {
439        return Err(format!("unsupported event kind {kind:?}"));
440    }
441    if !obj.contains_key(kind) {
442        return Err(format!("event payload field {kind:?} is required"));
443    }
444    for key in obj.keys() {
445        if key != "kind" && key != kind && key != "trace" {
446            return Err(format!("unexpected top-level field {key:?}"));
447        }
448    }
449    if let Some(trace) = obj.get("trace")
450        && !trace.is_object()
451    {
452        return Err("event.trace must be a JSON object when present".to_string());
453    }
454    if kind == "error" {
455        validate_error_payload(obj.get("error"))?;
456    }
457    Ok(())
458}
459
460fn validate_error_payload(error: Option<&Value>) -> Result<(), String> {
461    let Some(error) = error.and_then(Value::as_object) else {
462        return Err("event.error must be a JSON object".to_string());
463    };
464    match error.get("code").and_then(Value::as_str) {
465        Some(code) if !code.is_empty() => {}
466        _ => return Err("event.error.code must be a non-empty string".to_string()),
467    }
468    match error.get("message").and_then(Value::as_str) {
469        Some(message) if !message.is_empty() => {}
470        _ => return Err("event.error.message must be a non-empty string".to_string()),
471    }
472    if error.get("hint").is_some_and(|hint| !hint.is_string()) {
473        return Err("event.error.hint must be a string when present".to_string());
474    }
475    Ok(())
476}
477
478/// Validate a finite structured CLI event stream:
479/// `(log | progress)* -> exactly one (result | error) -> end`.
480///
481/// `strict` is forwarded to [`validate_protocol_event`] for every event.
482pub fn validate_protocol_stream(events: &[Value], strict: bool) -> Result<(), String> {
483    let mut terminal_kind: Option<&str> = None;
484    for (idx, event) in events.iter().enumerate() {
485        validate_protocol_event(event, strict).map_err(|err| format!("event {idx}: {err}"))?;
486        let Some(kind) = event.get("kind").and_then(Value::as_str) else {
487            return Err(format!("event {idx}: missing kind"));
488        };
489        match kind {
490            "log" | "progress" => {
491                if terminal_kind.is_some() {
492                    return Err(format!("event {idx}: non-terminal event after terminal"));
493                }
494            }
495            "result" | "error" => {
496                if terminal_kind.is_some() {
497                    return Err(format!("event {idx}: duplicate terminal event"));
498                }
499                terminal_kind = Some(kind);
500            }
501            _ => return Err(format!("event {idx}: unsupported event kind {kind:?}")),
502        }
503    }
504    if terminal_kind.is_none() {
505        return Err("event stream must contain exactly one terminal result or error".to_string());
506    }
507    Ok(())
508}
509
510/// Validate one protocol v1 event's payload against the recommended strict profile.
511///
512/// Assumes the base envelope shape (see [`validate_protocol_event_base`]) already passed.
513fn validate_protocol_event_strict_payload(event: &Value) -> Result<(), String> {
514    if !event.get("trace").is_some_and(Value::is_object) {
515        return Err("event.trace is required by the strict profile".to_string());
516    }
517    match event.get("kind").and_then(Value::as_str) {
518        Some("error") => validate_strict_error_payload(event.get("error")),
519        _ => Ok(()),
520    }
521}
522
523fn validate_strict_error_payload(error: Option<&Value>) -> Result<(), String> {
524    let Some(error) = error.and_then(Value::as_object) else {
525        return Err("event.error must be a JSON object in the strict profile".to_string());
526    };
527    require_non_empty_string(error, "code", "event.error")?;
528    require_non_empty_string(error, "message", "event.error")?;
529    if error.get("retryable").and_then(Value::as_bool).is_none() {
530        return Err("event.error.retryable must be a boolean in the strict profile".to_string());
531    }
532    if error.contains_key("hint") && !error.get("hint").is_some_and(Value::is_string) {
533        return Err("event.error.hint must be a string when present".to_string());
534    }
535    Ok(())
536}
537
538fn require_non_empty_string(
539    payload: &serde_json::Map<String, Value>,
540    field: &str,
541    path: &str,
542) -> Result<(), String> {
543    if payload
544        .get(field)
545        .and_then(Value::as_str)
546        .is_some_and(|value| !value.is_empty())
547    {
548        return Ok(());
549    }
550    Err(format!(
551        "{path}.{field} must be a non-empty string in the strict profile"
552    ))
553}
554
555// ═══════════════════════════════════════════
556// Reader API: decode_protocol_event
557// ═══════════════════════════════════════════
558
559/// A decoded, strict-valid AFDATA protocol v1 event, typed by kind.
560#[derive(Clone, Debug, PartialEq)]
561pub enum DecodedEvent {
562    Result(DecodedResult),
563    Error(DecodedError),
564    Progress(DecodedProgress),
565    Log(DecodedLog),
566}
567
568/// Decoded `kind:"result"` event.
569#[derive(Clone, Debug, PartialEq)]
570pub struct DecodedResult {
571    /// The raw `result` payload value.
572    pub result: Value,
573    /// The raw `trace` object, when present.
574    pub trace: Option<Value>,
575}
576
577/// Decoded `kind:"error"` event.
578#[derive(Clone, Debug, PartialEq)]
579pub struct DecodedError {
580    pub code: String,
581    pub message: String,
582    pub retryable: bool,
583    pub hint: Option<String>,
584    /// Payload keys beyond `code`, `message`, `retryable`, `hint`.
585    pub fields: serde_json::Map<String, Value>,
586    /// The raw `trace` object, when present.
587    pub trace: Option<Value>,
588}
589
590/// Decoded `kind:"progress"` event.
591#[derive(Clone, Debug, PartialEq)]
592pub struct DecodedProgress {
593    /// The raw `progress` payload value.
594    pub progress: Value,
595    /// The raw `trace` object, when present.
596    pub trace: Option<Value>,
597}
598
599/// Decoded `kind:"log"` event.
600#[derive(Clone, Debug, PartialEq)]
601pub struct DecodedLog {
602    /// The raw `log` payload value.
603    pub log: Value,
604    /// The raw `trace` object, when present.
605    pub trace: Option<Value>,
606}
607
608/// Error returned by [`decode_protocol_event`].
609#[derive(Clone, Debug, PartialEq, Eq)]
610pub enum EventDecodeError {
611    /// `text` is not valid JSON.
612    InvalidJson(String),
613    /// The parsed JSON value failed strict protocol validation.
614    InvalidEvent(String),
615}
616
617impl fmt::Display for EventDecodeError {
618    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
619        match self {
620            Self::InvalidJson(err) => write!(f, "invalid JSON: {err}"),
621            Self::InvalidEvent(err) => write!(f, "invalid protocol event: {err}"),
622        }
623    }
624}
625
626impl std::error::Error for EventDecodeError {}
627
628/// Parse one protocol v1 line, strict-validate it, and return a typed decoded event.
629///
630/// `text` is a single JSON text value (one protocol line), not a JSONL stream.
631pub fn decode_protocol_event(text: &str) -> Result<DecodedEvent, EventDecodeError> {
632    let value: Value =
633        serde_json::from_str(text).map_err(|err| EventDecodeError::InvalidJson(err.to_string()))?;
634    validate_protocol_event(&value, true).map_err(EventDecodeError::InvalidEvent)?;
635
636    // Strict validation above guarantees the envelope is an object with a
637    // recognized `kind`; the `ok_or_else` fallbacks below are defensive only.
638    let malformed = || {
639        EventDecodeError::InvalidEvent(
640            "event passed strict validation but has an unexpected shape".to_string(),
641        )
642    };
643    let obj = value.as_object().ok_or_else(malformed)?;
644    let trace = obj.get("trace").cloned();
645    match obj.get("kind").and_then(Value::as_str) {
646        Some("result") => Ok(DecodedEvent::Result(DecodedResult {
647            result: obj.get("result").cloned().unwrap_or(Value::Null),
648            trace,
649        })),
650        Some("error") => {
651            let mut fields = obj
652                .get("error")
653                .and_then(Value::as_object)
654                .ok_or_else(malformed)?
655                .clone();
656            let code = fields
657                .remove("code")
658                .and_then(|v| v.as_str().map(str::to_string))
659                .ok_or_else(malformed)?;
660            let message = fields
661                .remove("message")
662                .and_then(|v| v.as_str().map(str::to_string))
663                .ok_or_else(malformed)?;
664            let retryable = fields
665                .remove("retryable")
666                .and_then(|v| v.as_bool())
667                .ok_or_else(malformed)?;
668            let hint = fields
669                .remove("hint")
670                .and_then(|v| v.as_str().map(str::to_string));
671            Ok(DecodedEvent::Error(DecodedError {
672                code,
673                message,
674                retryable,
675                hint,
676                fields,
677                trace,
678            }))
679        }
680        Some("progress") => Ok(DecodedEvent::Progress(DecodedProgress {
681            progress: obj.get("progress").cloned().unwrap_or(Value::Null),
682            trace,
683        })),
684        Some("log") => Ok(DecodedEvent::Log(DecodedLog {
685            log: obj.get("log").cloned().unwrap_or(Value::Null),
686            trace,
687        })),
688        _ => Err(malformed()),
689    }
690}