Kill TextDelta, Info — UiMessage is dead. RAII ActivityGuards replace all status feedback
Streaming text now goes directly to agent entries via append_streaming(). sync_from_agent diffs the growing entry each tick. The streaming entry is popped when the response completes; build_response_message pushes the final version. All status feedback uses RAII ActivityGuards: - push_activity() for long-running work (thinking, streaming, scoring) - notify() for instant feedback (compacted, DMN state changes, commands) - Guards auto-remove on Drop, appending "(complete)" and lingering 5s - expire_activities() cleans up timed-out notifications on render tick UiMessage enum reduced to a single Info variant with zero sends. The channel infrastructure remains for now (Mind/Agent still take UiSender in signatures) — mechanical cleanup for a follow-up. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
e7914e3d58
commit
cfddb55ed9
9 changed files with 201 additions and 186 deletions
|
|
@ -20,7 +20,7 @@ use tokio::sync::mpsc;
|
|||
|
||||
use crate::agent::tools::{self as agent_tools, summarize_args, ActiveToolCall};
|
||||
pub use types::ToolCall;
|
||||
use crate::user::ui_channel::{UiMessage, UiSender, StreamTarget};
|
||||
use crate::user::ui_channel::UiSender;
|
||||
|
||||
/// A JoinHandle that aborts its task when dropped.
|
||||
pub struct AbortOnDrop(tokio::task::JoinHandle<()>);
|
||||
|
|
@ -130,7 +130,7 @@ impl ApiClient {
|
|||
&reasoning_effort, sampling, priority,
|
||||
).await;
|
||||
if let Err(e) = result {
|
||||
let _ = tx.send(StreamEvent::Error(e.to_string();
|
||||
let _ = tx.send(StreamEvent::Error(e.to_string()));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -207,7 +207,6 @@ pub(crate) async fn send_and_check(
|
|||
body: &impl serde::Serialize,
|
||||
auth_header: (&str, &str),
|
||||
extra_headers: &[(&str, &str)],
|
||||
ui_tx: &UiSender,
|
||||
debug_label: &str,
|
||||
request_json: Option<&str>,
|
||||
) -> Result<reqwest::Response> {
|
||||
|
|
@ -619,8 +618,6 @@ pub struct StreamResult {
|
|||
/// - UI forwarding (text deltas, reasoning, tool call notifications)
|
||||
pub async fn collect_stream(
|
||||
rx: &mut mpsc::UnboundedReceiver<StreamEvent>,
|
||||
ui_tx: &UiSender,
|
||||
target: StreamTarget,
|
||||
agent: &std::sync::Arc<tokio::sync::Mutex<super::Agent>>,
|
||||
active_tools: &crate::user::ui_channel::SharedActiveTools,
|
||||
) -> StreamResult {
|
||||
|
|
@ -633,12 +630,13 @@ pub async fn collect_stream(
|
|||
let mut error = None;
|
||||
let mut first_content = true;
|
||||
let mut display_buf = String::new();
|
||||
let mut _streaming_guard: Option<super::ActivityGuard> = None;
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
StreamEvent::Content(text) => {
|
||||
if first_content {
|
||||
if let Ok(mut ag) = agent.try_lock() { ag.activity = "streaming...".into(); }
|
||||
_streaming_guard = Some(super::start_activity(agent, "streaming...").await);
|
||||
first_content = false;
|
||||
}
|
||||
content.push_str(&text);
|
||||
|
|
@ -683,7 +681,7 @@ pub async fn collect_stream(
|
|||
if let Some(pos) = display_buf.find("<tool_call>") {
|
||||
let before = &display_buf[..pos];
|
||||
if !before.is_empty() {
|
||||
let _ = ui_tx.send(UiMessage::TextDelta(before.to_string(), target));
|
||||
if let Ok(mut ag) = agent.try_lock() { ag.append_streaming(before); }
|
||||
}
|
||||
display_buf.clear();
|
||||
in_tool_call = true;
|
||||
|
|
@ -693,7 +691,7 @@ pub async fn collect_stream(
|
|||
if safe > 0 {
|
||||
let flush = display_buf[..safe].to_string();
|
||||
display_buf = display_buf[safe..].to_string();
|
||||
let _ = ui_tx.send(UiMessage::TextDelta(flush, target));
|
||||
if let Ok(mut ag) = agent.try_lock() { ag.append_streaming(&flush); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use reqwest::Client;
|
|||
use tokio::sync::mpsc;
|
||||
|
||||
use super::types::*;
|
||||
use crate::user::ui_channel::{UiMessage, UiSender};
|
||||
use crate::user::ui_channel::UiSender;
|
||||
use super::StreamEvent;
|
||||
|
||||
/// Stream SSE events from an OpenAI-compatible endpoint, sending
|
||||
|
|
@ -66,7 +66,6 @@ pub(super) async fn stream_events(
|
|||
&request,
|
||||
("Authorization", &format!("Bearer {}", api_key)),
|
||||
&[],
|
||||
ui_tx,
|
||||
&debug_label,
|
||||
request_json.as_deref(),
|
||||
)
|
||||
|
|
@ -105,7 +104,7 @@ pub(super) async fn stream_events(
|
|||
};
|
||||
|
||||
if let Some(ref u) = chunk.usage {
|
||||
let _ = tx.send(StreamEvent::Usage(u.clone();
|
||||
let _ = tx.send(StreamEvent::Usage(u.clone()));
|
||||
usage = chunk.usage;
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +125,7 @@ pub(super) async fn stream_events(
|
|||
reasoning_chars += r.len();
|
||||
has_reasoning = true;
|
||||
if !r.is_empty() {
|
||||
let _ = tx.send(StreamEvent::Reasoning(r.clone();
|
||||
let _ = tx.send(StreamEvent::Reasoning(r.clone()));
|
||||
}
|
||||
}
|
||||
if let Some(ref r) = choice.delta.reasoning_details {
|
||||
|
|
@ -143,7 +142,7 @@ pub(super) async fn stream_events(
|
|||
first_content_at = Some(reader.stream_start.elapsed());
|
||||
}
|
||||
content_len += text_delta.len();
|
||||
let _ = tx.send(StreamEvent::Content(text_delta.clone();
|
||||
let _ = tx.send(StreamEvent::Content(text_delta.clone()));
|
||||
}
|
||||
|
||||
if let Some(ref tc_deltas) = choice.delta.tool_calls {
|
||||
|
|
|
|||
|
|
@ -202,6 +202,15 @@ impl Message {
|
|||
self.content.as_ref().map_or("", |c| c.as_text())
|
||||
}
|
||||
|
||||
/// Append text to existing content (for streaming).
|
||||
pub fn append_content(&mut self, text: &str) {
|
||||
match self.content {
|
||||
Some(MessageContent::Text(ref mut s)) => s.push_str(text),
|
||||
None => self.content = Some(MessageContent::Text(text.to_string())),
|
||||
_ => {} // Parts — don't append to multimodal
|
||||
}
|
||||
}
|
||||
|
||||
pub fn role_str(&self) -> &str {
|
||||
match self.role {
|
||||
Role::System => "system",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue