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::supervisor::Supervisor;
17use crate::types::{AgentId, AgentStatus};
18use oxios_ouroboros::{Directive, ExecEnv, ExecutionResult};
19
20pub struct AgentLifecycleManager {
22 supervisor: Arc<dyn Supervisor>,
23 access_manager: Arc<parking_lot::Mutex<AccessManager>>,
24 a2a: Arc<A2AProtocol>,
25 event_bus: EventBus,
26 max_execution_time_secs: std::sync::atomic::AtomicU64,
28 allowed_tools: Vec<String>,
30 network_access: bool,
32 workspace_path: String,
34}
35
36impl Clone for AgentLifecycleManager {
37 fn clone(&self) -> Self {
38 Self {
39 supervisor: self.supervisor.clone(),
40 access_manager: self.access_manager.clone(),
41 a2a: self.a2a.clone(),
42 event_bus: self.event_bus.clone(),
43 max_execution_time_secs: std::sync::atomic::AtomicU64::new(
44 self.max_execution_time_secs
45 .load(std::sync::atomic::Ordering::Relaxed),
46 ),
47 allowed_tools: self.allowed_tools.clone(),
48 network_access: self.network_access,
49 workspace_path: self.workspace_path.clone(),
50 }
51 }
52}
53
54impl AgentLifecycleManager {
55 #[allow(clippy::too_many_arguments)]
57 pub fn new(
58 supervisor: Arc<dyn Supervisor>,
59 access_manager: Arc<parking_lot::Mutex<AccessManager>>,
60 a2a: Arc<A2AProtocol>,
61 event_bus: EventBus,
62 max_execution_time_secs: u64,
63 allowed_tools: Vec<String>,
64 network_access: bool,
65 workspace_path: String,
66 ) -> Self {
67 Self {
68 supervisor,
69 access_manager,
70 a2a,
71 event_bus,
72 max_execution_time_secs: std::sync::atomic::AtomicU64::new(max_execution_time_secs),
73 allowed_tools,
74 network_access,
75 workspace_path,
76 }
77 }
78
79 pub fn set_max_execution_time(&self, secs: u64) {
81 self.max_execution_time_secs
82 .store(secs, std::sync::atomic::Ordering::Relaxed);
83 tracing::info!(
84 max_execution_time_secs = secs,
85 "Lifecycle config hot-reloaded"
86 );
87 }
88
89 pub async fn execute_directive(
92 &self,
93 directive: &Directive,
94 env: &ExecEnv,
95 ) -> Result<ExecutionResult> {
96 let agent_id = self.supervisor.fork_directive(directive, env).await?;
98 let agent_name = format!("agent-{agent_id}");
99 tracing::info!(agent_id = %agent_id, "Agent forked from directive");
100
101 let card = self.build_agent_card_directive(agent_id, &agent_name, directive);
103 if let Err(e) = self.a2a.registry().register_agent(card).await {
104 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to register A2A card");
105 }
106
107 if let Err(e) = self.a2a.deliver_pending_messages(agent_id).await {
109 tracing::debug!(agent_id = %agent_id, error = %e, "No pending A2A messages");
110 }
111
112 self.ensure_permissions(&agent_name);
114
115 get_metrics().agents_forked.inc();
116
117 let max_secs = self
119 .max_execution_time_secs
120 .load(std::sync::atomic::Ordering::Relaxed);
121 let result = if max_secs > 0 {
122 let exec_timeout = Duration::from_secs(max_secs);
123 match timeout(
124 exec_timeout,
125 self.supervisor.run_with_directive(agent_id, directive, env),
126 )
127 .await
128 {
129 Ok(Ok(r)) => r,
130 Ok(Err(e)) => {
131 tracing::warn!(agent_id = %agent_id, error = %e, "Agent execution failed, cleaning up");
132 self.cleanup_on_failure(agent_id).await;
133 return Err(e);
134 }
135 Err(_) => {
136 let secs = exec_timeout.as_secs();
137 tracing::warn!(
138 agent_id = %agent_id,
139 secs,
140 "Agent execution timed out after {}s",
141 secs
142 );
143 self.cleanup_on_failure(agent_id).await;
147 bail!("Agent execution timed out after {secs} seconds");
148 }
149 }
150 } else {
151 match self
152 .supervisor
153 .run_with_directive(agent_id, directive, env)
154 .await
155 {
156 Ok(r) => r,
157 Err(e) => {
158 tracing::warn!(agent_id = %agent_id, error = %e, "Agent execution failed, cleaning up");
159 self.cleanup_on_failure(agent_id).await;
160 return Err(e);
161 }
162 }
163 };
164
165 self.cleanup(agent_id, &result).await;
167
168 Ok(result)
169 }
170
171 pub async fn execute_with_feedback(
176 &self,
177 directive: &Directive,
178 env: &ExecEnv,
179 prev_result: &ExecutionResult,
180 gaps: &[String],
181 ) -> Result<ExecutionResult> {
182 let mut augmented = directive.clone();
184 let feedback = format!(
185 "## Previous attempt failed\n{}\n\n## Unmet criteria\n{}\n\n\
186 Review the above output and fix the unmet criteria.",
187 prev_result.output,
188 gaps.iter()
189 .enumerate()
190 .map(|(i, g)| format!("{}. {g}", i + 1))
191 .collect::<Vec<_>>()
192 .join("\n")
193 );
194 augmented.constraints.push(feedback);
195
196 self.execute_directive(&augmented, env).await
197 }
198
199 pub async fn terminate(&self, agent_id: AgentId) -> Result<()> {
201 if let Err(e) = self.supervisor.kill(agent_id).await {
202 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to kill agent");
203 }
204 if let Err(e) = self.a2a.registry().unregister_agent(agent_id).await {
205 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to unregister A2A card");
206 }
207 let _ = self.event_bus.publish(KernelEvent::AgentStopped {
208 id: agent_id,
209 success: false,
210 });
211 Ok(())
212 }
213
214 fn build_agent_card_directive(
219 &self,
220 agent_id: AgentId,
221 agent_name: &str,
222 directive: &Directive,
223 ) -> AgentCard {
224 let goal_lower = directive.goal.to_lowercase();
225
226 let mut card = AgentCard::new(
227 agent_id,
228 agent_name,
229 format!("Agent executing directive: {}", directive.goal),
230 )
231 .with_capability("execute-directive")
232 .with_status(AgentStatus::Starting);
233
234 if goal_lower.contains("review") || goal_lower.contains("code") {
236 card = card.with_capability("code-review");
237 }
238 if goal_lower.contains("test") {
239 card = card.with_capability("testing");
240 }
241 if goal_lower.contains("refactor") || goal_lower.contains("improve") {
242 card = card.with_capability("refactoring");
243 }
244 if goal_lower.contains("write")
245 || goal_lower.contains("create")
246 || goal_lower.contains("implement")
247 {
248 card = card.with_capability("code-generation");
249 }
250 if goal_lower.contains("debug") || goal_lower.contains("fix") {
251 card = card.with_capability("debugging");
252 }
253
254 card
255 }
256
257 fn ensure_permissions(&self, agent_name: &str) {
265 let mut access = self.access_manager.lock();
266 let perms = access.get_or_create_permissions(agent_name);
267
268 for tool in &self.allowed_tools {
270 if !perms.allowed_tools.contains(tool.as_str()) {
271 perms.allow_tool(tool);
272 }
273 }
274
275 let ws_pattern = format!("{}/**", self.workspace_path.trim_end_matches('/'));
277 if !perms.allowed_paths.iter().any(|p| p == &ws_pattern) {
278 perms.allow_path(&ws_pattern);
279 }
280 if !perms.allowed_paths.iter().any(|p| p == "/tmp/**") {
282 perms.allow_path("/tmp/**");
283 }
284
285 if self.network_access {
287 perms.enable_network();
288 }
289
290 let subject = Subject::Agent(
293 agent_name
294 .strip_prefix("agent-")
295 .and_then(|s| s.parse().ok())
296 .unwrap_or_default(),
297 );
298 access
299 .rbac_manager_mut()
300 .assign_role(subject, Role::Superuser);
301 }
302
303 async fn cleanup(&self, agent_id: AgentId, _result: &ExecutionResult) {
305 if let Err(e) = self.a2a.registry().unregister_agent(agent_id).await {
306 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to unregister A2A card");
307 }
308 }
309
310 async fn cleanup_on_failure(&self, agent_id: AgentId) {
312 if let Err(e) = self.a2a.registry().unregister_agent(agent_id).await {
313 tracing::warn!(agent_id = %agent_id, error = %e, "Failed to unregister A2A card");
314 }
315 }
316}