1use std::convert::Infallible;
18
19use axum::response::sse::{Event, Sse};
20use axum::response::IntoResponse;
21use futures::stream::Stream;
22use futures::StreamExt;
23use serde::Serialize;
24use serde_json::Value;
25
26use super::error::{ErrorType, UhpError};
27use super::state::UhpState;
28use super::tasks::{self, Prepared, Response};
29
30#[derive(Debug, Clone, Serialize)]
32pub struct StreamEvent {
33 #[serde(rename = "type")]
35 pub kind: String,
36 pub sequence_number: u64,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub response: Option<Response>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub item: Option<Value>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub delta: Option<String>,
47}
48
49impl StreamEvent {
50 fn with_response(kind: &str, sequence_number: u64, response: Response) -> Self {
52 Self {
53 kind: kind.to_owned(),
54 sequence_number,
55 response: Some(response),
56 item: None,
57 delta: None,
58 }
59 }
60
61 fn delta(sequence_number: u64, text: &str) -> Self {
63 Self {
64 kind: "response.output_text.delta".to_owned(),
65 sequence_number,
66 response: None,
67 item: None,
68 delta: Some(text.to_owned()),
69 }
70 }
71}
72
73const fn terminal_kind(status: &str) -> &'static str {
79 match status.as_bytes() {
80 b"completed" => "response.completed",
81 b"failed" => "response.failed",
82 _ => "response.incomplete",
83 }
84}
85
86pub fn sse(state: UhpState, prepared: Prepared, previous: Option<String>) -> impl IntoResponse {
94 Sse::new(run_stream(state, prepared, previous))
95}
96
97fn run_stream(
99 state: UhpState,
100 prepared: Prepared,
101 previous: Option<String>,
102) -> impl Stream<Item = Result<Event, Infallible>> {
103 async_stream::stream! {
104 let mut seq = 0_u64;
105 let session_id = prepared.session_id.clone();
106 let created = tasks::pending_response(&prepared, previous.clone());
107 state.store.put(&session_id, created.clone());
108
109 yield emit(StreamEvent::with_response("response.created", seq, created));
110 seq += 1;
111
112 let mut text = String::new();
113 let outcome = prepared.runner.complete_stream(&prepared.chat).await;
114
115 let failure = match outcome {
116 Ok(mut chunks) => {
117 let mut err = None;
118 while let Some(next) = chunks.next().await {
119 match next {
120 Ok(chunk) => {
121 if !chunk.delta.is_empty() {
122 text.push_str(&chunk.delta);
123 yield emit(StreamEvent::delta(seq, &chunk.delta));
124 seq += 1;
125 }
126 }
127 Err(e) => {
128 err = Some(e.message);
129 break;
130 }
131 }
132 }
133 err
134 }
135 Err(e) => Some(e.message),
136 };
137
138 let model = prepared.model.clone();
139 let response = match failure {
140 None => tasks::completed_response(prepared, previous, text, None, model),
141 Some(message) => {
142 let mut r = tasks::pending_response(&prepared, previous);
143 r.status = "failed";
144 r.error = Some(UhpError {
145 kind: ErrorType::HarnessError,
146 code: "harness_error".to_owned(),
147 message,
148 detail: None,
149 });
150 r
151 }
152 };
153
154 state.store.settle(response.clone());
155 let kind = terminal_kind(response.status);
156 yield emit(StreamEvent::with_response(kind, seq, response));
157 }
158}
159
160#[allow(clippy::unnecessary_wraps)]
166fn emit(event: StreamEvent) -> Result<Event, Infallible> {
167 let json = serde_json::to_string(&event).unwrap_or_else(|_| "{}".to_owned());
168 Ok(Event::default().event(event.kind).data(json))
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use serde_json::Map;
175
176 fn response(status: &'static str) -> Response {
177 Response {
178 id: "resp_x".to_owned(),
179 object: "response",
180 created_at: 1,
181 status,
182 error: None,
183 previous_response_id: None,
184 model: "m".to_owned(),
185 output: Vec::new(),
186 store: true,
187 usage: None,
188 metadata: Map::new(),
189 }
190 }
191
192 #[test]
193 fn a_cancelled_task_never_streams_as_failed() {
194 assert_eq!(terminal_kind("cancelled"), "response.incomplete");
197 assert_eq!(terminal_kind("incomplete"), "response.incomplete");
198 assert_eq!(terminal_kind("completed"), "response.completed");
199 assert_eq!(terminal_kind("failed"), "response.failed");
200 }
201
202 #[test]
203 fn the_event_sequence_obeys_the_ordering_rules() {
204 let evs = [
208 StreamEvent::with_response("response.created", 0, response("in_progress")),
209 StreamEvent::delta(1, "ok"),
210 StreamEvent::with_response("response.completed", 2, response("completed")),
211 ];
212
213 let seqs: Vec<u64> = evs.iter().map(|e| e.sequence_number).collect();
214 assert_eq!(
215 seqs,
216 vec![0, 1, 2],
217 "starts at 0 and increases by exactly one"
218 );
219 assert_eq!(
220 evs[0].kind, "response.created",
221 "the first event is created"
222 );
223
224 let terminal = [
225 "response.completed",
226 "response.incomplete",
227 "response.failed",
228 ];
229 assert_eq!(
230 evs.iter()
231 .filter(|e| terminal.contains(&e.kind.as_str()))
232 .count(),
233 1,
234 "exactly one terminal event"
235 );
236 assert!(
237 evs.last()
238 .is_some_and(|e| terminal.contains(&e.kind.as_str())),
239 "and it is last"
240 );
241 assert!(
242 evs.last().and_then(|e| e.response.as_ref()).is_some(),
243 "the terminal event carries the complete response"
244 );
245 assert_eq!(
246 evs[1].delta.as_deref(),
247 Some("ok"),
248 "a delta event carries the text it adds, and no response object"
249 );
250 assert!(evs[1].response.is_none());
251 }
252}