feat(core): Introduce AppState and integrate with Engine Loop

This commit is contained in:
2025-12-26 19:12:02 +01:00
parent 352bbb76f2
commit 2bccb11e53
3 changed files with 108 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
use crate::messages::{Message, UserAction, AgentResponse};
use tokio::sync::mpsc;
use crate::state::AppState;
use tokio::sync::{mpsc, Mutex};
use std::sync::Arc;
use llm_core::{LlmProvider, ChatMessage, ChatOptions};
use futures::StreamExt;
@@ -9,21 +10,30 @@ pub async fn run_engine_loop(
mut rx: mpsc::Receiver<Message>,
tx_ui: mpsc::Sender<Message>,
client: Arc<dyn LlmProvider>,
state: Arc<Mutex<AppState>>,
) {
while let Some(msg) = rx.recv().await {
match msg {
Message::UserAction(UserAction::Input(text)) => {
// TODO: Maintain conversation history (AppState) - next task
let messages = vec![ChatMessage::user(text)];
// Update history with user message
let messages = {
let mut guard = state.lock().await;
guard.add_message(ChatMessage::user(text.clone()));
guard.messages.clone()
};
// Use default options for now
let options = ChatOptions::default();
match client.chat_stream(&messages, &options, None).await {
Ok(mut stream) => {
let mut full_response = String::new();
while let Some(result) = stream.next().await {
match result {
Ok(chunk) => {
if let Some(content) = chunk.content {
full_response.push_str(&content);
if let Err(e) = tx_ui.send(Message::AgentResponse(AgentResponse::Token(content))).await {
eprintln!("Failed to send token to UI: {}", e);
break;
@@ -35,6 +45,13 @@ pub async fn run_engine_loop(
}
}
}
// Add assistant response to history
{
let mut guard = state.lock().await;
guard.add_message(ChatMessage::assistant(full_response));
}
let _ = tx_ui.send(Message::AgentResponse(AgentResponse::Complete)).await;
}
Err(e) => {
@@ -82,10 +99,11 @@ mod tests {
let (tx_out, mut rx_out) = mpsc::channel(10);
let client = Arc::new(MockProvider);
let state = Arc::new(Mutex::new(AppState::new()));
// Spawn the engine loop
tokio::spawn(async move {
run_engine_loop(rx_in, tx_out, client).await;
run_engine_loop(rx_in, tx_out, client, state).await;
});
// Send a message
@@ -110,4 +128,41 @@ mod tests {
panic!("Expected Complete");
}
}
}
#[tokio::test]
async fn test_engine_state_update() {
let (tx_in, rx_in) = mpsc::channel(1);
let (tx_out, mut rx_out) = mpsc::channel(10);
let client = Arc::new(MockProvider);
let state = Arc::new(Mutex::new(AppState::new()));
let state_clone = state.clone();
// Spawn the engine loop
tokio::spawn(async move {
run_engine_loop(rx_in, tx_out, client, state_clone).await;
});
// Send a message
tx_in.send(Message::UserAction(UserAction::Input("Hi".to_string()))).await.unwrap();
// Wait for completion
while let Some(msg) = rx_out.recv().await {
if let Message::AgentResponse(AgentResponse::Complete) = msg {
break;
}
}
// Verify state
let guard = state.lock().await;
assert_eq!(guard.messages.len(), 2); // User + Assistant
match &guard.messages[0].role {
llm_core::Role::User => {},
_ => panic!("First message should be User"),
}
match &guard.messages[1].role {
llm_core::Role::Assistant => {},
_ => panic!("Second message should be Assistant"),
}
}
}

View File

@@ -1,6 +1,7 @@
mod commands;
mod messages;
mod engine;
mod state;
use clap::{Parser, ValueEnum};
use color_eyre::eyre::{Result, eyre};
@@ -763,11 +764,15 @@ async fn main() -> Result<()> {
// Keep tx for future use (will be passed to UI/REPL)
let _tx = tx.clone();
// Create shared state
let state = Arc::new(tokio::sync::Mutex::new(crate::state::AppState::new()));
// Spawn the Engine Loop
let client_clone = client.clone();
let tx_clone = tx.clone();
let state_clone = state.clone();
tokio::spawn(async move {
engine::run_engine_loop(rx, tx_clone, client_clone).await;
engine::run_engine_loop(rx, tx_clone, client_clone, state_clone).await;
});
// Check if interactive mode (no prompt provided)

View File

@@ -0,0 +1,42 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use llm_core::ChatMessage;
/// Shared application state
#[derive(Debug, Default)]
pub struct AppState {
pub messages: Vec<ChatMessage>,
pub running: bool,
}
impl AppState {
pub fn new() -> Self {
Self {
messages: Vec::new(),
running: true,
}
}
pub fn add_message(&mut self, message: ChatMessage) {
self.messages.push(message);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_app_state_sharing() {
let state = Arc::new(Mutex::new(AppState::new()));
let state_clone = state.clone();
tokio::spawn(async move {
let mut guard = state_clone.lock().await;
guard.add_message(ChatMessage::user("Test"));
}).await.unwrap();
let guard = state.lock().await;
assert_eq!(guard.messages.len(), 1);
}
}