1use async_trait::async_trait;
7use anyhow::Context;
8use tracing::{debug, info};
9
10use haki_llm::{CompletionRequest, LlmProvider, Message};
11
12use crate::{message::ToolCall, session::Session};
13
14#[async_trait]
17pub trait ToolExecutor: Send + Sync {
18 async fn execute(&self, call: &ToolCall) -> anyhow::Result<String>;
19 fn available_tools(&self) -> Vec<serde_json::Value>;
20}
21
22pub struct NoopToolExecutor;
25
26#[async_trait]
27impl ToolExecutor for NoopToolExecutor {
28 async fn execute(&self, call: &ToolCall) -> anyhow::Result<String> {
29 Ok(format!("[noop] tool '{}' not yet implemented", call.name))
30 }
31
32 fn available_tools(&self) -> Vec<serde_json::Value> {
33 vec![]
34 }
35}
36
37pub struct AgentLoop {
40 provider: LlmProvider,
41 #[allow(dead_code)]
42 executor: Box<dyn ToolExecutor>,
43 max_tokens: u32,
44}
45
46impl AgentLoop {
47 pub fn new(
48 provider: LlmProvider,
49 executor: Box<dyn ToolExecutor>,
50 max_tokens: u32,
51 ) -> Self {
52 Self { provider, executor, max_tokens }
53 }
54
55 pub async fn run_turn(
58 &self,
59 session: &mut Session,
60 user_input: &str,
61 ) -> anyhow::Result<String> {
62 session.push(Message::user(user_input));
63
64 let req = CompletionRequest {
65 model: session.model.clone(),
66 system: session.config.system_prompt.clone(),
67 messages: session.history().to_vec(),
68 max_tokens: self.max_tokens,
69 };
70
71 info!(model = %session.model, "sending completion request");
72 let resp = self.provider.complete(req).await.context("LLM completion failed")?;
73
74 debug!(
75 input_tokens = resp.usage.input_tokens,
76 output_tokens = resp.usage.output_tokens,
77 "token usage"
78 );
79
80 session.push(Message::assistant(&resp.content));
81 Ok(resp.content)
82 }
83}
84
85#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn noop_executor_tool_list_is_empty() {
93 let exec = NoopToolExecutor;
94 assert!(exec.available_tools().is_empty());
95 }
96
97 #[tokio::test]
98 async fn noop_executor_returns_noop_message() {
99 let call = ToolCall {
100 id: "c1".into(),
101 name: "bash".into(),
102 input: serde_json::json!({}),
103 };
104 let result = NoopToolExecutor.execute(&call).await.unwrap();
105 assert!(result.contains("noop"));
106 }
107}