Skip to main content

zag_agent/providers/
common.rs

1use crate::agent::OnSpawnHook;
2use crate::output::AgentOutput;
3use crate::sandbox::SandboxConfig;
4use anyhow::Context;
5use std::path::Path;
6use std::process::Stdio;
7use tokio::process::{Child, Command};
8
9/// Shared configuration state for CLI-based agent providers.
10///
11/// Embed this struct in each provider to avoid duplicating field
12/// declarations and trivial setter implementations.
13pub struct CommonAgentState {
14    pub system_prompt: String,
15    pub model: String,
16    pub root: Option<String>,
17    pub skip_permissions: bool,
18    pub output_format: Option<String>,
19    pub add_dirs: Vec<String>,
20    pub capture_output: bool,
21    pub sandbox: Option<SandboxConfig>,
22    pub max_turns: Option<u32>,
23    pub env_vars: Vec<(String, String)>,
24    /// Optional callback invoked with the OS pid of the spawned agent
25    /// subprocess. Threaded through from `AgentBuilder::on_spawn` /
26    /// `Agent::set_on_spawn_hook`. Providers call
27    /// [`CommonAgentState::notify_spawn`] right after `Command::spawn`
28    /// so callers can capture the child pid before the terminal wait.
29    pub on_spawn_hook: Option<OnSpawnHook>,
30}
31
32impl CommonAgentState {
33    pub fn new(default_model: &str) -> Self {
34        Self {
35            system_prompt: String::new(),
36            model: default_model.to_string(),
37            root: None,
38            skip_permissions: false,
39            output_format: None,
40            add_dirs: Vec::new(),
41            capture_output: false,
42            sandbox: None,
43            max_turns: None,
44            env_vars: Vec::new(),
45            on_spawn_hook: None,
46        }
47    }
48
49    /// Invoke the registered `on_spawn` hook with the pid of a freshly
50    /// spawned child, if any. Safe to call even when no hook is set.
51    pub fn notify_spawn(&self, child: &Child) {
52        if let (Some(cb), Some(pid)) = (self.on_spawn_hook.as_ref(), child.id()) {
53            cb(pid);
54        }
55    }
56
57    /// Get the effective base path (root directory or ".").
58    pub fn get_base_path(&self) -> &Path {
59        self.root.as_ref().map(Path::new).unwrap_or(Path::new("."))
60    }
61
62    /// Create a `Command` either directly or wrapped in sandbox.
63    ///
64    /// Standard pattern used by Claude, Copilot, and Gemini. Sets
65    /// `current_dir`, args, and env vars. Providers with custom sandbox
66    /// behavior (Codex, Ollama) keep their own `make_command()`.
67    pub fn make_command(&self, binary_name: &str, agent_args: Vec<String>) -> Command {
68        if let Some(ref sb) = self.sandbox {
69            let std_cmd = crate::sandbox::build_sandbox_command(sb, agent_args);
70            Command::from(std_cmd)
71        } else {
72            let mut cmd = Command::new(binary_name);
73            if let Some(ref root) = self.root {
74                cmd.current_dir(root);
75            }
76            cmd.args(&agent_args);
77            for (key, value) in &self.env_vars {
78                cmd.env(key, value);
79            }
80            cmd
81        }
82    }
83
84    /// Execute a command interactively (inheriting stdin/stdout/stderr).
85    ///
86    /// Returns `ProcessError` on non-zero exit.
87    pub async fn run_interactive_command(
88        cmd: &mut Command,
89        agent_display_name: &str,
90    ) -> anyhow::Result<()> {
91        Self::run_interactive_command_with_hook(cmd, agent_display_name, None).await
92    }
93
94    /// Same as [`run_interactive_command`], but invokes `on_spawn` once
95    /// with the child's OS pid right after spawn and before awaiting
96    /// the child's exit — that window is what lets callers register the
97    /// child pid with an external process store (e.g. so
98    /// `zag ps kill self` can SIGTERM the agent child rather than the
99    /// parent zag process).
100    pub async fn run_interactive_command_with_hook(
101        cmd: &mut Command,
102        agent_display_name: &str,
103        on_spawn: Option<&OnSpawnHook>,
104    ) -> anyhow::Result<()> {
105        cmd.stdin(Stdio::inherit())
106            .stdout(Stdio::inherit())
107            .stderr(Stdio::inherit());
108        let mut child = cmd.spawn().with_context(|| {
109            format!(
110                "Failed to execute '{}' CLI. Is it installed and in PATH?",
111                agent_display_name.to_lowercase()
112            )
113        })?;
114        if let (Some(cb), Some(pid)) = (on_spawn, child.id()) {
115            cb(pid);
116        }
117        let status = child.wait().await.with_context(|| {
118            format!(
119                "Failed waiting on '{}' CLI",
120                agent_display_name.to_lowercase()
121            )
122        })?;
123        if !status.success() {
124            return Err(crate::process::ProcessError {
125                exit_code: status.code(),
126                stderr: String::new(),
127                agent_name: agent_display_name.to_string(),
128            }
129            .into());
130        }
131        Ok(())
132    }
133
134    /// Execute a non-interactive command with simple capture-or-passthrough.
135    ///
136    /// If `capture_output` is set, captures stdout and returns `Some(AgentOutput)`.
137    /// Otherwise streams stdout to the terminal and returns `None`.
138    ///
139    /// Used by Copilot, Gemini, Ollama. Providers with custom output parsing
140    /// (Claude, Codex) keep their own non-interactive logic.
141    pub async fn run_non_interactive_simple(
142        &self,
143        cmd: &mut Command,
144        agent_display_name: &str,
145    ) -> anyhow::Result<Option<AgentOutput>> {
146        if self.capture_output {
147            let text = crate::process::run_captured(cmd, agent_display_name).await?;
148            log::debug!(
149                "{} raw response ({} bytes): {}",
150                agent_display_name,
151                text.len(),
152                text
153            );
154            Ok(Some(AgentOutput::from_text(
155                &agent_display_name.to_lowercase(),
156                &text,
157            )))
158        } else {
159            cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
160            crate::process::run_with_captured_stderr(cmd).await?;
161            Ok(None)
162        }
163    }
164}
165
166/// Delegate common Agent trait setter methods to `self.common`.
167///
168/// Generates the 12 trivial setters that are identical across all CLI-based
169/// providers. Excludes `set_skip_permissions` since Ollama overrides it.
170macro_rules! impl_common_agent_setters {
171    () => {
172        fn system_prompt(&self) -> &str {
173            &self.common.system_prompt
174        }
175
176        fn set_system_prompt(&mut self, prompt: String) {
177            self.common.system_prompt = prompt;
178        }
179
180        fn get_model(&self) -> &str {
181            &self.common.model
182        }
183
184        fn set_model(&mut self, model: String) {
185            self.common.model = model;
186        }
187
188        fn set_root(&mut self, root: String) {
189            self.common.root = Some(root);
190        }
191
192        fn set_output_format(&mut self, format: Option<String>) {
193            self.common.output_format = format;
194        }
195
196        fn set_add_dirs(&mut self, dirs: Vec<String>) {
197            self.common.add_dirs = dirs;
198        }
199
200        fn set_env_vars(&mut self, vars: Vec<(String, String)>) {
201            self.common.env_vars = vars;
202        }
203
204        fn set_capture_output(&mut self, capture: bool) {
205            self.common.capture_output = capture;
206        }
207
208        fn set_sandbox(&mut self, config: crate::sandbox::SandboxConfig) {
209            self.common.sandbox = Some(config);
210        }
211
212        fn set_max_turns(&mut self, turns: u32) {
213            self.common.max_turns = Some(turns);
214        }
215
216        fn set_on_spawn_hook(&mut self, hook: crate::agent::OnSpawnHook) {
217            self.common.on_spawn_hook = Some(hook);
218        }
219    };
220}
221pub(crate) use impl_common_agent_setters;
222
223/// Implement `as_any_ref` and `as_any_mut` for a concrete agent type.
224macro_rules! impl_as_any {
225    () => {
226        fn as_any_ref(&self) -> &dyn std::any::Any {
227            self
228        }
229
230        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
231            self
232        }
233    };
234}
235pub(crate) use impl_as_any;
236
237#[cfg(test)]
238#[path = "common_tests.rs"]
239mod tests;