1use anyhow::Result;
4use log::debug;
5use std::fs::{self, File};
6use std::path::PathBuf;
7use zag_agent::config::Config;
8use zag_agent::process_store::{ProcessEntry, ProcessStore};
9use zag_agent::session::{SessionEntry, SessionStore};
10
11use crate::types::SessionMetadata;
12use crate::util::current_workspace;
13
14pub struct SpawnParams {
16 pub prompt: String,
17 pub provider: String,
18 pub model: Option<String>,
19 pub root: Option<String>,
20 pub auto_approve: bool,
21 pub system_prompt: Option<String>,
22 pub add_dirs: Vec<String>,
23 pub size: Option<String>,
24 pub max_turns: Option<u32>,
25 pub json: bool,
26 pub metadata: SessionMetadata,
27 pub depends_on: Vec<String>,
28 pub inject_context: bool,
29 pub retried_from: Option<String>,
30}
31
32fn spawn_logs_dir() -> PathBuf {
34 Config::global_base_dir().join("logs").join("spawn")
35}
36
37pub fn run_spawn(params: SpawnParams) -> Result<()> {
39 let session_id = uuid::Uuid::new_v4().to_string();
40 let workspace = current_workspace(params.root.as_deref());
41
42 debug!(
43 "Spawning background session: id={}, provider={}, prompt='{}'",
44 session_id,
45 params.provider,
46 params.prompt.chars().take(100).collect::<String>()
47 );
48
49 let mut session_store = SessionStore::load(params.root.as_deref()).unwrap_or_default();
51 session_store.add(SessionEntry {
52 session_id: session_id.clone(),
53 provider: params.provider.clone(),
54 model: params.model.clone().unwrap_or_default(),
55 worktree_path: workspace.clone(),
56 worktree_name: String::new(),
57 created_at: chrono::Utc::now().to_rfc3339(),
58 provider_session_id: None,
59 sandbox_name: None,
60 is_worktree: false,
61 discovered: false,
62 discovery_source: None,
63 log_path: None,
64 log_completeness: "partial".to_string(),
65 name: params.metadata.name.clone(),
66 description: params.metadata.description.clone(),
67 tags: params.metadata.tags.clone(),
68 dependencies: params.depends_on.clone(),
69 retried_from: params.retried_from.clone(),
70 });
71 if let Err(e) = session_store.save(params.root.as_deref()) {
72 log::warn!("Failed to save session store: {}", e);
73 }
74
75 let zag_bin = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("zag"));
77 let mut args: Vec<String> = Vec::new();
78
79 args.push("--quiet".to_string());
81
82 args.push("-p".to_string());
84 args.push(params.provider.clone());
85
86 if let Some(ref model) = params.model {
88 args.push("--model".to_string());
89 args.push(model.clone());
90 }
91
92 if let Some(ref root) = params.root {
94 args.push("--root".to_string());
95 args.push(root.clone());
96 }
97
98 if params.auto_approve {
100 args.push("--auto-approve".to_string());
101 }
102
103 if let Some(ref sp) = params.system_prompt {
105 args.push("--system-prompt".to_string());
106 args.push(sp.clone());
107 }
108
109 for dir in ¶ms.add_dirs {
111 args.push("--add-dir".to_string());
112 args.push(dir.clone());
113 }
114
115 if let Some(ref size) = params.size {
117 args.push("--size".to_string());
118 args.push(size.clone());
119 }
120
121 args.push("exec".to_string());
123 args.push("--session".to_string());
124 args.push(session_id.clone());
125
126 if let Some(max_turns) = params.max_turns {
128 args.push("--max-turns".to_string());
129 args.push(max_turns.to_string());
130 }
131
132 if let Some(ref name) = params.metadata.name {
134 args.push("--name".to_string());
135 args.push(name.clone());
136 }
137 if let Some(ref desc) = params.metadata.description {
138 args.push("--description".to_string());
139 args.push(desc.clone());
140 }
141 for tag in ¶ms.metadata.tags {
142 args.push("--tag".to_string());
143 args.push(tag.clone());
144 }
145
146 args.push(params.prompt.clone());
148
149 let logs_dir = spawn_logs_dir();
151 fs::create_dir_all(&logs_dir)?;
152 let log_path = logs_dir.join(format!("{}.log", session_id));
153 let stdout_file = File::create(&log_path)?;
154 let stderr_file = stdout_file.try_clone()?;
155
156 if params.inject_context {
158 for dep in ¶ms.depends_on {
159 let prompt = args.pop().unwrap();
161 args.push("--context".to_string());
162 args.push(dep.clone());
163 args.push(prompt);
164 }
165 }
166
167 debug!("Spawning: {} {}", zag_bin.display(), args.join(" "));
168
169 let child = if !params.depends_on.is_empty() {
171 let wait_args: Vec<String> = params
172 .depends_on
173 .iter()
174 .map(|id| format!("\"{}\"", id))
175 .collect();
176 let wait_cmd = format!(
177 "{} wait {} && {} {}",
178 zag_bin.display(),
179 wait_args.join(" "),
180 zag_bin.display(),
181 args.iter()
182 .map(|a| format!("\"{}\"", a.replace('"', "\\\"")))
183 .collect::<Vec<_>>()
184 .join(" ")
185 );
186 debug!("Spawn with deps: sh -c '{}'", wait_cmd);
187 std::process::Command::new("sh")
188 .arg("-c")
189 .arg(&wait_cmd)
190 .stdin(std::process::Stdio::null())
191 .stdout(stdout_file)
192 .stderr(stderr_file)
193 .spawn()?
194 } else {
195 std::process::Command::new(&zag_bin)
196 .args(&args)
197 .stdin(std::process::Stdio::null())
198 .stdout(stdout_file)
199 .stderr(stderr_file)
200 .spawn()?
201 };
202
203 let child_pid = child.id();
204
205 let mut proc_store = ProcessStore::load().unwrap_or_default();
207 proc_store.add(ProcessEntry {
208 id: uuid::Uuid::new_v4().to_string(),
209 pid: child_pid,
210 session_id: Some(session_id.clone()),
211 provider: params.provider.clone(),
212 model: params.model.clone().unwrap_or_default(),
213 command: "exec".to_string(),
214 prompt: Some(params.prompt.chars().take(100).collect()),
215 started_at: chrono::Utc::now().to_rfc3339(),
216 status: "running".to_string(),
217 exit_code: None,
218 exited_at: None,
219 root: Some(workspace),
220 parent_process_id: std::env::var("ZAG_PROCESS_ID").ok(),
221 parent_session_id: std::env::var("ZAG_SESSION_ID").ok(),
222 });
223 if let Err(e) = proc_store.save() {
224 log::warn!("Failed to save process store: {}", e);
225 }
226
227 if params.json {
229 println!(
230 "{}",
231 serde_json::json!({
232 "session_id": session_id,
233 "pid": child_pid,
234 "log_path": log_path.to_string_lossy(),
235 })
236 );
237 } else {
238 println!("{}", session_id);
239 }
240
241 Ok(())
242}
243
244#[cfg(test)]
245#[path = "spawn_tests.rs"]
246mod tests;