Skip to main content

hot_dev/
streaming.rs

1use std::pin::Pin;
2use std::sync::Arc;
3use std::time::Duration;
4
5use futures_core::Stream;
6use futures_util::StreamExt;
7use reqwest::header::ACCEPT;
8use reqwest::Method;
9use serde_json::Value;
10
11use crate::error::parse_api_error;
12use crate::sse::consume_sse_blocks;
13use crate::transport::{enc, Transport};
14use crate::{Error, JsonObject, StreamEvent};
15
16/// A stream of Hot run-stream events.
17pub type EventStream = Pin<Box<dyn Stream<Item = crate::Result<StreamEvent>> + Send>>;
18
19/// Convenience accessors for [`StreamEvent`] wire-format fields.
20pub trait StreamEventExt {
21    /// The event's "type" field, or "" when absent.
22    fn event_type(&self) -> &str;
23    /// The event's "run" object, when present.
24    fn run(&self) -> Option<&JsonObject>;
25    /// run.run_id, when present.
26    fn run_id(&self) -> Option<&str>;
27}
28
29impl StreamEventExt for StreamEvent {
30    fn event_type(&self) -> &str {
31        self.get("type").and_then(Value::as_str).unwrap_or("")
32    }
33
34    fn run(&self) -> Option<&JsonObject> {
35        self.get("run").and_then(Value::as_object)
36    }
37
38    fn run_id(&self) -> Option<&str> {
39        self.run()?.get("run_id")?.as_str()
40    }
41}
42
43/// Opens an SSE request and yields decoded events. The stream ends after
44/// yielding an error.
45pub(crate) fn iter_stream(
46    transport: Arc<Transport>,
47    method: Method,
48    path: String,
49    body: Option<Value>,
50    query: Vec<(String, String)>,
51) -> EventStream {
52    Box::pin(async_stream::stream! {
53        let mut builder = transport
54            .request_builder(method, &path)
55            .header(ACCEPT, "text/event-stream");
56        if !query.is_empty() {
57            builder = builder.query(&query);
58        }
59        if let Some(body) = &body {
60            builder = builder.json(body);
61        }
62
63        let response = match builder.send().await {
64            Ok(response) => response,
65            Err(error) => {
66                yield Err(Error::Http(error));
67                return;
68            }
69        };
70        let status = response.status();
71        if status.is_client_error() || status.is_server_error() {
72            let headers = response.headers().clone();
73            let text = response.text().await.unwrap_or_default();
74            yield Err(Error::Api(parse_api_error(status.as_u16(), &text, &headers)));
75            return;
76        }
77
78        // Bytes are decoded through a byte buffer so multi-byte UTF-8
79        // sequences split across chunks survive intact.
80        let mut pending: Vec<u8> = Vec::new();
81        let mut buffer = String::new();
82        let mut chunks = response.bytes_stream();
83        while let Some(chunk) = chunks.next().await {
84            let chunk = match chunk {
85                Ok(chunk) => chunk,
86                Err(error) => {
87                    yield Err(Error::Http(error));
88                    return;
89                }
90            };
91            pending.extend_from_slice(&chunk);
92            let valid_len = match std::str::from_utf8(&pending) {
93                Ok(_) => pending.len(),
94                Err(error) => error.valid_up_to(),
95            };
96            buffer.push_str(std::str::from_utf8(&pending[..valid_len]).unwrap_or(""));
97            pending.drain(..valid_len);
98
99            let (events, rest) = consume_sse_blocks(&buffer);
100            buffer = rest;
101            for event in events {
102                yield Ok(event);
103            }
104        }
105
106        buffer.push_str("\n\n");
107        let (events, _) = consume_sse_blocks(&buffer);
108        for event in events {
109            yield Ok(event);
110        }
111    })
112}
113
114pub(crate) async fn wait_for_run_result(
115    transport: Arc<Transport>,
116    stream_id: &str,
117    event_id: &str,
118    timeout: Duration,
119    mut on_chunk: Option<&mut (dyn FnMut(&str) + Send)>,
120) -> crate::Result<JsonObject> {
121    let deadline = tokio::time::Instant::now() + timeout;
122    let mut current_run_id: Option<String> = None;
123    let mut source = iter_stream(
124        transport,
125        Method::GET,
126        format!("/streams/{}/subscribe", enc(stream_id)),
127        None,
128        Vec::new(),
129    );
130
131    loop {
132        let item = match tokio::time::timeout_at(deadline, source.next()).await {
133            Ok(item) => item,
134            Err(_) => return Err(Error::Timeout),
135        };
136        let Some(item) = item else { break };
137        let event = item?;
138
139        match event.event_type() {
140            "run:start" => {
141                if event_id_of_run(event.run()) == Some(event_id) {
142                    current_run_id = event.run_id().map(str::to_string);
143                }
144            }
145            "stream:data" => {
146                let matches_run = current_run_id.as_deref().is_some_and(|current| {
147                    event.get("run_id").and_then(Value::as_str) == Some(current)
148                });
149                if matches_run {
150                    if let Some(on_chunk) = on_chunk.as_deref_mut() {
151                        if let Some(text) = event
152                            .get("payload")
153                            .and_then(Value::as_object)
154                            .and_then(|payload| payload.get("text"))
155                            .and_then(Value::as_str)
156                        {
157                            on_chunk(text);
158                        }
159                    }
160                }
161            }
162            event_type @ ("run:stop" | "run:fail" | "run:cancel") => {
163                let Some(run) = event.run() else { continue };
164                if event_id_of_run(Some(run)) != Some(event_id) {
165                    continue;
166                }
167                match event_type {
168                    "run:fail" => {
169                        return Err(Error::RunFailed(run_result_message(run, "run failed")))
170                    }
171                    "run:cancel" => {
172                        return Err(Error::RunFailed(run_result_message(run, "run cancelled")))
173                    }
174                    _ => return Ok(run.clone()),
175                }
176            }
177            _ => {}
178        }
179    }
180
181    Err(Error::Protocol("stream ended before run completed".into()))
182}
183
184pub(crate) fn event_id_of_run(run: Option<&JsonObject>) -> Option<&str> {
185    run?.get("event_id")?.as_str()
186}
187
188/// Digs a human-readable message out of a run result value.
189fn result_message(value: Option<&Value>) -> Option<&str> {
190    match value? {
191        Value::String(text) if !text.is_empty() => Some(text),
192        Value::Object(object) => ["$err", "$val", "error", "message", "msg", "reason", "err"]
193            .iter()
194            .find_map(|key| result_message(object.get(*key))),
195        _ => None,
196    }
197}
198
199pub(crate) fn run_result_message(run: &JsonObject, fallback: &str) -> String {
200    if let Some(message) = result_message(run.get("result")) {
201        return message.to_string();
202    }
203    match run.get("status").and_then(Value::as_str) {
204        Some(status) if !status.is_empty() => format!("{fallback} ({status})"),
205        _ => fallback.to_string(),
206    }
207}
208
209/// Unwraps a `{"$ok": ...}` run result.
210pub(crate) fn extract_run_result(result: Option<Value>) -> Value {
211    match result {
212        Some(Value::Object(mut object)) if object.contains_key("$ok") => {
213            object.remove("$ok").unwrap_or(Value::Null)
214        }
215        Some(other) => other,
216        None => Value::Null,
217    }
218}