feat(tui): implement streaming support for LLM responses and update transcript handling
This commit is contained in:
@@ -158,6 +158,20 @@ pub fn apply_action(state: &mut crate::state::AppStateRest, action: Action) {
|
||||
zesdex_infrastructure::TurnEvent::Error(msg) => {
|
||||
state.toast_error(msg);
|
||||
}
|
||||
zesdex_infrastructure::TurnEvent::StreamStart => {
|
||||
state.push_transcript(crate::state::ChatMessageDisplay::new(
|
||||
zesdex_domain::core::Role::Assistant,
|
||||
String::new(),
|
||||
));
|
||||
}
|
||||
zesdex_infrastructure::TurnEvent::StreamToken(text) => {
|
||||
state.append_to_last_transcript(&text);
|
||||
}
|
||||
zesdex_infrastructure::TurnEvent::StreamDone(_msg) => {
|
||||
// The final message is already accumulated in the transcript.
|
||||
// We might want to trigger a save or something here, but for UI, we just mark dirty.
|
||||
state.dirty = true;
|
||||
}
|
||||
zesdex_infrastructure::TurnEvent::Compacted(msgs) => {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.messages = msgs;
|
||||
|
||||
@@ -1008,6 +1008,17 @@ impl AppStateRest {
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Append text to the last assistant message in the transcript, if one exists.
|
||||
pub fn append_to_last_transcript(&mut self, text: &str) {
|
||||
if let Some(msg) = self.transcript_cache.messages.back_mut() {
|
||||
if msg.role == zesdex_domain::core::Role::Assistant {
|
||||
msg.content.push_str(text);
|
||||
self.transcript_cache.dirty = true;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark the app state as dirty, triggering a TUI re-render.
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.dirty = true;
|
||||
|
||||
@@ -128,13 +128,27 @@ fn run_turn(
|
||||
|
||||
debug!("agent turn iteration {iteration}");
|
||||
|
||||
// Blocking LLM call (reqwest::blocking::Client is sync)
|
||||
let result = client.chat_with_tools_non_streaming(
|
||||
// Stream the LLM response
|
||||
push_event(turn_events, TurnEvent::StreamStart);
|
||||
|
||||
let result = client.chat_with_tools_streaming(
|
||||
messages,
|
||||
Some(defs.clone()),
|
||||
Some(4096),
|
||||
Some(0.7),
|
||||
None, // no atomic abort flag for the sync API
|
||||
Some(4096),
|
||||
|event| {
|
||||
if abort.load(Ordering::SeqCst) {
|
||||
return false;
|
||||
}
|
||||
match event {
|
||||
zesdex_domain::core::StreamEvent::Token(s) | zesdex_domain::core::StreamEvent::Reasoning(s) => {
|
||||
push_event(turn_events, TurnEvent::StreamToken(s.clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
true
|
||||
},
|
||||
Some(&abort),
|
||||
);
|
||||
|
||||
match result {
|
||||
@@ -142,6 +156,8 @@ fn run_turn(
|
||||
let content = assistant_msg.content.clone().unwrap_or_default();
|
||||
let tool_calls = assistant_msg.tool_calls.clone().unwrap_or_default();
|
||||
|
||||
push_event(turn_events, TurnEvent::StreamDone(assistant_msg.clone()));
|
||||
|
||||
if let Some((tokens_in, tokens_out)) = usage {
|
||||
push_event(turn_events, TurnEvent::Usage {
|
||||
tokens_in,
|
||||
@@ -149,10 +165,6 @@ fn run_turn(
|
||||
});
|
||||
}
|
||||
|
||||
if !content.is_empty() {
|
||||
push_event(turn_events, TurnEvent::AssistantMessage(assistant_msg.clone()));
|
||||
}
|
||||
|
||||
if tool_calls.is_empty() {
|
||||
messages.push(ChatMessage::assistant(Some(content)));
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user