Skip to main content

tea_protocol/
event.rs

1use std::str::FromStr;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use serde_json::{Map, Value, json};
5use thiserror::Error;
6
7use crate::envelope::{deserialize_unique_value, validate_read_version};
8use crate::metadata::validate_json_bounds;
9use crate::{
10    ApprovalId, BranchId, CURRENT_PROTOCOL_VERSION, ContentBlock, EventId, ExactCost,
11    HostedToolOutcome, MAX_HOSTED_TOOL_SOURCES, MessageId, ProtocolMetadata, ProtocolTimestamp,
12    ProtocolVersion, RunId, SessionId, SessionSequence, ToolCallId, ToolPresentation, TurnId,
13    Usage,
14};
15
16/// Maximum UTF-8 bytes in one streaming delta.
17pub const MAX_EVENT_DELTA_BYTES: usize = 64 * 1024;
18/// Maximum UTF-8 bytes in one progress diagnostic.
19pub const MAX_PROGRESS_MESSAGE_BYTES: usize = 4096;
20/// Maximum capabilities or resources in one approval observation.
21pub const MAX_APPROVAL_ITEMS: usize = 64;
22/// Maximum UTF-8 bytes in one capability or resource string.
23pub const MAX_APPROVAL_ITEM_BYTES: usize = 1024;
24/// Maximum encoded JSON bytes retained while inspecting an unknown event.
25pub const MAX_UNKNOWN_EVENT_BYTES: usize = 64 * 1024;
26
27/// Compatibility classification for observable events.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum EventCompatibility {
31    /// Older observers may skip the event with a diagnostic.
32    SkippableObservation,
33    /// The event carries lifecycle or state information and cannot be skipped.
34    RequiredStateBearing,
35}
36
37/// Stable known event discriminators.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum AgentEventType {
41    /// A model/tool run started.
42    RunStarted,
43    /// A bounded streaming message fragment was produced.
44    MessageDelta,
45    /// A complete canonical tool call was requested.
46    ToolCallRequested,
47    /// Policy requires an approval decision.
48    ApprovalRequested,
49    /// A tool emitted non-durable progress.
50    ToolExecutionProgress,
51    /// A tool produced a non-durable execution preview.
52    ToolExecutionPreview,
53    /// A provider-hosted tool started executing inside the model response.
54    HostedToolStarted,
55    /// A provider-hosted tool completed inside the model response.
56    HostedToolCompleted,
57    /// A retryable model failure entered bounded backoff.
58    ModelRetryScheduled,
59    /// A scheduled model retry started its next provider request.
60    ModelRetryStarted,
61    /// A turn reached a durable checkpoint.
62    TurnCheckpointed,
63    /// A session compaction was committed.
64    SessionCompacted,
65    /// A session branch was forked.
66    SessionForked,
67    /// A run reached a terminal state.
68    RunFinished,
69}
70
71impl AgentEventType {
72    /// All currently supported event types.
73    pub const ALL: [Self; 14] = [
74        Self::RunStarted,
75        Self::MessageDelta,
76        Self::ToolCallRequested,
77        Self::ApprovalRequested,
78        Self::ToolExecutionProgress,
79        Self::ToolExecutionPreview,
80        Self::HostedToolStarted,
81        Self::HostedToolCompleted,
82        Self::ModelRetryScheduled,
83        Self::ModelRetryStarted,
84        Self::TurnCheckpointed,
85        Self::SessionCompacted,
86        Self::SessionForked,
87        Self::RunFinished,
88    ];
89
90    /// Returns whether an older observer may skip this event kind.
91    #[must_use]
92    pub const fn compatibility(self) -> EventCompatibility {
93        match self {
94            Self::MessageDelta
95            | Self::ToolExecutionProgress
96            | Self::ToolExecutionPreview
97            | Self::HostedToolStarted
98            | Self::HostedToolCompleted
99            | Self::ModelRetryScheduled
100            | Self::ModelRetryStarted => EventCompatibility::SkippableObservation,
101            Self::RunStarted
102            | Self::ToolCallRequested
103            | Self::ApprovalRequested
104            | Self::TurnCheckpointed
105            | Self::SessionCompacted
106            | Self::SessionForked
107            | Self::RunFinished => EventCompatibility::RequiredStateBearing,
108        }
109    }
110}
111
112/// A bounded streaming content delta.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum EventDelta {
115    /// Visible text fragment.
116    TextDelta {
117        /// Bounded UTF-8 fragment.
118        text: String,
119    },
120    /// Reasoning text fragment.
121    ThinkingDelta {
122        /// Bounded UTF-8 fragment.
123        text: String,
124    },
125}
126
127impl EventDelta {
128    fn validate(&self) -> Result<(), EventValidationError> {
129        let text = match self {
130            Self::TextDelta { text } | Self::ThinkingDelta { text } => text,
131        };
132        if text.is_empty() || text.len() > MAX_EVENT_DELTA_BYTES || text.contains('\0') {
133            Err(EventValidationError::InvalidDelta)
134        } else {
135            Ok(())
136        }
137    }
138}
139
140#[derive(Serialize, Deserialize)]
141#[serde(remote = "EventDelta", tag = "type", rename_all = "snake_case")]
142enum EventDeltaDef {
143    TextDelta { text: String },
144    ThinkingDelta { text: String },
145}
146
147impl Serialize for EventDelta {
148    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: Serializer,
151    {
152        self.validate().map_err(serde::ser::Error::custom)?;
153        EventDeltaDef::serialize(self, serializer)
154    }
155}
156
157impl<'de> Deserialize<'de> for EventDelta {
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: Deserializer<'de>,
161    {
162        let delta = EventDeltaDef::deserialize(deserializer)?;
163        delta.validate().map_err(serde::de::Error::custom)?;
164        Ok(delta)
165    }
166}
167
168/// Terminal status of an observable run.
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "snake_case")]
171pub enum RunStatus {
172    /// The run completed normally.
173    Completed,
174    /// The run was cancelled.
175    Cancelled,
176    /// The run failed with a known error.
177    Failed,
178    /// The process stopped before a provider/tool operation completed.
179    Interrupted,
180}
181
182/// A provider- and UI-neutral observable runtime event.
183#[derive(Debug, Clone, PartialEq)]
184pub enum AgentEvent {
185    /// A run started.
186    RunStarted {},
187    /// A streaming message fragment was produced.
188    MessageDelta {
189        /// Message receiving the fragment.
190        message_id: MessageId,
191        /// Zero-based content-block index.
192        content_index: u32,
193        /// Bounded content fragment.
194        delta: EventDelta,
195    },
196    /// A complete tool call was requested by the model.
197    ToolCallRequested {
198        /// Canonical tool-call identifier.
199        tool_call_id: ToolCallId,
200        /// Registered tool name.
201        tool_name: String,
202        /// Validated JSON object arguments.
203        arguments: Value,
204    },
205    /// A tool call requires approval.
206    ApprovalRequested {
207        /// Approval request identifier.
208        approval_id: ApprovalId,
209        /// Tool call awaiting approval.
210        tool_call_id: ToolCallId,
211        /// Bounded required capability names.
212        capabilities: Vec<String>,
213        /// Bounded affected resource identifiers.
214        resources: Vec<String>,
215        /// Decision deadline.
216        expires_at: ProtocolTimestamp,
217    },
218    /// Non-durable tool progress suitable for UI display.
219    ToolExecutionProgress {
220        /// Tool call reporting progress.
221        tool_call_id: ToolCallId,
222        /// English technical progress diagnostic.
223        message: String,
224        /// Completed work units.
225        completed_units: u64,
226        /// Total work units when known.
227        total_units: Option<u64>,
228    },
229    /// A trusted tool computed a bounded preview before a pending approval.
230    ToolExecutionPreview {
231        /// Tool call the preview describes.
232        tool_call_id: ToolCallId,
233        /// Typed UI-only data produced from the tool's current workspace view.
234        presentation: ToolPresentation,
235    },
236    /// A provider-hosted tool started inside the active model response.
237    HostedToolStarted {
238        /// Kernel-owned activity identifier.
239        tool_call_id: ToolCallId,
240        /// Stable hosted tool name.
241        tool_name: String,
242    },
243    /// A provider-hosted tool completed inside the active model response.
244    HostedToolCompleted {
245        /// Kernel-owned activity identifier.
246        tool_call_id: ToolCallId,
247        /// Stable hosted tool name.
248        tool_name: String,
249        /// Validated provider-neutral arguments, without continuation state.
250        arguments: Value,
251        /// Normalized terminal outcome.
252        outcome: HostedToolOutcome,
253        /// Number of normalized sources retained in the eventual durable block.
254        source_count: u32,
255    },
256    /// A retryable model failure is waiting before another provider request.
257    ModelRetryScheduled {
258        /// Ephemeral assistant message whose failed partial output is discarded.
259        message_id: MessageId,
260        /// One-based retry number.
261        attempt: u32,
262        /// Maximum retry count, excluding the initial request.
263        max_retries: u32,
264        /// Selected bounded delay in milliseconds.
265        delay_ms: u64,
266    },
267    /// A scheduled retry completed its backoff and is starting another request.
268    ModelRetryStarted {
269        /// Ephemeral assistant message reused by the next attempt.
270        message_id: MessageId,
271        /// One-based retry number.
272        attempt: u32,
273        /// Maximum retry count, excluding the initial request.
274        max_retries: u32,
275    },
276    /// A turn reached a durable checkpoint.
277    TurnCheckpointed {},
278    /// Session history was compacted through a source message.
279    SessionCompacted {
280        /// Summary message introduced by compaction.
281        summary_message_id: MessageId,
282        /// Last source message covered by the summary.
283        compacted_through_message_id: MessageId,
284    },
285    /// A new branch was forked from an existing branch/message.
286    SessionForked {
287        /// Source branch identifier.
288        source_branch_id: BranchId,
289        /// New branch identifier.
290        branch_id: BranchId,
291        /// Message at the fork point.
292        from_message_id: MessageId,
293    },
294    /// A run reached a terminal state.
295    RunFinished {
296        /// Terminal run status.
297        status: RunStatus,
298        /// Provider-neutral usage when available.
299        usage: Option<Usage>,
300        /// Exact billable cost when available.
301        cost: Option<ExactCost>,
302    },
303}
304
305impl AgentEvent {
306    /// Returns the stable event discriminator.
307    #[must_use]
308    pub const fn event_type(&self) -> AgentEventType {
309        match self {
310            Self::RunStarted {} => AgentEventType::RunStarted,
311            Self::MessageDelta { .. } => AgentEventType::MessageDelta,
312            Self::ToolCallRequested { .. } => AgentEventType::ToolCallRequested,
313            Self::ApprovalRequested { .. } => AgentEventType::ApprovalRequested,
314            Self::ToolExecutionProgress { .. } => AgentEventType::ToolExecutionProgress,
315            Self::ToolExecutionPreview { .. } => AgentEventType::ToolExecutionPreview,
316            Self::HostedToolStarted { .. } => AgentEventType::HostedToolStarted,
317            Self::HostedToolCompleted { .. } => AgentEventType::HostedToolCompleted,
318            Self::ModelRetryScheduled { .. } => AgentEventType::ModelRetryScheduled,
319            Self::ModelRetryStarted { .. } => AgentEventType::ModelRetryStarted,
320            Self::TurnCheckpointed {} => AgentEventType::TurnCheckpointed,
321            Self::SessionCompacted { .. } => AgentEventType::SessionCompacted,
322            Self::SessionForked { .. } => AgentEventType::SessionForked,
323            Self::RunFinished { .. } => AgentEventType::RunFinished,
324        }
325    }
326
327    fn validate(&self) -> Result<(), EventValidationError> {
328        match self {
329            Self::MessageDelta { delta, .. } => delta.validate(),
330            Self::ToolCallRequested {
331                tool_call_id,
332                tool_name,
333                arguments,
334            } => {
335                ContentBlock::tool_call(*tool_call_id, tool_name.clone(), arguments.clone())?;
336                Ok(())
337            }
338            Self::ApprovalRequested {
339                capabilities,
340                resources,
341                ..
342            } => {
343                validate_items(capabilities)?;
344                validate_items(resources)
345            }
346            Self::ToolExecutionProgress {
347                message,
348                completed_units,
349                total_units,
350                ..
351            } => {
352                if message.is_empty()
353                    || message.len() > MAX_PROGRESS_MESSAGE_BYTES
354                    || message.contains('\0')
355                    || *completed_units > crate::MAX_SAFE_INTEGER
356                    || total_units.is_some_and(|total| {
357                        total > crate::MAX_SAFE_INTEGER || *completed_units > total
358                    })
359                {
360                    return Err(EventValidationError::InvalidProgress);
361                }
362                Ok(())
363            }
364            Self::HostedToolStarted {
365                tool_call_id,
366                tool_name,
367            } => {
368                ContentBlock::tool_call(*tool_call_id, tool_name.clone(), json!({}))?;
369                Ok(())
370            }
371            Self::HostedToolCompleted {
372                tool_call_id,
373                tool_name,
374                arguments,
375                source_count,
376                ..
377            } => {
378                ContentBlock::tool_call(*tool_call_id, tool_name.clone(), arguments.clone())?;
379                let Ok(source_count) = usize::try_from(*source_count) else {
380                    return Err(EventValidationError::InvalidHostedToolObservation);
381                };
382                if source_count > MAX_HOSTED_TOOL_SOURCES {
383                    return Err(EventValidationError::InvalidHostedToolObservation);
384                }
385                Ok(())
386            }
387            Self::ModelRetryScheduled {
388                attempt,
389                max_retries,
390                delay_ms,
391                ..
392            } => validate_model_retry(*attempt, *max_retries, Some(*delay_ms)),
393            Self::ModelRetryStarted {
394                attempt,
395                max_retries,
396                ..
397            } => validate_model_retry(*attempt, *max_retries, None),
398            Self::ToolExecutionPreview { .. }
399            | Self::RunStarted {}
400            | Self::TurnCheckpointed {}
401            | Self::SessionCompacted { .. }
402            | Self::SessionForked { .. }
403            | Self::RunFinished { .. } => Ok(()),
404        }
405    }
406}
407
408#[derive(Serialize, Deserialize)]
409#[serde(
410    remote = "AgentEvent",
411    tag = "type",
412    content = "payload",
413    rename_all = "snake_case"
414)]
415enum AgentEventDef {
416    RunStarted {},
417    MessageDelta {
418        #[serde(rename = "messageId")]
419        message_id: MessageId,
420        #[serde(rename = "contentIndex")]
421        content_index: u32,
422        delta: EventDelta,
423    },
424    ToolCallRequested {
425        #[serde(rename = "toolCallId")]
426        tool_call_id: ToolCallId,
427        #[serde(rename = "toolName")]
428        tool_name: String,
429        arguments: Value,
430    },
431    ApprovalRequested {
432        #[serde(rename = "approvalId")]
433        approval_id: ApprovalId,
434        #[serde(rename = "toolCallId")]
435        tool_call_id: ToolCallId,
436        capabilities: Vec<String>,
437        resources: Vec<String>,
438        #[serde(rename = "expiresAt")]
439        expires_at: ProtocolTimestamp,
440    },
441    ToolExecutionProgress {
442        #[serde(rename = "toolCallId")]
443        tool_call_id: ToolCallId,
444        message: String,
445        #[serde(rename = "completedUnits")]
446        completed_units: u64,
447        #[serde(rename = "totalUnits", skip_serializing_if = "Option::is_none")]
448        total_units: Option<u64>,
449    },
450    ToolExecutionPreview {
451        #[serde(rename = "toolCallId")]
452        tool_call_id: ToolCallId,
453        presentation: ToolPresentation,
454    },
455    HostedToolStarted {
456        #[serde(rename = "toolCallId")]
457        tool_call_id: ToolCallId,
458        #[serde(rename = "toolName")]
459        tool_name: String,
460    },
461    HostedToolCompleted {
462        #[serde(rename = "toolCallId")]
463        tool_call_id: ToolCallId,
464        #[serde(rename = "toolName")]
465        tool_name: String,
466        arguments: Value,
467        outcome: HostedToolOutcome,
468        #[serde(rename = "sourceCount")]
469        source_count: u32,
470    },
471    ModelRetryScheduled {
472        #[serde(rename = "messageId")]
473        message_id: MessageId,
474        attempt: u32,
475        #[serde(rename = "maxRetries")]
476        max_retries: u32,
477        #[serde(rename = "delayMs")]
478        delay_ms: u64,
479    },
480    ModelRetryStarted {
481        #[serde(rename = "messageId")]
482        message_id: MessageId,
483        attempt: u32,
484        #[serde(rename = "maxRetries")]
485        max_retries: u32,
486    },
487    TurnCheckpointed {},
488    SessionCompacted {
489        #[serde(rename = "summaryMessageId")]
490        summary_message_id: MessageId,
491        #[serde(rename = "compactedThroughMessageId")]
492        compacted_through_message_id: MessageId,
493    },
494    SessionForked {
495        #[serde(rename = "sourceBranchId")]
496        source_branch_id: BranchId,
497        #[serde(rename = "branchId")]
498        branch_id: BranchId,
499        #[serde(rename = "fromMessageId")]
500        from_message_id: MessageId,
501    },
502    RunFinished {
503        status: RunStatus,
504        #[serde(skip_serializing_if = "Option::is_none")]
505        usage: Option<Usage>,
506        #[serde(skip_serializing_if = "Option::is_none")]
507        cost: Option<ExactCost>,
508    },
509}
510
511impl Serialize for AgentEvent {
512    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
513    where
514        S: Serializer,
515    {
516        self.validate().map_err(serde::ser::Error::custom)?;
517        AgentEventDef::serialize(self, serializer)
518    }
519}
520
521impl<'de> Deserialize<'de> for AgentEvent {
522    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
523    where
524        D: Deserializer<'de>,
525    {
526        let event = AgentEventDef::deserialize(deserializer)?;
527        event.validate().map_err(serde::de::Error::custom)?;
528        Ok(event)
529    }
530}
531
532/// A versioned observable event envelope.
533#[derive(Debug, Clone, PartialEq)]
534pub struct EventEnvelope {
535    protocol_version: ProtocolVersion,
536    event_id: EventId,
537    session_id: SessionId,
538    run_id: Option<RunId>,
539    turn_id: Option<TurnId>,
540    sequence: SessionSequence,
541    timestamp: ProtocolTimestamp,
542    metadata: ProtocolMetadata,
543    event: AgentEvent,
544}
545
546impl EventEnvelope {
547    /// Creates a validated current-version event envelope.
548    ///
549    /// # Errors
550    ///
551    /// Returns an error when references do not match the event kind or the
552    /// payload exceeds protocol bounds.
553    #[allow(clippy::too_many_arguments)]
554    pub fn new(
555        event_id: EventId,
556        session_id: SessionId,
557        run_id: Option<RunId>,
558        turn_id: Option<TurnId>,
559        sequence: SessionSequence,
560        timestamp: ProtocolTimestamp,
561        metadata: ProtocolMetadata,
562        event: AgentEvent,
563    ) -> Result<Self, EventValidationError> {
564        let envelope = Self {
565            protocol_version: CURRENT_PROTOCOL_VERSION,
566            event_id,
567            session_id,
568            run_id,
569            turn_id,
570            sequence,
571            timestamp,
572            metadata,
573            event,
574        };
575        envelope.validate()?;
576        Ok(envelope)
577    }
578
579    /// Inspects untrusted event JSON with explicit unknown-event behavior.
580    ///
581    /// # Errors
582    ///
583    /// Returns an error for malformed known events, unknown state-bearing
584    /// events, invalid compatibility markers, or unsupported protocol majors.
585    pub fn inspect_value(value: Value) -> Result<EventInspection, EventDecodeError> {
586        let discriminator = value
587            .as_object()
588            .and_then(|object| object.get("type"))
589            .and_then(Value::as_str)
590            .ok_or_else(|| EventDecodeError::Invalid("missing event type".to_owned()))?
591            .to_owned();
592        if AgentEventTypeText::from_str(&discriminator).is_ok() {
593            return serde_json::from_value(value)
594                .map(EventInspection::Known)
595                .map_err(|error| EventDecodeError::Invalid(error.to_string()));
596        }
597        inspect_unknown(&value, discriminator)
598    }
599
600    /// Returns the envelope protocol version.
601    #[must_use]
602    pub const fn protocol_version(&self) -> ProtocolVersion {
603        self.protocol_version
604    }
605
606    /// Returns the observable event identifier.
607    #[must_use]
608    pub const fn event_id(&self) -> EventId {
609        self.event_id
610    }
611
612    /// Returns the session identifier.
613    #[must_use]
614    pub const fn session_id(&self) -> SessionId {
615        self.session_id
616    }
617
618    /// Returns the related run identifier when present.
619    #[must_use]
620    pub const fn run_id(&self) -> Option<RunId> {
621        self.run_id
622    }
623
624    /// Returns the related turn identifier when present.
625    #[must_use]
626    pub const fn turn_id(&self) -> Option<TurnId> {
627        self.turn_id
628    }
629
630    /// Returns the authoritative session-local event sequence.
631    #[must_use]
632    pub const fn sequence(&self) -> SessionSequence {
633        self.sequence
634    }
635
636    /// Returns the event timestamp.
637    #[must_use]
638    pub const fn timestamp(&self) -> ProtocolTimestamp {
639        self.timestamp
640    }
641
642    /// Returns bounded extension metadata.
643    #[must_use]
644    pub const fn metadata(&self) -> &ProtocolMetadata {
645        &self.metadata
646    }
647
648    /// Returns the typed event payload.
649    #[must_use]
650    pub const fn event(&self) -> &AgentEvent {
651        &self.event
652    }
653
654    /// Returns the stable event discriminator.
655    #[must_use]
656    pub const fn event_type(&self) -> AgentEventType {
657        self.event.event_type()
658    }
659
660    fn validate(&self) -> Result<(), EventValidationError> {
661        self.event.validate()?;
662        let (requires_run, requires_turn) = match self.event.event_type() {
663            AgentEventType::RunStarted | AgentEventType::RunFinished => (true, false),
664            AgentEventType::MessageDelta
665            | AgentEventType::ToolCallRequested
666            | AgentEventType::ApprovalRequested
667            | AgentEventType::ToolExecutionProgress
668            | AgentEventType::ToolExecutionPreview
669            | AgentEventType::HostedToolStarted
670            | AgentEventType::HostedToolCompleted
671            | AgentEventType::ModelRetryScheduled
672            | AgentEventType::ModelRetryStarted
673            | AgentEventType::TurnCheckpointed => (true, true),
674            AgentEventType::SessionCompacted | AgentEventType::SessionForked => (false, false),
675        };
676        if self.run_id.is_some() != requires_run || self.turn_id.is_some() != requires_turn {
677            return Err(EventValidationError::InvalidReferences);
678        }
679        Ok(())
680    }
681}
682
683impl Serialize for EventEnvelope {
684    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
685    where
686        S: Serializer,
687    {
688        self.validate().map_err(serde::ser::Error::custom)?;
689        let mut value = serde_json::to_value(&self.event).map_err(serde::ser::Error::custom)?;
690        let object = value
691            .as_object_mut()
692            .ok_or_else(|| serde::ser::Error::custom("event must encode as object"))?;
693        object.insert("protocolVersion".to_owned(), json!(self.protocol_version));
694        object.insert("eventId".to_owned(), json!(self.event_id));
695        object.insert("sessionId".to_owned(), json!(self.session_id));
696        if let Some(run_id) = self.run_id {
697            object.insert("runId".to_owned(), json!(run_id));
698        }
699        if let Some(turn_id) = self.turn_id {
700            object.insert("turnId".to_owned(), json!(turn_id));
701        }
702        object.insert("sequence".to_owned(), json!(self.sequence));
703        object.insert("timestamp".to_owned(), json!(self.timestamp));
704        if self.event_type().compatibility() == EventCompatibility::SkippableObservation {
705            object.insert(
706                "compatibility".to_owned(),
707                json!(EventCompatibility::SkippableObservation),
708            );
709        }
710        if !self.metadata.is_empty() {
711            object.insert("metadata".to_owned(), json!(self.metadata));
712        }
713        value.serialize(serializer)
714    }
715}
716
717impl<'de> Deserialize<'de> for EventEnvelope {
718    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
719    where
720        D: Deserializer<'de>,
721    {
722        let mut value = deserialize_unique_value(deserializer)?;
723        let object = value
724            .as_object_mut()
725            .ok_or_else(|| serde::de::Error::custom("event envelope must be an object"))?;
726        let protocol_version = take(object, "protocolVersion").map_err(serde::de::Error::custom)?;
727        validate_read_version(protocol_version).map_err(serde::de::Error::custom)?;
728        let event_id = take(object, "eventId").map_err(serde::de::Error::custom)?;
729        let session_id = take(object, "sessionId").map_err(serde::de::Error::custom)?;
730        let run_id = take_optional(object, "runId").map_err(serde::de::Error::custom)?;
731        let turn_id = take_optional(object, "turnId").map_err(serde::de::Error::custom)?;
732        let sequence = take(object, "sequence").map_err(serde::de::Error::custom)?;
733        let timestamp = take(object, "timestamp").map_err(serde::de::Error::custom)?;
734        let metadata = take_optional(object, "metadata")
735            .map_err(serde::de::Error::custom)?
736            .unwrap_or_default();
737        object.remove("compatibility");
738        let event = AgentEvent::deserialize(Value::Object(std::mem::take(object)))
739            .map_err(serde::de::Error::custom)?;
740        let envelope = Self {
741            protocol_version,
742            event_id,
743            session_id,
744            run_id,
745            turn_id,
746            sequence,
747            timestamp,
748            metadata,
749            event,
750        };
751        envelope.validate().map_err(serde::de::Error::custom)?;
752        Ok(envelope)
753    }
754}
755
756/// Result of inspecting a known or forward-compatible observable event.
757#[derive(Debug, Clone, PartialEq)]
758pub enum EventInspection {
759    /// A fully understood typed event.
760    Known(EventEnvelope),
761    /// An explicitly skippable unknown observational event.
762    UnknownSkippable(UnknownSkippableEvent),
763}
764
765/// Validated common fields retained when skipping an unknown observation.
766#[derive(Debug, Clone, PartialEq, Eq)]
767pub struct UnknownSkippableEvent {
768    event_type: String,
769    protocol_version: ProtocolVersion,
770    event_id: EventId,
771    session_id: SessionId,
772    run_id: Option<RunId>,
773    turn_id: Option<TurnId>,
774    sequence: SessionSequence,
775    timestamp: ProtocolTimestamp,
776}
777
778impl UnknownSkippableEvent {
779    /// Returns the unknown canonical discriminator for bounded diagnostics.
780    #[must_use]
781    pub fn event_type(&self) -> &str {
782        &self.event_type
783    }
784
785    /// Returns the event's protocol version.
786    #[must_use]
787    pub const fn protocol_version(&self) -> ProtocolVersion {
788        self.protocol_version
789    }
790
791    /// Returns the authoritative sequence that observers must still advance.
792    #[must_use]
793    pub const fn sequence(&self) -> SessionSequence {
794        self.sequence
795    }
796
797    /// Returns the event identifier.
798    #[must_use]
799    pub const fn event_id(&self) -> EventId {
800        self.event_id
801    }
802
803    /// Returns the session identifier.
804    #[must_use]
805    pub const fn session_id(&self) -> SessionId {
806        self.session_id
807    }
808
809    /// Returns the related run identifier when present.
810    #[must_use]
811    pub const fn run_id(&self) -> Option<RunId> {
812        self.run_id
813    }
814
815    /// Returns the related turn identifier when present.
816    #[must_use]
817    pub const fn turn_id(&self) -> Option<TurnId> {
818        self.turn_id
819    }
820
821    /// Returns the event timestamp.
822    #[must_use]
823    pub const fn timestamp(&self) -> ProtocolTimestamp {
824        self.timestamp
825    }
826}
827
828/// Failure while inspecting an untrusted observable event.
829#[derive(Debug, Error)]
830pub enum EventDecodeError {
831    /// The unknown event is not explicitly safe for observers to skip.
832    #[error("unsupported state-bearing event type: {event_type}")]
833    UnsupportedStateBearing {
834        /// Bounded canonical unknown discriminator.
835        event_type: String,
836    },
837    /// The envelope, payload, or compatibility marker is malformed.
838    #[error("invalid event: {0}")]
839    Invalid(String),
840}
841
842/// Error returned when validating event data.
843#[derive(Debug, Error)]
844pub enum EventValidationError {
845    /// A streaming delta is empty, oversized, or contains a null character.
846    #[error("streaming delta is invalid")]
847    InvalidDelta,
848    /// Tool-call payload validation failed.
849    #[error("tool-call event payload is invalid: {0}")]
850    InvalidToolCall(#[from] crate::ContentValidationError),
851    /// Approval capabilities or resources exceed collection/string bounds.
852    #[error("approval event items are invalid")]
853    InvalidApprovalItems,
854    /// Progress details, units, or unit relationship are invalid.
855    #[error("tool progress event is invalid")]
856    InvalidProgress,
857    /// Hosted-tool observation source count exceeds the durable protocol bound.
858    #[error("hosted tool observation is invalid")]
859    InvalidHostedToolObservation,
860    /// Model retry attempt counts or delay exceed protocol bounds.
861    #[error("model retry observation is invalid")]
862    InvalidModelRetry,
863    /// Run and turn references do not match the event kind.
864    #[error("event runId/turnId references do not match the event kind")]
865    InvalidReferences,
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq)]
869struct AgentEventTypeText;
870
871impl FromStr for AgentEventTypeText {
872    type Err = ();
873
874    fn from_str(value: &str) -> Result<Self, Self::Err> {
875        if [
876            "run_started",
877            "message_delta",
878            "tool_call_requested",
879            "approval_requested",
880            "tool_execution_progress",
881            "tool_execution_preview",
882            "hosted_tool_started",
883            "hosted_tool_completed",
884            "model_retry_scheduled",
885            "model_retry_started",
886            "turn_checkpointed",
887            "session_compacted",
888            "session_forked",
889            "run_finished",
890        ]
891        .contains(&value)
892        {
893            Ok(Self)
894        } else {
895            Err(())
896        }
897    }
898}
899
900fn inspect_unknown(value: &Value, event_type: String) -> Result<EventInspection, EventDecodeError> {
901    if !valid_discriminator(&event_type) {
902        return Err(EventDecodeError::Invalid("invalid event type".to_owned()));
903    }
904    validate_json_bounds(value, MAX_UNKNOWN_EVENT_BYTES, 32)
905        .map_err(|error| EventDecodeError::Invalid(error.to_string()))?;
906    let object = value
907        .as_object()
908        .ok_or_else(|| EventDecodeError::Invalid("event must be an object".to_owned()))?;
909    let compatibility =
910        object
911            .get("compatibility")
912            .ok_or_else(|| EventDecodeError::UnsupportedStateBearing {
913                event_type: event_type.clone(),
914            })?;
915    let compatibility: EventCompatibility = serde_json::from_value(compatibility.clone())
916        .map_err(|error| EventDecodeError::Invalid(error.to_string()))?;
917    if compatibility != EventCompatibility::SkippableObservation {
918        return Err(EventDecodeError::UnsupportedStateBearing { event_type });
919    }
920    let protocol_version = field(object, "protocolVersion")?;
921    validate_read_version(protocol_version)
922        .map_err(|message| EventDecodeError::Invalid(message.to_owned()))?;
923    let payload = object
924        .get("payload")
925        .ok_or_else(|| EventDecodeError::Invalid("missing event payload".to_owned()))?;
926    validate_json_bounds(payload, MAX_UNKNOWN_EVENT_BYTES, 32)
927        .map_err(|error| EventDecodeError::Invalid(error.to_string()))?;
928    Ok(EventInspection::UnknownSkippable(UnknownSkippableEvent {
929        event_type,
930        protocol_version,
931        event_id: field(object, "eventId")?,
932        session_id: field(object, "sessionId")?,
933        run_id: optional_field(object, "runId")?,
934        turn_id: optional_field(object, "turnId")?,
935        sequence: field(object, "sequence")?,
936        timestamp: field(object, "timestamp")?,
937    }))
938}
939
940fn validate_items(items: &[String]) -> Result<(), EventValidationError> {
941    if items.is_empty()
942        || items.len() > MAX_APPROVAL_ITEMS
943        || items.iter().any(|item| {
944            item.is_empty()
945                || item.len() > MAX_APPROVAL_ITEM_BYTES
946                || item.chars().any(char::is_control)
947        })
948    {
949        Err(EventValidationError::InvalidApprovalItems)
950    } else {
951        Ok(())
952    }
953}
954
955fn validate_model_retry(
956    attempt: u32,
957    max_retries: u32,
958    delay_ms: Option<u64>,
959) -> Result<(), EventValidationError> {
960    if attempt == 0
961        || max_retries == 0
962        || attempt > max_retries
963        || delay_ms.is_some_and(|delay| delay > crate::MAX_SAFE_INTEGER)
964    {
965        Err(EventValidationError::InvalidModelRetry)
966    } else {
967        Ok(())
968    }
969}
970
971fn valid_discriminator(value: &str) -> bool {
972    !value.is_empty()
973        && value.len() <= 128
974        && value
975            .bytes()
976            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
977}
978
979fn take<T>(object: &mut Map<String, Value>, key: &str) -> Result<T, serde_json::Error>
980where
981    T: for<'de> Deserialize<'de>,
982{
983    serde_json::from_value(object.remove(key).unwrap_or(Value::Null))
984}
985
986fn take_optional<T>(
987    object: &mut Map<String, Value>,
988    key: &str,
989) -> Result<Option<T>, serde_json::Error>
990where
991    T: for<'de> Deserialize<'de>,
992{
993    object.remove(key).map_or(Ok(None), serde_json::from_value)
994}
995
996fn field<T>(object: &Map<String, Value>, key: &str) -> Result<T, EventDecodeError>
997where
998    T: for<'de> Deserialize<'de>,
999{
1000    serde_json::from_value(object.get(key).cloned().unwrap_or(Value::Null))
1001        .map_err(|error| EventDecodeError::Invalid(error.to_string()))
1002}
1003
1004fn optional_field<T>(object: &Map<String, Value>, key: &str) -> Result<Option<T>, EventDecodeError>
1005where
1006    T: for<'de> Deserialize<'de>,
1007{
1008    object
1009        .get(key)
1010        .cloned()
1011        .map_or(Ok(None), serde_json::from_value)
1012        .map_err(|error| EventDecodeError::Invalid(error.to_string()))
1013}