1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use std::sync::Arc;
5use tokio::sync::RwLock;
6
7use crate::utils::MutexExt;
8
9use crate::{
10 agents::{execute_action, ActionResult as AgentActionResult, AgentAction},
11 app::Config,
12 cli::OutputFormat,
13 models::{ChatMessage, MessageRole, Model, ModelConfig, ModelFactory},
14};
15
16#[derive(Debug, Serialize, Deserialize)]
18pub struct NonInteractiveResult {
19 pub prompt: String,
21 pub response: String,
23 pub actions: Vec<ActionResult>,
25 pub errors: Vec<String>,
27 pub metadata: ExecutionMetadata,
29}
30
31#[derive(Debug, Serialize, Deserialize)]
32pub struct ActionResult {
33 pub action_type: String,
35 pub target: String,
37 pub success: bool,
39 pub output: Option<String>,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44pub struct ExecutionMetadata {
45 pub model: String,
47 pub tokens_used: Option<usize>,
49 pub duration_ms: u128,
51 pub actions_executed: bool,
53}
54
55pub struct NonInteractiveRunner {
57 model: Arc<RwLock<Box<dyn Model>>>,
58 no_execute: bool,
59 max_tokens: Option<usize>,
60}
61
62impl NonInteractiveRunner {
63 pub async fn new(
65 model_id: String,
66 _project_path: PathBuf, config: Config,
68 no_execute: bool,
69 max_tokens: Option<usize>,
70 backend: Option<&str>,
71 ) -> Result<Self> {
72 let model = ModelFactory::create_with_backend(&model_id, Some(&config), backend).await?;
74
75 Ok(Self {
76 model: Arc::new(RwLock::new(model)),
77 no_execute,
78 max_tokens,
79 })
80 }
81
82 pub async fn execute(&self, prompt: String) -> Result<NonInteractiveResult> {
84 let start_time = std::time::Instant::now();
85 let mut errors = Vec::new();
86 let mut actions = Vec::new();
87
88 let system_content = "You are an AI coding assistant. Use tools to explore and modify the codebase as needed."
90 .to_string();
91
92 let system_message = ChatMessage {
93 role: MessageRole::System,
94 content: system_content,
95 timestamp: chrono::Local::now(),
96 actions: Vec::new(),
97 thinking: None,
98 images: None,
99 tool_calls: None,
100 tool_call_id: None,
101 tool_name: None,
102 };
103
104 let user_message = ChatMessage {
105 role: MessageRole::User,
106 content: prompt.clone(),
107 timestamp: chrono::Local::now(),
108 actions: Vec::new(),
109 thinking: None,
110 images: None,
111 tool_calls: None,
112 tool_call_id: None,
113 tool_name: None,
114 };
115
116 let messages = vec![system_message, user_message];
117
118 let model_guard = self.model.read().await;
120 let model_name = model_guard.name().to_string();
121 drop(model_guard);
122
123 let model_config = ModelConfig {
125 model: model_name,
126 temperature: 0.7,
127 max_tokens: self.max_tokens.unwrap_or(4096),
128 top_p: Some(1.0),
129 frequency_penalty: None,
130 presence_penalty: None,
131 system_prompt: None,
132 thinking_enabled: false, backend_options: std::collections::HashMap::new(),
134 };
135
136 let full_response;
138 let tokens_used;
139
140 let response_text = Arc::new(std::sync::Mutex::new(String::new()));
142 let response_clone = Arc::clone(&response_text);
143 let callback = Arc::new(move |chunk: &str| {
144 let mut resp = response_clone.lock_mut_safe();
145 resp.push_str(chunk);
146 });
147
148 let model_name;
150 let result = {
151 let model = self.model.write().await;
152 model_name = model.name().to_string();
153 model
154 .chat(&messages, &model_config, Some(callback))
155 .await
156 };
157
158 let parsed_actions: Vec<AgentAction> = match result {
160 Ok(response) => {
161 let callback_content = response_text.lock_mut_safe().clone();
163 if !callback_content.is_empty() {
164 full_response = callback_content;
165 } else {
166 full_response = response.content;
167 }
168 tokens_used = response.usage.map(|u| u.total_tokens).unwrap_or(0);
169
170 if let Some(tool_calls) = response.tool_calls {
172 tool_calls
173 .iter()
174 .filter_map(|tc| tc.to_agent_action().ok())
175 .collect()
176 } else {
177 vec![]
178 }
179 },
180 Err(e) => {
181 errors.push(format!("Model error: {}", e));
182 full_response = response_text.lock_mut_safe().clone();
183 tokens_used = 0;
184 vec![]
185 },
186 };
187
188 if !self.no_execute && !parsed_actions.is_empty() {
190 for action in parsed_actions {
191 let (action_type, target) = match &action {
192 AgentAction::WriteFile { path, .. } => ("file_write", path.clone()),
193 AgentAction::EditFile { path, .. } => ("edit_file", path.clone()),
194 AgentAction::ExecuteCommand { command, .. } => ("command", command.clone()),
195 AgentAction::ReadFile { paths } => {
196 if paths.len() == 1 {
197 ("file_read", paths[0].clone())
198 } else {
199 ("file_read", format!("{} files", paths.len()))
200 }
201 }
202 AgentAction::CreateDirectory { path } => ("create_dir", path.clone()),
203 AgentAction::DeleteFile { path } => ("delete_file", path.clone()),
204 AgentAction::GitDiff { paths } => {
205 if paths.len() == 1 {
206 ("git_diff", paths[0].as_deref().unwrap_or("*").to_string())
207 } else {
208 ("git_diff", format!("{} paths", paths.len()))
209 }
210 }
211 AgentAction::GitStatus => ("git_status", "git status".to_string()),
212 AgentAction::GitCommit { message, .. } => ("git_commit", message.clone()),
213 AgentAction::WebSearch { queries } => {
214 if queries.len() == 1 {
215 ("web_search", queries[0].0.clone())
216 } else {
217 ("web_search", format!("{} queries", queries.len()))
218 }
219 }
220 AgentAction::WebFetch { url } => ("web_fetch", url.clone()),
221 };
222
223 let result = execute_action(&action).await;
224
225 let action_result = match result {
226 AgentActionResult::Success { output } => ActionResult {
227 action_type: action_type.to_string(),
228 target,
229 success: true,
230 output: Some(output),
231 },
232 AgentActionResult::Error { error } => ActionResult {
233 action_type: action_type.to_string(),
234 target,
235 success: false,
236 output: Some(error),
237 },
238 };
239
240 actions.push(action_result);
241 }
242 } else if !parsed_actions.is_empty() {
243 for action in parsed_actions {
245 let (action_type, target) = match &action {
246 AgentAction::WriteFile { path, .. } => ("file_write", path.clone()),
247 AgentAction::EditFile { path, .. } => ("edit_file", path.clone()),
248 AgentAction::ExecuteCommand { command, .. } => ("command", command.clone()),
249 AgentAction::ReadFile { paths } => {
250 if paths.len() == 1 {
251 ("file_read", paths[0].clone())
252 } else {
253 ("file_read", format!("{} files", paths.len()))
254 }
255 }
256 AgentAction::CreateDirectory { path } => ("create_dir", path.clone()),
257 AgentAction::DeleteFile { path } => ("delete_file", path.clone()),
258 AgentAction::GitDiff { paths } => {
259 if paths.len() == 1 {
260 ("git_diff", paths[0].as_deref().unwrap_or("*").to_string())
261 } else {
262 ("git_diff", format!("{} paths", paths.len()))
263 }
264 }
265 AgentAction::GitStatus => ("git_status", "git status".to_string()),
266 AgentAction::GitCommit { message, .. } => ("git_commit", message.clone()),
267 AgentAction::WebSearch { queries } => {
268 if queries.len() == 1 {
269 ("web_search", queries[0].0.clone())
270 } else {
271 ("web_search", format!("{} queries", queries.len()))
272 }
273 }
274 AgentAction::WebFetch { url } => ("web_fetch", url.clone()),
275 };
276
277 actions.push(ActionResult {
278 action_type: action_type.to_string(),
279 target,
280 success: false,
281 output: Some("Not executed (--no-execute mode)".to_string()),
282 });
283 }
284 }
285
286 let duration_ms = start_time.elapsed().as_millis();
287 let actions_executed = !self.no_execute && !actions.is_empty();
288
289 Ok(NonInteractiveResult {
290 prompt,
291 response: full_response,
292 actions,
293 errors,
294 metadata: ExecutionMetadata {
295 model: model_name,
296 tokens_used: Some(tokens_used),
297 duration_ms,
298 actions_executed,
299 },
300 })
301 }
302
303 pub fn format_result(&self, result: &NonInteractiveResult, format: OutputFormat) -> String {
305 match format {
306 OutputFormat::Json => serde_json::to_string_pretty(result).unwrap_or_else(|e| {
307 format!("{{\"error\": \"Failed to serialize result: {}\"}}", e)
308 }),
309 OutputFormat::Text => {
310 let mut output = String::new();
311 output.push_str(&result.response);
312
313 if !result.actions.is_empty() {
314 output.push_str("\n\n--- Actions ---\n");
315 for action in &result.actions {
316 output.push_str(&format!(
317 "[{}] {} - {}\n",
318 if action.success { "OK" } else { "FAIL" },
319 action.action_type,
320 action.target
321 ));
322 if let Some(ref out) = action.output {
323 output.push_str(&format!(" {}\n", out));
324 }
325 }
326 }
327
328 if !result.errors.is_empty() {
329 output.push_str("\n--- Errors ---\n");
330 for error in &result.errors {
331 output.push_str(&format!("• {}\n", error));
332 }
333 }
334
335 output
336 },
337 OutputFormat::Markdown => {
338 let mut output = String::new();
339
340 output.push_str("## Response\n\n");
341 output.push_str(&result.response);
342 output.push_str("\n\n");
343
344 if !result.actions.is_empty() {
345 output.push_str("## Actions Executed\n\n");
346 for action in &result.actions {
347 let status = if action.success { "SUCCESS" } else { "FAILED" };
348 output.push_str(&format!(
349 "- {} **{}**: `{}`\n",
350 status, action.action_type, action.target
351 ));
352 if let Some(ref out) = action.output {
353 output.push_str(&format!(" ```\n {}\n ```\n", out));
354 }
355 }
356 output.push_str("\n");
357 }
358
359 if !result.errors.is_empty() {
360 output.push_str("## Errors\n\n");
361 for error in &result.errors {
362 output.push_str(&format!("- {}\n", error));
363 }
364 output.push_str("\n");
365 }
366
367 output.push_str("---\n");
368 output.push_str(&format!(
369 "*Model: {} | Tokens: {} | Duration: {}ms*\n",
370 result.metadata.model,
371 result.metadata.tokens_used.unwrap_or(0),
372 result.metadata.duration_ms
373 ));
374
375 output
376 },
377 }
378 }
379}