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;