use std::io::Write as _;
use std::path::Path;
use std::process::ExitCode;
use std::sync::Arc;
use anyhow::Context as _;
use futures_util::stream::StreamExt as _;
use paigasus_helikon_core::{
AgentEvent, AgentInput, MemorySession, RunConfig, RunContext, Runner as _, Session,
};
use paigasus_helikon_runtime_tokio::TokioRunner;
use tokio::io::{AsyncBufReadExt as _, BufReader};
use crate::cli::ReplArgs;
use crate::registry::AgentRegistry;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplCommand {
Agents,
Switch(String),
Reload,
Quit,
Unknown(String),
Say(String),
}
pub fn parse_repl_command(line: &str) -> ReplCommand {
let trimmed = line.trim();
let mut parts = trimmed.splitn(2, ' ');
let head = parts.next().unwrap_or("");
let rest = parts.next().unwrap_or("").trim();
match head {
"/agents" => ReplCommand::Agents,
"/switch" => ReplCommand::Switch(rest.to_owned()),
"/reload" => ReplCommand::Reload,
"/quit" | "/exit" => ReplCommand::Quit,
_ if head.starts_with('/') => ReplCommand::Unknown(trimmed.to_owned()),
_ => ReplCommand::Say(trimmed.to_owned()),
}
}
pub async fn run(args: ReplArgs) -> anyhow::Result<ExitCode> {
let registry = Arc::new(AgentRegistry::load(&args.agents)?);
let (mut current, auto_selected) = match &args.agent {
Some(name) => {
if !registry.has_agent(name) {
anyhow::bail!("no such agent '{name}' in '{}'", args.agents.display());
}
(name.clone(), false)
}
None => {
let first = registry.first_agent().context("sidecar has no agents")?;
(first, true)
}
};
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Result<(), String>>();
let _debouncer = registry.watch(move |res| {
let _ = tx.send(res.map_err(|e| format!("{e:#}")));
})?;
let session: Arc<dyn Session> = Arc::new(MemorySession::new());
print_banner(®istry, ¤t, auto_selected, &args.agents);
let mut lines = BufReader::new(tokio::io::stdin()).lines();
loop {
print!("{current}> ");
std::io::stdout().flush().ok();
tokio::select! {
reload = rx.recv() => {
match reload {
Some(Ok(())) => println!("\nreloaded {}", args.agents.display()),
Some(Err(err)) => {
println!("\nreload failed (keeping previous definitions): {err}");
}
None => {
}
}
}
line = lines.next_line() => {
let Some(line) = line? else {
break;
};
match parse_repl_command(&line) {
ReplCommand::Agents => print_agents(®istry, ¤t),
ReplCommand::Switch(name) => {
if name.is_empty() {
println!("usage: /switch NAME");
} else if registry.has_agent(&name) {
current = name;
println!("switched to '{current}'");
} else {
println!("no such agent '{name}'");
}
}
ReplCommand::Reload => match registry.reload() {
Ok(()) => println!("reloaded {}", args.agents.display()),
Err(e) => println!("reload failed (keeping previous definitions): {e:#}"),
},
ReplCommand::Quit => break,
ReplCommand::Unknown(cmd) => println!("unknown command: {cmd}"),
ReplCommand::Say(text) => {
if text.is_empty() {
continue;
}
run_turn(®istry, ¤t, Arc::clone(&session), &text).await;
}
}
}
}
}
Ok(ExitCode::SUCCESS)
}
fn print_banner(registry: &AgentRegistry, current: &str, auto_selected: bool, path: &Path) {
let names = registry.agent_names();
println!("Paigasus Helikon REPL — sidecar: {}", path.display());
println!("agents: {}", names.join(", "));
if auto_selected {
println!("current agent: {current} (alphabetically first — no --agent given)");
} else {
println!("current agent: {current}");
}
println!("commands: /agents /switch NAME /reload /quit (or /exit)");
println!("editing the sidecar file hot-reloads its definitions on your next turn");
println!();
}
fn print_agents(registry: &AgentRegistry, current: &str) {
for name in registry.agent_names() {
if name == current {
println!("* {name}");
} else {
println!(" {name}");
}
}
}
async fn run_turn(registry: &AgentRegistry, current: &str, session: Arc<dyn Session>, text: &str) {
let agent = match registry.build_agent(current) {
Ok(agent) => agent,
Err(e) => {
println!("error: {e:#}");
return;
}
};
let ctx = RunContext::ephemeral(()).with_session(session);
let streaming = match TokioRunner
.run_streamed(
&agent,
ctx,
AgentInput::from_user_text(text),
RunConfig::default(),
)
.await
{
Ok(streaming) => streaming,
Err(e) => {
println!("error: {e}");
return;
}
};
let mut events = streaming.events;
while let Some(event) = events.next().await {
match event {
AgentEvent::TokenDelta { text } => {
print!("{text}");
std::io::stdout().flush().ok();
}
AgentEvent::MessageOutput { .. } => println!(),
AgentEvent::RunFailed { error } => println!("error: {error}"),
_ => {}
}
}
}