1use super::history::{ChatHistory, HistoryEntryType};
4use super::ui::{self, AgentId};
5use anyhow::Result;
6use futures::{FutureExt, StreamExt};
7use llimorse::line_format::{ChatMessage, ToolCall};
8use llimorse::{Agent, StreamingChunk};
9use std::collections::VecDeque;
10use std::mem;
11use std::sync::{Arc, Mutex};
12use tokio::sync::mpsc;
13
14pub(super) struct ChatAgent {
16 chat_history: Arc<Mutex<ChatHistory>>,
18
19 ui_notifications: Arc<mpsc::UnboundedSender<ui::Notification>>,
21
22 incoming: Option<Incoming>,
27}
28
29struct Incoming {
31 notifications: mpsc::UnboundedReceiver<Notification>,
33
34 queued_messages: VecDeque<String>,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
40pub(super) enum Notification {
41 Exit,
43
44 QueuePrompt(String),
46
47 ForceSubmitQueued,
49
50 Continue,
53}
54
55impl ChatAgent {
56 pub fn new(
58 chat_history: Arc<Mutex<ChatHistory>>,
59 notifications: mpsc::UnboundedReceiver<Notification>,
60 ui_notifications: Arc<mpsc::UnboundedSender<ui::Notification>>,
61 ) -> Self {
62 ChatAgent {
63 chat_history,
64 incoming: Some(Incoming {
65 notifications,
66 queued_messages: VecDeque::new(),
67 }),
68 ui_notifications,
69 }
70 }
71
72 pub async fn run(&mut self, agent: llimorse::Agent) -> Result<()> {
76 let result = self.do_run(agent).await;
77 let _ = self.ui_notifications.send(ui::Notification::Exit);
78 result
79 }
80
81 async fn process_notifications(&mut self) -> bool {
88 let incoming = self.incoming.as_mut().expect("Notifications object taken");
89
90 while let Some(notification) = incoming.notifications.recv().await {
91 match notification {
92 Notification::Exit => return false,
93
94 Notification::QueuePrompt(p) => {
95 incoming.queued_messages.push_back(p);
96 return true;
97 }
98
99 Notification::Continue => return true,
100
101 Notification::ForceSubmitQueued => (),
103 }
104 }
105
106 false
107 }
108
109 fn process_available_notifications(&mut self) -> bool {
116 let incoming = self.incoming.as_mut().expect("Notifications object taken");
117
118 while let Ok(notification) = incoming.notifications.try_recv() {
119 match notification {
120 Notification::Exit => return false,
121
122 Notification::QueuePrompt(p) => incoming.queued_messages.push_back(p),
123
124 Notification::ForceSubmitQueued => (),
126
127 Notification::Continue => (),
128 }
129 }
130
131 true
132 }
133
134 fn submit_queued_user_messages(&mut self, agent: &mut llimorse::Agent) -> bool {
140 let incoming = self.incoming.as_mut().expect("Notifications object taken");
141
142 let messages = mem::take(&mut incoming.queued_messages);
143 let any_messages = !messages.is_empty();
144 for message in messages {
145 let _ = self
146 .ui_notifications
147 .send(ui::Notification::PromptSubmitted);
148 self.push_history(&message, HistoryEntryType::User);
149 agent.push_user(message);
150 }
151
152 any_messages
153 }
154
155 fn can_continue(history: &[ChatMessage]) -> bool {
158 matches!(
159 history.last(),
160 Some(ChatMessage::Tool(_)) | Some(ChatMessage::User(_))
161 )
162 }
163
164 async fn do_run(&mut self, mut agent: llimorse::Agent) -> Result<()> {
166 while self.process_notifications().await && self.process_available_notifications() {
167 if !self.submit_queued_user_messages(&mut agent) && !Self::can_continue(agent.history())
170 {
171 continue;
172 }
173
174 let response = loop {
175 let mut streaming = agent.submit().await?;
176
177 loop {
178 let incoming = self.incoming.as_mut().expect("Notifications object taken");
179
180 futures::select! {
181 chunk = streaming.next() => {
182 if let Some(chunk) = chunk {
183 self.process_chunk(chunk?);
184 } else {
185 break;
186 }
187 }
188
189 exit = incoming.wait_for_abort().fuse() => if exit {
190 return Ok(())
191 } else {
192 streaming.force_finalize();
193 break;
194 }
195 }
196 }
197
198 drop(streaming);
199
200 let mut incoming = self.incoming.take().expect("Notifications object taken");
201
202 let check_calls = |agent: &Agent, call: &ToolCall| {
203 self.push_history(
204 &format!("[{}] {}\n", call.id, agent.display_call(&call.call)),
205 HistoryEntryType::ToolCall,
206 );
207 Ok(())
208 };
209
210 let check_results = |agent: &Agent, call: &ToolCall, result: &Result<String>| {
211 let name = call.call.name();
212 let id = &call.id;
213 match result {
214 Ok(result) => self.push_history(
215 &format!(
216 "=[{name}/{id}]=> {}\n",
217 agent.display_call_result(&call.call, result)
218 ),
219 HistoryEntryType::ToolResultOk,
220 ),
221 Err(err) => self.push_history(
222 &format!("=[{name}/{id}]=> {err}\n"),
223 HistoryEntryType::ToolResultErr,
224 ),
225 }
226 Ok(())
227 };
228
229 let mut pending = futures::select! {
230 pending = agent.execute_pending_calls(check_calls, check_results).fuse() => pending,
231 exit = incoming.wait_for_abort().fuse() => if exit {
232 return Ok(());
233 } else {
234 false }
236 };
237 self.incoming = Some(incoming);
238
239 if !self.process_available_notifications() {
240 return Ok(());
241 }
242 if self.submit_queued_user_messages(&mut agent) {
243 pending = true;
244 }
245
246 if !pending {
247 break agent.history().last().and_then(|cm| {
248 if let ChatMessage::Assistant(msg) = cm
249 && let Some(response) = &msg.content
250 && !response.is_empty()
251 {
252 Some(response.clone())
253 } else {
254 None
255 }
256 });
257 }
258 };
259
260 let _ = self
264 .ui_notifications
265 .send(ui::Notification::AwaitingPrompt { response });
266 }
267
268 Ok(())
269 }
270
271 fn process_chunk(&self, chunk: StreamingChunk) {
273 let (string, kind) = match chunk {
274 StreamingChunk::Content(content) => (content, HistoryEntryType::Content),
275 StreamingChunk::Reasoning(content) => (content, HistoryEntryType::Reasoning),
276 };
277
278 self.push_history(&string, kind);
279 }
280
281 fn push_history(&self, string: &str, kind: HistoryEntryType) {
283 let mut history = self.chat_history.lock().unwrap();
284 history.push_lines(string, kind, kind == HistoryEntryType::User);
286 drop(history);
287
288 let _ = self.ui_notifications.send(ui::Notification::UpdateAgent {
289 agent_id: AgentId::Main,
290 });
291 }
292}
293
294impl Incoming {
295 async fn wait_for_abort(&mut self) -> bool {
302 while let Some(notification) = self.notifications.recv().await {
303 match notification {
304 Notification::Exit => return true,
305 Notification::ForceSubmitQueued => return false,
306
307 Notification::Continue => (),
308
309 Notification::QueuePrompt(p) => self.queued_messages.push_back(p),
310 }
311 }
312
313 true
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::ChatAgent;
320 use llimorse::line_format::{AssistantMessage, ChatMessage, ToolResult, UserMessage};
321
322 #[test]
323 fn can_continue_on_tool_result() {
324 let result = ToolResult::new("call-1".to_string(), Ok("done".to_string()));
325 let history = vec![ChatMessage::Tool(result)];
326 assert!(ChatAgent::can_continue(&history));
327 }
328
329 #[test]
330 fn can_continue_on_unanswered_user_message() {
331 let history = vec![ChatMessage::User(UserMessage {
332 content: "hello".to_string(),
333 })];
334 assert!(ChatAgent::can_continue(&history));
335 }
336
337 #[test]
338 fn cannot_continue_after_assistant_message() {
339 let history = vec![ChatMessage::Assistant(AssistantMessage {
340 reasoning_content: None,
341 content: Some("hi there".to_string()),
342 tool_calls: None,
343 })];
344 assert!(!ChatAgent::can_continue(&history));
345 }
346
347 #[test]
348 fn cannot_continue_on_empty_history() {
349 assert!(!ChatAgent::can_continue(&[]));
350 }
351}