use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::config::Config;
use crate::conversation::Conversation;
use crate::ctx::CtxBuilder;
use crate::i18n::{t, Msg};
use crate::provider::LlmProvider;
use crate::tool::{ToolContext, ToolRegistry};
use crate::turn::event::TurnResult;
use crate::turn::permission::{AutoPermissionDecider, AutoPermissionMode};
use crate::turn::runner::TurnRunner;
use super::AgentEvent;
const MAX_BACKGROUND_TURNS: usize = 5;
const BACKGROUND_TIMEOUT: Duration = Duration::from_secs(120);
pub async fn run_background_task(
task: &str,
provider: Arc<dyn LlmProvider>,
tools: Arc<ToolRegistry>,
context: ToolContext,
config: Config,
ctx: Arc<dyn CtxBuilder>,
_progress_tx: mpsc::UnboundedSender<AgentEvent>,
) -> AgentEvent {
match tokio::time::timeout(
BACKGROUND_TIMEOUT,
run_background_inner(task, provider, tools, context, config, ctx),
)
.await
{
Ok(result) => result,
Err(_) => AgentEvent::BackgroundComplete {
summary: t(Msg::BgTaskTimedOut { secs: BACKGROUND_TIMEOUT.as_secs() }).into_owned(),
files_edited: vec![],
turns: 0,
success: false,
},
}
}
async fn run_background_inner(
task: &str,
provider: Arc<dyn LlmProvider>,
tools: Arc<ToolRegistry>,
context: ToolContext,
config: Config,
ctx: Arc<dyn CtxBuilder>,
) -> AgentEvent {
let bg_context = context.isolate().await;
let permission = Box::new(AutoPermissionDecider::new(AutoPermissionMode::AcceptEdits));
let bg_tools = crate::tool::ToolRegistry::new();
let essential = ["read_file", "write_file", "edit_file", "glob", "grep", "list_directory", "search_replace"];
for name in &essential {
if let Some(tool) = tools.get(name).await {
bg_tools.register_arc(name.to_string(), tool).await;
}
}
let bg_working_dir = bg_context.working_dir
.try_read()
.map(|g| g.clone())
.unwrap_or_else(|_| std::path::PathBuf::from("."));
let mut hook_engine = crate::hook::HookEngine::new();
hook_engine.load_all(&bg_working_dir);
let mut runner = TurnRunner {
provider,
tools: Arc::new(bg_tools),
context: bg_context,
config: config.clone(),
ctx,
permission,
recently_edited_files: Vec::new(),
hook_engine: std::sync::Arc::new(hook_engine),
loop_guard: Default::default(),
current_turn_number: 0,
};
let mut conversation = Conversation::new();
conversation.add_user_message(task);
let system_prompt = "You are a background agent. Complete the task autonomously.\n\
You can read and edit files. You CANNOT run bash commands.\n\
Be concise. Report what you changed when done.";
let cancel = CancellationToken::new();
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
let mut turns = 0usize;
let mut last_text = String::new();
for _ in 0..MAX_BACKGROUND_TURNS {
let result = runner
.run(&mut conversation, system_prompt, &event_tx, cancel.clone())
.await;
turns += 1;
while event_rx.try_recv().is_ok() {}
match result {
TurnResult::Responded { text, .. } => {
last_text = text;
break;
}
TurnResult::UsedTools { text, .. } => {
if let Some(t) = text {
last_text = t;
}
}
TurnResult::Failed(e) => {
return AgentEvent::BackgroundComplete {
summary: t(Msg::BgTaskError { error: &e.to_string() }).into_owned(),
files_edited: std::mem::take(&mut runner.recently_edited_files),
turns,
success: false,
};
}
TurnResult::Cancelled => {
return AgentEvent::BackgroundComplete {
summary: t(Msg::BgTaskCancelled).into_owned(),
files_edited: std::mem::take(&mut runner.recently_edited_files),
turns,
success: false,
};
}
}
}
let summary = if last_text.len() > 500 {
let mut boundary = 497;
while boundary > 0 && !last_text.is_char_boundary(boundary) {
boundary -= 1;
}
format!("{}...", &last_text[..boundary])
} else if last_text.is_empty() {
t(Msg::BgTaskNoSummary).into_owned()
} else {
last_text
};
AgentEvent::BackgroundComplete {
summary,
files_edited: runner.recently_edited_files,
turns,
success: true,
}
}