tirea_protocol_ai_sdk_v6/
events.rs

1use serde::de::Error as DeError;
2use serde::{Deserialize, Deserializer, Serialize};
3use serde_json::Value;
4
5/// Stream event types compatible with AI SDK v6.
6///
7/// These events map directly to the AI SDK UI Message Stream protocol.
8/// See: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10#[serde(tag = "type", rename_all = "kebab-case")]
11pub enum UIStreamEvent {
12    // ========================================================================
13    // Message Lifecycle
14    // ========================================================================
15    /// Indicates the beginning of a new message with metadata.
16    ///
17    /// AI SDK v6 expects this as `{"type":"start","messageId":"..."}`.
18    #[serde(rename = "start")]
19    MessageStart {
20        /// Unique identifier for this message.
21        #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
22        message_id: Option<String>,
23        /// Optional message metadata.
24        #[serde(rename = "messageMetadata", skip_serializing_if = "Option::is_none")]
25        message_metadata: Option<Value>,
26    },
27
28    // ========================================================================
29    // Text Streaming (start/delta/end pattern)
30    // ========================================================================
31    /// Indicates the beginning of a text block.
32    TextStart {
33        /// Unique identifier for this text block.
34        id: String,
35        /// Optional provider metadata.
36        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
37        provider_metadata: Option<Value>,
38    },
39
40    /// Contains incremental text content for the text block.
41    TextDelta {
42        /// Identifier matching the text-start event.
43        id: String,
44        /// Incremental text content.
45        delta: String,
46        /// Optional provider metadata.
47        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
48        provider_metadata: Option<Value>,
49    },
50
51    /// Indicates the end of a text block.
52    TextEnd {
53        /// Identifier matching the text-start event.
54        id: String,
55        /// Optional provider metadata.
56        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
57        provider_metadata: Option<Value>,
58    },
59
60    // ========================================================================
61    // Reasoning Streaming
62    // ========================================================================
63    /// Indicates the beginning of a reasoning block.
64    ReasoningStart {
65        /// Unique identifier for this reasoning block.
66        id: String,
67        /// Optional provider metadata.
68        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
69        provider_metadata: Option<Value>,
70    },
71
72    /// Contains incremental reasoning content.
73    ReasoningDelta {
74        /// Identifier matching the reasoning-start event.
75        id: String,
76        /// Incremental reasoning content.
77        delta: String,
78        /// Optional provider metadata.
79        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
80        provider_metadata: Option<Value>,
81    },
82
83    /// Indicates the end of a reasoning block.
84    ReasoningEnd {
85        /// Identifier matching the reasoning-start event.
86        id: String,
87        /// Optional provider metadata.
88        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
89        provider_metadata: Option<Value>,
90    },
91
92    // ========================================================================
93    // Tool Input Streaming
94    // ========================================================================
95    /// Indicates the beginning of tool input streaming.
96    ToolInputStart {
97        /// Unique identifier for this tool call.
98        #[serde(rename = "toolCallId")]
99        tool_call_id: String,
100        /// Name of the tool being called.
101        #[serde(rename = "toolName")]
102        tool_name: String,
103        /// Whether the provider executed this tool directly.
104        #[serde(rename = "providerExecuted", skip_serializing_if = "Option::is_none")]
105        provider_executed: Option<bool>,
106        /// Whether this is a dynamic tool part.
107        #[serde(skip_serializing_if = "Option::is_none")]
108        dynamic: Option<bool>,
109        /// Optional UI title.
110        #[serde(skip_serializing_if = "Option::is_none")]
111        title: Option<String>,
112    },
113
114    /// Contains incremental chunks of tool input as it's being generated.
115    ToolInputDelta {
116        /// Identifier matching the tool-input-start event.
117        #[serde(rename = "toolCallId")]
118        tool_call_id: String,
119        /// Incremental tool input text.
120        #[serde(rename = "inputTextDelta")]
121        input_text_delta: String,
122    },
123
124    /// Indicates that tool input is complete and ready for execution.
125    ToolInputAvailable {
126        /// Identifier matching the tool-input-start event.
127        #[serde(rename = "toolCallId")]
128        tool_call_id: String,
129        /// Name of the tool being called.
130        #[serde(rename = "toolName")]
131        tool_name: String,
132        /// Complete tool input as JSON.
133        input: Value,
134        /// Whether the provider executed this tool directly.
135        #[serde(rename = "providerExecuted", skip_serializing_if = "Option::is_none")]
136        provider_executed: Option<bool>,
137        /// Optional provider metadata.
138        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
139        provider_metadata: Option<Value>,
140        /// Whether this is a dynamic tool part.
141        #[serde(skip_serializing_if = "Option::is_none")]
142        dynamic: Option<bool>,
143        /// Optional UI title.
144        #[serde(skip_serializing_if = "Option::is_none")]
145        title: Option<String>,
146    },
147
148    /// Indicates tool input validation failed before execution.
149    ///
150    /// NOTE: Not currently emitted. Reserved for future client-side tool
151    /// input validation. Backend tool input errors surface via `tool-output-error`.
152    ToolInputError {
153        /// Identifier matching the tool-input-start event.
154        #[serde(rename = "toolCallId")]
155        tool_call_id: String,
156        /// Name of the tool being called.
157        #[serde(rename = "toolName")]
158        tool_name: String,
159        /// Tool input payload.
160        input: Value,
161        /// Whether the provider executed this tool directly.
162        #[serde(rename = "providerExecuted", skip_serializing_if = "Option::is_none")]
163        provider_executed: Option<bool>,
164        /// Optional provider metadata.
165        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
166        provider_metadata: Option<Value>,
167        /// Whether this is a dynamic tool part.
168        #[serde(skip_serializing_if = "Option::is_none")]
169        dynamic: Option<bool>,
170        /// Error text.
171        #[serde(rename = "errorText")]
172        error_text: String,
173        /// Optional UI title.
174        #[serde(skip_serializing_if = "Option::is_none")]
175        title: Option<String>,
176    },
177
178    /// Requests user approval for a tool call.
179    ToolApprovalRequest {
180        /// Approval request ID.
181        #[serde(rename = "approvalId")]
182        approval_id: String,
183        /// Tool call ID this approval applies to.
184        #[serde(rename = "toolCallId")]
185        tool_call_id: String,
186    },
187
188    // ========================================================================
189    // Tool Output
190    // ========================================================================
191    /// Contains the result of tool execution.
192    ToolOutputAvailable {
193        /// Identifier matching the tool-input-start event.
194        #[serde(rename = "toolCallId")]
195        tool_call_id: String,
196        /// Tool execution result as JSON.
197        output: Value,
198        /// Whether the provider executed this tool directly.
199        #[serde(rename = "providerExecuted", skip_serializing_if = "Option::is_none")]
200        provider_executed: Option<bool>,
201        /// Whether this is a dynamic tool part.
202        #[serde(skip_serializing_if = "Option::is_none")]
203        dynamic: Option<bool>,
204        /// Marks provisional tool output.
205        #[serde(skip_serializing_if = "Option::is_none")]
206        preliminary: Option<bool>,
207    },
208
209    /// Indicates the tool output was denied by user approval.
210    ToolOutputDenied {
211        /// Identifier matching the tool-input-start event.
212        #[serde(rename = "toolCallId")]
213        tool_call_id: String,
214    },
215
216    /// Indicates tool output failed with an execution error.
217    ToolOutputError {
218        /// Identifier matching the tool-input-start event.
219        #[serde(rename = "toolCallId")]
220        tool_call_id: String,
221        /// Error text.
222        #[serde(rename = "errorText")]
223        error_text: String,
224        /// Whether the provider executed this tool directly.
225        #[serde(rename = "providerExecuted", skip_serializing_if = "Option::is_none")]
226        provider_executed: Option<bool>,
227        /// Whether this is a dynamic tool part.
228        #[serde(skip_serializing_if = "Option::is_none")]
229        dynamic: Option<bool>,
230    },
231
232    // ========================================================================
233    // Step Boundaries (for multi-step agents)
234    // ========================================================================
235    /// Marks the beginning of a step.
236    StartStep,
237
238    /// Marks the completion of an LLM API call step.
239    FinishStep,
240
241    // ========================================================================
242    // Source References (for RAG)
243    // ========================================================================
244    /// References an external URL.
245    SourceUrl {
246        /// Unique identifier for this source.
247        #[serde(rename = "sourceId")]
248        source_id: String,
249        /// The URL being referenced.
250        url: String,
251        /// Optional title for the source.
252        #[serde(skip_serializing_if = "Option::is_none")]
253        title: Option<String>,
254        /// Optional provider metadata.
255        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
256        provider_metadata: Option<Value>,
257    },
258
259    /// References a document or file.
260    SourceDocument {
261        /// Unique identifier for this source.
262        #[serde(rename = "sourceId")]
263        source_id: String,
264        /// IANA media type of the document.
265        #[serde(rename = "mediaType")]
266        media_type: String,
267        /// Title of the document.
268        title: String,
269        /// Optional filename.
270        #[serde(skip_serializing_if = "Option::is_none")]
271        filename: Option<String>,
272        /// Optional provider metadata.
273        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
274        provider_metadata: Option<Value>,
275    },
276
277    /// Contains a file reference.
278    File {
279        /// URL to the file.
280        url: String,
281        /// IANA media type.
282        #[serde(rename = "mediaType")]
283        media_type: String,
284        /// Optional provider metadata.
285        #[serde(rename = "providerMetadata", skip_serializing_if = "Option::is_none")]
286        provider_metadata: Option<Value>,
287    },
288
289    // ========================================================================
290    // Stream Lifecycle
291    // ========================================================================
292    /// Indicates message completion.
293    Finish {
294        /// Optional reason for finishing (stop, length, content-filter, tool-calls, error, other).
295        #[serde(rename = "finishReason", skip_serializing_if = "Option::is_none")]
296        finish_reason: Option<String>,
297        /// Optional message metadata.
298        #[serde(rename = "messageMetadata", skip_serializing_if = "Option::is_none")]
299        message_metadata: Option<Value>,
300    },
301
302    /// Signals stream abortion with a reason.
303    Abort {
304        /// Optional reason for the abort.
305        #[serde(skip_serializing_if = "Option::is_none")]
306        reason: Option<String>,
307    },
308
309    /// Emits incremental metadata updates for the active message.
310    MessageMetadata {
311        /// Message metadata payload.
312        #[serde(rename = "messageMetadata")]
313        message_metadata: Value,
314    },
315
316    /// Appends error messages to stream.
317    Error {
318        /// Error text.
319        #[serde(rename = "errorText")]
320        error_text: String,
321    },
322
323    // ========================================================================
324    // Custom Data (extensible)
325    // ========================================================================
326    /// Custom data event with a type prefix pattern (data-*).
327    #[serde(untagged)]
328    Data {
329        /// Custom type name (must start with "data-").
330        #[serde(rename = "type", deserialize_with = "deserialize_data_event_type")]
331        data_type: String,
332        /// Optional stable data part ID.
333        #[serde(skip_serializing_if = "Option::is_none")]
334        id: Option<String>,
335        /// Custom data payload.
336        data: Value,
337        /// Whether the data should be treated as transient.
338        #[serde(skip_serializing_if = "Option::is_none")]
339        transient: Option<bool>,
340    },
341}
342
343impl UIStreamEvent {
344    // ========================================================================
345    // Factory Methods
346    // ========================================================================
347
348    /// Create a start event (message start).
349    pub fn message_start(message_id: impl Into<String>) -> Self {
350        Self::MessageStart {
351            message_id: Some(message_id.into()),
352            message_metadata: None,
353        }
354    }
355
356    /// Create a start event without message ID.
357    pub fn start() -> Self {
358        Self::MessageStart {
359            message_id: None,
360            message_metadata: None,
361        }
362    }
363
364    /// Create a text-start event.
365    pub fn text_start(id: impl Into<String>) -> Self {
366        Self::TextStart {
367            id: id.into(),
368            provider_metadata: None,
369        }
370    }
371
372    /// Create a text-delta event.
373    pub fn text_delta(id: impl Into<String>, delta: impl Into<String>) -> Self {
374        Self::TextDelta {
375            id: id.into(),
376            delta: delta.into(),
377            provider_metadata: None,
378        }
379    }
380
381    /// Create a text-end event.
382    pub fn text_end(id: impl Into<String>) -> Self {
383        Self::TextEnd {
384            id: id.into(),
385            provider_metadata: None,
386        }
387    }
388
389    /// Create a reasoning-start event.
390    pub fn reasoning_start(id: impl Into<String>) -> Self {
391        Self::ReasoningStart {
392            id: id.into(),
393            provider_metadata: None,
394        }
395    }
396
397    /// Create a reasoning-delta event.
398    pub fn reasoning_delta(id: impl Into<String>, delta: impl Into<String>) -> Self {
399        Self::ReasoningDelta {
400            id: id.into(),
401            delta: delta.into(),
402            provider_metadata: None,
403        }
404    }
405
406    /// Create a reasoning-end event.
407    pub fn reasoning_end(id: impl Into<String>) -> Self {
408        Self::ReasoningEnd {
409            id: id.into(),
410            provider_metadata: None,
411        }
412    }
413
414    /// Create a tool-input-start event.
415    pub fn tool_input_start(tool_call_id: impl Into<String>, tool_name: impl Into<String>) -> Self {
416        Self::ToolInputStart {
417            tool_call_id: tool_call_id.into(),
418            tool_name: tool_name.into(),
419            provider_executed: None,
420            dynamic: None,
421            title: None,
422        }
423    }
424
425    /// Create a tool-input-delta event.
426    pub fn tool_input_delta(tool_call_id: impl Into<String>, delta: impl Into<String>) -> Self {
427        Self::ToolInputDelta {
428            tool_call_id: tool_call_id.into(),
429            input_text_delta: delta.into(),
430        }
431    }
432
433    /// Create a tool-input-available event.
434    pub fn tool_input_available(
435        tool_call_id: impl Into<String>,
436        tool_name: impl Into<String>,
437        input: Value,
438    ) -> Self {
439        Self::ToolInputAvailable {
440            tool_call_id: tool_call_id.into(),
441            tool_name: tool_name.into(),
442            input,
443            provider_executed: None,
444            provider_metadata: None,
445            dynamic: None,
446            title: None,
447        }
448    }
449
450    /// Create a tool-input-error event.
451    pub fn tool_input_error(
452        tool_call_id: impl Into<String>,
453        tool_name: impl Into<String>,
454        input: Value,
455        error_text: impl Into<String>,
456    ) -> Self {
457        Self::ToolInputError {
458            tool_call_id: tool_call_id.into(),
459            tool_name: tool_name.into(),
460            input,
461            provider_executed: None,
462            provider_metadata: None,
463            dynamic: None,
464            error_text: error_text.into(),
465            title: None,
466        }
467    }
468
469    /// Create a tool-approval-request event.
470    pub fn tool_approval_request(
471        approval_id: impl Into<String>,
472        tool_call_id: impl Into<String>,
473    ) -> Self {
474        Self::ToolApprovalRequest {
475            approval_id: approval_id.into(),
476            tool_call_id: tool_call_id.into(),
477        }
478    }
479
480    /// Create a tool-output-available event.
481    pub fn tool_output_available(tool_call_id: impl Into<String>, output: Value) -> Self {
482        Self::ToolOutputAvailable {
483            tool_call_id: tool_call_id.into(),
484            output,
485            provider_executed: None,
486            dynamic: None,
487            preliminary: None,
488        }
489    }
490
491    /// Create a tool-output-denied event.
492    pub fn tool_output_denied(tool_call_id: impl Into<String>) -> Self {
493        Self::ToolOutputDenied {
494            tool_call_id: tool_call_id.into(),
495        }
496    }
497
498    /// Create a tool-output-error event.
499    pub fn tool_output_error(
500        tool_call_id: impl Into<String>,
501        error_text: impl Into<String>,
502    ) -> Self {
503        Self::ToolOutputError {
504            tool_call_id: tool_call_id.into(),
505            error_text: error_text.into(),
506            provider_executed: None,
507            dynamic: None,
508        }
509    }
510
511    /// Create a start-step event.
512    pub fn start_step() -> Self {
513        Self::StartStep
514    }
515
516    /// Create a finish-step event.
517    pub fn finish_step() -> Self {
518        Self::FinishStep
519    }
520
521    /// Create a source-url event.
522    pub fn source_url(
523        source_id: impl Into<String>,
524        url: impl Into<String>,
525        title: Option<String>,
526    ) -> Self {
527        Self::SourceUrl {
528            source_id: source_id.into(),
529            url: url.into(),
530            title,
531            provider_metadata: None,
532        }
533    }
534
535    /// Create a source-document event.
536    pub fn source_document(
537        source_id: impl Into<String>,
538        media_type: impl Into<String>,
539        title: impl Into<String>,
540        filename: Option<String>,
541    ) -> Self {
542        Self::SourceDocument {
543            source_id: source_id.into(),
544            media_type: media_type.into(),
545            title: title.into(),
546            filename,
547            provider_metadata: None,
548        }
549    }
550
551    /// Create a file event.
552    pub fn file(url: impl Into<String>, media_type: impl Into<String>) -> Self {
553        Self::File {
554            url: url.into(),
555            media_type: media_type.into(),
556            provider_metadata: None,
557        }
558    }
559
560    /// Create a finish event.
561    pub fn finish() -> Self {
562        Self::Finish {
563            finish_reason: None,
564            message_metadata: None,
565        }
566    }
567
568    /// Create a finish event with a reason.
569    pub fn finish_with_reason(reason: impl Into<String>) -> Self {
570        Self::Finish {
571            finish_reason: Some(reason.into()),
572            message_metadata: None,
573        }
574    }
575
576    /// Create an abort event.
577    pub fn abort(reason: impl Into<String>) -> Self {
578        Self::Abort {
579            reason: Some(reason.into()),
580        }
581    }
582
583    /// Create a message-metadata event.
584    pub fn message_metadata(message_metadata: Value) -> Self {
585        Self::MessageMetadata { message_metadata }
586    }
587
588    /// Create an error event.
589    pub fn error(error_text: impl Into<String>) -> Self {
590        Self::Error {
591            error_text: error_text.into(),
592        }
593    }
594
595    /// Create a custom data event.
596    pub fn data(name: impl Into<String>, data: Value) -> Self {
597        Self::data_with_options(name, data, None, None)
598    }
599
600    /// Create a custom data event with optional id/transient flags.
601    pub fn data_with_options(
602        name: impl Into<String>,
603        data: Value,
604        id: Option<String>,
605        transient: Option<bool>,
606    ) -> Self {
607        let name = name.into();
608        let data_type = if name.starts_with("data-") {
609            name
610        } else {
611            format!("data-{name}")
612        };
613        Self::Data {
614            data_type,
615            id,
616            data,
617            transient,
618        }
619    }
620}
621
622fn deserialize_data_event_type<'de, D>(deserializer: D) -> Result<String, D::Error>
623where
624    D: Deserializer<'de>,
625{
626    let value = String::deserialize(deserializer)?;
627    if value.starts_with("data-") {
628        Ok(value)
629    } else {
630        Err(D::Error::custom(format!(
631            "invalid data event type '{value}', expected 'data-*'"
632        )))
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use serde_json::json;
640
641    #[test]
642    fn data_event_with_options_serializes_id_and_transient() {
643        let event = UIStreamEvent::data_with_options(
644            "reasoning-encrypted",
645            json!({ "encryptedValue": "opaque" }),
646            Some("reasoning_msg_1".to_string()),
647            Some(true),
648        );
649        let value = serde_json::to_value(event).expect("serialize data event");
650
651        assert_eq!(value["type"], "data-reasoning-encrypted");
652        assert_eq!(value["id"], "reasoning_msg_1");
653        assert_eq!(value["transient"], true);
654    }
655
656    #[test]
657    fn data_event_rejects_non_prefixed_type() {
658        let err = serde_json::from_value::<UIStreamEvent>(json!({
659            "type": "reasoning-encrypted",
660            "data": { "encryptedValue": "opaque" }
661        }))
662        .expect_err("invalid data type must be rejected");
663
664        assert!(!err.to_string().is_empty());
665    }
666
667    #[test]
668    fn tool_input_error_roundtrip() {
669        let event = UIStreamEvent::tool_input_error(
670            "call_1",
671            "search",
672            json!({ "q": "rust" }),
673            "invalid args",
674        );
675        let raw = serde_json::to_string(&event).expect("serialize tool-input-error");
676        let restored: UIStreamEvent =
677            serde_json::from_str(&raw).expect("deserialize tool-input-error");
678
679        assert!(matches!(
680            restored,
681            UIStreamEvent::ToolInputError {
682                tool_call_id,
683                tool_name,
684                error_text,
685                ..
686            } if tool_call_id == "call_1" && tool_name == "search" && error_text == "invalid args"
687        ));
688    }
689
690    #[test]
691    fn message_metadata_roundtrip() {
692        let event = UIStreamEvent::message_metadata(json!({ "step": 2 }));
693        let raw = serde_json::to_string(&event).expect("serialize message metadata");
694        let restored: UIStreamEvent =
695            serde_json::from_str(&raw).expect("deserialize message metadata");
696
697        assert!(matches!(
698            restored,
699            UIStreamEvent::MessageMetadata { message_metadata } if message_metadata["step"] == 2
700        ));
701    }
702
703    #[test]
704    fn text_delta_roundtrip_preserves_provider_metadata() {
705        let event = UIStreamEvent::TextDelta {
706            id: "txt_1".to_string(),
707            delta: "hello".to_string(),
708            provider_metadata: Some(json!({ "model": "x" })),
709        };
710        let raw = serde_json::to_string(&event).expect("serialize text delta");
711        let restored: UIStreamEvent = serde_json::from_str(&raw).expect("deserialize text delta");
712
713        assert!(matches!(
714            restored,
715            UIStreamEvent::TextDelta {
716                id,
717                delta,
718                provider_metadata: Some(provider_metadata),
719            } if id == "txt_1" && delta == "hello" && provider_metadata["model"] == "x"
720        ));
721    }
722}