use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use anyhow::Context as _;
use notify::{RecommendedWatcher, RecursiveMode};
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
use paigasus_helikon_core::{Handoff, LlmAgent, Tool};
use crate::model::{build_model, build_model_for_case, CliModel};
use crate::rhai_tool::RhaiTool;
use crate::sidecar::{EvalSection, InstructionsDef, Sidecar, ToolDefToml};
pub struct AgentRegistry {
path: PathBuf,
inner: RwLock<Sidecar>,
}
impl AgentRegistry {
pub fn load(path: &Path) -> anyhow::Result<Self> {
let sidecar = Sidecar::load(path)?;
Ok(Self {
path: path.to_path_buf(),
inner: RwLock::new(sidecar),
})
}
pub fn reload(&self) -> anyhow::Result<()> {
let fresh = Sidecar::load(&self.path)?;
*self.inner.write().unwrap_or_else(|e| e.into_inner()) = fresh;
Ok(())
}
pub fn agent_names(&self) -> Vec<String> {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.agents
.keys()
.cloned()
.collect()
}
pub fn has_agent(&self, name: &str) -> bool {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.agents
.contains_key(name)
}
pub fn first_agent(&self) -> Option<String> {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.first_agent()
.map(str::to_owned)
}
pub fn build_agent(&self, name: &str) -> anyhow::Result<LlmAgent<(), CliModel>> {
let sidecar = self.inner.read().unwrap_or_else(|e| e.into_inner());
build_agent_recursive(&sidecar, name, None)
}
pub fn build_agent_for_case(
&self,
name: &str,
case_id: &str,
) -> anyhow::Result<LlmAgent<(), CliModel>> {
let sidecar = self.inner.read().unwrap_or_else(|e| e.into_inner());
build_agent_recursive(&sidecar, name, Some(case_id))
}
pub fn eval_section(&self) -> Option<EvalSection> {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.eval
.clone()
}
pub fn base_dir(&self) -> PathBuf {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.base_dir
.clone()
}
pub fn watch(
self: &Arc<Self>,
on_reload: impl Fn(anyhow::Result<()>) + Send + 'static,
) -> anyhow::Result<Debouncer<RecommendedWatcher>> {
let registry = Arc::clone(self);
let mut debouncer = new_debouncer(
Duration::from_millis(300),
move |res: DebounceEventResult| {
if res.is_ok() {
on_reload(registry.reload());
}
},
)
.context("failed to start sidecar file watcher")?;
let base_dir = self
.path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
debouncer
.watcher()
.watch(&base_dir, RecursiveMode::Recursive)
.with_context(|| {
format!("failed to watch sidecar directory '{}'", base_dir.display())
})?;
Ok(debouncer)
}
}
fn build_tool(base_dir: &Path, name: &str, def: &ToolDefToml) -> anyhow::Result<Arc<dyn Tool<()>>> {
let source = if let Some(script) = &def.script {
let path = base_dir.join(script);
std::fs::read_to_string(&path).with_context(|| {
format!(
"failed to read script '{}' for tool '{name}'",
path.display()
)
})?
} else if let Some(inline) = &def.inline {
inline.clone()
} else {
anyhow::bail!("tool '{name}' has neither 'script' nor 'inline'");
};
let tool = RhaiTool::new(
name.to_owned(),
def.description.clone(),
def.params.clone(),
&source,
)?;
Ok(Arc::new(tool) as Arc<dyn Tool<()>>)
}
fn resolve_instructions(base_dir: &Path, def: &InstructionsDef) -> anyhow::Result<String> {
match def {
InstructionsDef::Inline(text) => Ok(text.clone()),
InstructionsDef::File { file } => {
let path = base_dir.join(file);
std::fs::read_to_string(&path)
.with_context(|| format!("failed to read instructions file '{}'", path.display()))
}
}
}
fn build_agent_recursive(
sidecar: &Sidecar,
name: &str,
case_id: Option<&str>,
) -> anyhow::Result<LlmAgent<(), CliModel>> {
let def = sidecar
.agents
.get(name)
.with_context(|| format!("no such agent '{name}'"))?;
let mut handoffs = Vec::with_capacity(def.handoffs.len());
for target in &def.handoffs {
let target_agent = build_agent_recursive(sidecar, target, case_id)?;
handoffs.push(Handoff::to(target_agent));
}
let mut tools: Vec<Arc<dyn Tool<()>>> = Vec::with_capacity(def.tools.len());
for tool_name in &def.tools {
let tool_def = sidecar
.tools
.get(tool_name)
.with_context(|| format!("agent '{name}' references unknown tool '{tool_name}'"))?;
tools.push(build_tool(&sidecar.base_dir, tool_name, tool_def)?);
}
let instructions = resolve_instructions(&sidecar.base_dir, &def.instructions)
.with_context(|| format!("resolving instructions for agent '{name}'"))?;
let model = match case_id {
Some(case_id) => build_model_for_case(&def.model, &sidecar.base_dir, case_id)?,
None => build_model(&def.model, &sidecar.base_dir)?,
};
let mut builder = LlmAgent::builder::<()>()
.name(name.to_owned())
.model(model)
.instructions(instructions)
.tools(tools)
.handoffs(handoffs);
if let Some(description) = &def.description {
builder = builder.description(description.clone());
}
if let Some(max_turns) = def.max_turns {
builder = builder.max_turns(max_turns);
}
Ok(builder.build())
}