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 Ok(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 Ok(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 Ok(emit(StreamEvent::with_response(kind, seq, response)));
157 }
158}
159
160fn emit(event: StreamEvent) -> Event {
166 let json = serde_json::to_string(&event).unwrap_or_else(|_| "{}".to_owned());
167 Event::default().event(event.kind).data(json)
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use serde_json::Map;
174
175 fn response(status: &'static str) -> Response {
176 Response {
177 id: "resp_x".to_owned(),
178 object: "response",
179 created_at: 1,
180 status,
181 error: None,
182 previous_response_id: None,
183 model: "m".to_owned(),
184 output: Vec::new(),
185 store: true,
186 usage: None,
187 metadata: Map::new(),
188 }
189 }
190
191 #[test]
192 fn a_cancelled_task_never_streams_as_failed() {
193 assert_eq!(terminal_kind("cancelled"), "response.incomplete");
196 assert_eq!(terminal_kind("incomplete"), "response.incomplete");
197 assert_eq!(terminal_kind("completed"), "response.completed");
198 assert_eq!(terminal_kind("failed"), "response.failed");
199 }
200
201 #[test]
202 fn the_event_sequence_obeys_the_ordering_rules() {
203 let evs = [
207 StreamEvent::with_response("response.created", 0, response("in_progress")),
208 StreamEvent::delta(1, "ok"),
209 StreamEvent::with_response("response.completed", 2, response("completed")),
210 ];
211
212 let seqs: Vec<u64> = evs.iter().map(|e| e.sequence_number).collect();
213 assert_eq!(
214 seqs,
215 vec![0, 1, 2],
216 "starts at 0 and increases by exactly one"
217 );
218 assert_eq!(
219 evs[0].kind, "response.created",
220 "the first event is created"
221 );
222
223 let terminal = [
224 "response.completed",
225 "response.incomplete",
226 "response.failed",
227 ];
228 assert_eq!(
229 evs.iter()
230 .filter(|e| terminal.contains(&e.kind.as_str()))
231 .count(),
232 1,
233 "exactly one terminal event"
234 );
235 assert!(
236 evs.last()
237 .is_some_and(|e| terminal.contains(&e.kind.as_str())),
238 "and it is last"
239 );
240 assert!(
241 evs.last().and_then(|e| e.response.as_ref()).is_some(),
242 "the terminal event carries the complete response"
243 );
244 assert_eq!(
245 evs[1].delta.as_deref(),
246 Some("ok"),
247 "a delta event carries the text it adds, and no response object"
248 );
249 assert!(evs[1].response.is_none());
250 }
251}