1use crate::agent::{Agent, ModelSize};
3use crate::output::AgentOutput;
4use crate::sandbox::SandboxConfig;
5use crate::session_log::HistoricalLogAdapter;
6use anyhow::{Context, Result};
7use async_trait::async_trait;
8use std::process::Stdio;
9use tokio::process::Command;
10
11pub const DEFAULT_MODEL: &str = "qwen3.5";
12pub const DEFAULT_SIZE: &str = "9b";
13
14pub const AVAILABLE_SIZES: &[&str] = &["0.8b", "2b", "4b", "9b", "27b", "35b", "122b"];
15
16pub struct Ollama {
17 system_prompt: String,
18 model: String,
19 size: String,
20 root: Option<String>,
21 skip_permissions: bool,
22 output_format: Option<String>,
23 add_dirs: Vec<String>,
24 capture_output: bool,
25 max_turns: Option<u32>,
26 sandbox: Option<SandboxConfig>,
27 env_vars: Vec<(String, String)>,
28}
29
30pub struct OllamaHistoricalLogAdapter;
31
32impl Ollama {
33 pub fn new() -> Self {
34 Self {
35 system_prompt: String::new(),
36 model: DEFAULT_MODEL.to_string(),
37 size: DEFAULT_SIZE.to_string(),
38 root: None,
39 skip_permissions: false,
40 output_format: None,
41 add_dirs: Vec::new(),
42 capture_output: false,
43 max_turns: None,
44 sandbox: None,
45 env_vars: Vec::new(),
46 }
47 }
48
49 pub fn set_size(&mut self, size: String) {
50 self.size = size;
51 }
52
53 pub fn display_model(&self) -> String {
55 self.model_tag()
56 }
57
58 fn model_tag(&self) -> String {
60 format!("{}:{}", self.model, self.size)
61 }
62
63 fn build_run_args(&self, interactive: bool, prompt: Option<&str>) -> Vec<String> {
65 let mut args = vec!["run".to_string()];
66
67 if let Some(ref format) = self.output_format
68 && format == "json"
69 {
70 args.extend(["--format".to_string(), "json".to_string()]);
71 }
72
73 if !interactive {
74 args.push("--nowordwrap".to_string());
76 }
77
78 args.push("--hidethinking".to_string());
79
80 args.push(self.model_tag());
81
82 let effective_prompt = match (self.system_prompt.is_empty(), prompt) {
84 (false, Some(p)) => Some(format!("{}\n\n{}", self.system_prompt, p)),
85 (false, None) => Some(self.system_prompt.clone()),
86 (true, p) => p.map(String::from),
87 };
88
89 if let Some(p) = effective_prompt {
90 args.push(p);
91 }
92
93 args
94 }
95
96 fn make_command(&self, agent_args: Vec<String>) -> Command {
98 if let Some(ref sb) = self.sandbox {
99 let shell_cmd = format!(
102 "ollama {}",
103 agent_args
104 .iter()
105 .map(|a| shell_escape(a))
106 .collect::<Vec<_>>()
107 .join(" ")
108 );
109 let mut std_cmd = std::process::Command::new("docker");
110 std_cmd.args([
111 "sandbox",
112 "run",
113 "--name",
114 &sb.name,
115 &sb.template,
116 &sb.workspace,
117 "--",
118 "-c",
119 &shell_cmd,
120 ]);
121 log::debug!(
122 "Sandbox command: docker sandbox run --name {} {} {} -- -c {:?}",
123 sb.name,
124 sb.template,
125 sb.workspace,
126 shell_cmd
127 );
128 Command::from(std_cmd)
129 } else {
130 let mut cmd = Command::new("ollama");
131 if let Some(ref root) = self.root {
132 cmd.current_dir(root);
133 }
134 cmd.args(&agent_args);
135 for (key, value) in &self.env_vars {
136 cmd.env(key, value);
137 }
138 cmd
139 }
140 }
141
142 async fn execute(
143 &self,
144 interactive: bool,
145 prompt: Option<&str>,
146 ) -> Result<Option<AgentOutput>> {
147 let agent_args = self.build_run_args(interactive, prompt);
148 log::debug!("Ollama command: ollama {}", agent_args.join(" "));
149 if !self.system_prompt.is_empty() {
150 log::debug!("Ollama system prompt: {}", self.system_prompt);
151 }
152 if let Some(p) = prompt {
153 log::debug!("Ollama user prompt: {}", p);
154 }
155 let mut cmd = self.make_command(agent_args);
156
157 if interactive {
158 cmd.stdin(Stdio::inherit())
159 .stdout(Stdio::inherit())
160 .stderr(Stdio::inherit());
161 let status = cmd
162 .status()
163 .await
164 .context("Failed to execute 'ollama' CLI. Is it installed and in PATH?")?;
165 if !status.success() {
166 anyhow::bail!("Ollama command failed with status: {}", status);
167 }
168 Ok(None)
169 } else if self.capture_output {
170 let text = crate::process::run_captured(&mut cmd, "Ollama").await?;
171 log::debug!("Ollama raw response ({} bytes): {}", text.len(), text);
172 Ok(Some(AgentOutput::from_text("ollama", &text)))
173 } else {
174 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
175 crate::process::run_with_captured_stderr(&mut cmd).await?;
176 Ok(None)
177 }
178 }
179
180 pub fn size_for_model_size(size: ModelSize) -> &'static str {
182 match size {
183 ModelSize::Small => "2b",
184 ModelSize::Medium => "9b",
185 ModelSize::Large => "35b",
186 }
187 }
188}
189
190fn shell_escape(s: &str) -> String {
192 if s.contains(' ')
193 || s.contains('\'')
194 || s.contains('"')
195 || s.contains('\\')
196 || s.contains('$')
197 || s.contains('`')
198 || s.contains('!')
199 {
200 format!("'{}'", s.replace('\'', "'\\''"))
201 } else {
202 s.to_string()
203 }
204}
205
206#[cfg(test)]
207#[path = "ollama_tests.rs"]
208mod tests;
209
210impl Default for Ollama {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216impl HistoricalLogAdapter for OllamaHistoricalLogAdapter {
217 fn backfill(&self, _root: Option<&str>) -> Result<Vec<crate::session_log::BackfilledSession>> {
218 Ok(Vec::new())
219 }
220}
221
222#[async_trait]
223impl Agent for Ollama {
224 fn name(&self) -> &str {
225 "ollama"
226 }
227
228 fn default_model() -> &'static str
229 where
230 Self: Sized,
231 {
232 DEFAULT_MODEL
233 }
234
235 fn model_for_size(size: ModelSize) -> &'static str
236 where
237 Self: Sized,
238 {
239 Self::size_for_model_size(size)
241 }
242
243 fn available_models() -> &'static [&'static str]
244 where
245 Self: Sized,
246 {
247 AVAILABLE_SIZES
249 }
250
251 fn validate_model(_model: &str, _agent_name: &str) -> Result<()>
253 where
254 Self: Sized,
255 {
256 Ok(())
257 }
258
259 fn system_prompt(&self) -> &str {
260 &self.system_prompt
261 }
262
263 fn set_system_prompt(&mut self, prompt: String) {
264 self.system_prompt = prompt;
265 }
266
267 fn get_model(&self) -> &str {
268 &self.model
269 }
270
271 fn set_model(&mut self, model: String) {
272 self.model = model;
273 }
274
275 fn set_root(&mut self, root: String) {
276 self.root = Some(root);
277 }
278
279 fn set_skip_permissions(&mut self, _skip: bool) {
280 self.skip_permissions = true;
282 }
283
284 fn set_output_format(&mut self, format: Option<String>) {
285 self.output_format = format;
286 }
287
288 fn set_capture_output(&mut self, capture: bool) {
289 self.capture_output = capture;
290 }
291
292 fn set_max_turns(&mut self, turns: u32) {
293 self.max_turns = Some(turns);
294 }
295
296 fn set_sandbox(&mut self, config: SandboxConfig) {
297 self.sandbox = Some(config);
298 }
299
300 fn set_add_dirs(&mut self, dirs: Vec<String>) {
301 self.add_dirs = dirs;
302 }
303
304 fn set_env_vars(&mut self, vars: Vec<(String, String)>) {
305 self.env_vars = vars;
306 }
307
308 fn as_any_ref(&self) -> &dyn std::any::Any {
309 self
310 }
311
312 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
313 self
314 }
315
316 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
317 self.execute(false, prompt).await
318 }
319
320 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
321 self.execute(true, prompt).await?;
322 Ok(())
323 }
324
325 async fn run_resume(&self, _session_id: Option<&str>, _last: bool) -> Result<()> {
326 anyhow::bail!("Ollama does not support session resume")
327 }
328
329 async fn cleanup(&self) -> Result<()> {
330 Ok(())
331 }
332}