1pub mod logs;
3pub mod models;
10pub mod usage_limits;
11
12use crate::agent::{Agent, ModelSize};
13
14pub fn projects_dir() -> Option<std::path::PathBuf> {
16 dirs::home_dir().map(|h| h.join(".claude/projects"))
17}
18use crate::output::AgentOutput;
19use crate::providers::common::CommonAgentState;
20use anyhow::{Context, Result};
21use async_trait::async_trait;
22use std::process::Stdio;
23use tokio::io::{AsyncBufReadExt, BufReader};
24use tokio::process::Command;
25
26pub const DEFAULT_MODEL: &str = "default";
27
28pub const AVAILABLE_MODELS: &[&str] = &["default", "sonnet", "opus", "haiku"];
29
30pub const ALLOW_PRINT_ENV: &str = "ZAG_CLAUDE_ALLOW_PRINT";
38
39pub fn allow_print_value_is_truthy(value: &str) -> bool {
47 !value.is_empty() && value != "0" && !value.eq_ignore_ascii_case("false")
48}
49
50fn print_disabled_error() -> anyhow::Error {
52 anyhow::anyhow!(
53 "Claude --print mode is disabled because it consumes API tokens. \
54 Set {ALLOW_PRINT_ENV}=1 to enable it, or run interactively with \
55 `zag -p claude run --exit '<hint>' \"<prompt>\"` to capture a result via \
56 `zag ps kill self <result>` without paying for --print."
57 )
58}
59
60pub fn check_print_allowed() -> Result<()> {
63 match std::env::var(ALLOW_PRINT_ENV) {
64 Ok(ref v) if allow_print_value_is_truthy(v) => Ok(()),
65 _ => Err(print_disabled_error()),
66 }
67}
68
69pub type EventHandler = Box<dyn Fn(&crate::output::Event, bool) + Send + Sync>;
72
73pub struct Claude {
74 pub common: CommonAgentState,
75 pub session_id: Option<String>,
76 pub input_format: Option<String>,
77 pub verbose: bool,
78 pub json_schema: Option<String>,
79 pub event_handler: Option<EventHandler>,
80 pub replay_user_messages: bool,
81 pub include_partial_messages: bool,
82 pub mcp_config_path: Option<String>,
83}
84
85impl Claude {
86 pub fn new() -> Self {
87 Self {
88 common: CommonAgentState::new(DEFAULT_MODEL),
89 session_id: None,
90 input_format: None,
91 verbose: false,
92 json_schema: None,
93 event_handler: None,
94 replay_user_messages: false,
95 include_partial_messages: false,
96 mcp_config_path: None,
97 }
98 }
99
100 pub fn set_input_format(&mut self, format: Option<String>) {
101 self.input_format = format;
102 }
103
104 pub fn set_session_id(&mut self, session_id: String) {
105 self.session_id = Some(session_id);
106 }
107
108 pub fn set_verbose(&mut self, verbose: bool) {
109 self.verbose = verbose;
110 }
111
112 pub fn set_json_schema(&mut self, schema: Option<String>) {
113 self.json_schema = schema;
114 }
115
116 pub fn set_replay_user_messages(&mut self, replay: bool) {
117 self.replay_user_messages = replay;
118 }
119
120 pub fn set_include_partial_messages(&mut self, include: bool) {
121 self.include_partial_messages = include;
122 }
123
124 pub fn set_mcp_config(&mut self, config: Option<String>) {
126 self.mcp_config_path = config.map(|c| {
127 if c.trim_start().starts_with('{') {
128 let path =
129 std::env::temp_dir().join(format!("zag-mcp-{}.json", uuid::Uuid::new_v4()));
130 if let Err(e) = std::fs::write(&path, &c) {
131 log::warn!("Failed to write MCP config temp file: {e}");
132 return c;
133 }
134 path.to_string_lossy().into_owned()
135 } else {
136 c
137 }
138 });
139 }
140
141 pub fn set_event_handler(&mut self, handler: EventHandler) {
146 self.event_handler = Some(handler);
147 }
148
149 fn build_run_args(
151 &self,
152 interactive: bool,
153 prompt: Option<&str>,
154 effective_output_format: &Option<String>,
155 ) -> Vec<String> {
156 let mut args = Vec::new();
157 let in_sandbox = self.common.sandbox.is_some();
158
159 if !interactive {
160 args.push("--print".to_string());
161
162 match effective_output_format.as_deref() {
163 Some("json") | Some("json-pretty") => {
164 args.extend(["--verbose", "--output-format", "json"].map(String::from));
165 }
166 Some("stream-json") | None => {
167 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
168 }
169 Some("native-json") => {
170 args.extend(["--verbose", "--output-format", "json"].map(String::from));
171 }
172 Some("text") => {}
173 _ => {}
174 }
175 }
176
177 if self.common.skip_permissions && !in_sandbox {
179 args.push("--dangerously-skip-permissions".to_string());
180 }
181
182 args.extend(["--model".to_string(), self.common.model.clone()]);
183
184 if interactive && let Some(session_id) = &self.session_id {
185 args.extend(["--session-id".to_string(), session_id.clone()]);
186 }
187
188 for dir in &self.common.add_dirs {
189 args.extend(["--add-dir".to_string(), dir.clone()]);
190 }
191
192 if !self.common.system_prompt.is_empty() {
193 args.extend([
194 "--append-system-prompt".to_string(),
195 self.common.system_prompt.clone(),
196 ]);
197 }
198
199 if !interactive && let Some(ref input_fmt) = self.input_format {
200 args.extend(["--input-format".to_string(), input_fmt.clone()]);
201 }
202
203 if !interactive && self.replay_user_messages {
204 args.push("--replay-user-messages".to_string());
205 }
206
207 if !interactive && self.include_partial_messages {
208 args.push("--include-partial-messages".to_string());
209 }
210
211 if let Some(ref schema) = self.json_schema {
212 args.extend(["--json-schema".to_string(), schema.clone()]);
213 }
214
215 if let Some(turns) = self.common.max_turns {
216 args.extend(["--max-turns".to_string(), turns.to_string()]);
217 }
218
219 if let Some(ref path) = self.mcp_config_path {
220 args.extend(["--mcp-config".to_string(), path.clone()]);
221 }
222
223 if let Some(p) = prompt {
224 args.push("--".to_string());
229 args.push(p.to_string());
230 }
231
232 args
233 }
234
235 fn build_resume_args(&self, session_id: Option<&str>) -> Vec<String> {
237 let mut args = Vec::new();
238 let in_sandbox = self.common.sandbox.is_some();
239
240 if let Some(id) = session_id {
241 args.extend(["--resume".to_string(), id.to_string()]);
242 } else {
243 args.push("--continue".to_string());
244 }
245
246 if self.common.skip_permissions && !in_sandbox {
247 args.push("--dangerously-skip-permissions".to_string());
248 }
249
250 args.extend(["--model".to_string(), self.common.model.clone()]);
251
252 for dir in &self.common.add_dirs {
253 args.extend(["--add-dir".to_string(), dir.clone()]);
254 }
255
256 args
257 }
258
259 fn make_command(&self, agent_args: Vec<String>) -> Command {
261 self.common.make_command("claude", agent_args)
262 }
263
264 pub fn execute_streaming(
279 &self,
280 prompt: Option<&str>,
281 ) -> Result<crate::streaming::StreamingSession> {
282 check_print_allowed()?;
283 let mut args = Vec::new();
285 let in_sandbox = self.common.sandbox.is_some();
286
287 args.push("--print".to_string());
288 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
289
290 if self.common.skip_permissions && !in_sandbox {
291 args.push("--dangerously-skip-permissions".to_string());
292 }
293
294 args.extend(["--model".to_string(), self.common.model.clone()]);
295
296 for dir in &self.common.add_dirs {
297 args.extend(["--add-dir".to_string(), dir.clone()]);
298 }
299
300 if !self.common.system_prompt.is_empty() {
301 args.extend([
302 "--append-system-prompt".to_string(),
303 self.common.system_prompt.clone(),
304 ]);
305 }
306
307 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
308 args.push("--replay-user-messages".to_string());
309
310 if self.include_partial_messages {
311 args.push("--include-partial-messages".to_string());
312 }
313
314 if let Some(ref schema) = self.json_schema {
315 args.extend(["--json-schema".to_string(), schema.clone()]);
316 }
317
318 if let Some(p) = prompt {
319 args.push("--".to_string());
320 args.push(p.to_string());
321 }
322
323 log::debug!("Claude streaming command: claude {}", args.join(" "));
324
325 let mut cmd = self.make_command(args);
326 cmd.stdin(Stdio::piped())
327 .stdout(Stdio::piped())
328 .stderr(Stdio::piped());
329
330 let child = cmd
331 .spawn()
332 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
333 crate::streaming::StreamingSession::new(child)
334 }
335
336 fn build_streaming_resume_args(&self, session_id: &str) -> Vec<String> {
338 let mut args = Vec::new();
339 let in_sandbox = self.common.sandbox.is_some();
340
341 args.push("--print".to_string());
342 args.extend(["--resume".to_string(), session_id.to_string()]);
343 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
344
345 if self.common.skip_permissions && !in_sandbox {
346 args.push("--dangerously-skip-permissions".to_string());
347 }
348
349 args.extend(["--model".to_string(), self.common.model.clone()]);
350
351 for dir in &self.common.add_dirs {
352 args.extend(["--add-dir".to_string(), dir.clone()]);
353 }
354
355 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
356 args.push("--replay-user-messages".to_string());
357
358 if self.include_partial_messages {
359 args.push("--include-partial-messages".to_string());
360 }
361
362 args
363 }
364
365 pub fn execute_streaming_resume(
374 &self,
375 session_id: &str,
376 ) -> Result<crate::streaming::StreamingSession> {
377 check_print_allowed()?;
378 let args = self.build_streaming_resume_args(session_id);
379
380 log::debug!("Claude streaming resume command: claude {}", args.join(" "));
381
382 let mut cmd = self.make_command(args);
383 cmd.stdin(Stdio::piped())
384 .stdout(Stdio::piped())
385 .stderr(Stdio::piped());
386
387 let child = cmd
388 .spawn()
389 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
390 crate::streaming::StreamingSession::new(child)
391 }
392
393 async fn execute(
394 &self,
395 interactive: bool,
396 prompt: Option<&str>,
397 ) -> Result<Option<AgentOutput>> {
398 if !interactive {
399 check_print_allowed()?;
400 }
401 let effective_output_format =
404 if self.common.capture_output && self.common.output_format.is_none() {
405 Some("json".to_string())
406 } else {
407 self.common.output_format.clone()
408 };
409
410 let capture_json = !interactive
413 && effective_output_format
414 .as_ref()
415 .is_none_or(|f| f == "json" || f == "json-pretty" || f == "stream-json");
416
417 let agent_args = self.build_run_args(interactive, prompt, &effective_output_format);
418 log::debug!("Claude command: claude {}", agent_args.join(" "));
419 if !self.common.system_prompt.is_empty() {
420 log::debug!("Claude system prompt: {}", self.common.system_prompt);
421 }
422 if let Some(p) = prompt {
423 log::debug!("Claude user prompt: {p}");
424 }
425 log::debug!(
426 "Claude mode: interactive={interactive}, capture_json={capture_json}, output_format={effective_output_format:?}"
427 );
428 let mut cmd = self.make_command(agent_args);
429
430 let is_native_json = effective_output_format.as_deref() == Some("native-json");
432
433 if interactive {
434 cmd.stdin(Stdio::inherit())
439 .stdout(Stdio::inherit())
440 .stderr(Stdio::inherit());
441
442 let mut child = cmd
443 .spawn()
444 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
445 self.common.notify_spawn(&child);
446 let status = child
447 .wait()
448 .await
449 .context("Failed waiting on 'claude' CLI")?;
450 if !status.success() {
451 return Err(crate::process::ProcessError {
452 exit_code: status.code(),
453 stderr: String::new(),
454 agent_name: "Claude".to_string(),
455 }
456 .into());
457 }
458 Ok(None)
459 } else if is_native_json {
460 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
462
463 crate::process::run_with_captured_stderr(&mut cmd).await?;
464 Ok(None)
465 } else if capture_json {
466 let output_format = effective_output_format.as_deref();
467 let is_streaming = output_format == Some("stream-json") || output_format.is_none();
468
469 if is_streaming {
470 cmd.stdin(Stdio::inherit());
472 cmd.stdout(Stdio::piped());
473
474 let mut child = crate::process::spawn_with_captured_stderr(&mut cmd).await?;
475 let stdout = child
476 .stdout
477 .take()
478 .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
479
480 let reader = BufReader::new(stdout);
481 let mut lines = reader.lines();
482
483 let format_as_text = output_format.is_none(); let format_as_json = output_format == Some("stream-json"); let mut translator = ClaudeEventTranslator::new();
490
491 while let Some(line) = lines.next_line().await? {
493 if format_as_text || format_as_json {
494 match serde_json::from_str::<models::ClaudeEvent>(&line) {
495 Ok(claude_event) => {
496 for unified_event in translator.translate(&claude_event) {
497 if let Some(ref handler) = self.event_handler {
498 handler(&unified_event, self.verbose);
499 }
500 }
501 }
502 Err(e) => {
503 log::debug!(
504 "Failed to parse streaming Claude event: {}. Line: {}",
505 e,
506 crate::truncate_str(&line, 200)
507 );
508 }
509 }
510 }
511 }
512
513 if let Some(ref handler) = self.event_handler {
515 handler(
517 &crate::output::Event::Result {
518 success: true,
519 message: None,
520 duration_ms: None,
521 num_turns: None,
522 },
523 self.verbose,
524 );
525 }
526
527 crate::process::wait_with_stderr(child).await?;
528
529 Ok(None)
531 } else {
532 cmd.stdin(Stdio::inherit());
534 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
535
536 let output = cmd.output().await?;
537
538 crate::process::handle_output(&output, "Claude")?;
539
540 let json_str = String::from_utf8(output.stdout)?;
542 log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
543 let claude_output: models::ClaudeOutput =
544 serde_json::from_str(&json_str).map_err(|e| {
545 log::debug!(
546 "Failed to parse Claude JSON output: {}. First 500 chars: {}",
547 e,
548 crate::truncate_str(&json_str, 500)
549 );
550 anyhow::anyhow!("Failed to parse Claude JSON output: {e}")
551 })?;
552 log::debug!("Parsed {} Claude events successfully", claude_output.len());
553
554 if let Ok(raw_events) = serde_json::from_str::<Vec<serde_json::Value>>(&json_str) {
556 let known = ["system", "assistant", "user", "result"];
557 for raw in &raw_events {
558 if let Some(t) = raw.get("type").and_then(|v| v.as_str()) {
559 if !known.contains(&t) {
560 log::debug!(
561 "Unknown Claude event type: {:?} (first 300 chars: {})",
562 t,
563 crate::truncate_str(
564 &serde_json::to_string(raw).unwrap_or_default(),
565 300
566 )
567 );
568 }
569 }
570 }
571 }
572
573 let agent_output: AgentOutput =
575 models::claude_output_to_agent_output(claude_output);
576 Ok(Some(agent_output))
577 }
578 } else {
579 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
581
582 crate::process::run_with_captured_stderr(&mut cmd).await?;
583 Ok(None)
584 }
585 }
586}
587
588#[derive(Debug, Default)]
601pub(crate) struct ClaudeEventTranslator {
602 pending_stop_reason: Option<String>,
605 pending_usage: Option<crate::output::Usage>,
607 next_turn_index: u32,
610 last_assistant_text: Option<String>,
613 tool_name_by_id: std::collections::HashMap<String, String>,
617 pending_usage_limit: Option<crate::output::Event>,
620}
621
622impl ClaudeEventTranslator {
623 pub(crate) fn new() -> Self {
624 Self::default()
625 }
626
627 pub(crate) fn translate(&mut self, event: &models::ClaudeEvent) -> Vec<crate::output::Event> {
633 use crate::output::{Event as UnifiedEvent, Usage as UnifiedUsage};
634
635 if let models::ClaudeEvent::Assistant { message, .. } = event {
641 if let Some(reason) = &message.stop_reason {
642 self.pending_stop_reason = Some(reason.clone());
643 }
644 self.pending_usage = Some(UnifiedUsage {
645 input_tokens: message.usage.input_tokens,
646 output_tokens: message.usage.output_tokens,
647 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
648 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
649 web_search_requests: message
650 .usage
651 .server_tool_use
652 .as_ref()
653 .map(|s| s.web_search_requests),
654 web_fetch_requests: message
655 .usage
656 .server_tool_use
657 .as_ref()
658 .map(|s| s.web_fetch_requests),
659 });
660
661 let text_parts: Vec<&str> = message
663 .content
664 .iter()
665 .filter_map(|b| match b {
666 models::ContentBlock::Text { text } => Some(text.as_str()),
667 _ => None,
668 })
669 .collect();
670 if !text_parts.is_empty() {
671 self.last_assistant_text = Some(text_parts.join("\n"));
672 }
673
674 let cfg = crate::usage_limits::UsageLimitConfig::default();
679 for text in &text_parts {
680 if let Some(hit) = usage_limits::detect_text(text, &cfg) {
681 self.pending_usage_limit = Some(crate::output::Event::UsageLimitDetected {
682 provider: hit.provider.to_string(),
683 scope: hit.scope.as_str().to_string(),
684 reset_at: hit.reset_at.map(|t| t.to_rfc3339()),
685 raw: Some(hit.raw),
686 });
687 break;
688 }
689 }
690
691 for block in &message.content {
694 if let models::ContentBlock::ToolUse { id, name, .. } = block {
695 self.tool_name_by_id.insert(id.clone(), name.clone());
696 }
697 }
698 }
699
700 let unified = convert_claude_event_to_unified(event);
701 let usage_limit = self.pending_usage_limit.take();
702
703 let mut events = match unified {
704 Some(UnifiedEvent::Result {
705 success,
706 message,
707 duration_ms,
708 num_turns,
709 }) if message.as_deref() == Some("") => {
710 let fallback = self.last_assistant_text.take();
712 if fallback.is_some() {
713 log::debug!(
714 "Streaming Result.message is empty; using last assistant text as fallback"
715 );
716 }
717 let result_event = UnifiedEvent::Result {
718 success,
719 message: fallback.or(message),
720 duration_ms,
721 num_turns,
722 };
723 let turn_complete = UnifiedEvent::TurnComplete {
724 stop_reason: self.pending_stop_reason.take(),
725 turn_index: self.next_turn_index,
726 usage: self.pending_usage.take(),
727 };
728 self.next_turn_index = self.next_turn_index.saturating_add(1);
729 vec![turn_complete, result_event]
730 }
731 Some(UnifiedEvent::Result { .. }) => {
732 let turn_complete = UnifiedEvent::TurnComplete {
733 stop_reason: self.pending_stop_reason.take(),
734 turn_index: self.next_turn_index,
735 usage: self.pending_usage.take(),
736 };
737 self.next_turn_index = self.next_turn_index.saturating_add(1);
738 vec![turn_complete, unified.unwrap()]
739 }
740 Some(UnifiedEvent::ToolExecution {
741 tool_name,
742 tool_id,
743 input,
744 result,
745 parent_tool_use_id,
746 }) => {
747 let resolved_name = self
750 .tool_name_by_id
751 .get(&tool_id)
752 .cloned()
753 .unwrap_or(tool_name);
754 vec![UnifiedEvent::ToolExecution {
755 tool_name: resolved_name,
756 tool_id,
757 input,
758 result,
759 parent_tool_use_id,
760 }]
761 }
762 Some(ev) => vec![ev],
763 None => Vec::new(),
764 };
765
766 if let Some(ul) = usage_limit {
767 events.push(ul);
768 }
769 events
770 }
771}
772
773pub(crate) fn convert_claude_event_to_unified(
780 event: &models::ClaudeEvent,
781) -> Option<crate::output::Event> {
782 use crate::output::{
783 ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
784 Usage as UnifiedUsage,
785 };
786 use models::ClaudeEvent;
787
788 match event {
789 ClaudeEvent::System {
790 model, tools, cwd, ..
791 } => {
792 let mut metadata = std::collections::HashMap::new();
793 if let Some(cwd_val) = cwd {
794 metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
795 }
796
797 Some(UnifiedEvent::Init {
798 model: model.clone(),
799 tools: tools.clone(),
800 working_directory: cwd.clone(),
801 metadata,
802 })
803 }
804
805 ClaudeEvent::Assistant {
806 message,
807 parent_tool_use_id,
808 ..
809 } => {
810 let content: Vec<UnifiedContentBlock> = message
812 .content
813 .iter()
814 .filter_map(|block| match block {
815 models::ContentBlock::Text { text } => {
816 Some(UnifiedContentBlock::Text { text: text.clone() })
817 }
818 models::ContentBlock::ToolUse { id, name, input } => {
819 Some(UnifiedContentBlock::ToolUse {
820 id: id.clone(),
821 name: name.clone(),
822 input: input.clone(),
823 })
824 }
825 models::ContentBlock::Thinking { .. } | models::ContentBlock::Other => None,
826 })
827 .collect();
828
829 let usage = Some(UnifiedUsage {
831 input_tokens: message.usage.input_tokens,
832 output_tokens: message.usage.output_tokens,
833 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
834 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
835 web_search_requests: message
836 .usage
837 .server_tool_use
838 .as_ref()
839 .map(|s| s.web_search_requests),
840 web_fetch_requests: message
841 .usage
842 .server_tool_use
843 .as_ref()
844 .map(|s| s.web_fetch_requests),
845 });
846
847 Some(UnifiedEvent::AssistantMessage {
848 content,
849 usage,
850 parent_tool_use_id: parent_tool_use_id.clone(),
851 })
852 }
853
854 ClaudeEvent::User {
855 message,
856 tool_use_result,
857 parent_tool_use_id,
858 ..
859 } => {
860 let first_tool_result = message.content.iter().find_map(|b| {
864 if let models::UserContentBlock::ToolResult {
865 tool_use_id,
866 content,
867 is_error,
868 } = b
869 {
870 Some((tool_use_id, content, is_error))
871 } else {
872 None
873 }
874 });
875
876 if let Some((tool_use_id, content, is_error)) = first_tool_result {
877 let tool_result = ToolResult {
878 success: !is_error,
879 output: if !is_error {
880 Some(content.clone())
881 } else {
882 None
883 },
884 error: if *is_error {
885 Some(content.clone())
886 } else {
887 None
888 },
889 data: tool_use_result.clone(),
890 };
891
892 Some(UnifiedEvent::ToolExecution {
893 tool_name: "unknown".to_string(),
894 tool_id: tool_use_id.clone(),
895 input: serde_json::Value::Null,
896 result: tool_result,
897 parent_tool_use_id: parent_tool_use_id.clone(),
898 })
899 } else {
900 let text_blocks: Vec<UnifiedContentBlock> = message
902 .content
903 .iter()
904 .filter_map(|b| {
905 if let models::UserContentBlock::Text { text } = b {
906 Some(UnifiedContentBlock::Text { text: text.clone() })
907 } else {
908 None
909 }
910 })
911 .collect();
912
913 if !text_blocks.is_empty() {
914 Some(UnifiedEvent::UserMessage {
915 content: text_blocks,
916 })
917 } else {
918 None
919 }
920 }
921 }
922
923 ClaudeEvent::Other => {
924 log::debug!("Skipping unknown Claude event type during streaming conversion");
925 None
926 }
927
928 ClaudeEvent::Result {
929 is_error,
930 result,
931 duration_ms,
932 num_turns,
933 structured_output,
934 ..
935 } => {
936 let effective_result = if result.is_empty() {
939 if let Some(so) = structured_output {
940 log::debug!("Streaming Result.result is empty; using structured_output");
941 serde_json::to_string(so).unwrap_or_default()
942 } else {
943 result.clone()
944 }
945 } else {
946 result.clone()
947 };
948 Some(UnifiedEvent::Result {
949 success: !is_error,
950 message: Some(effective_result),
951 duration_ms: Some(*duration_ms),
952 num_turns: Some(*num_turns),
953 })
954 }
955 }
956}
957
958#[cfg(test)]
959#[path = "claude_tests.rs"]
960mod tests;
961
962impl Default for Claude {
963 fn default() -> Self {
964 Self::new()
965 }
966}
967
968#[async_trait]
969impl Agent for Claude {
970 fn name(&self) -> &str {
971 "claude"
972 }
973
974 fn default_model() -> &'static str {
975 DEFAULT_MODEL
976 }
977
978 fn model_for_size(size: ModelSize) -> &'static str {
979 match size {
980 ModelSize::Small => "haiku",
981 ModelSize::Medium => "sonnet",
982 ModelSize::Large => "default",
983 }
984 }
985
986 fn available_models() -> &'static [&'static str] {
987 AVAILABLE_MODELS
988 }
989
990 crate::providers::common::impl_common_agent_setters!();
991
992 fn set_skip_permissions(&mut self, skip: bool) {
993 self.common.skip_permissions = skip;
994 }
995
996 crate::providers::common::impl_as_any!();
997
998 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
999 self.execute(false, prompt).await
1000 }
1001
1002 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
1003 self.execute(true, prompt).await?;
1004 Ok(())
1005 }
1006
1007 async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
1008 let agent_args = self.build_resume_args(session_id);
1009 let mut cmd = self.make_command(agent_args);
1010
1011 cmd.stdin(Stdio::inherit())
1012 .stdout(Stdio::inherit())
1013 .stderr(Stdio::inherit());
1014
1015 let mut child = cmd
1018 .spawn()
1019 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
1020 self.common.notify_spawn(&child);
1021 let status = child
1022 .wait()
1023 .await
1024 .context("Failed waiting on 'claude' CLI")?;
1025 if !status.success() {
1026 return Err(crate::process::ProcessError {
1027 exit_code: status.code(),
1028 stderr: String::new(),
1029 agent_name: "Claude".to_string(),
1030 }
1031 .into());
1032 }
1033 Ok(())
1034 }
1035
1036 async fn run_resume_with_prompt(
1037 &self,
1038 session_id: &str,
1039 prompt: &str,
1040 ) -> Result<Option<AgentOutput>> {
1041 check_print_allowed()?;
1042 log::debug!("Claude resume with prompt: session={session_id}, prompt={prompt}");
1043 let in_sandbox = self.common.sandbox.is_some();
1044 let mut args = vec!["--print".to_string()];
1045 args.extend(["--resume".to_string(), session_id.to_string()]);
1046 args.extend(["--verbose", "--output-format", "json"].map(String::from));
1047
1048 if self.common.skip_permissions && !in_sandbox {
1049 args.push("--dangerously-skip-permissions".to_string());
1050 }
1051
1052 args.extend(["--model".to_string(), self.common.model.clone()]);
1053
1054 for dir in &self.common.add_dirs {
1055 args.extend(["--add-dir".to_string(), dir.clone()]);
1056 }
1057
1058 if let Some(ref schema) = self.json_schema {
1059 args.extend(["--json-schema".to_string(), schema.clone()]);
1060 }
1061
1062 args.push("--".to_string());
1063 args.push(prompt.to_string());
1064
1065 let mut cmd = self.make_command(args);
1066
1067 cmd.stdin(Stdio::inherit());
1068 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
1069
1070 let output = cmd.output().await?;
1071
1072 crate::process::handle_output(&output, "Claude")?;
1073
1074 let json_str = String::from_utf8(output.stdout)?;
1076 log::debug!(
1077 "Parsing Claude resume JSON output ({} bytes)",
1078 json_str.len()
1079 );
1080 let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
1081 .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {e}"))?;
1082
1083 let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
1084 Ok(Some(agent_output))
1085 }
1086
1087 async fn cleanup(&self) -> Result<()> {
1088 Ok(())
1089 }
1090}