1use crate::agent::Agent;
32use crate::attachment::{self, Attachment};
33use crate::config::Config;
34use crate::factory::AgentFactory;
35use crate::json_validation;
36use crate::listen::{self, ListenFormat};
37use crate::output::AgentOutput;
38use crate::progress::{ProgressHandler, SilentProgress};
39use crate::providers::claude::Claude;
40use crate::providers::ollama::Ollama;
41use crate::sandbox::SandboxConfig;
42use crate::session::{SessionEntry, SessionStore};
43use crate::session_log::{
44 AgentLogEvent, LiveLogContext, LogEventCallback, SessionLogCoordinator, SessionLogMetadata,
45 live_adapter_for_provider, logs_dir,
46};
47use crate::streaming::StreamingSession;
48use crate::worktree;
49use anyhow::{Result, bail};
50use log::{debug, warn};
51use std::sync::Arc;
52use std::time::Duration;
53
54fn format_duration(d: Duration) -> String {
56 let total_secs = d.as_secs();
57 let h = total_secs / 3600;
58 let m = (total_secs % 3600) / 60;
59 let s = total_secs % 60;
60 let mut parts = Vec::new();
61 if h > 0 {
62 parts.push(format!("{h}h"));
63 }
64 if m > 0 {
65 parts.push(format!("{m}m"));
66 }
67 if s > 0 || parts.is_empty() {
68 parts.push(format!("{s}s"));
69 }
70 parts.join("")
71}
72
73#[derive(Debug, Clone, Default)]
78pub struct SessionMetadata {
79 pub name: Option<String>,
80 pub description: Option<String>,
81 pub tags: Vec<String>,
82}
83
84struct SessionLogGuard {
89 coordinator: Option<SessionLogCoordinator>,
92 wrapper_session_id: String,
93 log_path: Option<std::path::PathBuf>,
94 external_writer: Option<crate::session_log::SessionLogWriter>,
97 _owned_external: Option<SessionLogCoordinator>,
101}
102
103impl SessionLogGuard {
104 fn log_path_string(&self) -> Option<String> {
105 self.log_path
106 .as_ref()
107 .map(|p| p.to_string_lossy().to_string())
108 }
109
110 async fn finish(mut self, success: bool, error: Option<String>) {
115 if let Some(coord) = self.coordinator.take() {
118 if let Err(e) = coord.finish(success, error).await {
119 warn!("Failed to finalize session log: {e}");
120 }
121 }
122 if let Some(w) = self.external_writer.take() {
123 let _ = w.clear_event_callback();
124 }
125 }
126}
127
128impl Drop for SessionLogGuard {
129 fn drop(&mut self) {
130 if let Some(ref w) = self.external_writer {
135 let _ = w.clear_event_callback();
136 }
137 if let Some(ref c) = self.coordinator {
138 let _ = c.writer().clear_event_callback();
139 }
140 }
141}
142
143#[derive(Default)]
155pub enum SessionLogMode {
156 #[default]
159 Disabled,
160 Auto,
164 External(SessionLogCoordinator),
167}
168
169pub struct AgentBuilder {
174 provider: Option<String>,
175 provider_explicit: bool,
179 model: Option<String>,
180 system_prompt: Option<String>,
181 root: Option<String>,
182 auto_approve: bool,
183 add_dirs: Vec<String>,
184 files: Vec<String>,
185 env_vars: Vec<(String, String)>,
186 worktree: Option<Option<String>>,
187 sandbox: Option<Option<String>>,
188 size: Option<String>,
189 json_mode: bool,
190 json_schema: Option<serde_json::Value>,
191 session_id: Option<String>,
192 metadata: SessionMetadata,
193 output_format: Option<String>,
194 input_format: Option<String>,
195 replay_user_messages: bool,
196 include_partial_messages: bool,
197 verbose: bool,
198 quiet: bool,
199 show_usage: bool,
200 max_turns: Option<u32>,
201 timeout: Option<std::time::Duration>,
202 mcp_config: Option<String>,
203 progress: Box<dyn ProgressHandler>,
204 session_log_mode: SessionLogMode,
205 log_event_callback: Option<LogEventCallback>,
209 stream_events_format: Option<ListenFormat>,
212 stream_show_thinking: bool,
215}
216
217impl Default for AgentBuilder {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl AgentBuilder {
224 pub fn new() -> Self {
226 Self {
227 provider: None,
228 provider_explicit: false,
229 model: None,
230 system_prompt: None,
231 root: None,
232 auto_approve: false,
233 add_dirs: Vec::new(),
234 files: Vec::new(),
235 env_vars: Vec::new(),
236 worktree: None,
237 sandbox: None,
238 size: None,
239 json_mode: false,
240 json_schema: None,
241 session_id: None,
242 metadata: SessionMetadata::default(),
243 output_format: None,
244 input_format: None,
245 replay_user_messages: false,
246 include_partial_messages: false,
247 verbose: false,
248 quiet: false,
249 show_usage: false,
250 max_turns: None,
251 timeout: None,
252 mcp_config: None,
253 progress: Box::new(SilentProgress),
254 session_log_mode: SessionLogMode::Disabled,
255 log_event_callback: None,
256 stream_events_format: None,
257 stream_show_thinking: false,
258 }
259 }
260
261 pub fn provider(mut self, provider: &str) -> Self {
268 self.provider = Some(provider.to_string());
269 self.provider_explicit = true;
270 self
271 }
272
273 pub fn model(mut self, model: &str) -> Self {
275 self.model = Some(model.to_string());
276 self
277 }
278
279 pub fn system_prompt(mut self, prompt: &str) -> Self {
281 self.system_prompt = Some(prompt.to_string());
282 self
283 }
284
285 pub fn root(mut self, root: &str) -> Self {
287 self.root = Some(root.to_string());
288 self
289 }
290
291 pub fn auto_approve(mut self, approve: bool) -> Self {
293 self.auto_approve = approve;
294 self
295 }
296
297 pub fn add_dir(mut self, dir: &str) -> Self {
299 self.add_dirs.push(dir.to_string());
300 self
301 }
302
303 pub fn file(mut self, path: &str) -> Self {
305 self.files.push(path.to_string());
306 self
307 }
308
309 pub fn env(mut self, key: &str, value: &str) -> Self {
311 self.env_vars.push((key.to_string(), value.to_string()));
312 self
313 }
314
315 pub fn worktree(mut self, name: Option<&str>) -> Self {
317 self.worktree = Some(name.map(String::from));
318 self
319 }
320
321 pub fn sandbox(mut self, name: Option<&str>) -> Self {
323 self.sandbox = Some(name.map(String::from));
324 self
325 }
326
327 pub fn size(mut self, size: &str) -> Self {
329 self.size = Some(size.to_string());
330 self
331 }
332
333 pub fn json(mut self) -> Self {
335 self.json_mode = true;
336 self
337 }
338
339 pub fn json_schema(mut self, schema: serde_json::Value) -> Self {
342 self.json_schema = Some(schema);
343 self.json_mode = true;
344 self
345 }
346
347 pub fn session_id(mut self, id: &str) -> Self {
349 self.session_id = Some(id.to_string());
350 self
351 }
352
353 pub fn name(mut self, name: &str) -> Self {
360 self.metadata.name = Some(name.to_string());
361 self
362 }
363
364 pub fn description(mut self, description: &str) -> Self {
367 self.metadata.description = Some(description.to_string());
368 self
369 }
370
371 pub fn tag(mut self, tag: &str) -> Self {
374 self.metadata.tags.push(tag.to_string());
375 self
376 }
377
378 pub fn metadata(mut self, metadata: SessionMetadata) -> Self {
380 self.metadata = metadata;
381 self
382 }
383
384 pub fn output_format(mut self, format: &str) -> Self {
386 self.output_format = Some(format.to_string());
387 self
388 }
389
390 pub fn input_format(mut self, format: &str) -> Self {
395 self.input_format = Some(format.to_string());
396 self
397 }
398
399 pub fn replay_user_messages(mut self, replay: bool) -> Self {
405 self.replay_user_messages = replay;
406 self
407 }
408
409 pub fn include_partial_messages(mut self, include: bool) -> Self {
419 self.include_partial_messages = include;
420 self
421 }
422
423 pub fn verbose(mut self, v: bool) -> Self {
425 self.verbose = v;
426 self
427 }
428
429 pub fn quiet(mut self, q: bool) -> Self {
431 self.quiet = q;
432 self
433 }
434
435 pub fn show_usage(mut self, show: bool) -> Self {
437 self.show_usage = show;
438 self
439 }
440
441 pub fn max_turns(mut self, turns: u32) -> Self {
443 self.max_turns = Some(turns);
444 self
445 }
446
447 pub fn timeout(mut self, duration: std::time::Duration) -> Self {
450 self.timeout = Some(duration);
451 self
452 }
453
454 pub fn mcp_config(mut self, config: &str) -> Self {
461 self.mcp_config = Some(config.to_string());
462 self
463 }
464
465 pub fn on_progress(mut self, handler: Box<dyn ProgressHandler>) -> Self {
467 self.progress = handler;
468 self
469 }
470
471 pub fn session_log(mut self, mode: SessionLogMode) -> Self {
473 self.session_log_mode = mode;
474 self
475 }
476
477 pub fn enable_session_log(mut self, enable: bool) -> Self {
480 self.session_log_mode = if enable {
481 SessionLogMode::Auto
482 } else {
483 SessionLogMode::Disabled
484 };
485 self
486 }
487
488 pub fn on_log_event<F>(mut self, f: F) -> Self
493 where
494 F: Fn(&AgentLogEvent) + Send + Sync + 'static,
495 {
496 self.log_event_callback = Some(Arc::new(f));
497 if matches!(self.session_log_mode, SessionLogMode::Disabled) {
498 self.session_log_mode = SessionLogMode::Auto;
499 }
500 self
501 }
502
503 pub fn stream_events_to_stderr(mut self, format: ListenFormat) -> Self {
510 self.stream_events_format = Some(format);
511 if matches!(self.session_log_mode, SessionLogMode::Disabled) {
512 self.session_log_mode = SessionLogMode::Auto;
513 }
514 self
515 }
516
517 pub fn stream_show_thinking(mut self, show: bool) -> Self {
520 self.stream_show_thinking = show;
521 self
522 }
523
524 fn persist_session_metadata_with_id(
534 &self,
535 provider: &str,
536 model: &str,
537 effective_root: Option<&str>,
538 explicit_session_id: Option<&str>,
539 ) -> Option<String> {
540 let has_metadata = self.metadata.name.is_some()
541 || self.metadata.description.is_some()
542 || !self.metadata.tags.is_empty();
543 if !has_metadata {
544 return None;
545 }
546
547 let session_id = explicit_session_id
548 .map(String::from)
549 .or_else(|| self.session_id.clone())
550 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
551 let workspace_path = effective_root
552 .map(String::from)
553 .or_else(|| self.root.clone())
554 .unwrap_or_else(|| {
555 std::env::current_dir()
556 .map(|p| p.to_string_lossy().to_string())
557 .unwrap_or_default()
558 });
559
560 let entry = SessionEntry {
561 session_id: session_id.clone(),
562 provider: provider.to_string(),
563 model: model.to_string(),
564 worktree_path: workspace_path,
565 worktree_name: String::new(),
566 created_at: chrono::Utc::now().to_rfc3339(),
567 provider_session_id: None,
568 sandbox_name: None,
569 is_worktree: self.worktree.is_some(),
570 discovered: false,
571 discovery_source: None,
572 log_path: None,
573 log_completeness: "partial".to_string(),
574 name: self.metadata.name.clone(),
575 description: self.metadata.description.clone(),
576 tags: self.metadata.tags.clone(),
577 dependencies: Vec::new(),
578 retried_from: None,
579 interactive: false,
580 };
581
582 let mut store = SessionStore::load(self.root.as_deref()).unwrap_or_default();
583 store.add(entry);
584 if let Err(e) = store.save(self.root.as_deref()) {
585 warn!("Failed to persist session metadata: {e}");
586 }
587
588 Some(session_id)
589 }
590
591 fn prepend_files(&self, prompt: &str) -> Result<String> {
593 if self.files.is_empty() {
594 return Ok(prompt.to_string());
595 }
596 let attachments: Vec<Attachment> = self
597 .files
598 .iter()
599 .map(|f| Attachment::from_path(std::path::Path::new(f)))
600 .collect::<Result<Vec<_>>>()?;
601 let prefix = attachment::format_attachments_prefix(&attachments);
602 Ok(format!("{prefix}{prompt}"))
603 }
604
605 fn resolve_provider(&self) -> Result<String> {
607 if let Some(ref p) = self.provider {
608 let p = p.to_lowercase();
609 if !Config::VALID_PROVIDERS.contains(&p.as_str()) {
610 bail!(
611 "Invalid provider '{}'. Available: {}",
612 p,
613 Config::VALID_PROVIDERS.join(", ")
614 );
615 }
616 return Ok(p);
617 }
618 let config = Config::load(self.root.as_deref()).unwrap_or_default();
619 if let Some(p) = config.provider() {
620 return Ok(p.to_string());
621 }
622 Ok("claude".to_string())
623 }
624
625 async fn create_agent(&self, provider: &str) -> Result<(Box<dyn Agent + Send + Sync>, String)> {
632 let base_system_prompt = self.system_prompt.clone().or_else(|| {
634 Config::load(self.root.as_deref())
635 .unwrap_or_default()
636 .system_prompt()
637 .map(String::from)
638 });
639
640 let system_prompt = if self.json_mode && provider != "claude" {
642 let mut prompt = base_system_prompt.unwrap_or_default();
643 if let Some(ref schema) = self.json_schema {
644 let schema_str = serde_json::to_string_pretty(schema).unwrap_or_default();
645 prompt.push_str(&format!(
646 "\n\nYou MUST respond with valid JSON only. No markdown fences, no explanations. \
647 Your response must conform to this JSON schema:\n{schema_str}"
648 ));
649 } else {
650 prompt.push_str(
651 "\n\nYou MUST respond with valid JSON only. No markdown fences, no explanations.",
652 );
653 }
654 Some(prompt)
655 } else {
656 base_system_prompt
657 };
658
659 self.progress
660 .on_spinner_start(&format!("Initializing {provider} agent"));
661
662 let progress = &*self.progress;
663 let mut on_downgrade = |from: &str, to: &str, reason: &str| {
664 progress.on_warning(&format!("Downgrading provider: {from} → {to} ({reason})"));
665 };
666 let (mut agent, effective_provider) = AgentFactory::create_with_fallback(
667 provider,
668 self.provider_explicit,
669 system_prompt,
670 self.model.clone(),
671 self.root.clone(),
672 self.auto_approve,
673 self.add_dirs.clone(),
674 &mut on_downgrade,
675 )
676 .await?;
677 let provider = effective_provider.as_str();
678
679 let effective_max_turns = self.max_turns.or_else(|| {
681 Config::load(self.root.as_deref())
682 .unwrap_or_default()
683 .max_turns()
684 });
685 if let Some(turns) = effective_max_turns {
686 agent.set_max_turns(turns);
687 }
688
689 let mut output_format = self.output_format.clone();
691 if self.json_mode && output_format.is_none() {
692 output_format = Some("json".to_string());
693 if provider != "claude" {
694 agent.set_capture_output(true);
695 }
696 }
697 agent.set_output_format(output_format);
698
699 if provider == "claude"
701 && let Some(claude_agent) = agent.as_any_mut().downcast_mut::<Claude>()
702 {
703 claude_agent.set_verbose(self.verbose);
704 if let Some(ref session_id) = self.session_id {
705 claude_agent.set_session_id(session_id.clone());
706 }
707 if let Some(ref input_fmt) = self.input_format {
708 claude_agent.set_input_format(Some(input_fmt.clone()));
709 }
710 if self.replay_user_messages {
711 claude_agent.set_replay_user_messages(true);
712 }
713 if self.include_partial_messages {
714 claude_agent.set_include_partial_messages(true);
715 }
716 if self.json_mode
717 && let Some(ref schema) = self.json_schema
718 {
719 let schema_str = serde_json::to_string(schema).unwrap_or_default();
720 claude_agent.set_json_schema(Some(schema_str));
721 }
722 if self.mcp_config.is_some() {
723 claude_agent.set_mcp_config(self.mcp_config.clone());
724 }
725 }
726
727 if provider == "ollama"
729 && let Some(ollama_agent) = agent.as_any_mut().downcast_mut::<Ollama>()
730 {
731 let config = Config::load(self.root.as_deref()).unwrap_or_default();
732 if let Some(ref size) = self.size {
733 let resolved = config.ollama_size_for(size);
734 ollama_agent.set_size(resolved.to_string());
735 }
736 }
737
738 if let Some(ref sandbox_opt) = self.sandbox {
740 let sandbox_name = sandbox_opt
741 .as_deref()
742 .map(String::from)
743 .unwrap_or_else(crate::sandbox::generate_name);
744 let template = crate::sandbox::template_for_provider(provider);
745 let workspace = self.root.clone().unwrap_or_else(|| ".".to_string());
746 agent.set_sandbox(SandboxConfig {
747 name: sandbox_name,
748 template: template.to_string(),
749 workspace,
750 });
751 }
752
753 if !self.env_vars.is_empty() {
754 agent.set_env_vars(self.env_vars.clone());
755 }
756
757 self.progress.on_spinner_finish();
758 self.progress.on_success(&format!(
759 "{} initialized with model {}",
760 provider,
761 agent.get_model()
762 ));
763
764 Ok((agent, effective_provider))
765 }
766
767 fn start_session_log(
775 &mut self,
776 command: &str,
777 resumed: bool,
778 provider: &str,
779 model: &str,
780 ) -> Option<SessionLogGuard> {
781 let mode = std::mem::replace(&mut self.session_log_mode, SessionLogMode::Disabled);
782 match mode {
783 SessionLogMode::Disabled => None,
784 SessionLogMode::External(c) => {
785 let wrapper_session_id = c
786 .writer()
787 .log_path()
788 .ok()
789 .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
790 .unwrap_or_default();
791 let log_path = c.writer().log_path().ok();
792 self.apply_event_callback(c.writer());
793 Some(SessionLogGuard {
794 coordinator: None, wrapper_session_id,
796 log_path,
797 external_writer: Some(c.writer().clone()),
798 _owned_external: Some(c),
799 })
800 }
801 SessionLogMode::Auto => {
802 let wrapper_session_id = self
803 .session_id
804 .clone()
805 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
806 let metadata = SessionLogMetadata {
807 provider: provider.to_string(),
808 wrapper_session_id: wrapper_session_id.clone(),
809 provider_session_id: None,
810 workspace_path: self.root.clone().or_else(|| {
811 std::env::current_dir()
812 .ok()
813 .map(|p| p.to_string_lossy().to_string())
814 }),
815 command: command.to_string(),
816 model: Some(model.to_string()),
817 resumed,
818 backfilled: false,
819 };
820 let live_ctx = LiveLogContext {
821 root: self.root.clone(),
822 provider_session_id: metadata.provider_session_id.clone(),
823 workspace_path: metadata.workspace_path.clone(),
824 started_at: chrono::Utc::now(),
825 is_worktree: self.worktree.is_some(),
826 };
827 let adapter = live_adapter_for_provider(provider, live_ctx, true);
828 let callback = self.build_event_callback();
829 match SessionLogCoordinator::start_with_callback(
830 &logs_dir(self.root.as_deref()),
831 metadata,
832 adapter,
833 callback,
834 ) {
835 Ok(c) => {
836 let _ = c.writer().set_global_index_dir(Config::global_base_dir());
837 let log_path = c.writer().log_path().ok();
838 Some(SessionLogGuard {
839 coordinator: Some(c),
840 wrapper_session_id,
841 log_path,
842 external_writer: None,
843 _owned_external: None,
844 })
845 }
846 Err(e) => {
847 warn!("Failed to start session log coordinator: {e}");
848 None
849 }
850 }
851 }
852 }
853 }
854
855 fn build_event_callback(&self) -> Option<LogEventCallback> {
859 let user_cb = self.log_event_callback.clone();
860 let stream_fmt = self.stream_events_format;
861 let show_thinking = self.stream_show_thinking;
862
863 if user_cb.is_none() && stream_fmt.is_none() {
864 return None;
865 }
866
867 Some(Arc::new(move |event: &AgentLogEvent| {
868 if let Some(ref user) = user_cb {
869 user(event);
870 }
871 if let Some(fmt) = stream_fmt
872 && let Some(text) = listen::format_event(event, fmt, show_thinking)
873 {
874 eprintln!("{text}");
875 }
876 }))
877 }
878
879 fn apply_event_callback(&self, writer: &crate::session_log::SessionLogWriter) {
884 if let Some(cb) = self.build_event_callback() {
885 if let Err(e) = writer.set_event_callback(cb) {
886 warn!("Failed to register session log event callback: {e}");
887 }
888 }
889 }
890
891 pub async fn exec(self, prompt: &str) -> Result<AgentOutput> {
895 let provider = self.resolve_provider()?;
896 debug!("exec: provider={provider}");
897
898 let effective_root = if let Some(ref wt_opt) = self.worktree {
900 let wt_name = wt_opt
901 .as_deref()
902 .map(String::from)
903 .unwrap_or_else(worktree::generate_name);
904 let repo_root = worktree::git_repo_root(self.root.as_deref())?;
905 let wt_path = worktree::create_worktree(&repo_root, &wt_name)?;
906 self.progress
907 .on_success(&format!("Worktree created at {}", wt_path.display()));
908 Some(wt_path.to_string_lossy().to_string())
909 } else {
910 self.root.clone()
911 };
912
913 let mut builder = self;
914 if effective_root.is_some() {
915 builder.root = effective_root;
916 }
917
918 let (agent, provider) = builder.create_agent(&provider).await?;
919
920 let log_guard = builder.start_session_log("exec", false, &provider, agent.get_model());
924
925 let _ = builder.persist_session_metadata_with_id(
930 &provider,
931 agent.get_model(),
932 builder.root.as_deref(),
933 log_guard.as_ref().map(|g| g.wrapper_session_id.as_str()),
934 );
935
936 let prompt_with_files = builder.prepend_files(prompt)?;
938
939 let effective_prompt = if builder.json_mode && provider != "claude" {
941 format!(
942 "IMPORTANT: You MUST respond with valid JSON only. No markdown, no explanation.\n\n{prompt_with_files}"
943 )
944 } else {
945 prompt_with_files
946 };
947
948 let result = if let Some(timeout_dur) = builder.timeout {
949 match tokio::time::timeout(timeout_dur, agent.run(Some(&effective_prompt))).await {
950 Ok(r) => r?,
951 Err(_) => {
952 agent.cleanup().await.ok();
953 bail!("Agent timed out after {}", format_duration(timeout_dur));
954 }
955 }
956 } else {
957 agent.run(Some(&effective_prompt)).await?
958 };
959
960 agent.cleanup().await?;
962
963 let log_path_string = log_guard.as_ref().and_then(|g| g.log_path_string());
964
965 if let Some(mut output) = result {
966 if let Some(ref schema) = builder.json_schema {
968 if !builder.json_mode {
969 warn!(
970 "json_schema is set but json_mode is false — \
971 schema will not be sent to the agent, only used for output validation"
972 );
973 }
974 if let Some(ref result_text) = output.result {
975 debug!(
976 "exec: validating result ({} bytes): {:.300}",
977 result_text.len(),
978 result_text
979 );
980 if let Err(errors) = json_validation::validate_json_schema(result_text, schema)
981 {
982 let preview = if result_text.len() > 500 {
983 &result_text[..500]
984 } else {
985 result_text.as_str()
986 };
987 bail!(
988 "JSON schema validation failed: {}\nRaw agent output ({} bytes):\n{}",
989 errors.join("; "),
990 result_text.len(),
991 preview
992 );
993 }
994 }
995 }
996 output.log_path = log_path_string;
997 let success = !output.is_error;
998 let err_msg = output.error_message.clone();
999 if let Some(g) = log_guard {
1000 g.finish(success, err_msg).await;
1001 }
1002 Ok(output)
1003 } else {
1004 let mut output = AgentOutput::from_text(&provider, "");
1006 output.log_path = log_path_string;
1007 if let Some(g) = log_guard {
1008 g.finish(true, None).await;
1009 }
1010 Ok(output)
1011 }
1012 }
1013
1014 pub async fn exec_streaming(self, prompt: &str) -> Result<StreamingSession> {
1083 let provider = self.resolve_provider()?;
1084 debug!("exec_streaming: provider={provider}");
1085
1086 if provider != "claude" {
1087 bail!("Streaming input is only supported by the Claude provider");
1088 }
1089
1090 let prompt_with_files = self.prepend_files(prompt)?;
1092
1093 let mut builder = self;
1096 builder.provider_explicit = true;
1097 let (agent, _provider) = builder.create_agent(&provider).await?;
1098
1099 let claude_agent = agent
1101 .as_any_ref()
1102 .downcast_ref::<Claude>()
1103 .ok_or_else(|| anyhow::anyhow!("Failed to downcast agent to Claude"))?;
1104
1105 claude_agent.execute_streaming(Some(&prompt_with_files))
1106 }
1107
1108 pub async fn run(self, prompt: Option<&str>) -> Result<()> {
1112 let provider = self.resolve_provider()?;
1113 debug!("run: provider={provider}");
1114
1115 let prompt_with_files = match prompt {
1117 Some(p) => Some(self.prepend_files(p)?),
1118 None if !self.files.is_empty() => {
1119 let attachments: Vec<Attachment> = self
1120 .files
1121 .iter()
1122 .map(|f| Attachment::from_path(std::path::Path::new(f)))
1123 .collect::<Result<Vec<_>>>()?;
1124 Some(attachment::format_attachments_prefix(&attachments))
1125 }
1126 None => None,
1127 };
1128
1129 let mut builder = self;
1130 let (agent, effective_provider) = builder.create_agent(&provider).await?;
1131 let log_guard =
1132 builder.start_session_log("run", false, &effective_provider, agent.get_model());
1133 let _ = builder.persist_session_metadata_with_id(
1134 &effective_provider,
1135 agent.get_model(),
1136 builder.root.as_deref(),
1137 log_guard.as_ref().map(|g| g.wrapper_session_id.as_str()),
1138 );
1139 agent.run_interactive(prompt_with_files.as_deref()).await?;
1140 agent.cleanup().await?;
1141 if let Some(g) = log_guard {
1142 g.finish(true, None).await;
1143 }
1144 Ok(())
1145 }
1146
1147 pub async fn resume(self, session_id: &str) -> Result<()> {
1149 let provider = self.resolve_provider()?;
1150 debug!("resume: provider={provider}, session={session_id}");
1151
1152 let mut builder = self;
1154 builder.provider_explicit = true;
1155 let (agent, effective_provider) = builder.create_agent(&provider).await?;
1156 let log_guard =
1157 builder.start_session_log("resume", true, &effective_provider, agent.get_model());
1158 agent.run_resume(Some(session_id), false).await?;
1159 agent.cleanup().await?;
1160 if let Some(g) = log_guard {
1161 g.finish(true, None).await;
1162 }
1163 Ok(())
1164 }
1165
1166 pub async fn continue_last(self) -> Result<()> {
1168 let provider = self.resolve_provider()?;
1169 debug!("continue_last: provider={provider}");
1170
1171 let mut builder = self;
1173 builder.provider_explicit = true;
1174 let (agent, effective_provider) = builder.create_agent(&provider).await?;
1175 let log_guard =
1176 builder.start_session_log("resume", true, &effective_provider, agent.get_model());
1177 agent.run_resume(None, true).await?;
1178 agent.cleanup().await?;
1179 if let Some(g) = log_guard {
1180 g.finish(true, None).await;
1181 }
1182 Ok(())
1183 }
1184}
1185
1186#[cfg(test)]
1187#[path = "builder_tests.rs"]
1188mod tests;