1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::sync::Arc;
4use tokio::sync::RwLock;
5
6use crate::utils::MutexExt;
7
8use crate::{
9 agents::{ActionResult as AgentActionResult, AgentAction},
10 app::Config,
11 cli::OutputFormat,
12 constants::{DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE},
13 models::{ChatMessage, Model, ModelConfig, ModelFactory},
14 prompts,
15};
16
17use super::agent_loop::{self, AgentObserver, LoopControl, MAX_AGENT_ITERATIONS};
18
19#[derive(Debug, Serialize, Deserialize)]
21pub struct NonInteractiveResult {
22 pub prompt: String,
24 pub response: String,
26 pub actions: Vec<ActionResult>,
28 pub errors: Vec<String>,
30 pub metadata: ExecutionMetadata,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct ActionResult {
36 pub action_type: String,
38 pub target: String,
40 pub success: bool,
42 pub output: Option<String>,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct ExecutionMetadata {
48 pub model: String,
50 pub tokens_used: Option<usize>,
52 pub duration_ms: u128,
54 pub actions_executed: bool,
56}
57
58pub struct NonInteractiveRunner {
60 model: Arc<RwLock<Box<dyn Model>>>,
61 no_execute: bool,
62 max_tokens: Option<usize>,
63}
64
65impl NonInteractiveRunner {
66 pub async fn new(
68 model_id: String,
69 config: Config,
70 no_execute: bool,
71 max_tokens: Option<usize>,
72 ) -> Result<Self> {
73 let model = ModelFactory::create(&model_id, Some(&config)).await?;
75
76 Ok(Self {
77 model: Arc::new(RwLock::new(model)),
78 no_execute,
79 max_tokens,
80 })
81 }
82
83 pub async fn execute(&self, prompt: String) -> Result<NonInteractiveResult> {
85 let start_time = std::time::Instant::now();
86 let mut errors = Vec::new();
87 let mut total_tokens = 0;
88
89 let system_message = ChatMessage::system(prompts::get_system_prompt());
91 let user_message = ChatMessage::user(prompt.clone());
92 let mut messages = vec![system_message, user_message];
93
94 let model_name = {
96 let model = self.model.read().await;
97 model.name().to_string()
98 };
99 let model_config = ModelConfig {
100 model: model_name.clone(),
101 temperature: DEFAULT_TEMPERATURE,
102 max_tokens: self.max_tokens.unwrap_or(DEFAULT_MAX_TOKENS),
103 thinking_enabled: Some(false),
104 ..ModelConfig::default()
105 };
106
107 let response_text = Arc::new(std::sync::Mutex::new(String::new()));
109 let response_clone = Arc::clone(&response_text);
110 let callback = Arc::new(move |chunk: &str| {
111 let mut resp = response_clone.lock_mut_safe();
112 resp.push_str(chunk);
113 });
114
115 let result = {
116 let model = self.model.read().await;
117 model.chat(&messages, &model_config, Some(callback)).await
118 };
119
120 let (content, initial_tool_calls) = match result {
121 Ok(response) => {
122 let callback_content = response_text.lock_mut_safe().clone();
123 let content = if !callback_content.is_empty() {
124 callback_content
125 } else {
126 response.content
127 };
128 total_tokens += response.usage.map(|u| u.total_tokens).unwrap_or(0);
129 let tool_calls = response.tool_calls.unwrap_or_default();
130 (content, tool_calls)
131 },
132 Err(e) => {
133 errors.push(format!("Model error: {}", e));
134 let content = response_text.lock_mut_safe().clone();
135 (content, vec![])
136 },
137 };
138
139 if initial_tool_calls.is_empty() {
141 let duration_ms = start_time.elapsed().as_millis();
142 return Ok(NonInteractiveResult {
143 prompt,
144 response: content,
145 actions: vec![],
146 errors,
147 metadata: ExecutionMetadata {
148 model: model_name,
149 tokens_used: Some(total_tokens),
150 duration_ms,
151 actions_executed: false,
152 },
153 });
154 }
155
156 let assistant_msg =
158 ChatMessage::assistant(content.clone()).with_tool_calls(initial_tool_calls.clone());
159 messages.push(assistant_msg);
160
161 if self.no_execute {
163 let actions = build_no_execute_actions(&initial_tool_calls, &mut messages);
164 let duration_ms = start_time.elapsed().as_millis();
165 return Ok(NonInteractiveResult {
166 prompt,
167 response: content,
168 actions,
169 errors,
170 metadata: ExecutionMetadata {
171 model: model_name,
172 tokens_used: Some(total_tokens),
173 duration_ms,
174 actions_executed: false,
175 },
176 });
177 }
178
179 let mut observer = SilentObserver;
181 let loop_result = agent_loop::run_agent_loop(
182 Arc::clone(&self.model),
183 &model_config,
184 &mut messages,
185 initial_tool_calls,
186 &mut observer,
187 MAX_AGENT_ITERATIONS,
188 )
189 .await?;
190
191 total_tokens += loop_result.total_tokens;
193 let final_response = if loop_result.final_response.is_empty() {
194 content
195 } else {
196 loop_result.final_response
197 };
198
199 let actions: Vec<ActionResult> = loop_result
200 .tool_results
201 .iter()
202 .map(|tr| {
203 let (action_type, target) = extract_action_info(&tr.action);
204 ActionResult {
205 action_type,
206 target,
207 success: tr.success,
208 output: Some(tr.output.clone()),
209 }
210 })
211 .collect();
212
213 if loop_result.interrupted {
214 errors.push("Agent loop was interrupted".to_string());
215 }
216
217 let duration_ms = start_time.elapsed().as_millis();
218 let actions_executed = !actions.is_empty();
219 Ok(NonInteractiveResult {
220 prompt,
221 response: final_response,
222 actions,
223 errors,
224 metadata: ExecutionMetadata {
225 model: model_name,
226 tokens_used: Some(total_tokens),
227 duration_ms,
228 actions_executed,
229 },
230 })
231 }
232
233 pub fn format_result(&self, result: &NonInteractiveResult, format: OutputFormat) -> String {
235 match format {
236 OutputFormat::Json => serde_json::to_string_pretty(result).unwrap_or_else(|e| {
237 format!("{{\"error\": \"Failed to serialize result: {}\"}}", e)
238 }),
239 OutputFormat::Text => {
240 let mut output = String::new();
241 output.push_str(&result.response);
242
243 if !result.actions.is_empty() {
244 output.push_str("\n\n--- Actions ---\n");
245 for action in &result.actions {
246 output.push_str(&format!(
247 "[{}] {} - {}\n",
248 if action.success { "OK" } else { "FAIL" },
249 action.action_type,
250 action.target
251 ));
252 if let Some(ref out) = action.output {
253 output.push_str(&format!(" {}\n", out));
254 }
255 }
256 }
257
258 if !result.errors.is_empty() {
259 output.push_str("\n--- Errors ---\n");
260 for error in &result.errors {
261 output.push_str(&format!("• {}\n", error));
262 }
263 }
264
265 output
266 },
267 OutputFormat::Markdown => {
268 let mut output = String::new();
269
270 output.push_str("## Response\n\n");
271 output.push_str(&result.response);
272 output.push_str("\n\n");
273
274 if !result.actions.is_empty() {
275 output.push_str("## Actions Executed\n\n");
276 for action in &result.actions {
277 let status = if action.success { "SUCCESS" } else { "FAILED" };
278 output.push_str(&format!(
279 "- {} **{}**: `{}`\n",
280 status, action.action_type, action.target
281 ));
282 if let Some(ref out) = action.output {
283 output.push_str(&format!(" ```\n {}\n ```\n", out));
284 }
285 }
286 output.push('\n');
287 }
288
289 if !result.errors.is_empty() {
290 output.push_str("## Errors\n\n");
291 for error in &result.errors {
292 output.push_str(&format!("- {}\n", error));
293 }
294 output.push('\n');
295 }
296
297 output.push_str("---\n");
298 output.push_str(&format!(
299 "*Model: {} | Tokens: {} | Duration: {}ms*\n",
300 result.metadata.model,
301 result.metadata.tokens_used.unwrap_or(0),
302 result.metadata.duration_ms
303 ));
304
305 output
306 },
307 }
308 }
309}
310
311fn extract_action_info(action: &AgentAction) -> (String, String) {
313 let (label, target) = action.display_info();
314 (label.to_lowercase().replace(' ', "_"), target)
315}
316
317fn build_no_execute_actions(
319 tool_calls: &[crate::models::ToolCall],
320 messages: &mut Vec<ChatMessage>,
321) -> Vec<ActionResult> {
322 let mut actions = Vec::new();
323 for tc in tool_calls {
324 let tool_call_id = tc.id.clone().unwrap_or_else(|| "call_noexec".to_string());
325 let tool_name = tc.function.name.clone();
326
327 let (action_type, target) = match tc.to_agent_action() {
328 Ok(action) => extract_action_info(&action),
329 Err(_) => (tool_name.clone(), String::new()),
330 };
331
332 let msg = "Not executed (--no-execute mode)".to_string();
333 messages.push(ChatMessage::tool(&tool_call_id, &tool_name, &msg));
334 actions.push(ActionResult {
335 action_type,
336 target,
337 success: false,
338 output: Some(msg),
339 });
340 }
341 actions
342}
343
344struct SilentObserver;
346
347impl AgentObserver for SilentObserver {
348 fn check_interrupt(&mut self) -> LoopControl {
349 LoopControl::Continue
350 }
351 fn on_status(&mut self, _: &str) {}
352 fn on_tool_result(&mut self, _: &str, _: &str, _: &AgentAction, _: &AgentActionResult) {}
353 fn on_error(&mut self, _: &str) {}
354 fn on_generation_start(&mut self) {}
355 fn on_generation_complete(&mut self, _: usize) {}
356}