Skip to main content

atomcode_core/agent/
background.rs

1//! Background agent — runs a task in an isolated context.
2
3use 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
21/// Maximum turns for a background task.
22const MAX_BACKGROUND_TURNS: usize = 5;
23/// Overall timeout for the entire background task.
24const BACKGROUND_TIMEOUT: Duration = Duration::from_secs(120);
25
26/// Run a background task with an independent conversation and turn runner.
27/// Sends progress events through `progress_tx` so the TUI can show updates.
28pub 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    // Build a minimal tool registry for background tasks — only file I/O tools.
65    // Excludes MCP tools and bash to keep the prompt small and responses fast.
66    // The full Config is passed (not stripped) because the provider needs
67    // api_key, base_url, model, and context_window from it. Only the tool
68    // set is restricted; config fields are read-only and pose no risk.
69    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    // Internal event channel — drain only, tool events not forwarded to TUI.
106    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        // Drain internal events to prevent channel backpressure
118        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}