Skip to main content

rig_core/streaming/
event.rs

1//! The events of a completion stream. A completion decoder never builds one:
2//! it writes through the reply's part handles
3//! ([`Out`](crate::wire::Out)), and the writer emits each part's start, its
4//! fragments and its end, in that order. A [`Part`] is a part's position in
5//! the response's `choice`. A stream transform that splits or adds parts
6//! (reasoning a model wrote inside its text, tool calls written as text)
7//! numbers its own with [`Part::new`]. An item also reads back from its
8//! serialized form on its own, unchecked, so a client can decode a relayed
9//! stream as it arrives; [`StreamRecord::parse`] is the one place a sequence
10//! is checked.
11//!
12//! ```
13//! use rig_core::streaming::{PartKind, StreamEvent, StreamRecord};
14//!
15//! let transcript = StreamRecord::parse(serde_json::json!([
16//!     {"item": "event", "value": {"event": "start", "part": 0, "kind": "text"}},
17//!     {"item": "event", "value": {"event": "text", "part": 0, "text": "Hello"}},
18//!     {"item": "event", "value": {"event": "end", "part": 0,
19//!         "content": {"type": "text", "text": "Hello"}}},
20//! ]))?;
21//! assert!(matches!(
22//!     transcript.events().next(),
23//!     Some(StreamEvent::Start { kind: PartKind::Text, .. })
24//! ));
25//! # Ok::<(), rig_core::streaming::SequenceError>(())
26//! ```
27
28use serde::{Deserialize, Serialize};
29
30use crate::message::{AssistantContent, ToolName};
31
32use super::UnknownPayload;
33
34/// A part's position in the response's `choice`. A transform that rewrites
35/// a stream numbers the parts it emits with [`Self::new`], and keeps the
36/// order [`StreamRecord::parse`] checks: each part starts, grows and ends, and
37/// parts are numbered in the order they start.
38///
39/// ```
40/// use rig_core::streaming::Part;
41///
42/// assert_eq!(Part::new(2).index(), 2);
43/// ```
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
45#[serde(transparent)]
46pub struct Part(u32);
47
48impl Part {
49    /// The part at position `index` of the response's `choice`.
50    pub const fn new(index: u32) -> Self {
51        Self(index)
52    }
53
54    /// The part's position in the response's `choice`.
55    pub const fn index(self) -> usize {
56        self.0 as usize
57    }
58}
59
60/// What kind of part a [`StreamEvent::Start`] opened.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum PartKind {
64    /// Answer text.
65    Text,
66    /// Reasoning.
67    Reasoning,
68    /// A tool call.
69    ToolCall,
70    /// An image.
71    Image,
72    /// A provider item with no canonical meaning.
73    Opaque,
74}
75
76/// One event of a completion stream: a part starts, grows, or ends with the
77/// content it finalized. Deserializing one event checks nothing about its
78/// order; read a sequence through [`StreamRecord::parse`] to check it.
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80#[serde(tag = "event", rename_all = "snake_case")]
81pub enum StreamEvent {
82    /// A part opened. A tool call opens once its tool is named, so a call
83    /// streams its arguments under a known tool.
84    Start {
85        /// The part.
86        part: Part,
87        /// What it holds.
88        kind: PartKind,
89        /// The tool a [`PartKind::ToolCall`] names as it opens; `None` for
90        /// every other kind. The call's end states the name it settled on.
91        #[serde(default, skip_serializing_if = "Option::is_none")]
92        name: Option<ToolName>,
93    },
94    /// A text part grew.
95    Text {
96        /// The part.
97        part: Part,
98        /// The fragment.
99        text: String,
100    },
101    /// A reasoning part grew.
102    Reasoning {
103        /// The part.
104        part: Part,
105        /// The fragment.
106        text: String,
107    },
108    /// A tool call's arguments grew. The fragments of one call, joined,
109    /// are the argument JSON the provider sent; read an incomplete prefix
110    /// with [`parse_partial_arguments`](super::parse_partial_arguments).
111    Arguments {
112        /// The part.
113        part: Part,
114        /// The fragment of raw argument JSON.
115        json: String,
116    },
117    /// A part ended with the content it finalized.
118    End {
119        /// The part.
120        part: Part,
121        /// The finalized content.
122        content: AssistantContent,
123    },
124}
125
126impl StreamEvent {
127    /// The part this event is about.
128    pub const fn part(&self) -> Part {
129        match self {
130            Self::Start { part, .. }
131            | Self::Text { part, .. }
132            | Self::Reasoning { part, .. }
133            | Self::Arguments { part, .. }
134            | Self::End { part, .. } => *part,
135        }
136    }
137
138    /// A stable variant name that exposes no payload to logs.
139    pub const fn name(&self) -> &'static str {
140        match self {
141            Self::Start { .. } => "Start",
142            Self::Text { .. } => "Text",
143            Self::Reasoning { .. } => "Reasoning",
144            Self::Arguments { .. } => "Arguments",
145            Self::End { .. } => "End",
146        }
147    }
148}
149
150/// One item of a stream: an event of the operation, or a payload the
151/// provider sent that the decoder does not model. An unmodeled payload
152/// always reaches the consumer.
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
154#[serde(tag = "item", content = "value", rename_all = "snake_case")]
155pub enum Item<E> {
156    /// An event of the operation.
157    Event(E),
158    /// A payload the decoder does not model.
159    Unknown(UnknownPayload),
160}
161
162/// A completion stream's items read back from their serialized form, in an
163/// order the writer could have produced: each part starts once, at a
164/// position no part took before, grows only while open and with its own
165/// kind of fragment, and ends once. Positions follow the order provider
166/// items opened, so a dropped item leaves a gap. Unmodeled payloads may come
167/// anywhere.
168#[derive(Debug, Clone, Default)]
169pub struct StreamRecord {
170    items: Vec<Item<StreamEvent>>,
171    /// Each started part's kind, and whether it ended.
172    parts: std::collections::BTreeMap<usize, (PartKind, bool)>,
173}
174
175impl PartialEq for StreamRecord {
176    fn eq(&self, other: &Self) -> bool {
177        self.items == other.items
178    }
179}
180
181impl Serialize for StreamRecord {
182    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
183        self.items.serialize(serializer)
184    }
185}
186
187/// Why a serialized event sequence is not one the writer could produce.
188#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
189pub enum SequenceError {
190    /// The value is not a list of stream items.
191    #[error("not a list of stream items: {0}")]
192    Shape(String),
193    /// An event names a part that has not started, or a start reuses a
194    /// position.
195    #[error("item {0} names a part that has not started")]
196    UnknownPart(usize),
197    /// A part ended twice, or grew after its end.
198    #[error("item {0} touches a part that already ended")]
199    EndedTwice(usize),
200    /// A fragment or end does not match the part's kind, or a start names a
201    /// tool for a part that is not a call, or none for one that is.
202    #[error("item {0} does not match its part's kind")]
203    WrongKind(usize),
204    /// A part started and never ended.
205    #[error("part {0} never ended")]
206    Unclosed(usize),
207}
208
209impl StreamRecord {
210    /// Read a serialized item sequence, refusing one the writer could not
211    /// have produced. Every part must have ended.
212    pub fn parse(value: serde_json::Value) -> Result<Self, SequenceError> {
213        let transcript = Self::parse_prefix(value)?;
214        transcript.check_closed()?;
215        Ok(transcript)
216    }
217
218    /// [`Self::parse`] for a stream that stopped early (an error or a
219    /// cancellation): parts may still be open at its end.
220    pub fn parse_prefix(value: serde_json::Value) -> Result<Self, SequenceError> {
221        let items: Vec<Item<StreamEvent>> = serde_json::from_value(value)
222            .map_err(|error| SequenceError::Shape(error.to_string()))?;
223        Self::from_items(items)
224    }
225
226    /// Append the next item a stream yielded, refusing one the writer could
227    /// not have produced after the items so far.
228    pub fn push(&mut self, item: Item<StreamEvent>) -> Result<(), SequenceError> {
229        let position = self.items.len();
230        if let Item::Event(event) = &item {
231            let index = event.part().index();
232            let kind = match event {
233                StreamEvent::Start { kind, name, .. } => {
234                    if (*kind == PartKind::ToolCall) != name.is_some() {
235                        return Err(SequenceError::WrongKind(position));
236                    }
237                    if self.parts.insert(index, (*kind, false)).is_some() {
238                        return Err(SequenceError::UnknownPart(position));
239                    }
240                    None
241                }
242                StreamEvent::Text { .. } => Some(PartKind::Text),
243                StreamEvent::Reasoning { .. } => Some(PartKind::Reasoning),
244                StreamEvent::Arguments { .. } => Some(PartKind::ToolCall),
245                StreamEvent::End { content, .. } => Some(match content {
246                    AssistantContent::Text(_) => PartKind::Text,
247                    AssistantContent::Reasoning(_) => PartKind::Reasoning,
248                    AssistantContent::ToolCall(_) => PartKind::ToolCall,
249                    AssistantContent::Image(_) => PartKind::Image,
250                    AssistantContent::Opaque(_) => PartKind::Opaque,
251                }),
252            };
253            if let Some(kind) = kind {
254                match self.parts.get_mut(&index) {
255                    None => return Err(SequenceError::UnknownPart(position)),
256                    Some((_, true)) => return Err(SequenceError::EndedTwice(position)),
257                    Some((open, false)) if *open != kind => {
258                        return Err(SequenceError::WrongKind(position));
259                    }
260                    Some((_, ended)) => *ended = matches!(event, StreamEvent::End { .. }),
261                }
262            }
263        }
264        self.items.push(item);
265        Ok(())
266    }
267
268    fn check_closed(&self) -> Result<(), SequenceError> {
269        match self.parts.iter().find(|(_, (_, ended))| !ended) {
270            Some((part, _)) => Err(SequenceError::Unclosed(*part)),
271            None => Ok(()),
272        }
273    }
274
275    /// Items a stream yielded, checked as [`Self::push`] checks them: a
276    /// stream that stopped early may leave parts open.
277    pub fn from_items(items: Vec<Item<StreamEvent>>) -> Result<Self, SequenceError> {
278        let mut transcript = Self::default();
279        for item in items {
280            transcript.push(item)?;
281        }
282        Ok(transcript)
283    }
284
285    /// The number of items.
286    pub fn len(&self) -> usize {
287        self.items.len()
288    }
289
290    /// Whether there are no items.
291    pub fn is_empty(&self) -> bool {
292        self.items.is_empty()
293    }
294
295    /// The items, in order.
296    pub fn items(&self) -> &[Item<StreamEvent>] {
297        &self.items
298    }
299
300    /// The items, in order.
301    pub fn into_items(self) -> Vec<Item<StreamEvent>> {
302        self.items
303    }
304
305    /// The events, in order, without the unmodeled payloads.
306    pub fn events(&self) -> impl Iterator<Item = &StreamEvent> {
307        self.items.iter().filter_map(|item| match item {
308            Item::Event(event) => Some(event),
309            Item::Unknown(_) => None,
310        })
311    }
312}
313
314impl From<StreamRecord> for Vec<Item<StreamEvent>> {
315    fn from(transcript: StreamRecord) -> Self {
316        transcript.items
317    }
318}
319
320/// Read as the item list it is, with no `Value` tree between.
321impl<'de> Deserialize<'de> for StreamRecord {
322    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
323        let items = Vec::<Item<StreamEvent>>::deserialize(deserializer)
324            .map_err(|error| serde::de::Error::custom(SequenceError::Shape(error.to_string())))?;
325        Self::from_items(items).map_err(serde::de::Error::custom)
326    }
327}
328
329// The stream vocabulary crosses threads on every target: a handler sends it
330// over a channel, the effect log records it, and a client reads it back.
331const _: fn() = || {
332    fn assert_wire<T: Clone + Send + Sync + 'static + Serialize + serde::de::DeserializeOwned>() {}
333    assert_wire::<StreamEvent>();
334    assert_wire::<StreamRecord>();
335    assert_wire::<Item<StreamEvent>>();
336};
337
338#[cfg(test)]
339mod tests;