atomcode_core/agent/
background.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use tokio::sync::mpsc;
7use tokio_util::sync::CancellationToken;
8
9use crate::config::Config;
10use crate::conversation::Conversation;
11use crate::ctx::CtxBuilder;
12use crate::i18n::{t, Msg};
13use crate::provider::LlmProvider;
14use crate::tool::{ToolContext, ToolRegistry};
15use crate::turn::event::TurnResult;
16use crate::turn::permission::{AutoPermissionDecider, AutoPermissionMode};
17use crate::turn::runner::TurnRunner;
18
19use super::AgentEvent;
20
21const MAX_BACKGROUND_TURNS: usize = 5;
23const BACKGROUND_TIMEOUT: Duration = Duration::from_secs(120);
25
26pub async fn run_background_task(
29 task: &str,
30 provider: Arc<dyn LlmProvider>,
31 tools: Arc<ToolRegistry>,
32 context: ToolContext,
33 config: Config,
34 ctx: Arc<dyn CtxBuilder>,
35 _progress_tx: mpsc::UnboundedSender<AgentEvent>,
36) -> AgentEvent {
37 match tokio::time::timeout(
38 BACKGROUND_TIMEOUT,
39 run_background_inner(task, provider, tools, context, config, ctx),
40 )
41 .await
42 {
43 Ok(result) => result,
44 Err(_) => AgentEvent::BackgroundComplete {
45 summary: t(Msg::BgTaskTimedOut { secs: BACKGROUND_TIMEOUT.as_secs() }).into_owned(),
46 files_edited: vec![],
47 turns: 0,
48 success: false,
49 },
50 }
51}
52
53async fn run_background_inner(
54 task: &str,
55 provider: Arc<dyn LlmProvider>,
56 tools: Arc<ToolRegistry>,
57 context: ToolContext,
58 config: Config,
59 ctx: Arc<dyn CtxBuilder>,
60) -> AgentEvent {
61 let bg_context = context.isolate().await;
62 let permission = Box::new(AutoPermissionDecider::new(AutoPermissionMode::AcceptEdits));
63
64 let bg_tools = crate::tool::ToolRegistry::new();
70 let essential = ["read_file", "write_file", "edit_file", "glob", "grep", "list_directory", "search_replace"];
71 for name in &essential {
72 if let Some(tool) = tools.get(name).await {
73 bg_tools.register_arc(name.to_string(), tool).await;
74 }
75 }
76
77 let bg_working_dir = bg_context.working_dir
78 .try_read()
79 .map(|g| g.clone())
80 .unwrap_or_else(|_| std::path::PathBuf::from("."));
81 let hooks = crate::hook::json_config::load_hooks_config(&bg_working_dir);
82
83 let mut runner = TurnRunner {
84 provider,
85 tools: Arc::new(bg_tools),
86 context: bg_context,
87 config: config.clone(),
88 ctx,
89 permission,
90 recently_edited_files: Vec::new(),
91 hook_executor: std::sync::Arc::new(
92 crate::hook::executor::HookExecutor::new(hooks)
93 ),
94 loop_guard: Default::default(),
95 };
96
97 let mut conversation = Conversation::new();
98 conversation.add_user_message(task);
99
100 let system_prompt = "You are a background agent. Complete the task autonomously.\n\
101 You can read and edit files. You CANNOT run bash commands.\n\
102 Be concise. Report what you changed when done.";
103
104 let cancel = CancellationToken::new();
105 let (event_tx, mut event_rx) = mpsc::unbounded_channel();
107 let mut turns = 0usize;
108 let mut last_text = String::new();
109
110 for _ in 0..MAX_BACKGROUND_TURNS {
111 let result = runner
112 .run(&mut conversation, system_prompt, &event_tx, cancel.clone())
113 .await;
114
115 turns += 1;
116
117 while event_rx.try_recv().is_ok() {}
119
120 match result {
121 TurnResult::Responded { text, .. } => {
122 last_text = text;
123 break;
124 }
125 TurnResult::UsedTools { text, .. } => {
126 if let Some(t) = text {
127 last_text = t;
128 }
129 }
130 TurnResult::Failed(e) => {
131 return AgentEvent::BackgroundComplete {
132 summary: t(Msg::BgTaskError { error: &e.to_string() }).into_owned(),
133 files_edited: std::mem::take(&mut runner.recently_edited_files),
134 turns,
135 success: false,
136 };
137 }
138 TurnResult::Cancelled => {
139 return AgentEvent::BackgroundComplete {
140 summary: t(Msg::BgTaskCancelled).into_owned(),
141 files_edited: std::mem::take(&mut runner.recently_edited_files),
142 turns,
143 success: false,
144 };
145 }
146 }
147 }
148
149 let summary = if last_text.len() > 500 {
150 let mut boundary = 497;
151 while boundary > 0 && !last_text.is_char_boundary(boundary) {
152 boundary -= 1;
153 }
154 format!("{}...", &last_text[..boundary])
155 } else if last_text.is_empty() {
156 t(Msg::BgTaskNoSummary).into_owned()
157 } else {
158 last_text
159 };
160
161 AgentEvent::BackgroundComplete {
162 summary,
163 files_edited: runner.recently_edited_files,
164 turns,
165 success: true,
166 }
167}