use aonyx_core::{Message, Result, Role};
use async_trait::async_trait;
use serde::Serialize;
use tokio::sync::mpsc::Sender;
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamFrame {
Delta {
text: String,
},
ToolStart {
name: String,
args: serde_json::Value,
class: String,
},
ToolEnd {
name: String,
ok: bool,
summary: String,
},
ToolRejected {
name: String,
class: String,
},
Iteration {
n: u32,
},
Done {
reply: String,
turns: u32,
},
Error {
message: String,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolInfo {
pub name: String,
pub description: String,
pub class: String,
pub schema: serde_json::Value,
}
#[derive(Debug, Clone, Serialize)]
pub struct SkillInfo {
pub id: String,
pub description: String,
pub triggers: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ConfigInfo {
pub provider: String,
pub model: String,
pub max_iterations: usize,
pub skill_autogen: bool,
}
#[async_trait]
pub trait ApiAgent: Send + Sync + 'static {
async fn run_turn(&self, history: Vec<Message>) -> Result<Vec<Message>>;
async fn run_turn_streaming(
&self,
history: Vec<Message>,
tx: Sender<StreamFrame>,
) -> Result<Vec<Message>> {
let log = self.run_turn(history).await?;
let reply = last_assistant_text(&log);
let _ = tx.send(StreamFrame::Delta { text: reply }).await;
Ok(log)
}
fn tools(&self) -> Vec<ToolInfo> {
Vec::new()
}
fn skills(&self) -> Vec<SkillInfo> {
Vec::new()
}
fn config(&self) -> ConfigInfo {
ConfigInfo::default()
}
}
pub(crate) 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())
}