use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
time::Duration,
};
use super::{
A2aAgent, A2aConfig, A2aError, A2aReload, DEFAULT_GLOBAL_A2A_FILE,
configured::ConfiguredAgents,
watch::{self, A2aWatch, A2aWatchError, Stamp},
};
use crate::{agents::AgentResolver, event::Binding};
use crate::{event::ContextFile, runtime::Runtime};
pub(crate) struct LiveA2a {
root: PathBuf,
config: A2aConfig,
paths: Vec<PathBuf>,
seen: Mutex<Vec<Stamp>>,
files: RwLock<Vec<ContextFile>>,
configured: Arc<ConfiguredAgents>,
reloading: tokio::sync::Mutex<()>,
}
impl LiveA2a {
pub(crate) async fn connect(
root: PathBuf,
config: A2aConfig,
discovery: Discovery,
runtime: &Arc<Runtime>,
) -> Self {
let configured = Arc::new(ConfiguredAgents::connect(discovery.agents).await);
configured.on_change(runtime.refresher());
Self {
paths: sources(&root, &config),
seen: Mutex::new(discovery.seen),
root,
config,
files: RwLock::new(discovery.files),
configured,
reloading: tokio::sync::Mutex::new(()),
}
}
pub(crate) fn configured(&self) -> Arc<dyn AgentResolver> {
Arc::clone(&self.configured) as Arc<dyn AgentResolver>
}
pub(crate) fn binding(&self) -> Binding {
Binding::new("a2a").with_files(self.files()).with_names(
self.configured
.taught()
.agents
.into_iter()
.map(|a| a.name)
.collect(),
)
}
pub(crate) fn binding_source(self: &Arc<Self>) -> crate::run::BindingSource {
let live = Arc::downgrade(self);
let last = std::sync::Mutex::new(self.binding());
Arc::new(move || {
let mut last = last.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(live) = live.upgrade() {
*last = live.binding();
}
last.clone()
})
}
#[cfg(test)]
pub(crate) fn names(&self) -> Vec<String> {
self.configured.names()
}
pub(crate) fn files(&self) -> Vec<ContextFile> {
self.files
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
pub(crate) async fn reload(&self) -> Result<A2aReload, A2aError> {
let _serial = self.reloading.lock().await;
let discovery = match discovered(&self.root, &self.config) {
Ok(discovery) => discovery,
Err((seen, error)) => {
self.saw(seen);
return Err(error);
}
};
let report = self.configured.reload(discovery.agents).await;
*self
.files
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = discovery.files;
self.saw(discovery.seen);
Ok(report)
}
fn saw(&self, seen: Vec<Stamp>) {
*self
.seen
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = seen;
}
pub(crate) fn seen(&self) -> Vec<Stamp> {
self.seen
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
pub(crate) fn watch(self: &Arc<Self>, interval: Duration) -> Result<A2aWatch, A2aWatchError> {
watch::start(Arc::downgrade(self), self.paths.clone(), interval)
}
}
pub(crate) struct Discovery {
files: Vec<ContextFile>,
agents: Vec<A2aAgent>,
seen: Vec<Stamp>,
}
fn sources(workspace: &Path, config: &A2aConfig) -> Vec<PathBuf> {
crate::paths::candidate(workspace, &config.workspace_file)
.into_iter()
.chain(
config
.global_dir
.as_ref()
.map(|global| global.join(DEFAULT_GLOBAL_A2A_FILE)),
)
.collect()
}
pub(crate) fn discovered(
workspace: &Path,
config: &A2aConfig,
) -> Result<Discovery, (Vec<Stamp>, A2aError)> {
let seen = watch::stamps(&sources(workspace, config));
let read = || -> Result<_, A2aError> {
let supplied = super::supplied(config)?;
let sources = super::discover(workspace, config)?;
let files = sources
.iter()
.map(|source| ContextFile {
path: source.path.clone(),
scope: source.scope.label(),
})
.collect();
Ok((files, super::layered(supplied, sources)))
};
match read() {
Ok((files, agents)) => Ok(Discovery {
files,
agents,
seen,
}),
Err(error) => Err((seen, error)),
}
}
impl std::fmt::Debug for LiveA2a {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LiveA2a")
.field("files", &self.files())
.field("agents", &self.configured)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests;