1use laser_wire::agent::{AgentEnvelope, AgentKind, TokenUsage};
2use serde::{Serialize, Serializer};
3
4pub const FINISH_REASON_ABANDONED: &str = "abandoned";
8pub const FINISH_REASON_GAP: &str = "gap";
11
12#[derive(Debug, Clone, PartialEq, Serialize)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum StreamEvent {
17 Body {
19 sequence: u64,
20 #[serde(serialize_with = "as_bytes")]
21 payload: Vec<u8>,
22 },
23 Finished {
26 finish_reason: Option<String>,
27 usage: Option<TokenUsage>,
28 synthetic: bool,
29 },
30 Failed {
33 #[serde(serialize_with = "as_bytes")]
34 body: Vec<u8>,
35 },
36}
37
38#[derive(Debug, Default)]
41pub struct ChunkAssembler {
42 next_sequence: u64,
43 finished: bool,
44 duplicates_dropped: u64,
45 late_dropped: u64,
46}
47
48impl ChunkAssembler {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn feed(&mut self, envelope: &AgentEnvelope) -> Vec<StreamEvent> {
61 match envelope.kind {
62 AgentKind::Chunk => self.feed_chunk(envelope),
63 AgentKind::Error => {
64 if self.finished {
65 self.late_dropped += 1;
66 return Vec::new();
67 }
68 self.finished = true;
69 vec![StreamEvent::Failed {
70 body: envelope.body.clone(),
71 }]
72 }
73 AgentKind::Command | AgentKind::Response | AgentKind::Event | AgentKind::Status => {
74 Vec::new()
75 }
76 }
77 }
78
79 pub fn abandon(&mut self) -> Option<StreamEvent> {
82 if self.finished {
83 return None;
84 }
85 self.finished = true;
86 Some(StreamEvent::Finished {
87 finish_reason: Some(FINISH_REASON_ABANDONED.to_owned()),
88 usage: None,
89 synthetic: true,
90 })
91 }
92
93 pub fn is_finished(&self) -> bool {
95 self.finished
96 }
97
98 pub fn duplicates_dropped(&self) -> u64 {
100 self.duplicates_dropped
101 }
102
103 pub fn late_dropped(&self) -> u64 {
105 self.late_dropped
106 }
107
108 fn feed_chunk(&mut self, envelope: &AgentEnvelope) -> Vec<StreamEvent> {
109 let Some(sequence) = envelope.sequence else {
110 return Vec::new();
113 };
114 if self.finished {
115 self.late_dropped += 1;
116 return Vec::new();
117 }
118 if sequence < self.next_sequence {
119 self.duplicates_dropped += 1;
120 return Vec::new();
121 }
122 if sequence > self.next_sequence {
123 self.finished = true;
124 return vec![StreamEvent::Finished {
125 finish_reason: Some(FINISH_REASON_GAP.to_owned()),
126 usage: None,
127 synthetic: true,
128 }];
129 }
130 self.next_sequence += 1;
131 let mut events = Vec::new();
132 if !envelope.body.is_empty() {
133 events.push(StreamEvent::Body {
134 sequence,
135 payload: envelope.body.clone(),
136 });
137 }
138 if envelope.last {
139 self.finished = true;
140 events.push(StreamEvent::Finished {
141 finish_reason: envelope.finish_reason.clone(),
142 usage: envelope.usage,
143 synthetic: false,
144 });
145 }
146 events
147 }
148}
149
150fn as_bytes<S: Serializer>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
153 serializer.serialize_bytes(value)
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use laser_wire::agent::{
160 AgentErrorBody, AgentErrorCode, ChannelId, ConversationId, CorrelationId, OPERATION_CHAT,
161 RecordId,
162 };
163 use laser_wire::framing::encode_named;
164
165 #[test]
166 fn given_an_ordered_stream_when_fed_then_should_emit_bodies_and_the_terminal() {
167 let mut assembler = ChunkAssembler::new();
168 assert_eq!(
169 assembler.feed(&chunk(0, b"he")),
170 vec![StreamEvent::Body {
171 sequence: 0,
172 payload: b"he".to_vec()
173 }]
174 );
175 assert_eq!(
176 assembler.feed(&chunk(1, b"llo")),
177 vec![StreamEvent::Body {
178 sequence: 1,
179 payload: b"llo".to_vec()
180 }]
181 );
182 assert_eq!(
183 assembler.feed(&terminal(2)),
184 vec![StreamEvent::Finished {
185 finish_reason: Some("stop".to_owned()),
186 usage: None,
187 synthetic: false
188 }]
189 );
190 assert!(assembler.is_finished());
191 assert_eq!(assembler.duplicates_dropped(), 0);
192 assert_eq!(assembler.late_dropped(), 0);
193 }
194
195 #[test]
196 fn given_a_redelivered_chunk_when_fed_then_should_drop_and_count_it() {
197 let mut assembler = ChunkAssembler::new();
198 assembler.feed(&chunk(0, b"a"));
199 assembler.feed(&chunk(1, b"b"));
200 assert!(assembler.feed(&chunk(0, b"a")).is_empty());
201 assert!(assembler.feed(&chunk(1, b"b")).is_empty());
202 assert_eq!(assembler.duplicates_dropped(), 2);
203 assert!(!assembler.is_finished());
204 assert_eq!(
206 assembler.feed(&chunk(2, b"c")),
207 vec![StreamEvent::Body {
208 sequence: 2,
209 payload: b"c".to_vec()
210 }]
211 );
212 }
213
214 #[test]
215 fn given_a_sequence_gap_when_fed_then_should_synthesize_the_gap_terminal() {
216 let mut assembler = ChunkAssembler::new();
217 assembler.feed(&chunk(0, b"a"));
218 let events = assembler.feed(&chunk(2, b"c"));
219 assert_eq!(
220 events,
221 vec![StreamEvent::Finished {
222 finish_reason: Some(FINISH_REASON_GAP.to_owned()),
223 usage: None,
224 synthetic: true
225 }]
226 );
227 assert!(assembler.is_finished());
228 assert!(assembler.feed(&chunk(1, b"b")).is_empty());
230 assert_eq!(assembler.late_dropped(), 1);
231 }
232
233 #[test]
234 fn given_chunks_after_the_terminal_when_fed_then_first_terminal_should_win() {
235 let mut assembler = ChunkAssembler::new();
236 assembler.feed(&chunk(0, b"a"));
237 assembler.feed(&terminal(1));
238 assert!(assembler.feed(&chunk(2, b"x")).is_empty());
240 assert!(assembler.feed(&terminal(3)).is_empty());
241 assert_eq!(assembler.late_dropped(), 2);
242 }
243
244 #[test]
245 fn given_a_non_empty_terminal_chunk_when_fed_then_should_emit_body_then_finished() {
246 let mut assembler = ChunkAssembler::new();
247 let mut closing = chunk(0, b"tail");
248 closing.last = true;
249 closing.finish_reason = Some("stop".to_owned());
250 let events = assembler.feed(&closing);
251 assert_eq!(events.len(), 2);
252 assert!(matches!(events[0], StreamEvent::Body { sequence: 0, .. }));
253 assert!(matches!(
254 events[1],
255 StreamEvent::Finished {
256 synthetic: false,
257 ..
258 }
259 ));
260 }
261
262 #[test]
263 fn given_an_error_terminal_when_fed_then_should_fail_the_stream() {
264 let error_body = encode_named(&AgentErrorBody {
265 code: AgentErrorCode::ToolFailure,
266 message: Some("boom".to_owned()),
267 retryable: false,
268 detail: None,
269 })
270 .expect("error body encodes");
271 let mut error = AgentEnvelope::error(
272 RecordId::from_u128(9),
273 ConversationId::from_u128(1),
274 "source-agent".parse().expect("valid agent id"),
275 CorrelationId::from_u128(3),
276 error_body.clone(),
277 );
278 error.channel = Some(ChannelId::from_u128(4));
279 error.sequence = Some(1);
280
281 let mut assembler = ChunkAssembler::new();
282 assembler.feed(&chunk(0, b"a"));
283 assert_eq!(
284 assembler.feed(&error),
285 vec![StreamEvent::Failed { body: error_body }]
286 );
287 assert!(assembler.is_finished());
288 assert!(assembler.feed(&error.clone()).is_empty());
290 assert_eq!(assembler.late_dropped(), 1);
291 }
292
293 #[test]
294 fn given_an_idle_stream_when_abandoned_then_should_synthesize_once() {
295 let mut assembler = ChunkAssembler::new();
296 assembler.feed(&chunk(0, b"a"));
297 let ending = assembler.abandon().expect("first abandonment synthesizes");
298 assert_eq!(
299 ending,
300 StreamEvent::Finished {
301 finish_reason: Some(FINISH_REASON_ABANDONED.to_owned()),
302 usage: None,
303 synthetic: true
304 }
305 );
306 assert!(assembler.abandon().is_none());
307 assert!(assembler.feed(&chunk(1, b"b")).is_empty());
309 assert_eq!(assembler.late_dropped(), 1);
310 }
311
312 #[test]
313 fn given_stream_events_when_serialized_then_should_tag_each_with_its_kind() {
314 let body = serde_json::to_value(StreamEvent::Body {
315 sequence: 2,
316 payload: b"a".to_vec(),
317 })
318 .expect("serializes");
319 assert_eq!(
320 body,
321 serde_json::json!({"kind": "body", "sequence": 2, "payload": [97]})
322 );
323 let finished = serde_json::to_value(StreamEvent::Finished {
324 finish_reason: Some("stop".to_owned()),
325 usage: None,
326 synthetic: false,
327 })
328 .expect("serializes");
329 assert_eq!(
330 finished,
331 serde_json::json!({"kind": "finished", "finish_reason": "stop", "usage": null, "synthetic": false})
332 );
333 let failed =
334 serde_json::to_value(StreamEvent::Failed { body: vec![1] }).expect("serializes");
335 assert_eq!(failed, serde_json::json!({"kind": "failed", "body": [1]}));
336 }
337
338 fn chunk(sequence: u64, body: &[u8]) -> AgentEnvelope {
339 let envelope = AgentEnvelope::chunk(
340 ConversationId::from_u128(1),
341 "source-agent".parse().expect("valid agent id"),
342 CorrelationId::from_u128(3),
343 ChannelId::from_u128(4),
344 sequence,
345 body.to_vec(),
346 );
347 if sequence == 0 {
348 envelope.with_operation(OPERATION_CHAT)
349 } else {
350 envelope
351 }
352 }
353
354 fn terminal(sequence: u64) -> AgentEnvelope {
355 let mut envelope = chunk(sequence, b"");
356 envelope.last = true;
357 envelope.finish_reason = Some("stop".to_owned());
358 envelope
359 }
360}