1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
use crate::agent::Agent;
use crate::config::Config;
use crate::providers::claude::Claude;
use crate::providers::codex::Codex;
use crate::providers::copilot::Copilot;
use crate::providers::gemini::Gemini;
#[cfg(test)]
use crate::providers::mock::MockAgent;
use crate::providers::ollama::Ollama;
use anyhow::{Result, bail};
use log::debug;
/// Ordered tier list used when downgrading through providers.
///
/// When the user does not pin a provider with `-p`, this list is consulted
/// after the requested/configured provider to pick the next-best fallback.
/// Order is rough preference: most-capable / most-commonly-available first.
pub const PROVIDER_TIER_LIST: &[&str] = &["claude", "codex", "gemini", "copilot", "ollama"];
/// Build the full fallback sequence starting with `start`, followed by the
/// rest of `PROVIDER_TIER_LIST` with duplicates removed.
pub fn fallback_sequence(start: &str) -> Vec<String> {
let start = start.to_lowercase();
let mut seq = vec![start.clone()];
for p in PROVIDER_TIER_LIST {
if *p != start.as_str() {
seq.push((*p).to_string());
}
}
seq
}
pub struct AgentFactory;
impl AgentFactory {
/// Create and configure an agent based on the provided parameters.
///
/// This handles:
/// - Loading config from ~/.zag/projects/<id>/zag.toml
/// - Creating the appropriate agent implementation
/// - Resolving model size aliases (small/medium/large)
/// - Merging CLI flags with config file settings
/// - Configuring the agent with all settings
pub fn create(
agent_name: &str,
system_prompt: Option<String>,
model: Option<String>,
root: Option<String>,
auto_approve: bool,
add_dirs: Vec<String>,
) -> Result<Box<dyn Agent + Send + Sync>> {
debug!("Creating agent: {}", agent_name);
// Skip pre-flight binary check for mock agent (test only)
#[cfg(test)]
let skip_preflight = agent_name == "mock";
#[cfg(not(test))]
let skip_preflight = false;
// Pre-flight: verify the agent CLI binary is available in PATH
if !skip_preflight {
crate::preflight::check_binary(agent_name)?;
}
// Initialize .agent directory and config on first run
let _ = Config::init(root.as_deref());
// Load config for defaults
let config = Config::load(root.as_deref()).unwrap_or_default();
debug!("Configuration loaded");
// Create the agent
let mut agent = Self::create_agent(agent_name)?;
debug!("Agent instance created");
// Configure system prompt
if let Some(ref sp) = system_prompt {
debug!("Setting system prompt (length: {})", sp.len());
agent.set_system_prompt(sp.clone());
}
// Configure model (CLI > config > agent default)
if let Some(model_input) = model {
let resolved = Self::resolve_model(agent_name, &model_input);
debug!("Model resolved from CLI: {} -> {}", model_input, resolved);
Self::validate_model(agent_name, &resolved)?;
agent.set_model(resolved);
} else if let Some(config_model) = config.get_model(agent_name) {
let resolved = Self::resolve_model(agent_name, config_model);
debug!(
"Model resolved from config: {} -> {}",
config_model, resolved
);
Self::validate_model(agent_name, &resolved)?;
agent.set_model(resolved);
} else {
debug!("Using default model for agent");
}
// Configure root directory
if let Some(root_dir) = root {
debug!("Setting root directory: {}", root_dir);
agent.set_root(root_dir);
}
// Configure permissions (CLI overrides config)
let skip = auto_approve || config.auto_approve();
agent.set_skip_permissions(skip);
// Configure additional directories
if !add_dirs.is_empty() {
agent.set_add_dirs(add_dirs);
}
Ok(agent)
}
/// Create an agent, downgrading through the tier list if the requested
/// provider's binary is missing or its startup probe fails.
///
/// If `provider_explicit` is true, this is equivalent to `create()` — no
/// fallback is attempted and the first failure is returned. If it is
/// false, this walks the `fallback_sequence(provider)` and logs each
/// downgrade via `on_downgrade(from, to, reason)` before trying the next
/// candidate.
///
/// Returns the constructed agent plus the provider name that actually
/// succeeded, which may differ from `provider`.
#[allow(clippy::too_many_arguments)]
pub async fn create_with_fallback(
provider: &str,
provider_explicit: bool,
system_prompt: Option<String>,
model: Option<String>,
root: Option<String>,
auto_approve: bool,
add_dirs: Vec<String>,
on_downgrade: &mut (dyn FnMut(&str, &str, &str) + Send),
) -> Result<(Box<dyn Agent + Send + Sync>, String)> {
// Explicit provider: no fallback, preserve existing behavior.
if provider_explicit {
let agent = Self::create(provider, system_prompt, model, root, auto_approve, add_dirs)?;
// Even for explicit, run the probe so auth/startup failures are
// surfaced with the same actionable error shape. A probe failure
// here bubbles up as a hard error.
agent.probe().await?;
return Ok((agent, provider.to_string()));
}
let sequence = fallback_sequence(provider);
let mut last_err: Option<anyhow::Error> = None;
let mut prev = provider.to_string();
for (i, candidate) in sequence.iter().enumerate() {
// Model, system_prompt, add_dirs: clone per attempt so we can
// retry with the next candidate on failure.
let attempt = Self::create(
candidate,
system_prompt.clone(),
// Only apply the user-supplied model to the originally-
// requested provider. Downgraded providers use their own
// default/config model because size aliases resolve per
// provider and specific model names almost never carry over.
if i == 0 { model.clone() } else { None },
root.clone(),
auto_approve,
add_dirs.clone(),
);
let agent = match attempt {
Ok(agent) => agent,
Err(e) => {
let reason = e.to_string();
debug!("Provider '{}' unavailable: {}", candidate, reason);
last_err = Some(e);
if let Some(next) = sequence.get(i + 1) {
on_downgrade(&prev, next, &reason);
prev = next.clone();
}
continue;
}
};
match agent.probe().await {
Ok(()) => return Ok((agent, candidate.clone())),
Err(e) => {
let reason = e.to_string();
debug!("Provider '{}' probe failed: {}", candidate, reason);
last_err = Some(e);
if let Some(next) = sequence.get(i + 1) {
on_downgrade(candidate, next, &reason);
prev = next.clone();
}
continue;
}
}
}
match last_err {
Some(e) => Err(e.context(format!(
"No working provider found in tier list: {:?}",
PROVIDER_TIER_LIST
))),
None => bail!(
"No working provider found in tier list: {:?}",
PROVIDER_TIER_LIST
),
}
}
/// Create the appropriate agent implementation based on name.
fn create_agent(agent_name: &str) -> Result<Box<dyn Agent + Send + Sync>> {
match agent_name.to_lowercase().as_str() {
"codex" => Ok(Box::new(Codex::new())),
"claude" => Ok(Box::new(Claude::new())),
"gemini" => Ok(Box::new(Gemini::new())),
"copilot" => Ok(Box::new(Copilot::new())),
"ollama" => Ok(Box::new(Ollama::new())),
#[cfg(test)]
"mock" => Ok(Box::new(MockAgent::new())),
_ => bail!("Unknown agent: {}", agent_name),
}
}
/// Resolve a model input (size alias or specific name) for a given agent.
fn resolve_model(agent_name: &str, model_input: &str) -> String {
match agent_name.to_lowercase().as_str() {
"claude" => Claude::resolve_model(model_input),
"codex" => Codex::resolve_model(model_input),
"gemini" => Gemini::resolve_model(model_input),
"copilot" => Copilot::resolve_model(model_input),
"ollama" => Ollama::resolve_model(model_input),
#[cfg(test)]
"mock" => MockAgent::resolve_model(model_input),
_ => model_input.to_string(), // Unknown agent, pass through
}
}
/// Validate a model for a given agent.
fn validate_model(agent_name: &str, model: &str) -> Result<()> {
match agent_name.to_lowercase().as_str() {
"claude" => Claude::validate_model(model, "Claude"),
"codex" => Codex::validate_model(model, "Codex"),
"gemini" => Gemini::validate_model(model, "Gemini"),
"copilot" => Copilot::validate_model(model, "Copilot"),
"ollama" => Ollama::validate_model(model, "Ollama"),
#[cfg(test)]
"mock" => MockAgent::validate_model(model, "Mock"),
_ => Ok(()), // Unknown agent, skip validation
}
}
}
#[cfg(test)]
#[path = "factory_tests.rs"]
mod tests;