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