use std::collections::HashMap;
use std::time::Instant;
use tokio::sync::RwLock;
fn max_concurrent() -> usize {
std::env::var("ELI_MAX_CONCURRENT_AGENTS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5)
}
pub struct TrackedAgent {
pub shell_id: Option<String>,
pub agent_type: String,
pub prompt_summary: String,
pub cwd: String,
pub cli: String,
pub started_at: Instant,
pub result: Option<AgentResult>,
}
#[derive(Clone)]
pub struct AgentResult {
pub exit_code: Option<i32>,
pub output: String,
pub artifacts: String,
pub duration_ms: u64,
}
impl TrackedAgent {
pub fn is_running(&self) -> bool {
self.result.is_none()
}
}
pub struct AgentTracker {
agents: RwLock<HashMap<String, TrackedAgent>>,
}
impl Default for AgentTracker {
fn default() -> Self {
Self::new()
}
}
impl AgentTracker {
pub fn new() -> Self {
Self {
agents: RwLock::new(HashMap::new()),
}
}
pub async fn running_count(&self) -> usize {
self.agents
.read()
.await
.values()
.filter(|a| a.is_running())
.count()
}
pub async fn can_spawn(&self) -> bool {
self.running_count().await < max_concurrent()
}
pub async fn register(
&self,
agent_id: &str,
shell_id: Option<String>,
agent_type: &str,
prompt_summary: &str,
cwd: &str,
cli: &str,
) -> bool {
if !self.can_spawn().await {
return false;
}
let entry = TrackedAgent {
shell_id,
agent_type: agent_type.to_owned(),
prompt_summary: prompt_summary.to_owned(),
cwd: cwd.to_owned(),
cli: cli.to_owned(),
started_at: Instant::now(),
result: None,
};
self.agents.write().await.insert(agent_id.to_owned(), entry);
true
}
pub async fn complete(&self, agent_id: &str, result: AgentResult) {
if let Some(agent) = self.agents.write().await.get_mut(agent_id) {
agent.result = Some(result);
}
}
pub async fn kill(&self, agent_id: &str) -> Option<AgentResult> {
let shell_id = {
let agents = self.agents.read().await;
let agent = agents.get(agent_id)?;
if !agent.is_running() {
return agent.result.clone();
}
agent.shell_id.clone()
};
if let Some(ref sid) = shell_id {
let mgr = crate::builtin::shell_manager::shell_manager();
let (output, exit_code, _) = mgr
.terminate(sid)
.await
.unwrap_or_else(|e| (format!("kill error: {e}"), Some(-1), "error".to_owned()));
let result = AgentResult {
exit_code,
output,
artifacts: "(killed before completion)".to_owned(),
duration_ms: {
let agents = self.agents.read().await;
agents
.get(agent_id)
.map(|a| a.started_at.elapsed().as_millis() as u64)
.unwrap_or(0)
},
};
self.complete(agent_id, result.clone()).await;
return Some(result);
}
None
}
pub async fn list(&self) -> Vec<(String, AgentSummary)> {
self.agents
.read()
.await
.iter()
.map(|(id, a)| {
(
id.clone(),
AgentSummary {
agent_type: a.agent_type.clone(),
prompt_summary: a.prompt_summary.clone(),
cli: a.cli.clone(),
running: a.is_running(),
elapsed_ms: a.started_at.elapsed().as_millis() as u64,
exit_code: a.result.as_ref().and_then(|r| r.exit_code),
},
)
})
.collect()
}
pub async fn get_result(&self, agent_id: &str) -> Option<AgentResult> {
self.agents
.read()
.await
.get(agent_id)
.and_then(|a| a.result.clone())
}
pub async fn evict_completed(&self, max_age: std::time::Duration) {
let mut agents = self.agents.write().await;
agents.retain(|_, a| a.is_running() || a.started_at.elapsed() < max_age);
}
}
pub struct AgentSummary {
pub agent_type: String,
pub prompt_summary: String,
pub cli: String,
pub running: bool,
pub elapsed_ms: u64,
pub exit_code: Option<i32>,
}
static AGENT_TRACKER: std::sync::LazyLock<AgentTracker> =
std::sync::LazyLock::new(AgentTracker::new);
pub fn agent_tracker() -> &'static AgentTracker {
&AGENT_TRACKER
}