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