use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
time::Duration,
};
use super::{
A2aAgent, A2aConfig, A2aError, A2aReload, DEFAULT_GLOBAL_A2A_FILE,
remote::{OnChange, RemoteAgents},
watch::{self, A2aWatch, A2aWatchError, Stamp},
};
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>>,
remote: Arc<RemoteAgents>,
reloading: tokio::sync::Mutex<()>,
}
impl LiveA2a {
pub(crate) async fn connect(
root: PathBuf,
config: A2aConfig,
discovery: Discovery,
runtime: &Arc<Runtime>,
) -> Self {
let remembered_in = runtime.a2a_conversations_dir().map(Path::to_path_buf);
let remote = RemoteAgents::connect(discovery.agents, remembered_in)
.await
.with_on_change(refresher(runtime))
.with_hop(runtime.a2a_hop());
Self {
paths: sources(&root, &config),
seen: Mutex::new(discovery.seen),
root,
config,
files: RwLock::new(discovery.files),
remote: Arc::new(remote),
reloading: tokio::sync::Mutex::new(()),
}
}
pub(crate) fn remote(&self) -> &Arc<RemoteAgents> {
&self.remote
}
pub(crate) fn names(&self) -> Vec<String> {
self.remote.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.remote.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()
}
fn refresher(runtime: &Arc<Runtime>) -> OnChange {
let runtime = Arc::downgrade(runtime);
Box::new(move || {
if let Some(runtime) = runtime.upgrade() {
runtime.mentra_runtime().refresh_tool_descriptors();
}
})
}
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.remote)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests;