Skip to main content

ferrin_core/stream_text/
events.rs

1//! Events emitted by `stream_text`.
2
3use ferrin_spec::CustomKind;
4use ferrin_spec::FinishReason;
5use ferrin_spec::JsonValue;
6use ferrin_spec::PartId;
7use ferrin_spec::ProviderMetadata;
8use ferrin_spec::ToolCallId;
9use ferrin_spec::ToolName;
10use ferrin_spec::Usage;
11use ferrin_spec::Warning;
12use ferrin_spec::language_model::Source;
13use serde::Deserialize;
14use serde::Serialize;
15
16use crate::error::Error;
17use crate::error::ErrorKind;
18use crate::generate_text::GeneratedFile;
19use crate::generate_text::ParsedToolCall;
20use crate::generate_text::StepPerformance;
21use crate::generate_text::StepRequest;
22use crate::generate_text::StepResponse;
23use crate::generate_text::ToolApprovalRequestContent;
24use crate::generate_text::ToolApprovalResponseContent;
25use crate::generate_text::ToolExecutionError;
26use crate::generate_text::ToolOutputDenied;
27use crate::generate_text::ToolResult;
28use crate::telemetry::ModelIdentity;
29
30/// One event of a text stream. Serializable, so applications can forward
31/// events over SSE or WebSocket frames unchanged.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(tag = "type", rename_all = "kebab-case")]
34#[non_exhaustive]
35#[allow(
36    clippy::large_enum_variant,
37    reason = "events flow through channels one at a time; flat variants keep pattern matching simple"
38)]
39pub enum StreamEvent {
40    /// The call started.
41    Start {
42        /// Call id.
43        call_id: String,
44    },
45    /// A step started.
46    StartStep {
47        /// Zero-based step index.
48        step_number: u32,
49        /// The model handling the step.
50        model: ModelIdentity,
51        /// Request metadata.
52        request: StepRequest,
53        /// Warnings from the adapter.
54        warnings: Vec<Warning>,
55    },
56    /// Start of a text part.
57    TextStart {
58        /// Part id.
59        id: PartId,
60        /// Provider-specific metadata.
61        #[serde(default, skip_serializing_if = "Option::is_none")]
62        provider_metadata: Option<ProviderMetadata>,
63    },
64    /// Text increment.
65    TextDelta {
66        /// Part id.
67        id: PartId,
68        /// Appended text.
69        text: String,
70        /// Provider-specific metadata.
71        #[serde(default, skip_serializing_if = "Option::is_none")]
72        provider_metadata: Option<ProviderMetadata>,
73    },
74    /// End of a text part.
75    TextEnd {
76        /// Part id.
77        id: PartId,
78        /// Provider-specific metadata.
79        #[serde(default, skip_serializing_if = "Option::is_none")]
80        provider_metadata: Option<ProviderMetadata>,
81    },
82    /// Start of a reasoning part.
83    ReasoningStart {
84        /// Part id.
85        id: PartId,
86        /// Provider-specific metadata.
87        #[serde(default, skip_serializing_if = "Option::is_none")]
88        provider_metadata: Option<ProviderMetadata>,
89    },
90    /// Reasoning increment.
91    ReasoningDelta {
92        /// Part id.
93        id: PartId,
94        /// Appended text.
95        text: String,
96        /// Provider-specific metadata.
97        #[serde(default, skip_serializing_if = "Option::is_none")]
98        provider_metadata: Option<ProviderMetadata>,
99    },
100    /// End of a reasoning part.
101    ReasoningEnd {
102        /// Part id.
103        id: PartId,
104        /// Provider-specific metadata.
105        #[serde(default, skip_serializing_if = "Option::is_none")]
106        provider_metadata: Option<ProviderMetadata>,
107    },
108    /// A reasoning artifact stored as a file.
109    ReasoningFile(GeneratedFile),
110    /// A generated file.
111    File(GeneratedFile),
112    /// A citation source.
113    Source(Source),
114    /// Provider-specific content.
115    Custom {
116        /// Kind of the content.
117        kind: CustomKind,
118        /// Provider-specific metadata.
119        #[serde(default, skip_serializing_if = "Option::is_none")]
120        provider_metadata: Option<ProviderMetadata>,
121    },
122    /// Start of streamed tool input.
123    ToolInputStart {
124        /// Tool call id.
125        id: ToolCallId,
126        /// Tool name.
127        tool_name: ToolName,
128        /// Whether the provider executes the tool.
129        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
130        provider_executed: bool,
131        /// Whether the tool is dynamic.
132        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
133        dynamic: bool,
134        /// Display title.
135        #[serde(default, skip_serializing_if = "Option::is_none")]
136        title: Option<String>,
137        /// Provider-specific metadata.
138        #[serde(default, skip_serializing_if = "Option::is_none")]
139        provider_metadata: Option<ProviderMetadata>,
140    },
141    /// Tool input increment (raw JSON text).
142    ToolInputDelta {
143        /// Tool call id.
144        id: ToolCallId,
145        /// Appended JSON text.
146        delta: String,
147        /// Provider-specific metadata.
148        #[serde(default, skip_serializing_if = "Option::is_none")]
149        provider_metadata: Option<ProviderMetadata>,
150    },
151    /// End of streamed tool input.
152    ToolInputEnd {
153        /// Tool call id.
154        id: ToolCallId,
155        /// Provider-specific metadata.
156        #[serde(default, skip_serializing_if = "Option::is_none")]
157        provider_metadata: Option<ProviderMetadata>,
158    },
159    /// A parsed tool call.
160    ToolCall(ParsedToolCall),
161    /// A tool result (may be preliminary).
162    ToolResult(ToolResult),
163    /// A tool error.
164    ToolError(ToolExecutionError),
165    /// A request to approve a tool call.
166    ToolApprovalRequest(ToolApprovalRequestContent),
167    /// An automatic approval decision.
168    ToolApprovalResponse(ToolApprovalResponseContent),
169    /// A denied tool call.
170    ToolOutputDenied(ToolOutputDenied),
171    /// A step finished.
172    FinishStep {
173        /// Zero-based step index.
174        step_number: u32,
175        /// Finish reason.
176        finish_reason: FinishReason,
177        /// Usage of the step.
178        usage: Usage,
179        /// Response metadata (without messages).
180        response: StepResponse,
181        /// Provider-specific metadata.
182        #[serde(default, skip_serializing_if = "Option::is_none")]
183        provider_metadata: Option<ProviderMetadata>,
184        /// Timing and throughput of the step.
185        performance: StepPerformance,
186    },
187    /// The call finished.
188    Finish {
189        /// Finish reason of the last step.
190        finish_reason: FinishReason,
191        /// Usage summed over all steps.
192        total_usage: Usage,
193    },
194    /// An error occurred (the stream may continue after a retry).
195    Error {
196        /// The error.
197        error: StreamErrorInfo,
198    },
199    /// The call was aborted by cancellation.
200    Abort,
201    /// The model call of the current step was retried after a stream error.
202    /// Content emitted by the previous attempt stays in the stream but is
203    /// excluded from the step result.
204    RetryAttempt {
205        /// Zero-based step index.
206        step_number: u32,
207        /// One-based retry attempt number.
208        attempt: u32,
209        /// Request metadata of the new attempt.
210        request: StepRequest,
211        /// Warnings from the new attempt's stream start.
212        warnings: Vec<Warning>,
213    },
214    /// A raw provider chunk (only with `include_raw_chunks`).
215    Raw {
216        /// The chunk.
217        raw_value: JsonValue,
218    },
219}
220
221impl StreamEvent {
222    /// Returns the wire name of the variant.
223    #[must_use]
224    pub fn kind_name(&self) -> &'static str {
225        match self {
226            Self::Start { .. } => "start",
227            Self::StartStep { .. } => "start-step",
228            Self::TextStart { .. } => "text-start",
229            Self::TextDelta { .. } => "text-delta",
230            Self::TextEnd { .. } => "text-end",
231            Self::ReasoningStart { .. } => "reasoning-start",
232            Self::ReasoningDelta { .. } => "reasoning-delta",
233            Self::ReasoningEnd { .. } => "reasoning-end",
234            Self::ReasoningFile(_) => "reasoning-file",
235            Self::File(_) => "file",
236            Self::Source(_) => "source",
237            Self::Custom { .. } => "custom",
238            Self::ToolInputStart { .. } => "tool-input-start",
239            Self::ToolInputDelta { .. } => "tool-input-delta",
240            Self::ToolInputEnd { .. } => "tool-input-end",
241            Self::ToolCall(_) => "tool-call",
242            Self::ToolResult(_) => "tool-result",
243            Self::ToolError(_) => "tool-error",
244            Self::ToolApprovalRequest(_) => "tool-approval-request",
245            Self::ToolApprovalResponse(_) => "tool-approval-response",
246            Self::ToolOutputDenied(_) => "tool-output-denied",
247            Self::FinishStep { .. } => "finish-step",
248            Self::Finish { .. } => "finish",
249            Self::Error { .. } => "error",
250            Self::Abort => "abort",
251            Self::RetryAttempt { .. } => "retry-attempt",
252            Self::Raw { .. } => "raw",
253        }
254    }
255
256    /// Returns the text of a text delta.
257    #[must_use]
258    pub fn as_text_delta(&self) -> Option<&str> {
259        match self {
260            Self::TextDelta { text, .. } => Some(text),
261            _ => None,
262        }
263    }
264}
265
266/// Serializable projection of an [`Error`] carried in a stream.
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
268pub struct StreamErrorInfo {
269    /// Coarse category.
270    pub kind: ErrorKind,
271    /// Display message.
272    pub message: String,
273    /// HTTP status when known.
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub status_code: Option<u16>,
276    /// Whether retrying may succeed.
277    pub is_retryable: bool,
278    /// Provider error payload when available.
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub provider_data: Option<JsonValue>,
281}
282
283impl StreamErrorInfo {
284    /// Projects an error.
285    #[must_use]
286    pub fn from_error(error: &Error) -> Self {
287        let provider_data = error
288            .as_provider()
289            .and_then(ferrin_spec::error::ProviderError::as_api_call)
290            .and_then(|api| api.data.clone());
291        Self {
292            kind: error.kind(),
293            message: error.to_string(),
294            status_code: error.status_code().map(|status| status.as_u16()),
295            is_retryable: error.is_retryable(),
296            provider_data,
297        }
298    }
299}
300
301impl From<&Error> for StreamErrorInfo {
302    fn from(error: &Error) -> Self {
303        Self::from_error(error)
304    }
305}