1#[cfg(feature = "json")]
9use std::time::Duration;
10
11#[cfg(all(feature = "json", feature = "async"))]
12use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
13#[cfg(all(feature = "json", feature = "async"))]
14use tokio::process::{ChildStderr, Command};
15#[cfg(feature = "json")]
16use tracing::{debug, warn};
17
18#[cfg(feature = "json")]
19use crate::Claude;
20#[cfg(feature = "json")]
21use crate::error::{Error, Result};
22#[cfg(feature = "json")]
23use crate::exec::CommandOutput;
24
25#[cfg(feature = "json")]
30#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
31pub struct StreamEvent {
32 #[serde(flatten)]
34 pub data: serde_json::Value,
35}
36
37#[cfg(feature = "json")]
38impl StreamEvent {
39 pub fn event_type(&self) -> Option<&str> {
41 self.data.get("type").and_then(|v| v.as_str())
42 }
43
44 pub fn role(&self) -> Option<&str> {
46 self.data.get("role").and_then(|v| v.as_str())
47 }
48
49 pub fn is_result(&self) -> bool {
51 self.event_type() == Some("result")
52 }
53
54 pub fn result_text(&self) -> Option<&str> {
56 self.data.get("result").and_then(|v| v.as_str())
57 }
58
59 pub fn session_id(&self) -> Option<&str> {
61 self.data.get("session_id").and_then(|v| v.as_str())
62 }
63
64 pub fn cost_usd(&self) -> Option<f64> {
69 self.data
70 .get("total_cost_usd")
71 .or_else(|| self.data.get("cost_usd"))
72 .and_then(|v| v.as_f64())
73 }
74
75 pub fn partial_message(&self) -> Option<PartialMessageEvent> {
116 let event = if self.event_type() == Some("stream_event") {
117 self.data.get("event")?
118 } else {
119 &self.data
120 };
121
122 let inner_type = event.get("type")?.as_str()?;
123 let index = event.get("index").and_then(serde_json::Value::as_u64)?;
124 let index = u32::try_from(index).ok()?;
125
126 match inner_type {
127 "content_block_start" => {
128 let block_type = parse_block_type(event.get("content_block")?);
129 Some(PartialMessageEvent::BlockStart { index, block_type })
130 }
131 "content_block_delta" => {
132 let delta = parse_block_delta(event.get("delta")?);
133 Some(PartialMessageEvent::BlockDelta { index, delta })
134 }
135 "content_block_stop" => Some(PartialMessageEvent::BlockStop { index }),
136 _ => None,
137 }
138 }
139}
140
141#[cfg(feature = "json")]
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum PartialMessageEvent {
149 BlockStart {
151 index: u32,
153 block_type: BlockType,
155 },
156 BlockDelta {
158 index: u32,
162 delta: BlockDelta,
164 },
165 BlockStop {
167 index: u32,
169 },
170}
171
172#[cfg(feature = "json")]
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum BlockType {
180 Text,
182 Thinking,
184 ToolUse {
186 id: String,
188 name: String,
190 },
191 Other(String),
193}
194
195#[cfg(feature = "json")]
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum BlockDelta {
204 Text(String),
206 Thinking(String),
208 InputJson(String),
211 Other,
214}
215
216#[cfg(feature = "json")]
217fn parse_block_type(content_block: &serde_json::Value) -> BlockType {
218 let Some(ty) = content_block
219 .get("type")
220 .and_then(serde_json::Value::as_str)
221 else {
222 return BlockType::Other(String::new());
223 };
224 match ty {
225 "text" => BlockType::Text,
226 "thinking" => BlockType::Thinking,
227 "tool_use" => {
228 let id = content_block
229 .get("id")
230 .and_then(serde_json::Value::as_str)
231 .unwrap_or("")
232 .to_string();
233 let name = content_block
234 .get("name")
235 .and_then(serde_json::Value::as_str)
236 .unwrap_or("")
237 .to_string();
238 BlockType::ToolUse { id, name }
239 }
240 other => BlockType::Other(other.to_string()),
241 }
242}
243
244#[cfg(feature = "json")]
245fn parse_block_delta(delta: &serde_json::Value) -> BlockDelta {
246 let Some(ty) = delta.get("type").and_then(serde_json::Value::as_str) else {
247 return BlockDelta::Other;
248 };
249 match ty {
250 "text_delta" => delta
251 .get("text")
252 .and_then(serde_json::Value::as_str)
253 .map(|s| BlockDelta::Text(s.to_string()))
254 .unwrap_or(BlockDelta::Other),
255 "thinking_delta" => delta
256 .get("thinking")
257 .and_then(serde_json::Value::as_str)
258 .map(|s| BlockDelta::Thinking(s.to_string()))
259 .unwrap_or(BlockDelta::Other),
260 "input_json_delta" => delta
261 .get("partial_json")
262 .and_then(serde_json::Value::as_str)
263 .map(|s| BlockDelta::InputJson(s.to_string()))
264 .unwrap_or(BlockDelta::Other),
265 _ => BlockDelta::Other,
266 }
267}
268
269#[cfg(all(feature = "json", feature = "async"))]
296pub async fn stream_query<F>(
297 claude: &Claude,
298 cmd: &crate::command::query::QueryCommand,
299 handler: F,
300) -> Result<CommandOutput>
301where
302 F: FnMut(StreamEvent),
303{
304 stream_query_impl(claude, cmd, handler, claude.timeout).await
305}
306
307#[cfg(all(feature = "json", feature = "async"))]
319async fn stream_query_impl<F>(
320 claude: &Claude,
321 cmd: &crate::command::query::QueryCommand,
322 mut handler: F,
323 timeout: Option<Duration>,
324) -> Result<CommandOutput>
325where
326 F: FnMut(StreamEvent),
327{
328 use crate::command::ClaudeCommand;
329
330 let args = cmd.args();
331
332 let mut command_args = Vec::new();
333 command_args.extend(claude.global_args.clone());
334 command_args.extend(args);
335
336 debug!(
337 binary = %claude.binary.display(),
338 args = ?command_args,
339 timeout = ?timeout,
340 "streaming claude command"
341 );
342
343 let mut cmd = Command::new(&claude.binary);
344 cmd.args(&command_args)
345 .env_remove("CLAUDECODE")
346 .envs(&claude.env)
347 .stdout(std::process::Stdio::piped())
348 .stderr(std::process::Stdio::piped())
349 .stdin(std::process::Stdio::null());
350
351 if let Some(ref dir) = claude.working_dir {
352 cmd.current_dir(dir);
353 }
354
355 let mut child = cmd.spawn().map_err(|e| Error::Io {
356 message: format!("failed to spawn claude: {e}"),
357 source: e,
358 working_dir: claude.working_dir.clone(),
359 })?;
360
361 let stdout = child.stdout.take().expect("stdout was piped");
362 let mut stderr = child.stderr.take().expect("stderr was piped");
363
364 let mut reader = BufReader::new(stdout).lines();
365
366 let drain = drain_stderr(&mut stderr);
371 let read_future = read_lines(&mut reader, &mut handler, claude.working_dir.clone());
372 let combined = async {
373 let (line_result, stderr_str) = tokio::join!(read_future, drain);
374 (line_result, stderr_str)
375 };
376
377 let (line_result, stderr_str) = match timeout {
378 Some(d) => match tokio::time::timeout(d, combined).await {
379 Ok(pair) => pair,
380 Err(_) => {
381 let _ = child.kill().await;
387 let drain_budget = Duration::from_millis(200);
388 let stderr_str = tokio::time::timeout(drain_budget, drain_stderr(&mut stderr))
389 .await
390 .unwrap_or_default();
391 if !stderr_str.is_empty() {
392 warn!(stderr = %stderr_str, "stderr from timed-out streaming process");
393 }
394 return Err(Error::Timeout {
395 timeout_seconds: d.as_secs(),
396 });
397 }
398 },
399 None => combined.await,
400 };
401
402 if let Err(e) = line_result {
405 let _ = child.kill().await;
406 return Err(e);
407 }
408
409 let status = child.wait().await.map_err(|e| Error::Io {
410 message: "failed to wait for claude process".to_string(),
411 source: e,
412 working_dir: claude.working_dir.clone(),
413 })?;
414
415 let exit_code = status.code().unwrap_or(-1);
416
417 if !status.success() {
418 return Err(Error::CommandFailed {
419 command: format!("{} {}", claude.binary.display(), command_args.join(" ")),
420 exit_code,
421 stdout: String::new(),
422 stderr: stderr_str,
423 working_dir: claude.working_dir.clone(),
424 });
425 }
426
427 Ok(CommandOutput {
428 stdout: String::new(), stderr: stderr_str,
430 exit_code,
431 success: true,
432 })
433}
434
435#[cfg(all(feature = "json", feature = "async"))]
436async fn drain_stderr(stderr: &mut ChildStderr) -> String {
437 let mut buf = Vec::new();
438 let _ = stderr.read_to_end(&mut buf).await;
439 String::from_utf8_lossy(&buf).into_owned()
440}
441
442#[cfg(all(feature = "json", feature = "async"))]
443async fn read_lines<F>(
444 reader: &mut tokio::io::Lines<BufReader<tokio::process::ChildStdout>>,
445 handler: &mut F,
446 working_dir: Option<std::path::PathBuf>,
447) -> Result<()>
448where
449 F: FnMut(StreamEvent),
450{
451 while let Some(line) = reader.next_line().await.map_err(|e| Error::Io {
452 message: "failed to read stdout line".to_string(),
453 source: e,
454 working_dir: working_dir.clone(),
455 })? {
456 if line.trim().is_empty() {
457 continue;
458 }
459 match serde_json::from_str::<StreamEvent>(&line) {
460 Ok(event) => handler(event),
461 Err(e) => {
462 debug!(line = %line, error = %e, "failed to parse stream event, skipping");
463 }
464 }
465 }
466
467 Ok(())
468}
469
470#[cfg(all(feature = "sync", feature = "json"))]
508pub fn stream_query_sync<F>(
509 claude: &Claude,
510 cmd: &crate::command::query::QueryCommand,
511 mut handler: F,
512) -> Result<CommandOutput>
513where
514 F: FnMut(StreamEvent),
515{
516 use std::io::{BufRead as _, Read as _};
517 use std::process::{Command as StdCommand, Stdio};
518 use std::sync::mpsc;
519 use std::thread;
520 use std::time::Instant;
521
522 use crate::command::ClaudeCommand;
523
524 let args = cmd.args();
525 let mut command_args = Vec::new();
526 command_args.extend(claude.global_args.clone());
527 command_args.extend(args);
528
529 debug!(
530 binary = %claude.binary.display(),
531 args = ?command_args,
532 timeout = ?claude.timeout,
533 "streaming claude command (sync)"
534 );
535
536 let mut cmd_builder = StdCommand::new(&claude.binary);
537 cmd_builder
538 .args(&command_args)
539 .env_remove("CLAUDECODE")
540 .env_remove("CLAUDE_CODE_ENTRYPOINT")
541 .envs(&claude.env)
542 .stdin(Stdio::null())
543 .stdout(Stdio::piped())
544 .stderr(Stdio::piped());
545
546 if let Some(ref dir) = claude.working_dir {
547 cmd_builder.current_dir(dir);
548 }
549
550 let mut child = cmd_builder.spawn().map_err(|e| Error::Io {
551 message: format!("failed to spawn claude: {e}"),
552 source: e,
553 working_dir: claude.working_dir.clone(),
554 })?;
555
556 let stdout = child.stdout.take().expect("stdout was piped");
557 let stderr = child.stderr.take().expect("stderr was piped");
558
559 let (tx, rx) = mpsc::channel::<StreamEvent>();
563 let reader_wd = claude.working_dir.clone();
564 let reader_thread = thread::spawn(move || -> Result<()> {
565 let reader = std::io::BufReader::new(stdout);
566 for line_res in reader.lines() {
567 let line = line_res.map_err(|e| Error::Io {
568 message: "failed to read stdout line".to_string(),
569 source: e,
570 working_dir: reader_wd.clone(),
571 })?;
572 if line.trim().is_empty() {
573 continue;
574 }
575 match serde_json::from_str::<StreamEvent>(&line) {
576 Ok(event) => {
577 if tx.send(event).is_err() {
578 return Ok(());
580 }
581 }
582 Err(e) => {
583 debug!(line = %line, error = %e, "failed to parse stream event, skipping");
584 }
585 }
586 }
587 Ok(())
588 });
589
590 let stderr_thread = thread::spawn(move || -> String {
591 let mut buf = Vec::new();
592 let mut stderr = stderr;
593 let _ = stderr.read_to_end(&mut buf);
594 String::from_utf8_lossy(&buf).into_owned()
595 });
596
597 let deadline = claude.timeout.map(|d| Instant::now() + d);
600 let mut timed_out = false;
601
602 loop {
603 let recv_result = match deadline {
604 Some(d) => {
605 let now = Instant::now();
606 if now >= d {
607 timed_out = true;
608 break;
609 }
610 rx.recv_timeout(d - now)
611 }
612 None => rx.recv().map_err(|_| mpsc::RecvTimeoutError::Disconnected),
613 };
614
615 match recv_result {
616 Ok(event) => handler(event),
617 Err(mpsc::RecvTimeoutError::Timeout) => {
618 timed_out = true;
619 break;
620 }
621 Err(mpsc::RecvTimeoutError::Disconnected) => break,
622 }
623 }
624
625 if timed_out {
626 let _ = child.kill();
627 let _ = child.wait();
628 let budget = Duration::from_millis(200);
635 let stderr_str = join_with_budget(stderr_thread, budget).unwrap_or_default();
636 let _ = join_with_budget(reader_thread, budget);
637 if !stderr_str.is_empty() {
638 warn!(stderr = %stderr_str, "stderr from timed-out streaming process");
639 }
640 return Err(Error::Timeout {
641 timeout_seconds: claude.timeout.map(|d| d.as_secs()).unwrap_or_default(),
642 });
643 }
644
645 let reader_result = reader_thread.join().unwrap_or(Ok(()));
647 if let Err(e) = reader_result {
648 let _ = child.kill();
649 let _ = child.wait();
650 let _ = stderr_thread.join();
651 return Err(e);
652 }
653
654 let status = child.wait().map_err(|e| Error::Io {
655 message: "failed to wait for claude process".to_string(),
656 source: e,
657 working_dir: claude.working_dir.clone(),
658 })?;
659 let stderr_str = stderr_thread.join().unwrap_or_default();
660 let exit_code = status.code().unwrap_or(-1);
661
662 if !status.success() {
663 return Err(Error::CommandFailed {
664 command: format!("{} {}", claude.binary.display(), command_args.join(" ")),
665 exit_code,
666 stdout: String::new(),
667 stderr: stderr_str,
668 working_dir: claude.working_dir.clone(),
669 });
670 }
671
672 Ok(CommandOutput {
673 stdout: String::new(),
674 stderr: stderr_str,
675 exit_code,
676 success: true,
677 })
678}
679
680#[cfg(all(feature = "sync", feature = "json"))]
685fn join_with_budget<T: Send + 'static>(
686 handle: std::thread::JoinHandle<T>,
687 budget: Duration,
688) -> Option<T> {
689 use std::sync::mpsc;
690 use std::thread;
691
692 let (tx, rx) = mpsc::channel::<T>();
693 thread::spawn(move || {
694 if let Ok(v) = handle.join() {
695 let _ = tx.send(v);
696 }
697 });
698 rx.recv_timeout(budget).ok()
699}
700
701#[cfg(all(test, feature = "json"))]
702mod tests {
703 use super::*;
704 use serde_json::json;
705
706 fn parse(v: serde_json::Value) -> StreamEvent {
707 serde_json::from_value(v).expect("valid StreamEvent")
708 }
709
710 fn wrap(inner: serde_json::Value) -> StreamEvent {
711 parse(json!({
712 "type": "stream_event",
713 "event": inner,
714 "session_id": "sess-1",
715 "parent_tool_use_id": null,
716 "uuid": "11111111-1111-1111-1111-111111111111"
717 }))
718 }
719
720 #[test]
721 fn partial_message_text_block_lifecycle() {
722 let start = wrap(json!({
723 "type": "content_block_start",
724 "index": 0,
725 "content_block": { "type": "text", "text": "" }
726 }));
727 assert_eq!(
728 start.partial_message(),
729 Some(PartialMessageEvent::BlockStart {
730 index: 0,
731 block_type: BlockType::Text,
732 })
733 );
734
735 let delta = wrap(json!({
736 "type": "content_block_delta",
737 "index": 0,
738 "delta": { "type": "text_delta", "text": "Hello" }
739 }));
740 assert_eq!(
741 delta.partial_message(),
742 Some(PartialMessageEvent::BlockDelta {
743 index: 0,
744 delta: BlockDelta::Text("Hello".into()),
745 })
746 );
747
748 let stop = wrap(json!({ "type": "content_block_stop", "index": 0 }));
749 assert_eq!(
750 stop.partial_message(),
751 Some(PartialMessageEvent::BlockStop { index: 0 })
752 );
753 }
754
755 #[test]
756 fn partial_message_thinking_block_lifecycle() {
757 let start = wrap(json!({
758 "type": "content_block_start",
759 "index": 1,
760 "content_block": { "type": "thinking", "thinking": "", "signature": "" }
761 }));
762 assert_eq!(
763 start.partial_message(),
764 Some(PartialMessageEvent::BlockStart {
765 index: 1,
766 block_type: BlockType::Thinking,
767 })
768 );
769
770 let delta = wrap(json!({
771 "type": "content_block_delta",
772 "index": 1,
773 "delta": { "type": "thinking_delta", "thinking": "weighing options" }
774 }));
775 assert_eq!(
776 delta.partial_message(),
777 Some(PartialMessageEvent::BlockDelta {
778 index: 1,
779 delta: BlockDelta::Thinking("weighing options".into()),
780 })
781 );
782
783 let stop = wrap(json!({ "type": "content_block_stop", "index": 1 }));
784 assert_eq!(
785 stop.partial_message(),
786 Some(PartialMessageEvent::BlockStop { index: 1 })
787 );
788 }
789
790 #[test]
791 fn partial_message_tool_use_block_carries_id_and_name() {
792 let start = wrap(json!({
793 "type": "content_block_start",
794 "index": 2,
795 "content_block": {
796 "type": "tool_use",
797 "id": "toolu_abc",
798 "name": "Bash",
799 "input": {}
800 }
801 }));
802 assert_eq!(
803 start.partial_message(),
804 Some(PartialMessageEvent::BlockStart {
805 index: 2,
806 block_type: BlockType::ToolUse {
807 id: "toolu_abc".into(),
808 name: "Bash".into(),
809 },
810 })
811 );
812
813 let delta = wrap(json!({
814 "type": "content_block_delta",
815 "index": 2,
816 "delta": { "type": "input_json_delta", "partial_json": "{\"cmd\":" }
817 }));
818 assert_eq!(
819 delta.partial_message(),
820 Some(PartialMessageEvent::BlockDelta {
821 index: 2,
822 delta: BlockDelta::InputJson("{\"cmd\":".into()),
823 })
824 );
825 }
826
827 #[test]
828 fn partial_message_unknown_kinds_fall_through_to_other() {
829 let unknown_block = wrap(json!({
830 "type": "content_block_start",
831 "index": 3,
832 "content_block": { "type": "redacted_thinking", "data": "..." }
833 }));
834 assert_eq!(
835 unknown_block.partial_message(),
836 Some(PartialMessageEvent::BlockStart {
837 index: 3,
838 block_type: BlockType::Other("redacted_thinking".into()),
839 })
840 );
841
842 let unknown_delta = wrap(json!({
843 "type": "content_block_delta",
844 "index": 3,
845 "delta": { "type": "signature_delta", "signature": "sig" }
846 }));
847 assert_eq!(
848 unknown_delta.partial_message(),
849 Some(PartialMessageEvent::BlockDelta {
850 index: 3,
851 delta: BlockDelta::Other,
852 })
853 );
854 }
855
856 #[test]
857 fn partial_message_returns_none_for_non_partial_events() {
858 let result = parse(json!({
859 "type": "result",
860 "result": "done",
861 "session_id": "sess-1",
862 "total_cost_usd": 0.01
863 }));
864 assert!(result.partial_message().is_none());
865
866 let assistant = parse(json!({
867 "type": "assistant",
868 "message": { "role": "assistant", "content": [] },
869 "session_id": "sess-1"
870 }));
871 assert!(assistant.partial_message().is_none());
872
873 let message_start = wrap(json!({
874 "type": "message_start",
875 "message": { "id": "msg_1", "role": "assistant", "content": [] }
876 }));
877 assert!(message_start.partial_message().is_none());
878 }
879
880 #[test]
881 fn partial_message_accepts_unwrapped_event() {
882 let raw = parse(json!({
883 "type": "content_block_delta",
884 "index": 0,
885 "delta": { "type": "text_delta", "text": "hi" }
886 }));
887 assert_eq!(
888 raw.partial_message(),
889 Some(PartialMessageEvent::BlockDelta {
890 index: 0,
891 delta: BlockDelta::Text("hi".into()),
892 })
893 );
894 }
895}