feat(core): Introduce AppState and integrate with Engine Loop
This commit is contained in:
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
42
crates/app/cli/src/state.rs
Normal file
42
crates/app/cli/src/state.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user