use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::utils::MutexExt;
use crate::{
agents::{execute_action, ActionResult as AgentActionResult, AgentAction},
app::Config,
cli::OutputFormat,
models::{ChatMessage, MessageRole, Model, ModelConfig, ModelFactory},
};
#[derive(Debug, Serialize, Deserialize)]
pub struct NonInteractiveResult {
pub prompt: String,
pub response: String,
pub actions: Vec<ActionResult>,
pub errors: Vec<String>,
pub metadata: ExecutionMetadata,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ActionResult {
pub action_type: String,
pub target: String,
pub success: bool,
pub output: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExecutionMetadata {
pub model: String,
pub tokens_used: Option<usize>,
pub duration_ms: u128,
pub actions_executed: bool,
}
pub struct NonInteractiveRunner {
model: Arc<RwLock<Box<dyn Model>>>,
no_execute: bool,
max_tokens: Option<usize>,
}
impl NonInteractiveRunner {
pub async fn new(
model_id: String,
_project_path: PathBuf, config: Config,
no_execute: bool,
max_tokens: Option<usize>,
backend: Option<&str>,
) -> Result<Self> {
let model = ModelFactory::create_with_backend(&model_id, Some(&config), backend).await?;
Ok(Self {
model: Arc::new(RwLock::new(model)),
no_execute,
max_tokens,
})
}
pub async fn execute(&self, prompt: String) -> Result<NonInteractiveResult> {
let start_time = std::time::Instant::now();
let mut errors = Vec::new();
let mut actions = Vec::new();
let system_content = "You are an AI coding assistant. Use tools to explore and modify the codebase as needed."
.to_string();
let system_message = ChatMessage {
role: MessageRole::System,
content: system_content,
timestamp: chrono::Local::now(),
actions: Vec::new(),
thinking: None,
images: None,
tool_calls: None,
tool_call_id: None,
tool_name: None,
};
let user_message = ChatMessage {
role: MessageRole::User,
content: prompt.clone(),
timestamp: chrono::Local::now(),
actions: Vec::new(),
thinking: None,
images: None,
tool_calls: None,
tool_call_id: None,
tool_name: None,
};
let messages = vec![system_message, user_message];
let model_guard = self.model.read().await;
let model_name = model_guard.name().to_string();
drop(model_guard);
let model_config = ModelConfig {
model: model_name,
temperature: 0.7,
max_tokens: self.max_tokens.unwrap_or(4096),
top_p: Some(1.0),
frequency_penalty: None,
presence_penalty: None,
system_prompt: None,
thinking_enabled: false, backend_options: std::collections::HashMap::new(),
};
let full_response;
let tokens_used;
let response_text = Arc::new(std::sync::Mutex::new(String::new()));
let response_clone = Arc::clone(&response_text);
let callback = Arc::new(move |chunk: &str| {
let mut resp = response_clone.lock_mut_safe();
resp.push_str(chunk);
});
let model_name;
let result = {
let model = self.model.write().await;
model_name = model.name().to_string();
model
.chat(&messages, &model_config, Some(callback))
.await
};
let parsed_actions: Vec<AgentAction> = match result {
Ok(response) => {
let callback_content = response_text.lock_mut_safe().clone();
if !callback_content.is_empty() {
full_response = callback_content;
} else {
full_response = response.content;
}
tokens_used = response.usage.map(|u| u.total_tokens).unwrap_or(0);
if let Some(tool_calls) = response.tool_calls {
tool_calls
.iter()
.filter_map(|tc| tc.to_agent_action().ok())
.collect()
} else {
vec![]
}
},
Err(e) => {
errors.push(format!("Model error: {}", e));
full_response = response_text.lock_mut_safe().clone();
tokens_used = 0;
vec![]
},
};
if !self.no_execute && !parsed_actions.is_empty() {
for action in parsed_actions {
let (action_type, target) = match &action {
AgentAction::WriteFile { path, .. } => ("file_write", path.clone()),
AgentAction::EditFile { path, .. } => ("edit_file", path.clone()),
AgentAction::ExecuteCommand { command, .. } => ("command", command.clone()),
AgentAction::ReadFile { paths } => {
if paths.len() == 1 {
("file_read", paths[0].clone())
} else {
("file_read", format!("{} files", paths.len()))
}
}
AgentAction::CreateDirectory { path } => ("create_dir", path.clone()),
AgentAction::DeleteFile { path } => ("delete_file", path.clone()),
AgentAction::GitDiff { paths } => {
if paths.len() == 1 {
("git_diff", paths[0].as_deref().unwrap_or("*").to_string())
} else {
("git_diff", format!("{} paths", paths.len()))
}
}
AgentAction::GitStatus => ("git_status", "git status".to_string()),
AgentAction::GitCommit { message, .. } => ("git_commit", message.clone()),
AgentAction::WebSearch { queries } => {
if queries.len() == 1 {
("web_search", queries[0].0.clone())
} else {
("web_search", format!("{} queries", queries.len()))
}
}
AgentAction::WebFetch { url } => ("web_fetch", url.clone()),
};
let result = execute_action(&action).await;
let action_result = match result {
AgentActionResult::Success { output } => ActionResult {
action_type: action_type.to_string(),
target,
success: true,
output: Some(output),
},
AgentActionResult::Error { error } => ActionResult {
action_type: action_type.to_string(),
target,
success: false,
output: Some(error),
},
};
actions.push(action_result);
}
} else if !parsed_actions.is_empty() {
for action in parsed_actions {
let (action_type, target) = match &action {
AgentAction::WriteFile { path, .. } => ("file_write", path.clone()),
AgentAction::EditFile { path, .. } => ("edit_file", path.clone()),
AgentAction::ExecuteCommand { command, .. } => ("command", command.clone()),
AgentAction::ReadFile { paths } => {
if paths.len() == 1 {
("file_read", paths[0].clone())
} else {
("file_read", format!("{} files", paths.len()))
}
}
AgentAction::CreateDirectory { path } => ("create_dir", path.clone()),
AgentAction::DeleteFile { path } => ("delete_file", path.clone()),
AgentAction::GitDiff { paths } => {
if paths.len() == 1 {
("git_diff", paths[0].as_deref().unwrap_or("*").to_string())
} else {
("git_diff", format!("{} paths", paths.len()))
}
}
AgentAction::GitStatus => ("git_status", "git status".to_string()),
AgentAction::GitCommit { message, .. } => ("git_commit", message.clone()),
AgentAction::WebSearch { queries } => {
if queries.len() == 1 {
("web_search", queries[0].0.clone())
} else {
("web_search", format!("{} queries", queries.len()))
}
}
AgentAction::WebFetch { url } => ("web_fetch", url.clone()),
};
actions.push(ActionResult {
action_type: action_type.to_string(),
target,
success: false,
output: Some("Not executed (--no-execute mode)".to_string()),
});
}
}
let duration_ms = start_time.elapsed().as_millis();
let actions_executed = !self.no_execute && !actions.is_empty();
Ok(NonInteractiveResult {
prompt,
response: full_response,
actions,
errors,
metadata: ExecutionMetadata {
model: model_name,
tokens_used: Some(tokens_used),
duration_ms,
actions_executed,
},
})
}
pub fn format_result(&self, result: &NonInteractiveResult, format: OutputFormat) -> String {
match format {
OutputFormat::Json => serde_json::to_string_pretty(result).unwrap_or_else(|e| {
format!("{{\"error\": \"Failed to serialize result: {}\"}}", e)
}),
OutputFormat::Text => {
let mut output = String::new();
output.push_str(&result.response);
if !result.actions.is_empty() {
output.push_str("\n\n--- Actions ---\n");
for action in &result.actions {
output.push_str(&format!(
"[{}] {} - {}\n",
if action.success { "OK" } else { "FAIL" },
action.action_type,
action.target
));
if let Some(ref out) = action.output {
output.push_str(&format!(" {}\n", out));
}
}
}
if !result.errors.is_empty() {
output.push_str("\n--- Errors ---\n");
for error in &result.errors {
output.push_str(&format!("• {}\n", error));
}
}
output
},
OutputFormat::Markdown => {
let mut output = String::new();
output.push_str("## Response\n\n");
output.push_str(&result.response);
output.push_str("\n\n");
if !result.actions.is_empty() {
output.push_str("## Actions Executed\n\n");
for action in &result.actions {
let status = if action.success { "SUCCESS" } else { "FAILED" };
output.push_str(&format!(
"- {} **{}**: `{}`\n",
status, action.action_type, action.target
));
if let Some(ref out) = action.output {
output.push_str(&format!(" ```\n {}\n ```\n", out));
}
}
output.push_str("\n");
}
if !result.errors.is_empty() {
output.push_str("## Errors\n\n");
for error in &result.errors {
output.push_str(&format!("- {}\n", error));
}
output.push_str("\n");
}
output.push_str("---\n");
output.push_str(&format!(
"*Model: {} | Tokens: {} | Duration: {}ms*\n",
result.metadata.model,
result.metadata.tokens_used.unwrap_or(0),
result.metadata.duration_ms
));
output
},
}
}
}