1mod handle;
8mod types;
9mod writer;
10
11use std::sync::{Arc, Mutex};
12
13use tokio::sync::mpsc;
14
15use self::types::{StreamReceivers, StreamSubscriptions};
16pub use self::{
17 handle::ChatResponseHandle,
18 types::{
19 ChatResponseSharedState, ChatResult, ResponseEvent, StreamChunk, StreamError, ToolCallEvent,
20 },
21 writer::{ChatResponseWriter, WriterError},
22};
23
24pub(crate) const DEFAULT_CHANNEL_BUFFER: usize = 256;
27
28#[must_use]
33pub fn channel() -> (ChatResponseWriter, ChatResponseHandle) {
34 channel_with_buffer(DEFAULT_CHANNEL_BUFFER)
35}
36
37#[must_use]
43pub fn channel_with_buffer(buffer: usize) -> (ChatResponseWriter, ChatResponseHandle) {
44 let (text_tx, text_rx) = mpsc::channel(buffer);
45 let (thought_tx, thought_rx) = mpsc::channel(buffer);
46 let (tool_call_tx, tool_call_rx) = mpsc::channel(buffer);
47 let (error_tx, error_rx) = mpsc::channel(1);
48 let (event_tx, event_rx) = mpsc::channel(buffer);
49 let (step_tx, step_rx) = mpsc::channel(buffer);
50 let (chunk_tx, chunk_rx) = mpsc::channel(buffer);
51
52 let shared_state = Arc::new(Mutex::new(ChatResponseSharedState::default()));
53 let subs = Arc::new(StreamSubscriptions::default());
54
55 let writer = ChatResponseWriter {
56 text_tx,
57 thought_tx,
58 tool_call_tx,
59 error_tx,
60 event_tx,
61 step_tx,
62 chunk_tx,
63 subs: Arc::clone(&subs),
64 shared_state: Arc::clone(&shared_state),
65 };
66
67 let handle = ChatResponseHandle {
68 rx: StreamReceivers::new(
69 text_rx,
70 thought_rx,
71 tool_call_rx,
72 error_rx,
73 event_rx,
74 step_rx,
75 chunk_rx,
76 ),
77 subs,
78 usage: None,
79 structured_output_value: None,
80 shared_state,
81 };
82
83 (writer, handle)
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[tokio::test]
91 async fn streaming_receives_all_tokens_in_order() {
92 let (writer, mut handle) = channel();
93
94 let tokens = ["Hello", " ", "world", "!"];
95 let expected: String = tokens.iter().copied().collect();
96
97 let send_task = tokio::spawn(async move {
99 for token in &["Hello", " ", "world", "!"] {
100 writer
101 .text_tx
102 .send((*token).to_owned())
103 .await
104 .expect("send should succeed");
105 }
106 });
108
109 let mut rx = handle.take_text_stream().expect("should get receiver");
111 let mut received = Vec::new();
112 while let Some(token) = rx.recv().await {
113 received.push(token);
114 }
115
116 send_task.await.expect("send task should complete");
117 let full: String = received.iter().map(String::as_str).collect();
118 assert_eq!(full, expected);
119 }
120
121 #[tokio::test]
122 async fn text_returns_complete_response() {
123 let (writer, handle) = channel();
124
125 tokio::spawn(async move {
126 for token in &["The ", "answer ", "is ", "42."] {
127 writer
128 .text_tx
129 .send((*token).to_owned())
130 .await
131 .expect("send");
132 }
133 });
134
135 let text = handle.text().await.expect("should succeed");
136 assert_eq!(text, "The answer is 42.");
137 }
138
139 #[tokio::test]
140 async fn text_returns_empty_when_no_tokens() {
141 let (writer, handle) = channel();
142 drop(writer);
144
145 let text = handle.text().await.expect("should succeed");
146 assert!(text.is_empty());
147 }
148
149 #[tokio::test]
150 async fn stream_error_propagated() {
151 let (writer, handle) = channel();
152
153 tokio::spawn(async move {
154 writer
155 .text_tx
156 .send("partial".to_owned())
157 .await
158 .expect("send");
159 writer
160 .error_tx
161 .send(StreamError {
162 message: "Python exception: quota exceeded".to_owned(),
163 })
164 .await
165 .expect("send error");
166 });
167
168 let result = handle.text().await;
169 assert!(result.is_err());
170 let err = result.unwrap_err();
171 assert!(err.message.contains("quota exceeded"));
172 }
173
174 #[tokio::test]
175 async fn thought_stream_works() {
176 let (writer, mut handle) = channel();
177
178 tokio::spawn(async move {
179 writer
180 .thought_tx
181 .send("thinking...".to_owned())
182 .await
183 .expect("send");
184 writer
185 .thought_tx
186 .send("done.".to_owned())
187 .await
188 .expect("send");
189 });
190
191 let mut rx = handle.take_thought_stream().expect("should get receiver");
192 let mut thoughts = Vec::new();
193 while let Some(t) = rx.recv().await {
194 thoughts.push(t);
195 }
196 assert_eq!(thoughts, vec!["thinking...", "done."]);
197 }
198
199 #[tokio::test]
200 async fn tool_call_stream_works() {
201 let (writer, mut handle) = channel();
202
203 let event = ToolCallEvent {
204 name: "view_file".to_owned(),
205 args: serde_json::json!({"path": "/tmp/test.txt"}),
206 id: Some("call_1".to_owned()),
207 canonical_path: None,
208 };
209
210 let event_clone = event.clone();
211 tokio::spawn(async move {
212 writer.tool_call_tx.send(event_clone).await.expect("send");
213 });
214
215 let mut rx = handle.take_tool_call_stream().expect("should get receiver");
216 let received = rx.recv().await.expect("should receive event");
217 assert_eq!(received.name, "view_file");
218 assert_eq!(received.id, Some("call_1".to_owned()));
219 }
220
221 #[tokio::test]
222 async fn usage_metadata_available_after_finalize() {
223 let (writer, mut handle) = channel();
224 assert!(handle.usage_metadata().is_none());
225
226 writer.set_usage(crate::types::UsageMetadata {
227 prompt_token_count: Some(100),
228 cached_content_token_count: Some(10),
229 candidates_token_count: Some(50),
230 thoughts_token_count: Some(20),
231 total_token_count: Some(170),
232 });
233 drop(writer);
234 handle.finalize();
235
236 let usage = handle.usage_metadata().expect("should have usage");
237 assert_eq!(usage.prompt_token_count, Some(100));
238 assert_eq!(usage.total_token_count, Some(170));
239 }
240
241 #[test]
242 fn take_text_stream_returns_none_second_time() {
243 let (_writer, mut handle) = channel();
244 assert!(handle.take_text_stream().is_some());
245 assert!(handle.take_text_stream().is_none());
246 }
247
248 #[test]
249 fn tool_call_event_serde_roundtrip() {
250 let event = ToolCallEvent {
251 name: "run_command".to_owned(),
252 args: serde_json::json!({"command": "ls"}),
253 id: Some("call_42".to_owned()),
254 canonical_path: None,
255 };
256 let json = serde_json::to_string(&event).expect("serialize");
257 let parsed: ToolCallEvent = serde_json::from_str(&json).expect("deserialize");
258 assert_eq!(parsed.name, event.name);
259 assert_eq!(parsed.args, event.args);
260 assert_eq!(parsed.id, event.id);
261 }
262
263 #[test]
264 fn take_thought_stream_returns_none_second_time() {
265 let (_writer, mut handle) = channel();
266 assert!(handle.take_thought_stream().is_some());
267 assert!(handle.take_thought_stream().is_none());
268 }
269
270 #[test]
271 fn take_tool_call_stream_returns_none_second_time() {
272 let (_writer, mut handle) = channel();
273 assert!(handle.take_tool_call_stream().is_some());
274 assert!(handle.take_tool_call_stream().is_none());
275 }
276
277 #[test]
278 fn stream_error_display() {
279 let err = StreamError {
280 message: "quota exceeded".to_owned(),
281 };
282 assert_eq!(format!("{err}"), "stream error: quota exceeded");
283 }
284
285 #[test]
286 fn stream_error_is_std_error() {
287 let err = StreamError {
288 message: "test".to_owned(),
289 };
290 let _: &dyn std::error::Error = &err;
292 }
293
294 #[tokio::test]
295 async fn concurrent_text_and_thought_streams() {
296 let (writer, mut handle) = channel();
297
298 tokio::spawn(async move {
299 writer
300 .text_tx
301 .send("Hello".to_owned())
302 .await
303 .expect("send text");
304 writer
305 .thought_tx
306 .send("thinking...".to_owned())
307 .await
308 .expect("send thought");
309 });
310
311 let mut text_rx = handle.take_text_stream().expect("text rx");
312 let mut thought_rx = handle.take_thought_stream().expect("thought rx");
313
314 let text = text_rx.recv().await.expect("receive text");
315 let thought = thought_rx.recv().await.expect("receive thought");
316
317 assert_eq!(text, "Hello");
318 assert_eq!(thought, "thinking...");
319 }
320
321 #[tokio::test]
322 async fn writer_dropped_without_sending_closes_text() {
323 let (writer, handle) = channel();
324 drop(writer);
325
326 let text = handle.text().await.expect("should succeed");
327 assert!(text.is_empty());
328 }
329
330 #[tokio::test]
331 async fn writer_dropped_without_sending_closes_thought_stream() {
332 let (writer, mut handle) = channel();
333 drop(writer);
334
335 let mut thought_rx = handle.take_thought_stream().expect("rx");
336 assert!(thought_rx.recv().await.is_none());
337 }
338
339 #[test]
340 fn tool_call_event_without_id() {
341 let event = ToolCallEvent {
342 name: "custom".to_owned(),
343 args: serde_json::json!(null),
344 id: None,
345 canonical_path: None,
346 };
347 let json = serde_json::to_string(&event).expect("serialize");
348 let parsed: ToolCallEvent = serde_json::from_str(&json).expect("deserialize");
349 assert_eq!(parsed.name, "custom");
350 assert_eq!(parsed.args, serde_json::json!(null));
351 }
352
353 #[tokio::test]
354 async fn large_token_stream() {
355 let (writer, handle) = channel();
356 let token_count = 200;
357
358 tokio::spawn(async move {
359 for i in 0..token_count {
360 writer.text_tx.send(format!("t{i}")).await.expect("send");
361 }
362 });
363
364 let text = handle.text().await.expect("should succeed");
365 for i in 0..token_count {
367 assert!(
368 text.contains(&format!("t{i}")),
369 "Missing token t{i} in output"
370 );
371 }
372 }
373
374 #[tokio::test]
375 async fn resolve_returns_events_in_order() {
376 let (writer, handle) = channel();
377
378 let tool_event = ToolCallEvent {
379 name: "view_file".to_owned(),
380 args: serde_json::json!({"path": "/tmp/x.rs"}),
381 id: Some("call_1".to_owned()),
382 canonical_path: None,
383 };
384
385 let tool_clone = tool_event.clone();
386 tokio::spawn(async move {
387 writer
388 .event_tx
389 .send(ResponseEvent::TextChunk("Hello ".to_owned()))
390 .await
391 .expect("send");
392 writer
393 .event_tx
394 .send(ResponseEvent::ThoughtChunk("hmm".to_owned()))
395 .await
396 .expect("send");
397 writer
398 .event_tx
399 .send(ResponseEvent::ToolCall(tool_clone))
400 .await
401 .expect("send");
402 writer
403 .event_tx
404 .send(ResponseEvent::TextChunk("world".to_owned()))
405 .await
406 .expect("send");
407 writer
408 .event_tx
409 .send(ResponseEvent::ToolResult(crate::types::ToolResult {
410 name: "view_file".to_owned(),
411 id: Some("call_1".to_owned()),
412 result: serde_json::json!({"output": "file contents"}),
413 error: None,
414 }))
415 .await
416 .expect("send");
417 });
419
420 let events = handle.resolve().await;
421 assert_eq!(events.len(), 5, "Expected 5 events, got {}", events.len());
422
423 assert!(
425 matches!(&events[0], ResponseEvent::TextChunk(s) if s == "Hello "),
426 "events[0] should be TextChunk(\"Hello \")"
427 );
428 assert!(
429 matches!(&events[1], ResponseEvent::ThoughtChunk(s) if s == "hmm"),
430 "events[1] should be ThoughtChunk(\"hmm\")"
431 );
432 assert!(
433 matches!(&events[2], ResponseEvent::ToolCall(tc) if tc.name == "view_file"),
434 "events[2] should be ToolCall(view_file)"
435 );
436 assert!(
437 matches!(&events[3], ResponseEvent::TextChunk(s) if s == "world"),
438 "events[3] should be TextChunk(\"world\")"
439 );
440 assert!(
441 matches!(&events[4], ResponseEvent::ToolResult(tr) if tr.name == "view_file"),
442 "events[4] should be ToolResult(view_file)"
443 );
444 }
445
446 #[test]
447 fn response_event_serde_roundtrip() {
448 let events = vec![
449 ResponseEvent::TextChunk("hello".to_owned()),
450 ResponseEvent::ThoughtChunk("thinking".to_owned()),
451 ResponseEvent::ToolCall(ToolCallEvent {
452 name: "run_command".to_owned(),
453 args: serde_json::json!({"cmd": "ls"}),
454 id: Some("c1".to_owned()),
455 canonical_path: None,
456 }),
457 ResponseEvent::ToolResult(crate::types::ToolResult {
458 name: "run_command".to_owned(),
459 id: Some("c1".to_owned()),
460 result: serde_json::json!({"output": "done"}),
461 error: None,
462 }),
463 ];
464
465 let json = serde_json::to_string(&events).expect("serialize");
466 let parsed: Vec<ResponseEvent> = serde_json::from_str(&json).expect("deserialize");
467 assert_eq!(parsed.len(), events.len());
468 }
469
470 #[tokio::test]
473 async fn receive_chunks_returns_chunks_in_order() {
474 use tokio_stream::StreamExt;
475
476 let (writer, mut handle) = channel();
477
478 tokio::spawn(async move {
479 writer
480 .chunk_tx
481 .send(StreamChunk::Text("hello".to_owned()))
482 .await
483 .expect("send");
484 writer
485 .chunk_tx
486 .send(StreamChunk::Thought("hmm".to_owned()))
487 .await
488 .expect("send");
489 writer
490 .chunk_tx
491 .send(StreamChunk::ToolCall(ToolCallEvent {
492 name: "view_file".to_owned(),
493 args: serde_json::json!({}),
494 id: None,
495 canonical_path: None,
496 }))
497 .await
498 .expect("send");
499 writer
500 .chunk_tx
501 .send(StreamChunk::Text(" world".to_owned()))
502 .await
503 .expect("send");
504 });
505
506 let mut stream = handle.receive_chunks().expect("should get stream");
507 let mut items = Vec::new();
508 while let Some(chunk) = stream.next().await {
509 items.push(chunk);
510 }
511
512 assert_eq!(items.len(), 4);
513 assert!(matches!(&items[0], StreamChunk::Text(t) if t == "hello"));
514 assert!(matches!(&items[1], StreamChunk::Thought(t) if t == "hmm"));
515 assert!(matches!(&items[2], StreamChunk::ToolCall(tc) if tc.name == "view_file"));
516 assert!(matches!(&items[3], StreamChunk::Text(t) if t == " world"));
517 }
518
519 #[tokio::test]
520 async fn receive_steps_returns_steps() {
521 use tokio_stream::StreamExt;
522
523 let (writer, mut handle) = channel();
524
525 tokio::spawn(async move {
526 writer
527 .step_tx
528 .send(crate::types::Step {
529 id: "step-0".to_owned(),
530 step_index: 0,
531 step_type: crate::types::StepType::TextResponse,
532 source: crate::types::StepSource::Model,
533 target: crate::types::StepTarget::User,
534 status: crate::types::StepStatus::Done,
535 content: "Hello".to_owned(),
536 content_delta: "Hello".to_owned(),
537 thinking: String::new(),
538 thinking_delta: String::new(),
539 tool_calls: vec![],
540 error: String::new(),
541 http_code: 0,
542 is_complete_response: Some(true),
543 structured_output: None,
544 usage_metadata: None,
545 })
546 .await
547 .expect("send");
548 });
549
550 let mut stream = handle.receive_steps().expect("should get stream");
551 let step = stream.next().await.expect("should get a step");
552 assert_eq!(step.id, "step-0");
553 assert_eq!(step.step_type, crate::types::StepType::TextResponse);
554 assert_eq!(step.content, "Hello");
555 }
556
557 #[tokio::test]
558 async fn existing_channels_work_alongside_chunk_stream() {
559 use tokio_stream::StreamExt;
560
561 let (writer, mut handle) = channel();
562
563 tokio::spawn(async move {
564 writer
566 .text_tx
567 .send("text-tok".to_owned())
568 .await
569 .expect("send text");
570 writer
571 .chunk_tx
572 .send(StreamChunk::Text("text-tok".to_owned()))
573 .await
574 .expect("send chunk");
575 });
576
577 let mut text_rx = handle.take_text_stream().expect("text rx");
578 let text = text_rx.recv().await.expect("receive text");
579 assert_eq!(text, "text-tok");
580
581 let mut chunk_stream = handle.receive_chunks().expect("chunk stream");
582 let chunk = chunk_stream.next().await.expect("receive chunk");
583 assert!(matches!(chunk, StreamChunk::Text(t) if t == "text-tok"));
584 }
585
586 #[test]
587 fn receive_chunks_returns_none_on_second_call() {
588 let (_writer, mut handle) = channel();
589 assert!(handle.receive_chunks().is_some());
590 assert!(handle.receive_chunks().is_none());
591 }
592
593 #[test]
594 fn receive_steps_returns_none_on_second_call() {
595 let (_writer, mut handle) = channel();
596 assert!(handle.receive_steps().is_some());
597 assert!(handle.receive_steps().is_none());
598 }
599
600 #[test]
601 fn take_event_stream_returns_none_second_time() {
602 let (_writer, mut handle) = channel();
603 assert!(handle.take_event_stream().is_some());
604 assert!(handle.take_event_stream().is_none());
605 }
606
607 #[test]
608 fn take_chunk_stream_returns_none_second_time() {
609 let (_writer, mut handle) = channel();
610 assert!(handle.take_chunk_stream().is_some());
611 assert!(handle.take_chunk_stream().is_none());
612 }
613
614 #[tokio::test]
620 async fn draining_event_stream_avoids_backpressure_beyond_buffer() {
621 let (writer, mut handle) = channel();
622 let total = DEFAULT_CHANNEL_BUFFER * 3;
623
624 let producer = tokio::spawn(async move {
625 for i in 0..total {
626 writer
627 .event_tx
628 .send(ResponseEvent::TextChunk(format!("e{i}")))
629 .await
630 .expect("send should not fail while consumer drains");
631 }
632 });
633
634 let mut rx = handle.take_event_stream().expect("event rx");
635 let mut count = 0usize;
636 while (rx.recv().await).is_some() {
637 count += 1;
638 }
639
640 producer.await.expect("producer task");
641 assert_eq!(
642 count, total,
643 "all {total} events must flow when the channel is drained concurrently"
644 );
645 }
646
647 #[tokio::test]
658 async fn unsubscribed_view_never_blocks_writer() {
659 let (writer, mut handle) = channel();
660 let mut text_rx = handle.take_text_stream().expect("text rx");
662 let total = DEFAULT_CHANNEL_BUFFER * 4;
663
664 let producer = async move {
665 for i in 0..total {
666 ChatResponseWriter::fan_out(
669 &writer.subs.event,
670 &writer.event_tx,
671 ResponseEvent::TextChunk(format!("e{i}")),
672 "event",
673 )
674 .await;
675 ChatResponseWriter::fan_out(
677 &writer.subs.text,
678 &writer.text_tx,
679 format!("t{i}"),
680 "text",
681 )
682 .await;
683 }
684 drop(writer);
685 };
686
687 let consumer = async {
688 let mut n = 0usize;
689 while text_rx.recv().await.is_some() {
690 n += 1;
691 }
692 n
693 };
694
695 let ((), delivered) = tokio::join!(producer, consumer);
696 assert_eq!(
697 delivered, total,
698 "every subscribed text item must be delivered while the unsubscribed \
699 event view is skipped without blocking"
700 );
701 assert!(handle.take_event_stream().is_some());
704 }
705
706 #[test]
707 fn stream_chunk_serde_roundtrip() {
708 let chunks = vec![
709 StreamChunk::Text("hello".to_owned()),
710 StreamChunk::Thought("hmm".to_owned()),
711 StreamChunk::ToolCall(ToolCallEvent {
712 name: "run".to_owned(),
713 args: serde_json::json!({"cmd": "ls"}),
714 id: Some("c1".to_owned()),
715 canonical_path: None,
716 }),
717 ];
718 for chunk in &chunks {
719 let json = serde_json::to_string(chunk).expect("serialize");
720 let parsed: StreamChunk = serde_json::from_str(&json).expect("deserialize");
721 match (chunk, &parsed) {
723 (StreamChunk::Text(a), StreamChunk::Text(b))
724 | (StreamChunk::Thought(a), StreamChunk::Thought(b)) => assert_eq!(a, b),
725 (StreamChunk::ToolCall(a), StreamChunk::ToolCall(b)) => {
726 assert_eq!(a.name, b.name);
727 assert_eq!(a.id, b.id);
728 }
729 _ => panic!("variant mismatch after roundtrip"),
730 }
731 }
732 }
733
734 #[tokio::test]
735 async fn usage_metadata_populated_from_writer_after_resolve() {
736 let (writer, handle) = channel();
737
738 tokio::spawn(async move {
739 writer
740 .event_tx
741 .send(ResponseEvent::TextChunk("hello".to_owned()))
742 .await
743 .unwrap();
744 writer.set_usage(crate::types::UsageMetadata {
745 prompt_token_count: Some(5),
746 cached_content_token_count: None,
747 candidates_token_count: Some(1),
748 thoughts_token_count: None,
749 total_token_count: Some(6),
750 });
751 writer.set_structured_output(serde_json::json!({"key": "value"}));
752 });
753
754 let shared = handle.shared_state();
757 let events = handle.resolve().await;
758 assert_eq!(events.len(), 1);
759
760 let state = shared.lock().expect("lock shared state");
761 assert_eq!(state.usage.as_ref().unwrap().total_token_count, Some(6));
762 assert_eq!(
763 state.structured_output.as_ref().unwrap(),
764 &serde_json::json!({"key": "value"})
765 );
766 }
767
768 #[test]
769 fn chat_result_into_string() {
770 let (writer, handle) = channel();
771 drop(writer);
772 let rt = tokio::runtime::Runtime::new().unwrap();
773 let result = rt.block_on(handle.text()).unwrap();
774 let s: String = result.into();
775 assert!(s.is_empty());
776 }
777
778 #[tokio::test]
779 async fn chat_result_ergonomics() {
780 let (writer, handle) = channel();
781 tokio::spawn(async move {
782 writer
783 .text_tx
784 .send("hello world".to_owned())
785 .await
786 .expect("send");
787 });
788
789 let result = handle.text().await.expect("text");
790
791 assert_eq!(result.len(), 11);
793 assert!(result.contains("world"));
794 assert_eq!(result.text(), "hello world");
795
796 assert_eq!(result, "hello world");
798 assert_eq!(result, "hello world".to_owned());
799
800 assert_eq!(format!("{result}"), "hello world");
802
803 assert!(result.usage().is_none());
805 assert!(result.structured_output().is_none());
806
807 assert_eq!(result.into_string(), "hello world");
809 }
810
811 #[tokio::test]
814 async fn error_step_routed_to_error_channel() {
815 let (writer, handle) = channel();
818
819 let ChatResponseWriter {
822 text_tx,
823 error_tx,
824 step_tx,
825 ..
826 } = writer;
827
828 let producer = async move {
829 error_tx
832 .try_send(StreamError {
833 message: "Agent execution terminated due to error. (request failed (code 503): APP_ERROR(2))".to_owned(),
834 })
835 .expect("error_tx should accept");
836 step_tx
837 .send(crate::types::Step {
838 status: crate::types::StepStatus::Error,
839 error: "Agent execution terminated due to error.".to_owned(),
840 ..crate::types::Step::default()
841 })
842 .await
843 .expect("step_tx should accept");
844 drop(text_tx);
846 };
847
848 let consumer = handle.text();
849
850 let ((), result) = tokio::join!(producer, consumer);
851 let err = result.expect_err("handle.text() must return Err when error step is sent");
852 assert!(
853 err.message.contains("503") || err.message.contains("Agent execution terminated"),
854 "Error message should contain the backend error: {}",
855 err.message
856 );
857 }
858
859 #[tokio::test]
860 async fn error_step_without_error_channel_returns_empty_ok() {
861 let (writer, handle) = channel();
865
866 let ChatResponseWriter {
867 text_tx, step_tx, ..
868 } = writer;
869
870 let producer = async move {
871 step_tx
873 .send(crate::types::Step {
874 status: crate::types::StepStatus::Error,
875 error: "Agent execution terminated".to_owned(),
876 ..crate::types::Step::default()
877 })
878 .await
879 .expect("step_tx should accept");
880 drop(text_tx);
882 };
883
884 let consumer = handle.text();
885
886 let ((), result) = tokio::join!(producer, consumer);
888 let text =
889 result.expect("Without error_tx, text() should return Ok (demonstrating the old bug)");
890 assert!(text.is_empty(), "Without error_tx, text should be empty");
891 }
892
893 #[tokio::test]
894 async fn error_tx_capacity_one_first_error_wins() {
895 let (writer, handle) = channel();
897
898 let ChatResponseWriter {
899 text_tx, error_tx, ..
900 } = writer;
901
902 let producer = async move {
903 error_tx
905 .try_send(StreamError {
906 message: "first error".to_owned(),
907 })
908 .expect("first try_send should succeed");
909 let second = error_tx.try_send(StreamError {
911 message: "second error".to_owned(),
912 });
913 second.expect_err("Second try_send should fail (channel full)");
914 drop(text_tx);
916 };
917
918 let consumer = handle.text();
919
920 let ((), result) = tokio::join!(producer, consumer);
921 assert!(result.is_err());
922 assert_eq!(
923 result.unwrap_err().message,
924 "first error",
925 "First error should win"
926 );
927 }
928
929 #[tokio::test]
930 async fn error_step_with_partial_text_still_returns_error() {
931 let (writer, handle) = channel();
933
934 let ChatResponseWriter {
935 text_tx, error_tx, ..
936 } = writer;
937
938 let producer = async move {
939 text_tx
941 .send("partial response...".to_owned())
942 .await
943 .expect("text send");
944 error_tx
946 .try_send(StreamError {
947 message: "connection reset during streaming".to_owned(),
948 })
949 .expect("error send");
950 };
952
953 let consumer = handle.text();
954
955 let ((), result) = tokio::join!(producer, consumer);
956 let err = result.expect_err("Error should take priority over partial text");
957 assert!(
958 err.message.contains("connection reset"),
959 "Should contain the error message"
960 );
961 }
962
963 #[tokio::test]
964 async fn step_stream_receives_error_steps() {
965 use tokio_stream::StreamExt;
967
968 let (writer, mut handle) = channel();
969
970 let error_step = crate::types::Step {
971 id: "err-step".to_owned(),
972 step_index: 0,
973 status: crate::types::StepStatus::Error,
974 error: "model 503".to_owned(),
975 ..crate::types::Step::default()
976 };
977
978 let ChatResponseWriter {
979 error_tx, step_tx, ..
980 } = writer;
981
982 let producer = async move {
983 error_tx
984 .try_send(StreamError {
985 message: "model 503".to_owned(),
986 })
987 .expect("error send");
988 step_tx.send(error_step).await.expect("step send");
989 };
990
991 let consumer = async {
992 let mut stream = handle.receive_steps().expect("should get stream");
993 let step = stream.next().await.expect("should get error step");
994 assert_eq!(step.status, crate::types::StepStatus::Error);
995 assert_eq!(step.error, "model 503");
996 };
997
998 tokio::join!(producer, consumer);
999 }
1000}