use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Peer {
pub name: String,
#[serde(default)]
pub role: String,
#[serde(default)]
pub expertise: String,
pub url: String,
#[serde(default)]
pub token: Option<String>,
}
fn store_path() -> PathBuf {
dirs_next::home_dir()
.unwrap_or_default()
.join(".aegis")
.join("peers.json")
}
pub fn load_all() -> HashMap<String, Peer> {
std::fs::read_to_string(store_path())
.ok()
.and_then(|c| serde_json::from_str(&c).ok())
.unwrap_or_default()
}
pub fn list() -> Vec<Peer> {
let mut v: Vec<Peer> = load_all().into_values().collect();
v.sort_by(|a, b| a.name.cmp(&b.name));
v
}
pub fn get(name: &str) -> Option<Peer> {
load_all().get(name).cloned()
}
pub fn save(peer: Peer) -> anyhow::Result<()> {
let mut all = load_all();
all.insert(peer.name.clone(), peer);
let path = store_path();
if let Some(p) = path.parent() {
std::fs::create_dir_all(p)?;
}
std::fs::write(&path, serde_json::to_string_pretty(&all)?)?;
Ok(())
}
pub fn remove(name: &str) -> anyhow::Result<bool> {
let mut all = load_all();
let existed = all.remove(name).is_some();
if existed {
std::fs::write(store_path(), serde_json::to_string_pretty(&all)?)?;
}
Ok(existed)
}
pub struct PeerTool;
#[async_trait::async_trait]
impl crate::registry::Tool for PeerTool {
fn name(&self) -> &str {
"peer"
}
fn description(&self) -> &str {
"Manage A2A coworker peers (other aegis instances) you can delegate to. \
`add` registers name + url (start that peer with `aegis a2a`); `list`; \
`remove`. Takes effect immediately — afterwards delegate to it via \
delegate_work/ask_question. Set any auth token locally (config), not here."
}
fn parameters(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"action": { "type": "string", "enum": ["add", "list", "remove"] },
"name": { "type": "string", "description": "Peer handle (add/remove)" },
"url": { "type": "string", "description": "A2A endpoint, e.g. http://127.0.0.1:41241 (add)" },
"role": { "type": "string", "description": "Short role label (add)" },
"expertise": { "type": "string", "description": "What it's good at (add)" }
},
"required": ["action"]
})
}
async fn execute(
&self,
args: serde_json::Value,
_ctx: &crate::registry::ToolContext<'_>,
) -> anyhow::Result<String> {
match args["action"].as_str().unwrap_or("list") {
"add" => {
let name = args["name"].as_str().unwrap_or("").trim();
let url = args["url"].as_str().unwrap_or("").trim();
if name.is_empty() || url.is_empty() {
return Ok("Error: `add` needs `name` and `url`.".to_string());
}
let existed = get(name).is_some();
save(Peer {
name: name.to_string(),
role: args["role"].as_str().unwrap_or("").to_string(),
expertise: args["expertise"].as_str().unwrap_or("").to_string(),
url: url.to_string(),
token: get(name).and_then(|p| p.token),
})?;
if existed {
Ok(format!(
"Updated peer '{name}' ({url}) — note: an entry with this name already existed and was overwritten (its token, if any, was kept)."
))
} else {
Ok(format!(
"Added peer '{name}' ({url}). You can now delegate to it (delegate_work coworker={name})."
))
}
}
"remove" => {
let name = args["name"].as_str().unwrap_or("");
match remove(name)? {
true => Ok(format!("Removed peer '{name}'.")),
false => Ok(format!("No peer '{name}'.")),
}
}
_ => {
let ps = list();
if ps.is_empty() {
Ok("No peers configured.".to_string())
} else {
Ok(ps
.iter()
.map(|p| format!("- {} ({}) {}", p.name, p.role, p.url))
.collect::<Vec<_>>()
.join("\n"))
}
}
}
}
}