#[cfg(feature = "telegram")]
pub async fn telegram() -> anyhow::Result<()> {
imp::telegram().await
}
#[cfg(not(feature = "telegram"))]
pub async fn telegram() -> anyhow::Result<()> {
anyhow::bail!(
"this build has no Telegram support — reinstall with \
`cargo install aonyx-agent --features telegram`, or grab a release binary"
)
}
#[cfg(feature = "discord")]
pub async fn discord() -> anyhow::Result<()> {
imp::discord().await
}
#[cfg(not(feature = "discord"))]
pub async fn discord() -> anyhow::Result<()> {
anyhow::bail!(
"this build has no Discord support — reinstall with \
`cargo install aonyx-agent --features discord`, or grab a release binary"
)
}
#[cfg(feature = "openai-server")]
pub async fn openai(port: u16, token: Option<String>) -> anyhow::Result<()> {
imp::openai(port, token).await
}
#[cfg(not(feature = "openai-server"))]
pub async fn openai(_port: u16, _token: Option<String>) -> anyhow::Result<()> {
anyhow::bail!(
"this build has no OpenAI-server support — reinstall with \
`cargo install aonyx-agent --features openai-server`, or grab a release binary"
)
}
#[cfg(any(feature = "telegram", feature = "discord", feature = "openai-server"))]
mod imp {
use std::collections::HashMap;
use std::sync::Arc;
use aonyx_adapters::AgentHandler;
use aonyx_agent::AgentRunner;
use aonyx_core::{Message, Role};
use async_trait::async_trait;
use tokio::sync::Mutex;
use crate::config::Config;
const MAX_HISTORY: usize = 40;
struct RunnerHandler {
runner: AgentRunner,
system_prompt: Option<String>,
chats: Mutex<HashMap<String, Vec<Message>>>,
}
impl RunnerHandler {
fn seed(&self) -> Vec<Message> {
match &self.system_prompt {
Some(p) => vec![Message::new(Role::System, p.clone())],
None => Vec::new(),
}
}
}
#[async_trait]
impl AgentHandler for RunnerHandler {
async fn handle(&self, chat_id: &str, text: &str) -> aonyx_core::Result<String> {
let mut history = {
let map = self.chats.lock().await;
map.get(chat_id).cloned().unwrap_or_else(|| self.seed())
};
history.push(Message::new(Role::User, text));
let result = self.runner.run(history).await?;
let reply = last_assistant_text(&result.messages);
let trimmed = trim_history(result.messages, MAX_HISTORY);
self.chats.lock().await.insert(chat_id.to_string(), trimmed);
Ok(reply)
}
async fn complete(&self, messages: Vec<(String, String)>) -> aonyx_core::Result<String> {
let msgs: Vec<Message> = messages
.into_iter()
.map(|(role, content)| Message::new(role_from_str(&role), content))
.collect();
let result = self.runner.run(msgs).await?;
Ok(last_assistant_text(&result.messages))
}
}
fn role_from_str(role: &str) -> Role {
match role {
"system" => Role::System,
"assistant" => Role::Assistant,
"tool" => Role::Tool,
_ => Role::User,
}
}
fn last_assistant_text(messages: &[Message]) -> String {
messages
.iter()
.rev()
.find(|m| matches!(m.role, Role::Assistant) && !m.content.trim().is_empty())
.map(|m| m.content.clone())
.unwrap_or_else(|| "(no reply)".to_string())
}
fn trim_history(mut msgs: Vec<Message>, max: usize) -> Vec<Message> {
if msgs.len() <= max {
return msgs;
}
let keep_system = msgs.first().is_some_and(|m| matches!(m.role, Role::System));
let start = msgs.len() - max;
if keep_system {
let system = msgs[0].clone();
let mut out = Vec::with_capacity(max + 1);
out.push(system);
out.extend_from_slice(&msgs[start..]);
out
} else {
msgs.split_off(start)
}
}
fn build_handler(config: &Config) -> anyhow::Result<Arc<RunnerHandler>> {
let provider = crate::build_provider(config)?;
let registry = crate::build_serve_registry()?;
let project = crate::project_slug(&std::env::current_dir()?);
let runner = AgentRunner::new(provider, registry, config.model.clone())
.with_max_iterations(config.max_iterations)
.with_skills(crate::load_all_skills())
.with_project(project);
Ok(Arc::new(RunnerHandler {
runner,
system_prompt: config.system_prompt.clone(),
chats: Mutex::new(HashMap::new()),
}))
}
#[cfg(any(feature = "telegram", feature = "discord"))]
fn announce(channel: &str, allowed: usize, setup_cmd: &str) {
if allowed == 0 {
eprintln!(
"aonyx: {channel} bot starting — OPEN to all chats \
(lock it down with `{setup_cmd}`). Ctrl-C to stop."
);
} else {
eprintln!("aonyx: {channel} bot starting — {allowed} allowed. Ctrl-C to stop.");
}
}
#[cfg(feature = "telegram")]
pub async fn telegram() -> anyhow::Result<()> {
use aonyx_adapters::{telegram::TelegramAdapter, ConversationAdapter};
let config = Config::load_or_init()?;
let token = crate::resolve_key(&None, "TELEGRAM_BOT_TOKEN", "telegram_bot_token").map_err(
|_| {
anyhow::anyhow!(
"no Telegram bot token — run `aonyx setup telegram`, or export TELEGRAM_BOT_TOKEN"
)
},
)?;
let handler = build_handler(&config)?;
let allowed = config.telegram_allowed_chats.clone();
announce("Telegram", allowed.len(), "aonyx setup telegram");
TelegramAdapter::new(token, allowed, handler)
.run()
.await
.map_err(|e| anyhow::anyhow!("telegram: {e}"))
}
#[cfg(feature = "discord")]
pub async fn discord() -> anyhow::Result<()> {
use aonyx_adapters::{discord::DiscordAdapter, ConversationAdapter};
let config = Config::load_or_init()?;
let token =
crate::resolve_key(&None, "DISCORD_BOT_TOKEN", "discord_bot_token").map_err(|_| {
anyhow::anyhow!(
"no Discord bot token — run `aonyx setup discord`, or export DISCORD_BOT_TOKEN"
)
})?;
let handler = build_handler(&config)?;
let allowed = config.discord_allowed_channels.clone();
announce("Discord", allowed.len(), "aonyx setup discord");
DiscordAdapter::new(token, allowed, handler)
.run()
.await
.map_err(|e| anyhow::anyhow!("discord: {e}"))
}
#[cfg(feature = "openai-server")]
pub async fn openai(port: u16, token: Option<String>) -> anyhow::Result<()> {
use aonyx_adapters::openai_server::OpenAiServer;
let config = Config::load_or_init()?;
let handler = build_handler(&config)?;
let token = token.or_else(|| std::env::var("AONYX_OPENAI_TOKEN").ok());
let addr = format!("127.0.0.1:{port}");
if token.is_some() {
eprintln!(
"aonyx: OpenAI-compatible server on http://{addr}/v1 (bearer auth ON). Ctrl-C to stop."
);
} else {
eprintln!(
"aonyx: OpenAI-compatible server on http://{addr}/v1 \
(no auth — keep it on localhost). Ctrl-C to stop."
);
}
OpenAiServer::new(addr, token, handler)
.run()
.await
.map_err(|e| anyhow::anyhow!("openai-server: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn msg(role: Role, c: &str) -> Message {
Message::new(role, c)
}
#[test]
fn trim_keeps_system_and_tail() {
let mut v = vec![msg(Role::System, "sys")];
for i in 0..100 {
v.push(msg(Role::User, &format!("u{i}")));
}
let out = trim_history(v, 10);
assert_eq!(out.len(), 11); assert!(matches!(out[0].role, Role::System));
assert_eq!(out[0].content, "sys");
assert_eq!(out.last().unwrap().content, "u99");
}
#[test]
fn trim_noop_when_small() {
let v = vec![msg(Role::User, "a"), msg(Role::Assistant, "b")];
assert_eq!(trim_history(v.clone(), 40).len(), v.len());
}
#[test]
fn last_assistant_text_picks_final_nonempty() {
let v = vec![
msg(Role::User, "q"),
msg(Role::Assistant, "first"),
msg(Role::User, "q2"),
msg(Role::Assistant, "final"),
];
assert_eq!(last_assistant_text(&v), "final");
}
#[test]
fn role_mapping() {
assert!(matches!(role_from_str("system"), Role::System));
assert!(matches!(role_from_str("assistant"), Role::Assistant));
assert!(matches!(role_from_str("tool"), Role::Tool));
assert!(matches!(role_from_str("user"), Role::User));
assert!(matches!(role_from_str("whatever"), Role::User));
}
}
}