use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
use tracing::warn;
use super::{AgentRuntime, AgentSpec, RuntimeError, RuntimeHealth, RuntimeStatus};
use crate::registry::AgentId;
struct Supervised {
spec: AgentSpec,
state: ProcState,
}
enum ProcState {
Provisioned,
Running(Box<Child>),
Stopped,
}
#[derive(Clone)]
pub struct LocalProcessRuntime {
exe: PathBuf,
agents: Arc<Mutex<HashMap<AgentId, Supervised>>>,
}
impl LocalProcessRuntime {
pub fn new() -> Self {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("a2a"));
Self::with_exe(exe)
}
pub fn with_exe(exe: impl Into<PathBuf>) -> Self {
Self {
exe: exe.into(),
agents: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Default for LocalProcessRuntime {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl AgentRuntime for LocalProcessRuntime {
async fn provision(&self, spec: AgentSpec) -> Result<AgentId, RuntimeError> {
let id = spec.id.clone();
self.agents.lock().await.insert(
id.clone(),
Supervised {
spec,
state: ProcState::Provisioned,
},
);
Ok(id)
}
async fn start(&self, id: &AgentId) -> Result<(), RuntimeError> {
let mut guard = self.agents.lock().await;
let entry = guard
.get_mut(id)
.ok_or_else(|| RuntimeError::NotFound(id.clone()))?;
if matches!(entry.state, ProcState::Running(_)) {
return Err(RuntimeError::AlreadyRunning(id.clone()));
}
let child = Command::new(&self.exe)
.arg("run")
.arg("--config")
.arg(&entry.spec.config_path)
.kill_on_drop(true)
.spawn()
.map_err(|source| RuntimeError::Spawn {
id: id.clone(),
source,
})?;
entry.state = ProcState::Running(Box::new(child));
Ok(())
}
async fn stop(&self, id: &AgentId) -> Result<(), RuntimeError> {
let mut guard = self.agents.lock().await;
let entry = guard
.get_mut(id)
.ok_or_else(|| RuntimeError::NotFound(id.clone()))?;
if let ProcState::Running(child) = &mut entry.state {
if let Err(e) = child.kill().await {
warn!("error killing agent '{}': {}", id, e);
}
}
entry.state = ProcState::Stopped;
Ok(())
}
async fn health(&self, id: &AgentId) -> Result<RuntimeHealth, RuntimeError> {
let endpoint = {
let mut guard = self.agents.lock().await;
let entry = guard
.get_mut(id)
.ok_or_else(|| RuntimeError::NotFound(id.clone()))?;
match &mut entry.state {
ProcState::Provisioned => return Ok(RuntimeHealth::Provisioned),
ProcState::Stopped => return Ok(RuntimeHealth::Stopped),
ProcState::Running(child) => match child.try_wait() {
Ok(Some(_)) => {
entry.state = ProcState::Stopped;
return Ok(RuntimeHealth::Stopped);
}
Ok(None) | Err(_) => entry.spec.endpoint.clone(),
},
}
};
match a2a_rs::fetch_agent_card(&endpoint).await {
Ok(_) => Ok(RuntimeHealth::Healthy),
Err(_) => Ok(RuntimeHealth::Unhealthy),
}
}
async fn list(&self) -> Result<Vec<RuntimeStatus>, RuntimeError> {
let ids: Vec<AgentId> = self.agents.lock().await.keys().cloned().collect();
let mut statuses = Vec::with_capacity(ids.len());
for id in ids {
let health = self.health(&id).await?;
let endpoint = match self.agents.lock().await.get(&id) {
Some(entry) => entry.spec.endpoint.clone(),
None => continue, };
statuses.push(RuntimeStatus {
id,
health,
endpoint,
});
}
Ok(statuses)
}
}