1#[cfg(feature = "json")]
9use std::collections::VecDeque;
10#[cfg(feature = "json")]
11use std::time::Duration;
12
13#[cfg(all(feature = "json", feature = "async"))]
14use tokio::io::{AsyncBufReadExt, BufReader};
15#[cfg(all(feature = "json", feature = "async"))]
16use tokio::process::Command;
17#[cfg(feature = "json")]
18use tracing::debug;
19#[cfg(all(feature = "json", feature = "sync"))]
20use tracing::warn;
21
22#[cfg(feature = "json")]
23use crate::Claude;
24#[cfg(feature = "json")]
25use crate::error::{Error, Result};
26#[cfg(feature = "json")]
27use crate::exec::CommandOutput;
28
29#[cfg(feature = "json")]
30const STREAM_DIAGNOSTIC_MAX_BYTES: usize = 16 * 1024;
31
32#[cfg(feature = "json")]
34#[derive(Default)]
35struct ParseFailureDiagnostics {
36 lines: VecDeque<String>,
37 bytes: usize,
38}
39
40#[cfg(feature = "json")]
41impl ParseFailureDiagnostics {
42 fn push(&mut self, line: &str) {
43 let max_line_bytes = STREAM_DIAGNOSTIC_MAX_BYTES - 1;
47 let line = if line.len() > max_line_bytes {
48 let mut end = max_line_bytes;
49 while !line.is_char_boundary(end) {
50 end -= 1;
51 }
52 &line[..end]
53 } else {
54 line
55 };
56 let line_bytes = line.len() + 1;
57
58 while self.bytes + line_bytes > STREAM_DIAGNOSTIC_MAX_BYTES {
59 let Some(removed) = self.lines.pop_front() else {
60 break;
61 };
62 self.bytes -= removed.len() + 1;
63 }
64
65 self.lines.push_back(line.to_string());
66 self.bytes += line_bytes;
67 }
68
69 fn into_string(self) -> String {
70 let mut output = String::with_capacity(self.bytes);
71 for line in self.lines {
72 output.push_str(&line);
73 output.push('\n');
74 }
75 output.pop();
76 output
77 }
78}
79
80#[cfg(feature = "json")]
85#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
86pub struct StreamEvent {
87 #[serde(flatten)]
89 pub data: serde_json::Value,
90}
91
92#[cfg(feature = "json")]
93impl StreamEvent {
94 pub fn event_type(&self) -> Option<&str> {
96 self.data.get("type").and_then(|v| v.as_str())
97 }
98
99 pub fn role(&self) -> Option<&str> {
101 self.data.get("role").and_then(|v| v.as_str())
102 }
103
104 pub fn is_result(&self) -> bool {
106 self.event_type() == Some("result")
107 }
108
109 pub fn result_text(&self) -> Option<&str> {
111 self.data.get("result").and_then(|v| v.as_str())
112 }
113
114 pub fn session_id(&self) -> Option<&str> {
116 self.data.get("session_id").and_then(|v| v.as_str())
117 }
118
119 pub fn cost_usd(&self) -> Option<f64> {
126 self.data
127 .get("total_cost_usd")
128 .or_else(|| self.data.get("cost_usd"))
129 .and_then(|v| v.as_f64())
130 }
131
132 pub fn partial_message(&self) -> Option<PartialMessageEvent> {
173 let event = if self.event_type() == Some("stream_event") {
174 self.data.get("event")?
175 } else {
176 &self.data
177 };
178
179 let inner_type = event.get("type")?.as_str()?;
180 let index = event.get("index").and_then(serde_json::Value::as_u64)?;
181 let index = u32::try_from(index).ok()?;
182
183 match inner_type {
184 "content_block_start" => {
185 let block_type = parse_block_type(event.get("content_block")?);
186 Some(PartialMessageEvent::BlockStart { index, block_type })
187 }
188 "content_block_delta" => {
189 let delta = parse_block_delta(event.get("delta")?);
190 Some(PartialMessageEvent::BlockDelta { index, delta })
191 }
192 "content_block_stop" => Some(PartialMessageEvent::BlockStop { index }),
193 _ => None,
194 }
195 }
196}
197
198#[cfg(feature = "json")]
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum PartialMessageEvent {
206 BlockStart {
208 index: u32,
210 block_type: BlockType,
212 },
213 BlockDelta {
215 index: u32,
219 delta: BlockDelta,
221 },
222 BlockStop {
224 index: u32,
226 },
227}
228
229#[cfg(feature = "json")]
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum BlockType {
237 Text,
239 Thinking,
241 ToolUse {
243 id: String,
245 name: String,
247 },
248 Other(String),
250}
251
252#[cfg(feature = "json")]
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub enum BlockDelta {
261 Text(String),
263 Thinking(String),
265 InputJson(String),
268 Other,
271}
272
273#[cfg(feature = "json")]
274fn parse_block_type(content_block: &serde_json::Value) -> BlockType {
275 let Some(ty) = content_block
276 .get("type")
277 .and_then(serde_json::Value::as_str)
278 else {
279 return BlockType::Other(String::new());
280 };
281 match ty {
282 "text" => BlockType::Text,
283 "thinking" => BlockType::Thinking,
284 "tool_use" => {
285 let id = content_block
286 .get("id")
287 .and_then(serde_json::Value::as_str)
288 .unwrap_or("")
289 .to_string();
290 let name = content_block
291 .get("name")
292 .and_then(serde_json::Value::as_str)
293 .unwrap_or("")
294 .to_string();
295 BlockType::ToolUse { id, name }
296 }
297 other => BlockType::Other(other.to_string()),
298 }
299}
300
301#[cfg(feature = "json")]
302fn parse_block_delta(delta: &serde_json::Value) -> BlockDelta {
303 let Some(ty) = delta.get("type").and_then(serde_json::Value::as_str) else {
304 return BlockDelta::Other;
305 };
306 match ty {
307 "text_delta" => delta
308 .get("text")
309 .and_then(serde_json::Value::as_str)
310 .map(|s| BlockDelta::Text(s.to_string()))
311 .unwrap_or(BlockDelta::Other),
312 "thinking_delta" => delta
313 .get("thinking")
314 .and_then(serde_json::Value::as_str)
315 .map(|s| BlockDelta::Thinking(s.to_string()))
316 .unwrap_or(BlockDelta::Other),
317 "input_json_delta" => delta
318 .get("partial_json")
319 .and_then(serde_json::Value::as_str)
320 .map(|s| BlockDelta::InputJson(s.to_string()))
321 .unwrap_or(BlockDelta::Other),
322 _ => BlockDelta::Other,
323 }
324}
325
326#[cfg(all(feature = "json", feature = "async"))]
358pub async fn stream_query<F>(
359 claude: &Claude,
360 cmd: &crate::command::query::QueryCommand,
361 handler: F,
362) -> Result<CommandOutput>
363where
364 F: FnMut(StreamEvent),
365{
366 stream_query_impl(claude, cmd, handler, std::future::pending()).await
367}
368
369#[cfg(all(feature = "json", feature = "async"))]
376pub async fn stream_query_cancellable<C, F>(
377 claude: &Claude,
378 cmd: &crate::command::query::QueryCommand,
379 cancel: C,
380 handler: F,
381) -> Result<CommandOutput>
382where
383 C: std::future::Future<Output = ()> + Send,
384 F: FnMut(StreamEvent),
385{
386 stream_query_impl(claude, cmd, handler, cancel).await
387}
388
389#[cfg(all(feature = "json", feature = "async"))]
396async fn stream_query_impl<C, F>(
397 claude: &Claude,
398 cmd: &crate::command::query::QueryCommand,
399 mut handler: F,
400 cancel: C,
401) -> Result<CommandOutput>
402where
403 C: std::future::Future<Output = ()> + Send,
404 F: FnMut(StreamEvent),
405{
406 use crate::command::ClaudeCommand;
407
408 let args = cmd.args();
409 let operation = crate::exec::span_command(&args);
410 let stdin_prompt = cmd.stdin_prompt();
411
412 let mut command_args = Vec::new();
413 command_args.extend(claude.global_args.clone());
414 command_args.extend(args);
415
416 let span = tracing::debug_span!(
422 "claude.stream",
423 command = operation,
424 working_dir_set = claude.working_dir.is_some(),
425 timeout_secs = claude.timeout.map(|t| t.as_secs()),
426 outcome = tracing::field::Empty,
427 events = tracing::field::Empty,
428 exit_code = tracing::field::Empty,
429 duration_ms = tracing::field::Empty,
430 );
431 let _enter = span.enter();
432 let started = std::time::Instant::now();
433 let mut event_count: u64 = 0;
434
435 debug!(
436 arg_count = command_args.len(),
437 timeout = ?claude.timeout,
438 "streaming claude command"
439 );
440
441 let mut cmd = Command::new(&claude.binary);
442 cmd.args(&command_args)
443 .stdout(std::process::Stdio::piped())
444 .stderr(std::process::Stdio::piped())
445 .kill_on_drop(true);
448 if stdin_prompt.is_some() {
449 cmd.stdin(std::process::Stdio::piped());
450 } else {
451 cmd.stdin(std::process::Stdio::null());
452 }
453 crate::exec::apply_child_environment(cmd.as_std_mut(), claude.clear_env, &claude.env);
454 crate::exec::apply_process_group(&mut cmd, claude.process_group);
458
459 if let Some(ref dir) = claude.working_dir {
460 cmd.current_dir(dir);
461 }
462
463 let mut child = cmd.spawn().map_err(|e| Error::Io {
464 message: format!("failed to spawn claude: {e}"),
465 source: e,
466 working_dir: claude.working_dir.clone(),
467 })?;
468 let mut group =
469 crate::exec::arm_and_notify(claude.process_group, child.id(), claude.on_spawn.as_ref());
470
471 let stdout = child.stdout.take().expect("stdout was piped");
472 let mut stderr = child.stderr.take().expect("stderr was piped");
473 let child_stdin = child.stdin.take();
474
475 let mut reader = BufReader::new(stdout);
476
477 let drain = crate::exec::capture_stream(
482 &mut stderr,
483 claude.output_limit,
484 crate::OutputStream::Stderr,
485 claude.working_dir.as_deref(),
486 );
487 let write = async {
488 let (Some(prompt), Some(mut stdin)) = (stdin_prompt, child_stdin) else {
489 return Ok::<_, Error>(());
490 };
491 use tokio::io::AsyncWriteExt;
492 stdin
493 .write_all(prompt.as_bytes())
494 .await
495 .map_err(|source| Error::Io {
496 message: "failed to write the prompt to claude stdin".to_string(),
497 source,
498 working_dir: claude.working_dir.clone(),
499 })?;
500 stdin.shutdown().await.map_err(|source| Error::Io {
501 message: "failed to close claude stdin".to_string(),
502 source,
503 working_dir: claude.working_dir.clone(),
504 })
505 };
506 let mut counting_handler = |event: StreamEvent| {
509 event_count += 1;
510 handler(event);
511 };
512 let read_future = read_lines(
513 &mut reader,
514 &mut counting_handler,
515 claude.working_dir.clone(),
516 claude.output_limit,
517 );
518 let run = async {
519 let ((), diagnostics, stderr_str) = tokio::try_join!(write, read_future, drain)?;
520 let status = child.wait().await.map_err(|source| Error::Io {
523 message: "failed to wait for claude process".to_string(),
524 source,
525 working_dir: claude.working_dir.clone(),
526 })?;
527 Ok::<_, Error>((diagnostics, stderr_str, status))
528 };
529 let stop = async {
530 match claude.timeout {
531 Some(timeout) => tokio::select! {
532 () = cancel => StreamStop::Cancelled,
533 () = tokio::time::sleep(timeout) => StreamStop::Timeout(timeout),
534 },
535 None => {
536 cancel.await;
537 StreamStop::Cancelled
538 }
539 }
540 };
541 let finished = tokio::select! {
542 result = run => Ok(result),
543 reason = stop => Err(reason),
544 };
545
546 let (stdout_diagnostics, stderr_str, status) = match finished {
547 Ok(Ok((diagnostics, stderr, status))) => (diagnostics.into_string(), stderr, status),
548 Ok(Err(error)) => {
549 crate::exec::drain_until(
550 crate::exec::stop_and_reap(
551 &mut child,
552 &mut group,
553 claude.kill_grace,
554 claude.termination_signal,
555 claude.working_dir.as_deref(),
556 ),
557 &mut reader,
558 &mut stderr,
559 )
560 .await?;
561 span.record("outcome", "failed");
562 span.record("events", event_count);
563 span.record("duration_ms", started.elapsed().as_millis() as u64);
564 return Err(error);
565 }
566 Err(reason) => {
567 crate::exec::drain_until(
568 crate::exec::stop_and_reap(
569 &mut child,
570 &mut group,
571 claude.kill_grace,
572 claude.termination_signal,
573 claude.working_dir.as_deref(),
574 ),
575 &mut reader,
576 &mut stderr,
577 )
578 .await?;
579 let (outcome, error) = match reason {
580 StreamStop::Cancelled => ("cancelled", Error::Cancelled),
581 StreamStop::Timeout(timeout) => (
582 "timeout",
583 Error::Timeout {
584 timeout_seconds: timeout.as_secs(),
585 },
586 ),
587 };
588 span.record("outcome", outcome);
589 span.record("events", event_count);
590 span.record("duration_ms", started.elapsed().as_millis() as u64);
591 return Err(error);
592 }
593 };
594 group.disarm();
595
596 let exit_code = status.code().unwrap_or(-1);
597
598 span.record("events", event_count);
599 span.record("exit_code", exit_code);
600 span.record("duration_ms", started.elapsed().as_millis() as u64);
601
602 if !status.success() {
603 span.record("outcome", "failed");
604 return Err(Error::from_command_failure(
605 format!("{} {}", claude.binary.display(), command_args.join(" ")),
606 exit_code,
607 stdout_diagnostics,
608 stderr_str,
609 claude.working_dir.clone(),
610 ));
611 }
612
613 span.record("outcome", "completed");
614 Ok(CommandOutput {
615 stdout: String::new(), stderr: stderr_str,
617 exit_code,
618 success: true,
619 })
620}
621
622#[cfg(all(feature = "json", feature = "async"))]
623enum StreamStop {
624 Cancelled,
625 Timeout(Duration),
626}
627
628#[cfg(all(feature = "json", feature = "async"))]
629async fn read_lines<F>(
630 reader: &mut BufReader<tokio::process::ChildStdout>,
631 handler: &mut F,
632 working_dir: Option<std::path::PathBuf>,
633 output_limit: Option<usize>,
634) -> Result<ParseFailureDiagnostics>
635where
636 F: FnMut(StreamEvent),
637{
638 let mut diagnostics = ParseFailureDiagnostics::default();
639 let mut captured_bytes = 0usize;
640 while let Some(line) =
641 read_bounded_line(reader, &mut captured_bytes, output_limit, &working_dir).await?
642 {
643 if line.trim().is_empty() {
644 continue;
645 }
646 match serde_json::from_str::<StreamEvent>(&line) {
647 Ok(event) => handler(event),
648 Err(e) => {
649 debug!(line_bytes = line.len(), json_error = ?e.classify(), "failed to parse stream event, skipping");
650 diagnostics.push(&line);
651 }
652 }
653 }
654
655 Ok(diagnostics)
656}
657
658#[cfg(all(feature = "json", feature = "async"))]
659async fn read_bounded_line<R: tokio::io::AsyncBufRead + Unpin>(
660 reader: &mut R,
661 captured_bytes: &mut usize,
662 output_limit: Option<usize>,
663 working_dir: &Option<std::path::PathBuf>,
664) -> Result<Option<String>> {
665 let mut line = Vec::new();
666 loop {
667 let available = reader.fill_buf().await.map_err(|source| Error::Io {
668 message: "failed to read stdout line".to_string(),
669 source,
670 working_dir: working_dir.clone(),
671 })?;
672 if available.is_empty() {
673 if line.is_empty() {
674 return Ok(None);
675 }
676 break;
677 }
678 let take = available
679 .iter()
680 .position(|byte| *byte == b'\n')
681 .map_or(available.len(), |index| index + 1);
682 if output_limit.is_some_and(|limit| captured_bytes.saturating_add(take) > limit) {
683 return Err(Error::OutputLimitExceeded {
684 stream: crate::OutputStream::Stdout,
685 limit_bytes: output_limit.unwrap_or_default(),
686 });
687 }
688 line.extend_from_slice(&available[..take]);
689 *captured_bytes = captured_bytes.saturating_add(take);
690 let complete = line.last() == Some(&b'\n');
691 reader.consume(take);
692 if complete {
693 line.pop();
694 break;
695 }
696 }
697 if line.last() == Some(&b'\r') {
698 line.pop();
699 }
700 String::from_utf8(line)
701 .map(Some)
702 .map_err(|source| Error::Io {
703 message: "failed to decode stdout line".to_string(),
704 source: std::io::Error::new(std::io::ErrorKind::InvalidData, source),
705 working_dir: working_dir.clone(),
706 })
707}
708
709#[cfg(all(feature = "sync", feature = "json"))]
712fn read_line_or_discard<R: std::io::BufRead>(
713 reader: &mut R,
714 discard: &std::sync::atomic::AtomicBool,
715) -> std::io::Result<Option<String>> {
716 let mut line = Vec::new();
717 loop {
718 if discard.load(std::sync::atomic::Ordering::Acquire) {
719 std::io::copy(reader, &mut std::io::sink())?;
720 return Ok(None);
721 }
722 let available = match reader.fill_buf() {
723 Ok(available) => available,
724 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
725 Err(error) => return Err(error),
726 };
727 if available.is_empty() {
728 if line.is_empty() {
729 return Ok(None);
730 }
731 break;
732 }
733 if discard.load(std::sync::atomic::Ordering::Acquire) {
736 continue;
737 }
738 let newline = available.iter().position(|b| *b == b'\n');
739 let used = newline.map_or(available.len(), |i| i + 1);
740 line.extend_from_slice(&available[..used]);
741 reader.consume(used);
742 if newline.is_some() {
743 break;
744 }
745 }
746 if line.last() == Some(&b'\n') {
748 line.pop();
749 if line.last() == Some(&b'\r') {
750 line.pop();
751 }
752 }
753 String::from_utf8(line)
754 .map(Some)
755 .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
756}
757
758#[cfg(all(feature = "sync", feature = "json"))]
794pub fn stream_query_sync<F>(
795 claude: &Claude,
796 cmd: &crate::command::query::QueryCommand,
797 mut handler: F,
798) -> Result<CommandOutput>
799where
800 F: FnMut(StreamEvent),
801{
802 use std::process::{Command as StdCommand, Stdio};
803 use std::sync::Arc;
804 use std::sync::atomic::{AtomicBool, Ordering};
805 use std::sync::mpsc;
806 use std::thread;
807 use std::time::Instant;
808
809 use crate::command::ClaudeCommand;
810
811 let args = cmd.args();
812 let mut command_args = Vec::new();
813 command_args.extend(claude.global_args.clone());
814 command_args.extend(args);
815
816 debug!(
817 arg_count = command_args.len(),
818 timeout = ?claude.timeout,
819 "streaming claude command (sync)"
820 );
821
822 let mut cmd_builder = StdCommand::new(&claude.binary);
823 cmd_builder
824 .args(&command_args)
825 .stdin(Stdio::null())
826 .stdout(Stdio::piped())
827 .stderr(Stdio::piped());
828 crate::exec::apply_child_environment(&mut cmd_builder, claude.clear_env, &claude.env);
829 crate::exec::apply_process_group_sync(&mut cmd_builder, claude.process_group);
833
834 if let Some(ref dir) = claude.working_dir {
835 cmd_builder.current_dir(dir);
836 }
837
838 let mut child = cmd_builder.spawn().map_err(|e| Error::Io {
839 message: format!("failed to spawn claude: {e}"),
840 source: e,
841 working_dir: claude.working_dir.clone(),
842 })?;
843 let mut group = crate::exec::arm_and_notify(
844 claude.process_group,
845 Some(child.id()),
846 claude.on_spawn.as_ref(),
847 );
848
849 let stdout = child.stdout.take().expect("stdout was piped");
850 let stderr = child.stderr.take().expect("stderr was piped");
851
852 let (tx, rx) = mpsc::channel::<StreamEvent>();
856 let reader_wd = claude.working_dir.clone();
857 let discard = Arc::new(AtomicBool::new(false));
858 let reader_discard = Arc::clone(&discard);
859 let stderr_discard = Arc::clone(&discard);
860 let reader_thread = thread::spawn(move || -> Result<ParseFailureDiagnostics> {
861 let mut reader = std::io::BufReader::new(stdout);
862 let mut diagnostics = ParseFailureDiagnostics::default();
863 while let Some(line) =
864 read_line_or_discard(&mut reader, &reader_discard).map_err(|e| Error::Io {
865 message: "failed to read stdout line".to_string(),
866 source: e,
867 working_dir: reader_wd.clone(),
868 })?
869 {
870 if line.trim().is_empty() {
871 continue;
872 }
873 match serde_json::from_str::<StreamEvent>(&line) {
874 Ok(event) => {
875 if tx.send(event).is_err() {
876 return Ok(diagnostics);
878 }
879 }
880 Err(e) => {
881 debug!(line_bytes = line.len(), json_error = ?e.classify(), "failed to parse stream event, skipping");
882 diagnostics.push(&line);
883 }
884 }
885 }
886 Ok(diagnostics)
887 });
888
889 let stderr_thread = thread::spawn(move || -> String {
890 let mut buf = Vec::new();
891 let _ = crate::exec::read_discardable(stderr, &stderr_discard, |chunk| {
892 buf.extend_from_slice(chunk);
893 });
894 String::from_utf8_lossy(&buf).into_owned()
895 });
896
897 let deadline = claude.timeout.map(|d| Instant::now() + d);
900 let mut timed_out = false;
901
902 loop {
903 let recv_result = match deadline {
904 Some(d) => {
905 let now = Instant::now();
906 if now >= d {
907 timed_out = true;
908 break;
909 }
910 rx.recv_timeout(d - now)
911 }
912 None => rx.recv().map_err(|_| mpsc::RecvTimeoutError::Disconnected),
913 };
914
915 match recv_result {
916 Ok(event) => handler(event),
917 Err(mpsc::RecvTimeoutError::Timeout) => {
918 timed_out = true;
919 break;
920 }
921 Err(mpsc::RecvTimeoutError::Disconnected) => break,
922 }
923 }
924
925 if timed_out {
926 discard.store(true, Ordering::Release);
927 crate::exec::kill_group_with_grace_sync(
931 &mut group,
932 claude.kill_grace,
933 claude.termination_signal,
934 );
935 let _ = child.kill();
936 let _ = child.wait();
937 let budget = Duration::from_millis(200);
944 let stderr_str = join_with_budget(stderr_thread, budget).unwrap_or_default();
945 let _ = join_with_budget(reader_thread, budget);
946 if !stderr_str.is_empty() {
947 warn!(
948 stderr_bytes = stderr_str.len(),
949 "stderr from timed-out streaming process"
950 );
951 }
952 return Err(Error::Timeout {
953 timeout_seconds: claude.timeout.map(|d| d.as_secs()).unwrap_or_default(),
954 });
955 }
956
957 let reader_result = reader_thread
959 .join()
960 .unwrap_or_else(|_| Ok(ParseFailureDiagnostics::default()));
961 let stdout_diagnostics = match reader_result {
962 Ok(diagnostics) => diagnostics.into_string(),
963 Err(e) => {
964 discard.store(true, Ordering::Release);
965 crate::exec::kill_group_with_grace_sync(
966 &mut group,
967 claude.kill_grace,
968 claude.termination_signal,
969 );
970 let _ = child.kill();
971 let _ = child.wait();
972 let _ = stderr_thread.join();
973 return Err(e);
974 }
975 };
976
977 let status = child.wait().map_err(|e| Error::Io {
978 message: "failed to wait for claude process".to_string(),
979 source: e,
980 working_dir: claude.working_dir.clone(),
981 })?;
982 group.disarm();
983 let stderr_str = stderr_thread.join().unwrap_or_default();
984 let exit_code = status.code().unwrap_or(-1);
985
986 if !status.success() {
987 return Err(Error::from_command_failure(
988 format!("{} {}", claude.binary.display(), command_args.join(" ")),
989 exit_code,
990 stdout_diagnostics,
991 stderr_str,
992 claude.working_dir.clone(),
993 ));
994 }
995
996 Ok(CommandOutput {
997 stdout: String::new(),
998 stderr: stderr_str,
999 exit_code,
1000 success: true,
1001 })
1002}
1003
1004#[cfg(all(feature = "sync", feature = "json"))]
1009fn join_with_budget<T: Send + 'static>(
1010 handle: std::thread::JoinHandle<T>,
1011 budget: Duration,
1012) -> Option<T> {
1013 use std::sync::mpsc;
1014 use std::thread;
1015
1016 let (tx, rx) = mpsc::channel::<T>();
1017 thread::spawn(move || {
1018 if let Ok(v) = handle.join() {
1019 let _ = tx.send(v);
1020 }
1021 });
1022 rx.recv_timeout(budget).ok()
1023}
1024
1025#[cfg(all(test, feature = "json"))]
1026mod tests {
1027 use super::*;
1028 use serde_json::json;
1029
1030 #[cfg(feature = "sync")]
1031 #[test]
1032 fn shutdown_line_reader_retries_interrupted_reads() {
1033 struct InterruptedReader {
1034 data: std::io::Cursor<&'static [u8]>,
1035 interrupt: bool,
1036 }
1037 impl std::io::Read for InterruptedReader {
1038 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1039 self.interrupt = !self.interrupt;
1040 if self.interrupt {
1041 return Err(std::io::ErrorKind::Interrupted.into());
1042 }
1043 let len = buf.len().min(2);
1044 std::io::Read::read(&mut self.data, &mut buf[..len])
1045 }
1046 }
1047
1048 let mut reader = std::io::BufReader::new(InterruptedReader {
1049 data: std::io::Cursor::new(b"first\r\nsecond"),
1050 interrupt: false,
1051 });
1052 let discard = std::sync::atomic::AtomicBool::new(false);
1053 assert_eq!(
1054 read_line_or_discard(&mut reader, &discard)
1055 .unwrap()
1056 .as_deref(),
1057 Some("first")
1058 );
1059 assert_eq!(
1060 read_line_or_discard(&mut reader, &discard)
1061 .unwrap()
1062 .as_deref(),
1063 Some("second")
1064 );
1065 assert!(
1066 read_line_or_discard(&mut reader, &discard)
1067 .unwrap()
1068 .is_none()
1069 );
1070 }
1071
1072 fn parse(v: serde_json::Value) -> StreamEvent {
1073 serde_json::from_value(v).expect("valid StreamEvent")
1074 }
1075
1076 fn wrap(inner: serde_json::Value) -> StreamEvent {
1077 parse(json!({
1078 "type": "stream_event",
1079 "event": inner,
1080 "session_id": "sess-1",
1081 "parent_tool_use_id": null,
1082 "uuid": "11111111-1111-1111-1111-111111111111"
1083 }))
1084 }
1085
1086 #[test]
1087 fn parse_failure_diagnostics_are_bounded_and_keep_recent_lines() {
1088 let mut diagnostics = ParseFailureDiagnostics::default();
1089 diagnostics.push(&"x".repeat(STREAM_DIAGNOSTIC_MAX_BYTES));
1090 diagnostics.push("Not authenticated. Run `claude login`.");
1091
1092 let output = diagnostics.into_string();
1093 assert!(output.len() <= STREAM_DIAGNOSTIC_MAX_BYTES);
1094 assert_eq!(output, "Not authenticated. Run `claude login`.");
1095 }
1096
1097 #[test]
1098 fn partial_message_text_block_lifecycle() {
1099 let start = wrap(json!({
1100 "type": "content_block_start",
1101 "index": 0,
1102 "content_block": { "type": "text", "text": "" }
1103 }));
1104 assert_eq!(
1105 start.partial_message(),
1106 Some(PartialMessageEvent::BlockStart {
1107 index: 0,
1108 block_type: BlockType::Text,
1109 })
1110 );
1111
1112 let delta = wrap(json!({
1113 "type": "content_block_delta",
1114 "index": 0,
1115 "delta": { "type": "text_delta", "text": "Hello" }
1116 }));
1117 assert_eq!(
1118 delta.partial_message(),
1119 Some(PartialMessageEvent::BlockDelta {
1120 index: 0,
1121 delta: BlockDelta::Text("Hello".into()),
1122 })
1123 );
1124
1125 let stop = wrap(json!({ "type": "content_block_stop", "index": 0 }));
1126 assert_eq!(
1127 stop.partial_message(),
1128 Some(PartialMessageEvent::BlockStop { index: 0 })
1129 );
1130 }
1131
1132 #[test]
1133 fn partial_message_thinking_block_lifecycle() {
1134 let start = wrap(json!({
1135 "type": "content_block_start",
1136 "index": 1,
1137 "content_block": { "type": "thinking", "thinking": "", "signature": "" }
1138 }));
1139 assert_eq!(
1140 start.partial_message(),
1141 Some(PartialMessageEvent::BlockStart {
1142 index: 1,
1143 block_type: BlockType::Thinking,
1144 })
1145 );
1146
1147 let delta = wrap(json!({
1148 "type": "content_block_delta",
1149 "index": 1,
1150 "delta": { "type": "thinking_delta", "thinking": "weighing options" }
1151 }));
1152 assert_eq!(
1153 delta.partial_message(),
1154 Some(PartialMessageEvent::BlockDelta {
1155 index: 1,
1156 delta: BlockDelta::Thinking("weighing options".into()),
1157 })
1158 );
1159
1160 let stop = wrap(json!({ "type": "content_block_stop", "index": 1 }));
1161 assert_eq!(
1162 stop.partial_message(),
1163 Some(PartialMessageEvent::BlockStop { index: 1 })
1164 );
1165 }
1166
1167 #[test]
1168 fn partial_message_tool_use_block_carries_id_and_name() {
1169 let start = wrap(json!({
1170 "type": "content_block_start",
1171 "index": 2,
1172 "content_block": {
1173 "type": "tool_use",
1174 "id": "toolu_abc",
1175 "name": "Bash",
1176 "input": {}
1177 }
1178 }));
1179 assert_eq!(
1180 start.partial_message(),
1181 Some(PartialMessageEvent::BlockStart {
1182 index: 2,
1183 block_type: BlockType::ToolUse {
1184 id: "toolu_abc".into(),
1185 name: "Bash".into(),
1186 },
1187 })
1188 );
1189
1190 let delta = wrap(json!({
1191 "type": "content_block_delta",
1192 "index": 2,
1193 "delta": { "type": "input_json_delta", "partial_json": "{\"cmd\":" }
1194 }));
1195 assert_eq!(
1196 delta.partial_message(),
1197 Some(PartialMessageEvent::BlockDelta {
1198 index: 2,
1199 delta: BlockDelta::InputJson("{\"cmd\":".into()),
1200 })
1201 );
1202 }
1203
1204 #[test]
1205 fn partial_message_unknown_kinds_fall_through_to_other() {
1206 let unknown_block = wrap(json!({
1207 "type": "content_block_start",
1208 "index": 3,
1209 "content_block": { "type": "redacted_thinking", "data": "..." }
1210 }));
1211 assert_eq!(
1212 unknown_block.partial_message(),
1213 Some(PartialMessageEvent::BlockStart {
1214 index: 3,
1215 block_type: BlockType::Other("redacted_thinking".into()),
1216 })
1217 );
1218
1219 let unknown_delta = wrap(json!({
1220 "type": "content_block_delta",
1221 "index": 3,
1222 "delta": { "type": "signature_delta", "signature": "sig" }
1223 }));
1224 assert_eq!(
1225 unknown_delta.partial_message(),
1226 Some(PartialMessageEvent::BlockDelta {
1227 index: 3,
1228 delta: BlockDelta::Other,
1229 })
1230 );
1231 }
1232
1233 #[test]
1234 fn partial_message_returns_none_for_non_partial_events() {
1235 let result = parse(json!({
1236 "type": "result",
1237 "result": "done",
1238 "session_id": "sess-1",
1239 "total_cost_usd": 0.01
1240 }));
1241 assert!(result.partial_message().is_none());
1242
1243 let assistant = parse(json!({
1244 "type": "assistant",
1245 "message": { "role": "assistant", "content": [] },
1246 "session_id": "sess-1"
1247 }));
1248 assert!(assistant.partial_message().is_none());
1249
1250 let message_start = wrap(json!({
1251 "type": "message_start",
1252 "message": { "id": "msg_1", "role": "assistant", "content": [] }
1253 }));
1254 assert!(message_start.partial_message().is_none());
1255 }
1256
1257 #[test]
1258 fn partial_message_accepts_unwrapped_event() {
1259 let raw = parse(json!({
1260 "type": "content_block_delta",
1261 "index": 0,
1262 "delta": { "type": "text_delta", "text": "hi" }
1263 }));
1264 assert_eq!(
1265 raw.partial_message(),
1266 Some(PartialMessageEvent::BlockDelta {
1267 index: 0,
1268 delta: BlockDelta::Text("hi".into()),
1269 })
1270 );
1271 }
1272}