use async_trait::async_trait;
use anyhow::Context;
use tracing::{debug, info};
use haki_llm::{CompletionRequest, LlmProvider, Message};
use crate::{message::ToolCall, session::Session};
#[async_trait]
pub trait ToolExecutor: Send + Sync {
async fn execute(&self, call: &ToolCall) -> anyhow::Result<String>;
fn available_tools(&self) -> Vec<serde_json::Value>;
}
pub struct NoopToolExecutor;
#[async_trait]
impl ToolExecutor for NoopToolExecutor {
async fn execute(&self, call: &ToolCall) -> anyhow::Result<String> {
Ok(format!("[noop] tool '{}' not yet implemented", call.name))
}
fn available_tools(&self) -> Vec<serde_json::Value> {
vec![]
}
}
pub struct AgentLoop {
provider: LlmProvider,
#[allow(dead_code)]
executor: Box<dyn ToolExecutor>,
max_tokens: u32,
}
impl AgentLoop {
pub fn new(
provider: LlmProvider,
executor: Box<dyn ToolExecutor>,
max_tokens: u32,
) -> Self {
Self { provider, executor, max_tokens }
}
pub async fn run_turn(
&self,
session: &mut Session,
user_input: &str,
) -> anyhow::Result<String> {
session.push(Message::user(user_input));
let req = CompletionRequest {
model: session.model.clone(),
system: session.config.system_prompt.clone(),
messages: session.history().to_vec(),
max_tokens: self.max_tokens,
};
info!(model = %session.model, "sending completion request");
let resp = self.provider.complete(req).await.context("LLM completion failed")?;
debug!(
input_tokens = resp.usage.input_tokens,
output_tokens = resp.usage.output_tokens,
"token usage"
);
session.push(Message::assistant(&resp.content));
Ok(resp.content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_executor_tool_list_is_empty() {
let exec = NoopToolExecutor;
assert!(exec.available_tools().is_empty());
}
#[tokio::test]
async fn noop_executor_returns_noop_message() {
let call = ToolCall {
id: "c1".into(),
name: "bash".into(),
input: serde_json::json!({}),
};
let result = NoopToolExecutor.execute(&call).await.unwrap();
assert!(result.contains("noop"));
}
}