1use anyhow::{Result, bail};
8use std::sync::Arc;
9
10use tokio::time::{Duration, timeout};
11
12use crate::a2a::{A2AProtocol, AgentCard};
13use crate::access_manager::{AccessManager, Role, Subject};
14use crate::event_bus::{EventBus, KernelEvent};
15use crate::metrics::get_metrics;
16use crate::scheduler::{AgentScheduler, Priority, ScheduledTask};
17use crate::supervisor::Supervisor;
18use crate::types::{AgentId, AgentStatus};
19use oxios_ouroboros::{Directive, ExecEnv, ExecutionResult};
20
21pub struct AgentLifecycleManager {
23 supervisor: Arc<dyn Supervisor>,
24 scheduler: Arc<AgentScheduler>,
25 access_manager: Arc<parking_lot::Mutex<AccessManager>>,
26 a2a: Arc<A2AProtocol>,
27 event_bus: EventBus,
28 max_execution_time_secs: std::sync::atomic::AtomicU64,
30 allowed_tools: Vec<String>,
32 network_access: bool,
34 workspace_path: String,
36}
37
38impl Clone for AgentLifecycleManager {
39 fn clone(&self) -> Self {
40 Self {
41 supervisor: self.supervisor.clone(),
42 scheduler: self.scheduler.clone(),
43 access_manager: self.access_manager.clone(),
44 a2a: self.a2a.clone(),
45 event_bus: self.event_bus.clone(),
46 max_execution_time_secs: std::sync::atomic::AtomicU64::new(
47 self.max_execution_time_secs
48 .load(std::sync::atomic::Ordering::Relaxed),
49 ),
50 allowed_tools: self.allowed_tools.clone(),
51 network_access: self.network_access,
52 workspace_path: self.workspace_path.clone(),
53 }
54 }
55}
56
57impl AgentLifecycleManager {
58 #[allow(clippy::too_many_arguments)]
60 pub fn new(
61 supervisor: Arc<dyn Supervisor>,
62 scheduler: Arc<AgentScheduler>,
63 access_manager: Arc<parking_lot::Mutex<AccessManager>>,
64 a2a: Arc<A2AProtocol>,
65 event_bus: EventBus,
66 max_execution_time_secs: u64,
67 allowed_tools: Vec<String>,
68 network_access: bool,
69 workspace_path: String,
70 ) -> Self {
71 Self {
72 supervisor,
73 scheduler,
74 access_manager,
75 a2a,
76 event_bus,
77 max_execution_time_secs: std::sync::atomic::AtomicU64::new(max_execution_time_secs),
78 allowed_tools,
79 network_access,
80 workspace_path,
81 }
82 }
83
84 pub fn set_max_execution_time(&self, secs: u64) {
86 self.max_execution_time_secs
87 .store(secs, std::sync::atomic::Ordering::Relaxed);
88 tracing::info!(
89 max_execution_time_secs = secs,
90 "Lifecycle config hot-reloaded"
91 );
92 }
93
94 pub async fn execute_directive(
97 &self,
98 directive: &Directive,
99 env: &ExecEnv,
100 priority: Priority,
101 ) -> Result<ExecutionResult> {
102 let agent_id = self.supervisor.fork_directive(directive, env).await?;
104 let agent_name = format!("agent-{agent_id}");
105 tracing::info!(agent_id = %agent_id, "Agent forked from directive");
106
107 let card = self.build_agent_card_directive(agent_id, &agent_name, directive);
109 if let Err(e) = self.a2a.registry().register_agent(card).await {
110 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to register A2A card");
111 }
112
113 if let Err(e) = self.a2a.deliver_pending_messages(agent_id).await {
115 tracing::debug!(agent_id = %agent_id, error = %e, "No pending A2A messages");
116 }
117
118 self.ensure_permissions(&agent_name);
120
121 get_metrics().agents_forked.inc();
123 let task = ScheduledTask::for_agent(
124 agent_id,
125 format!("Execute directive '{}'", directive.goal),
126 priority,
127 );
128 let task_id = self.scheduler.submit(task)?;
129 self.scheduler.start_task(task_id)?;
130
131 let max_secs = self
133 .max_execution_time_secs
134 .load(std::sync::atomic::Ordering::Relaxed);
135 let result = if max_secs > 0 {
136 let exec_timeout = Duration::from_secs(max_secs);
137 match timeout(
138 exec_timeout,
139 self.supervisor.run_with_directive(agent_id, directive, env),
140 )
141 .await
142 {
143 Ok(Ok(r)) => r,
144 Ok(Err(e)) => {
145 tracing::warn!(agent_id = %agent_id, error = %e, "Agent execution failed, cleaning up");
146 self.cleanup_on_failure(agent_id, task_id).await;
147 return Err(e);
148 }
149 Err(_) => {
150 let secs = exec_timeout.as_secs();
151 tracing::warn!(
152 agent_id = %agent_id,
153 secs,
154 "Agent execution timed out after {}s",
155 secs
156 );
157 let _ = self.supervisor.kill(agent_id).await;
161 self.cleanup_on_failure(agent_id, task_id).await;
162 bail!("Agent execution timed out after {secs} seconds");
163 }
164 }
165 } else {
166 match self
167 .supervisor
168 .run_with_directive(agent_id, directive, env)
169 .await
170 {
171 Ok(r) => r,
172 Err(e) => {
173 tracing::warn!(agent_id = %agent_id, error = %e, "Agent execution failed, cleaning up");
174 self.cleanup_on_failure(agent_id, task_id).await;
175 return Err(e);
176 }
177 }
178 };
179
180 self.cleanup(agent_id, task_id, &result).await;
182
183 Ok(result)
184 }
185
186 pub async fn execute_with_feedback(
191 &self,
192 directive: &Directive,
193 env: &ExecEnv,
194 prev_result: &ExecutionResult,
195 gaps: &[String],
196 priority: Priority,
197 ) -> Result<ExecutionResult> {
198 let mut augmented = directive.clone();
200 let feedback = format!(
201 "## Previous attempt failed\n{}\n\n## Unmet criteria\n{}\n\n\
202 Review the above output and fix the unmet criteria.",
203 prev_result.output,
204 gaps.iter()
205 .enumerate()
206 .map(|(i, g)| format!("{}. {g}", i + 1))
207 .collect::<Vec<_>>()
208 .join("\n")
209 );
210 augmented.constraints.push(feedback);
211
212 self.execute_directive(&augmented, env, priority).await
213 }
214
215 pub async fn terminate(&self, agent_id: AgentId) -> Result<()> {
217 if let Err(e) = self.supervisor.kill(agent_id).await {
218 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to kill agent");
219 }
220 if let Err(e) = self.a2a.registry().unregister_agent(agent_id).await {
221 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to unregister A2A card");
222 }
223 let _ = self.event_bus.publish(KernelEvent::AgentStopped {
224 id: agent_id,
225 success: false,
226 });
227 Ok(())
228 }
229
230 fn build_agent_card_directive(
235 &self,
236 agent_id: AgentId,
237 agent_name: &str,
238 directive: &Directive,
239 ) -> AgentCard {
240 let goal_lower = directive.goal.to_lowercase();
241
242 let mut card = AgentCard::new(
243 agent_id,
244 agent_name,
245 format!("Agent executing directive: {}", directive.goal),
246 )
247 .with_capability("execute-directive")
248 .with_status(AgentStatus::Starting);
249
250 if goal_lower.contains("review") || goal_lower.contains("code") {
252 card = card.with_capability("code-review");
253 }
254 if goal_lower.contains("test") {
255 card = card.with_capability("testing");
256 }
257 if goal_lower.contains("refactor") || goal_lower.contains("improve") {
258 card = card.with_capability("refactoring");
259 }
260 if goal_lower.contains("write")
261 || goal_lower.contains("create")
262 || goal_lower.contains("implement")
263 {
264 card = card.with_capability("code-generation");
265 }
266 if goal_lower.contains("debug") || goal_lower.contains("fix") {
267 card = card.with_capability("debugging");
268 }
269
270 card
271 }
272
273 fn ensure_permissions(&self, agent_name: &str) {
281 let mut access = self.access_manager.lock();
282 let perms = access.get_or_create_permissions(agent_name);
283
284 for tool in &self.allowed_tools {
286 if !perms.allowed_tools.contains(tool.as_str()) {
287 perms.allow_tool(tool);
288 }
289 }
290
291 let ws_pattern = format!("{}/**", self.workspace_path.trim_end_matches('/'));
293 if !perms.allowed_paths.iter().any(|p| p == &ws_pattern) {
294 perms.allow_path(&ws_pattern);
295 }
296 if !perms.allowed_paths.iter().any(|p| p == "/tmp/**") {
298 perms.allow_path("/tmp/**");
299 }
300
301 if self.network_access {
303 perms.enable_network();
304 }
305
306 let subject = Subject::Agent(
309 agent_name
310 .strip_prefix("agent-")
311 .and_then(|s| s.parse().ok())
312 .unwrap_or_default(),
313 );
314 access
315 .rbac_manager_mut()
316 .assign_role(subject, Role::Superuser);
317 }
318
319 async fn cleanup(&self, agent_id: AgentId, task_id: uuid::Uuid, result: &ExecutionResult) {
321 if let Err(e) = self.a2a.registry().unregister_agent(agent_id).await {
322 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to unregister A2A card");
323 }
324 if result.success {
325 let _ = self.scheduler.complete_task(task_id);
326 } else {
327 let _ = self.scheduler.fail_task(task_id, &result.output);
328 }
329 }
330
331 pub fn reap_zombies(&self) -> Vec<uuid::Uuid> {
333 let reaped = self.scheduler.reap_zombies();
334 if !reaped.is_empty() {
335 tracing::warn!(count = reaped.len(), "Zombie tasks reaped");
336 let mut access = self.access_manager.lock();
337 for task_id in &reaped {
338 access.log_access("scheduler", "zombie_reap", &task_id.to_string(), true, None);
339 }
340 }
341 reaped
342 }
343
344 async fn cleanup_on_failure(&self, agent_id: AgentId, task_id: uuid::Uuid) {
346 if let Err(e) = self.a2a.registry().unregister_agent(agent_id).await {
347 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to unregister A2A card");
348 }
349 let _ = self.scheduler.fail_task(task_id, "execution failed");
350 }
351}