everruns_core/capabilities/
background_execution.rs1use super::{Capability, CapabilityLocalization, CapabilityStatus};
14use crate::tools::{SpawnBackgroundTool, Tool};
15
16pub const BACKGROUND_EXECUTION_CAPABILITY_ID: &str = "background_execution";
19
20pub struct BackgroundExecutionCapability;
27
28impl Capability for BackgroundExecutionCapability {
29 fn id(&self) -> &str {
30 BACKGROUND_EXECUTION_CAPABILITY_ID
31 }
32
33 fn name(&self) -> &str {
34 "Background Execution"
35 }
36
37 fn description(&self) -> &str {
38 "Run any background-capable built-in tool asynchronously via \
39 `spawn_background`. Auto-activated whenever the agent has a tool \
40 that declares `supports_background=true`."
41 }
42
43 fn localizations(&self) -> Vec<CapabilityLocalization> {
44 vec![CapabilityLocalization::text(
45 "uk",
46 "Фонове виконання",
47 "Запускайте будь-який вбудований інструмент із підтримкою фонового режиму асинхронно через `spawn_background`. Активується автоматично, щойно агент має інструмент, який оголошує `supports_background=true`.",
48 )]
49 }
50
51 fn status(&self) -> CapabilityStatus {
52 CapabilityStatus::Available
53 }
54
55 fn icon(&self) -> Option<&str> {
56 Some("zap")
57 }
58
59 fn category(&self) -> Option<&str> {
60 Some("Execution")
61 }
62
63 fn tools(&self) -> Vec<Box<dyn Tool>> {
64 vec![Box::new(SpawnBackgroundTool)]
65 }
66}
67
68pub struct BackgroundToolTaskExecutor;
70
71#[async_trait::async_trait]
72impl crate::session_task::TaskExecutor for BackgroundToolTaskExecutor {
73 fn kind(&self) -> &str {
74 crate::session_task::TASK_KIND_BACKGROUND_TOOL
75 }
76
77 fn can_reattach_task(&self, task: &crate::session_task::SessionTask) -> bool {
81 task.spec
82 .get("reattachable")
83 .and_then(|v| v.as_bool())
84 .unwrap_or(false)
85 }
86
87 async fn start(
89 &self,
90 task: &crate::session_task::SessionTask,
91 context: &crate::traits::ToolContext,
92 ) -> crate::error::Result<()> {
93 crate::tools::reattach_background_run(task, context).await
94 }
95
96 async fn cancel(
101 &self,
102 _task: &crate::session_task::SessionTask,
103 _context: &crate::traits::ToolContext,
104 ) -> crate::error::Result<()> {
105 Ok(())
106 }
107}
108
109inventory::submit! {
110 crate::session_task::TaskExecutorPlugin {
111 executor: || std::sync::Arc::new(BackgroundToolTaskExecutor),
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
122 fn can_reattach_task_true_when_spec_reattachable_true() {
123 use crate::session_task::{SessionTask, SessionTaskState, TaskExecutor, TaskWakePolicy};
124 use chrono::Utc;
125
126 let exec = BackgroundToolTaskExecutor;
127 let task = SessionTask {
128 id: "t1".to_string(),
129 session_id: crate::SessionId::new(),
130 root_session_id: None,
131 kind: crate::session_task::TASK_KIND_BACKGROUND_TOOL.to_string(),
132 display_name: "test".to_string(),
133 spec: serde_json::json!({ "tool": "get_current_time", "reattachable": true }),
134 state: SessionTaskState::Running,
135 state_detail: None,
136 progress: None,
137 input_request: None,
138 cancel_requested_at: None,
139 summary: None,
140 result_path: None,
141 artifacts: vec![],
142 error: None,
143 attempt: 1,
144 worker_id: None,
145 heartbeat_at: None,
146 links: crate::session_task::TaskLinks::default(),
147 wake_policy: TaskWakePolicy::Silent,
148 created_at: Utc::now(),
149 started_at: None,
150 finished_at: None,
151 updated_at: Utc::now(),
152 };
153 assert!(exec.can_reattach_task(&task));
154 }
155
156 #[test]
157 fn can_reattach_task_false_when_spec_reattachable_false() {
158 use crate::session_task::{SessionTask, SessionTaskState, TaskExecutor, TaskWakePolicy};
159 use chrono::Utc;
160
161 let exec = BackgroundToolTaskExecutor;
162 let task = SessionTask {
163 id: "t2".to_string(),
164 session_id: crate::SessionId::new(),
165 root_session_id: None,
166 kind: crate::session_task::TASK_KIND_BACKGROUND_TOOL.to_string(),
167 display_name: "test".to_string(),
168 spec: serde_json::json!({ "tool": "some_side_effecting_tool", "reattachable": false }),
169 state: SessionTaskState::Running,
170 state_detail: None,
171 progress: None,
172 input_request: None,
173 cancel_requested_at: None,
174 summary: None,
175 result_path: None,
176 artifacts: vec![],
177 error: None,
178 attempt: 1,
179 worker_id: None,
180 heartbeat_at: None,
181 links: crate::session_task::TaskLinks::default(),
182 wake_policy: TaskWakePolicy::Silent,
183 created_at: Utc::now(),
184 started_at: None,
185 finished_at: None,
186 updated_at: Utc::now(),
187 };
188 assert!(!exec.can_reattach_task(&task));
189 }
190
191 #[test]
192 fn can_reattach_task_false_when_spec_has_no_reattachable_field() {
193 use crate::session_task::{SessionTask, SessionTaskState, TaskExecutor, TaskWakePolicy};
194 use chrono::Utc;
195
196 let exec = BackgroundToolTaskExecutor;
197 let task = SessionTask {
198 id: "t3".to_string(),
199 session_id: crate::SessionId::new(),
200 root_session_id: None,
201 kind: crate::session_task::TASK_KIND_BACKGROUND_TOOL.to_string(),
202 display_name: "test".to_string(),
203 spec: serde_json::json!({ "tool": "old_task_without_reattachable_flag" }),
204 state: SessionTaskState::Running,
205 state_detail: None,
206 progress: None,
207 input_request: None,
208 cancel_requested_at: None,
209 summary: None,
210 result_path: None,
211 artifacts: vec![],
212 error: None,
213 attempt: 1,
214 worker_id: None,
215 heartbeat_at: None,
216 links: crate::session_task::TaskLinks::default(),
217 wake_policy: TaskWakePolicy::Silent,
218 created_at: Utc::now(),
219 started_at: None,
220 finished_at: None,
221 updated_at: Utc::now(),
222 };
223 assert!(!exec.can_reattach_task(&task));
225 }
226}