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;