Skip to main content

zag_orch/
spawn.rs

1//! Spawn command: launch an agent session in the background and return the session ID.
2
3use 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
14/// Parameters for the spawn command.
15pub 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
32/// Directory for spawn log files.
33fn spawn_logs_dir() -> PathBuf {
34    Config::global_base_dir().join("logs").join("spawn")
35}
36
37/// Run the spawn command.
38pub 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    // Register in session store
50    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    // Build the zag exec command
76    let zag_bin = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("zag"));
77    let mut args: Vec<String> = Vec::new();
78
79    // Quiet mode for background
80    args.push("--quiet".to_string());
81
82    // Provider
83    args.push("-p".to_string());
84    args.push(params.provider.clone());
85
86    // Model
87    if let Some(ref model) = params.model {
88        args.push("--model".to_string());
89        args.push(model.clone());
90    }
91
92    // Root
93    if let Some(ref root) = params.root {
94        args.push("--root".to_string());
95        args.push(root.clone());
96    }
97
98    // Auto approve
99    if params.auto_approve {
100        args.push("--auto-approve".to_string());
101    }
102
103    // System prompt
104    if let Some(ref sp) = params.system_prompt {
105        args.push("--system-prompt".to_string());
106        args.push(sp.clone());
107    }
108
109    // Add dirs
110    for dir in &params.add_dirs {
111        args.push("--add-dir".to_string());
112        args.push(dir.clone());
113    }
114
115    // Size (ollama)
116    if let Some(ref size) = params.size {
117        args.push("--size".to_string());
118        args.push(size.clone());
119    }
120
121    // Exec subcommand with session ID
122    args.push("exec".to_string());
123    args.push("--session".to_string());
124    args.push(session_id.clone());
125
126    // Max turns
127    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    // Session metadata
133    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 &params.metadata.tags {
142        args.push("--tag".to_string());
143        args.push(tag.clone());
144    }
145
146    // The prompt
147    args.push(params.prompt.clone());
148
149    // Set up log file for stdout/stderr capture
150    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 --inject-context is set, add --context for each dependency
157    if params.inject_context {
158        for dep in &params.depends_on {
159            // Insert --context before the prompt (which is the last arg)
160            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    // If there are dependencies, wrap in a shell command that waits first
170    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    // Register in process store
206    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    // Output the session ID
228    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;