mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(auto-drive): stream coordinator reasoning
This commit is contained in:
@@ -1619,6 +1619,11 @@ impl App<'_> {
|
|||||||
widget.auto_handle_decision(status, thoughts, prompt);
|
widget.auto_handle_decision(status, thoughts, prompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppEvent::AutoCoordinatorThinking { delta } => {
|
||||||
|
if let AppState::Chat { widget } = &mut self.app_state {
|
||||||
|
widget.auto_handle_thinking(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
AppEvent::AutoCoordinatorCountdown { countdown_id, seconds_left } => {
|
AppEvent::AutoCoordinatorCountdown { countdown_id, seconds_left } => {
|
||||||
if let AppState::Chat { widget } = &mut self.app_state {
|
if let AppState::Chat { widget } = &mut self.app_state {
|
||||||
widget.auto_handle_countdown(countdown_id, seconds_left);
|
widget.auto_handle_countdown(countdown_id, seconds_left);
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ pub(crate) enum AppEvent {
|
|||||||
thoughts: String,
|
thoughts: String,
|
||||||
prompt: Option<String>,
|
prompt: Option<String>,
|
||||||
},
|
},
|
||||||
|
AutoCoordinatorThinking {
|
||||||
|
delta: String,
|
||||||
|
},
|
||||||
AutoCoordinatorCountdown {
|
AutoCoordinatorCountdown {
|
||||||
countdown_id: u64,
|
countdown_id: u64,
|
||||||
seconds_left: u8,
|
seconds_left: u8,
|
||||||
|
|||||||
@@ -70,3 +70,8 @@ pub fn next_auto_drive_phrase() -> &'static str {
|
|||||||
% AUTO_DRIVE_PHRASES.len();
|
% AUTO_DRIVE_PHRASES.len();
|
||||||
AUTO_DRIVE_PHRASES[idx]
|
AUTO_DRIVE_PHRASES[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_auto_drive_phrase(candidate: &str) -> bool {
|
||||||
|
let trimmed = candidate.trim();
|
||||||
|
AUTO_DRIVE_PHRASES.iter().any(|phrase| phrase == &trimmed)
|
||||||
|
}
|
||||||
|
|||||||
@@ -325,7 +325,6 @@ impl AutoCoordinatorView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hint_line) = self.manual_hint_line(ctx) {
|
if let Some(hint_line) = self.manual_hint_line(ctx) {
|
||||||
lines.push(Line::default());
|
|
||||||
lines.push(hint_line);
|
lines.push(hint_line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ struct AutoCoordinatorUiState {
|
|||||||
countdown_id: u64,
|
countdown_id: u64,
|
||||||
seconds_remaining: u8,
|
seconds_remaining: u8,
|
||||||
awaiting_goal_input: bool,
|
awaiting_goal_input: bool,
|
||||||
|
last_broadcast_thought: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AutoCoordinatorUiState {
|
impl AutoCoordinatorUiState {
|
||||||
@@ -6711,7 +6712,6 @@ impl ChatWidget<'_> {
|
|||||||
ok
|
ok
|
||||||
);
|
);
|
||||||
self.seed_stream_order_key(StreamKind::Reasoning, &id, ok);
|
self.seed_stream_order_key(StreamKind::Reasoning, &id, ok);
|
||||||
// Stream reasoning delta through StreamController
|
|
||||||
streaming::delta_text(
|
streaming::delta_text(
|
||||||
self,
|
self,
|
||||||
StreamKind::Reasoning,
|
StreamKind::Reasoning,
|
||||||
@@ -6838,6 +6838,7 @@ impl ChatWidget<'_> {
|
|||||||
self.maybe_hide_spinner();
|
self.maybe_hide_spinner();
|
||||||
self.emit_turn_complete_notification(last_agent_message);
|
self.emit_turn_complete_notification(last_agent_message);
|
||||||
self.mark_needs_redraw();
|
self.mark_needs_redraw();
|
||||||
|
|
||||||
}
|
}
|
||||||
EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent {
|
EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent {
|
||||||
delta,
|
delta,
|
||||||
@@ -9424,7 +9425,9 @@ impl ChatWidget<'_> {
|
|||||||
self.auto_state
|
self.auto_state
|
||||||
.current_thoughts
|
.current_thoughts
|
||||||
.replace(auto_drive_strings::next_auto_drive_phrase().to_string());
|
.replace(auto_drive_strings::next_auto_drive_phrase().to_string());
|
||||||
|
self.auto_state.last_broadcast_thought = None;
|
||||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||||
|
self.auto_state.waiting_for_response = true;
|
||||||
self.header_wave.set_enabled(true, Instant::now());
|
self.header_wave.set_enabled(true, Instant::now());
|
||||||
self.auto_rebuild_live_ring();
|
self.auto_rebuild_live_ring();
|
||||||
self.push_background_tail(format!("Auto Drive started: {goal_text}"));
|
self.push_background_tail(format!("Auto Drive started: {goal_text}"));
|
||||||
@@ -9449,6 +9452,12 @@ impl ChatWidget<'_> {
|
|||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
self.auto_stop(Some("Coordinator stopped unexpectedly.".to_string()));
|
self.auto_stop(Some("Coordinator stopped unexpectedly.".to_string()));
|
||||||
|
} else {
|
||||||
|
self.auto_state.waiting_for_response = true;
|
||||||
|
self.auto_state.current_thoughts = None;
|
||||||
|
self.auto_state.last_broadcast_thought = None;
|
||||||
|
self.auto_rebuild_live_ring();
|
||||||
|
self.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9462,7 +9471,7 @@ impl ChatWidget<'_> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.auto_state.current_thoughts = Some(thoughts.clone());
|
self.auto_on_reasoning_final(&thoughts);
|
||||||
self.auto_state.paused_for_manual_edit = false;
|
self.auto_state.paused_for_manual_edit = false;
|
||||||
self.auto_state.resume_after_manual_submit = false;
|
self.auto_state.resume_after_manual_submit = false;
|
||||||
self.auto_state.awaiting_submission = false;
|
self.auto_state.awaiting_submission = false;
|
||||||
@@ -9549,6 +9558,13 @@ impl ChatWidget<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn auto_handle_thinking(&mut self, delta: String) {
|
||||||
|
if !self.auto_state.active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.auto_on_reasoning_delta(&delta);
|
||||||
|
}
|
||||||
|
|
||||||
fn auto_submit_prompt(&mut self) {
|
fn auto_submit_prompt(&mut self) {
|
||||||
if !self.auto_state.active {
|
if !self.auto_state.active {
|
||||||
return;
|
return;
|
||||||
@@ -9567,6 +9583,8 @@ impl ChatWidget<'_> {
|
|||||||
self.auto_state.paused_for_manual_edit = false;
|
self.auto_state.paused_for_manual_edit = false;
|
||||||
self.auto_state.resume_after_manual_submit = false;
|
self.auto_state.resume_after_manual_submit = false;
|
||||||
self.auto_state.seconds_remaining = 0;
|
self.auto_state.seconds_remaining = 0;
|
||||||
|
self.auto_state.current_thoughts = None;
|
||||||
|
self.auto_state.last_broadcast_thought = None;
|
||||||
self.bottom_pane.update_status_text(String::new());
|
self.bottom_pane.update_status_text(String::new());
|
||||||
self.bottom_pane.set_task_running(false);
|
self.bottom_pane.set_task_running(false);
|
||||||
self.submit_text_message(prompt);
|
self.submit_text_message(prompt);
|
||||||
@@ -9742,56 +9760,74 @@ impl ChatWidget<'_> {
|
|||||||
.send(AppEvent::ScheduleFrameIn(Duration::from_millis(interval)));
|
.send(AppEvent::ScheduleFrameIn(Duration::from_millis(interval)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auto_collect_thinking_titles(
|
fn auto_broadcast_thoughts(&mut self, raw: &str) {
|
||||||
cell: &history_cell::CollapsibleReasoningCell,
|
if !self.auto_state.active {
|
||||||
) -> Option<String> {
|
|
||||||
let mut titles: Vec<String> = cell
|
|
||||||
.display_lines()
|
|
||||||
.into_iter()
|
|
||||||
.map(|line| {
|
|
||||||
line.spans
|
|
||||||
.iter()
|
|
||||||
.map(|span| span.content.as_ref())
|
|
||||||
.collect::<String>()
|
|
||||||
.trim()
|
|
||||||
.to_string()
|
|
||||||
})
|
|
||||||
.filter(|text| !text.is_empty())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if titles.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
titles.reverse();
|
|
||||||
Some(titles.join("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auto_update_thinking_titles_if_new(&mut self, joined: Option<String>) {
|
|
||||||
if !self.auto_state.active || !self.auto_state.waiting_for_response {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(joined) = joined else {
|
let Some(first_line) = raw
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
(!trimmed.is_empty()).then_some(trimmed.to_string())
|
||||||
|
})
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.auto_state
|
.auto_state
|
||||||
.current_thoughts
|
.last_broadcast_thought
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|existing| existing == &joined)
|
.map(|prev| prev == &first_line)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(first_line) = joined.lines().next().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
self.auto_state.last_broadcast_thought = Some(first_line.clone());
|
||||||
self.push_background_tail(format!("Auto Drive: {first_line}"));
|
self.push_background_tail(format!("Auto Drive: {first_line}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_on_reasoning_delta(&mut self, delta: &str) {
|
||||||
|
if !self.auto_state.active || delta.trim().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = {
|
||||||
|
let entry = self
|
||||||
|
.auto_state
|
||||||
|
.current_thoughts
|
||||||
|
.get_or_insert_with(String::new);
|
||||||
|
|
||||||
|
if auto_drive_strings::is_auto_drive_phrase(entry) {
|
||||||
|
entry.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.push_str(delta);
|
||||||
|
entry.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.auto_broadcast_thoughts(&payload);
|
||||||
|
|
||||||
|
if self.auto_state.waiting_for_response {
|
||||||
|
self.auto_rebuild_live_ring();
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_on_reasoning_final(&mut self, text: &str) {
|
||||||
|
if !self.auto_state.active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.auto_state.current_thoughts = Some(text.to_string());
|
||||||
|
self.auto_broadcast_thoughts(text);
|
||||||
|
|
||||||
|
if self.auto_state.waiting_for_response {
|
||||||
|
self.auto_rebuild_live_ring();
|
||||||
|
self.request_redraw();
|
||||||
}
|
}
|
||||||
self.auto_state.current_thoughts = Some(joined);
|
|
||||||
self.auto_rebuild_live_ring();
|
|
||||||
self.request_redraw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn truncate_with_ellipsis(text: &str, max_chars: usize) -> String {
|
fn truncate_with_ellipsis(text: &str, max_chars: usize) -> String {
|
||||||
@@ -12065,13 +12101,10 @@ impl ChatWidget<'_> {
|
|||||||
);
|
);
|
||||||
reasoning_cell.append_lines_dedup(lines);
|
reasoning_cell.append_lines_dedup(lines);
|
||||||
reasoning_cell.set_in_progress(true);
|
reasoning_cell.set_in_progress(true);
|
||||||
let updated_titles =
|
|
||||||
Self::auto_collect_thinking_titles(&*reasoning_cell);
|
|
||||||
self.invalidate_height_cache();
|
self.invalidate_height_cache();
|
||||||
self.autoscroll_if_near_bottom();
|
self.autoscroll_if_near_bottom();
|
||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
self.refresh_reasoning_collapsed_visibility();
|
self.refresh_reasoning_collapsed_visibility();
|
||||||
self.auto_update_thinking_titles_if_new(updated_titles);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12095,13 +12128,10 @@ impl ChatWidget<'_> {
|
|||||||
);
|
);
|
||||||
reasoning_cell.append_lines_dedup(lines);
|
reasoning_cell.append_lines_dedup(lines);
|
||||||
reasoning_cell.set_in_progress(true);
|
reasoning_cell.set_in_progress(true);
|
||||||
let updated_titles =
|
|
||||||
Self::auto_collect_thinking_titles(&*reasoning_cell);
|
|
||||||
self.invalidate_height_cache();
|
self.invalidate_height_cache();
|
||||||
self.autoscroll_if_near_bottom();
|
self.autoscroll_if_near_bottom();
|
||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
self.refresh_reasoning_collapsed_visibility();
|
self.refresh_reasoning_collapsed_visibility();
|
||||||
self.auto_update_thinking_titles_if_new(updated_titles);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -12143,14 +12173,7 @@ impl ChatWidget<'_> {
|
|||||||
if let Some(rid) = id {
|
if let Some(rid) = id {
|
||||||
self.reasoning_index.insert(rid, idx);
|
self.reasoning_index.insert(rid, idx);
|
||||||
}
|
}
|
||||||
if let Some(reasoning_cell) = self.history_cells[idx]
|
// Auto Drive status updates are handled via coordinator decisions.
|
||||||
.as_any()
|
|
||||||
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
|
|
||||||
{
|
|
||||||
let updated_titles =
|
|
||||||
Self::auto_collect_thinking_titles(reasoning_cell);
|
|
||||||
self.auto_update_thinking_titles_if_new(updated_titles);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
StreamKind::Answer => {
|
StreamKind::Answer => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ fn run_auto_loop(
|
|||||||
&developer_intro,
|
&developer_intro,
|
||||||
&schema,
|
&schema,
|
||||||
conv,
|
conv,
|
||||||
|
&app_event_tx,
|
||||||
) {
|
) {
|
||||||
Ok((status, thoughts, prompt_opt)) => {
|
Ok((status, thoughts, prompt_opt)) => {
|
||||||
let event = AppEvent::AutoCoordinatorDecision {
|
let event = AppEvent::AutoCoordinatorDecision {
|
||||||
@@ -199,6 +200,7 @@ fn request_coordinator_decision(
|
|||||||
developer_intro: &str,
|
developer_intro: &str,
|
||||||
schema: &Value,
|
schema: &Value,
|
||||||
mut conversation: Vec<ResponseItem>,
|
mut conversation: Vec<ResponseItem>,
|
||||||
|
app_event_tx: &AppEventSender,
|
||||||
) -> Result<(AutoCoordinatorStatus, String, Option<String>)> {
|
) -> Result<(AutoCoordinatorStatus, String, Option<String>)> {
|
||||||
let mut prompt = Prompt::default();
|
let mut prompt = Prompt::default();
|
||||||
prompt.store = true;
|
prompt.store = true;
|
||||||
@@ -215,7 +217,7 @@ fn request_coordinator_decision(
|
|||||||
.unwrap_or_else(|| derive_default_model_family(MODEL_SLUG));
|
.unwrap_or_else(|| derive_default_model_family(MODEL_SLUG));
|
||||||
prompt.model_family_override = Some(family);
|
prompt.model_family_override = Some(family);
|
||||||
|
|
||||||
let raw = request_decision(runtime, client, &prompt)?;
|
let raw = request_decision(runtime, client, &prompt, app_event_tx)?;
|
||||||
let (decision, value) = parse_decision(&raw)?;
|
let (decision, value) = parse_decision(&raw)?;
|
||||||
debug!("[Auto coordinator] model decision: {:?}", value);
|
debug!("[Auto coordinator] model decision: {:?}", value);
|
||||||
|
|
||||||
@@ -249,8 +251,10 @@ fn request_decision(
|
|||||||
runtime: &tokio::runtime::Runtime,
|
runtime: &tokio::runtime::Runtime,
|
||||||
client: &ModelClient,
|
client: &ModelClient,
|
||||||
prompt: &Prompt,
|
prompt: &Prompt,
|
||||||
|
app_event_tx: &AppEventSender,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
runtime.block_on(async {
|
let tx = app_event_tx.clone();
|
||||||
|
runtime.block_on(async move {
|
||||||
let mut stream = client.stream(prompt).await?;
|
let mut stream = client.stream(prompt).await?;
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
while let Some(ev) = stream.next().await {
|
while let Some(ev) = stream.next().await {
|
||||||
@@ -265,6 +269,11 @@ fn request_decision(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(ResponseEvent::ReasoningSummaryDelta { delta, .. })
|
||||||
|
| Ok(ResponseEvent::ReasoningContentDelta { delta, .. }) => {
|
||||||
|
let message = strip_role_prefix(&delta).to_string();
|
||||||
|
tx.send(AppEvent::AutoCoordinatorThinking { delta: message });
|
||||||
|
}
|
||||||
Ok(ResponseEvent::Completed { .. }) => break,
|
Ok(ResponseEvent::Completed { .. }) => break,
|
||||||
Err(err) => return Err(anyhow!("model stream error: {err}")),
|
Err(err) => return Err(anyhow!("model stream error: {err}")),
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -2419,9 +2419,7 @@ fn popular_commands_lines(_latest_version: Option<&str>) -> Vec<Line<'static>> {
|
|||||||
.style(Style::default().add_modifier(Modifier::DIM)),
|
.style(Style::default().add_modifier(Modifier::DIM)),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
" Experimental",
|
" Experimental",
|
||||||
Style::default()
|
Style::default().fg(crate::colors::primary()),
|
||||||
.fg(crate::colors::primary())
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user