Fix agent run completion and follow-up handling
Ensure parallel tool results finish before follow-ups, preserve warping status across exchanges, clean up streams before crosscheck reviews, and restore terminal focus when conversations complete. Remove temporary debug logging.
This commit is contained in:
@@ -828,19 +828,6 @@ impl Task {
|
||||
.apply()
|
||||
.map_err(UpdateTaskError::from)?;
|
||||
|
||||
let text_len = updated_message
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|m| match m {
|
||||
api::message::Message::AgentOutput(o) => o.text.len(),
|
||||
_ => 0,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
log::info!(
|
||||
"[bedrock-debug] append_to_message_content: accumulated text_len={}",
|
||||
text_len
|
||||
);
|
||||
|
||||
let id = self.id.clone();
|
||||
let exchange_to_update = self
|
||||
.exchange_mut(exchange_id)
|
||||
@@ -1069,34 +1056,11 @@ impl AIAgentExchange {
|
||||
.iter()
|
||||
.position(|m| m.id.0 == task_message.id);
|
||||
|
||||
let proto_text = task_message
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|m| match m {
|
||||
api::message::Message::AgentOutput(o) => {
|
||||
format!("AgentOutput(text_len={})", o.text.len())
|
||||
}
|
||||
api::message::Message::ToolCall(t) => {
|
||||
format!("ToolCall(id={})", t.tool_call_id)
|
||||
}
|
||||
other => format!("{:?}", std::mem::discriminant(other)),
|
||||
})
|
||||
.unwrap_or_else(|| "None".to_string());
|
||||
log::info!(
|
||||
"[bedrock-debug] upsert_output_for_message: id={}, proto_type={}",
|
||||
task_message.id,
|
||||
proto_text
|
||||
);
|
||||
|
||||
match task_message
|
||||
.clone()
|
||||
.to_client_output_message(conversion_params)?
|
||||
{
|
||||
MaybeAIAgentOutputMessage::Message(m) => {
|
||||
log::info!(
|
||||
"[bedrock-debug] upsert_output_for_message: client_message_type={:?}",
|
||||
std::mem::discriminant(&m.message)
|
||||
);
|
||||
output.extend_citations(m.citations.clone());
|
||||
if let Some(message_idx) = message_idx {
|
||||
output.messages[message_idx] = m;
|
||||
@@ -1104,11 +1068,7 @@ impl AIAgentExchange {
|
||||
output.messages.push(m);
|
||||
}
|
||||
}
|
||||
MaybeAIAgentOutputMessage::NoClientRepresentation => {
|
||||
log::warn!(
|
||||
"[bedrock-debug] upsert_output_for_message: NoClientRepresentation for msg_id={}", task_message.id
|
||||
);
|
||||
}
|
||||
MaybeAIAgentOutputMessage::NoClientRepresentation => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1317,23 +1317,26 @@ impl BlocklistAIActionModel {
|
||||
.or_default()
|
||||
.push(action_result);
|
||||
|
||||
ctx.emit(BlocklistAIActionEvent::FinishedAction {
|
||||
action_id,
|
||||
conversation_id,
|
||||
cancellation_reason,
|
||||
});
|
||||
if self
|
||||
.running_actions
|
||||
.get(&conversation_id)
|
||||
.is_some_and(|running| !running.is_empty())
|
||||
{
|
||||
// Wait until the entire phase drains before scheduling subsequent actions or deciding
|
||||
// whether to send a follow-up request.
|
||||
// whether to send a follow-up request. In particular, don't emit `FinishedAction` yet:
|
||||
// the controller treats that event as the phase-complete signal and could otherwise
|
||||
// drain only a prefix of parallel tool results into the next LLM request.
|
||||
return;
|
||||
}
|
||||
|
||||
// The phase is fully drained — sort results back into original tool-call order.
|
||||
// The phase is fully drained — sort results back into original tool-call order before
|
||||
// notifying the controller that it may send the follow-up request.
|
||||
self.sort_finished_results(conversation_id);
|
||||
ctx.emit(BlocklistAIActionEvent::FinishedAction {
|
||||
action_id,
|
||||
conversation_id,
|
||||
cancellation_reason,
|
||||
});
|
||||
|
||||
if self
|
||||
.pending_actions
|
||||
|
||||
@@ -119,9 +119,11 @@ pub struct BlocklistAIStatusBar {
|
||||
/// the warping indicator while the active block has a recorded LRC snapshot.
|
||||
last_read_refresh_handle: Option<SpawnedFutureHandle>,
|
||||
|
||||
/// The time the warping indicator started showing for the current exchange.
|
||||
/// The time the warping indicator started showing for the current conversation run.
|
||||
/// Preserved across model follow-up exchanges triggered by tool calls.
|
||||
warping_start_time: Option<Instant>,
|
||||
/// The random loading message chosen for the current exchange, stable across re-renders.
|
||||
/// The random loading message chosen for the current conversation run, stable across
|
||||
/// re-renders and tool-call follow-up exchanges.
|
||||
warping_message: Option<&'static str>,
|
||||
/// Handle for the periodic timer that updates the warping elapsed timer UI.
|
||||
warping_timer_handle: Option<SpawnedFutureHandle>,
|
||||
@@ -197,7 +199,18 @@ impl BlocklistAIStatusBar {
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
BlocklistAIHistoryEvent::UpdatedConversationStatus { .. } => {
|
||||
BlocklistAIHistoryEvent::UpdatedConversationStatus {
|
||||
conversation_id,
|
||||
new_status,
|
||||
..
|
||||
} => {
|
||||
let is_active_conversation = me
|
||||
.active_exchange_model
|
||||
.as_ref()
|
||||
.is_some_and(|model| model.conversation_id(ctx) == Some(*conversation_id));
|
||||
if is_active_conversation && !new_status.is_in_progress() {
|
||||
me.stop_warping_timer();
|
||||
}
|
||||
ctx.notify();
|
||||
}
|
||||
BlocklistAIHistoryEvent::SetActiveConversation {
|
||||
@@ -541,11 +554,20 @@ impl BlocklistAIStatusBar {
|
||||
AIBlockOutputStatus::Pending | AIBlockOutputStatus::Failed { .. } => (),
|
||||
}
|
||||
|
||||
// Stop the warping timer when the exchange is no longer streaming/pending.
|
||||
// Placed after the match so `model` (which borrows self.active_exchange_model)
|
||||
// is no longer used, avoiding borrow conflicts with &mut self.
|
||||
// An exchange finishing does not necessarily mean the logical run is complete: tool calls
|
||||
// can execute and append a follow-up exchange. Conversation status is the authoritative
|
||||
// lifecycle signal, so preserve the timer and phrase while it remains in progress.
|
||||
// Placed after the match so `model` (which borrows self.active_exchange_model) is no longer
|
||||
// used, avoiding borrow conflicts with &mut self.
|
||||
if is_finished {
|
||||
self.stop_warping_timer();
|
||||
let conversation_is_in_progress = self
|
||||
.active_exchange_model
|
||||
.as_ref()
|
||||
.and_then(|model| model.conversation(ctx))
|
||||
.is_some_and(|conversation| conversation.status().is_in_progress());
|
||||
if !conversation_is_in_progress {
|
||||
self.stop_warping_timer();
|
||||
}
|
||||
}
|
||||
|
||||
ctx.notify();
|
||||
@@ -692,9 +714,14 @@ impl BlocklistAIStatusBar {
|
||||
}
|
||||
|
||||
/// Starts a 1-second periodic timer that keeps the warping elapsed-time indicator fresh.
|
||||
///
|
||||
/// A tool call can append several exchanges during one logical agent run. Keep the original
|
||||
/// phrase and start time across those exchanges so the status does not thrash and the elapsed
|
||||
/// time represents the complete run.
|
||||
fn start_warping_timer(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.warping_start_time = Some(Instant::now());
|
||||
self.warping_message = Some(random_load_output_message());
|
||||
self.warping_start_time.get_or_insert_with(Instant::now);
|
||||
self.warping_message
|
||||
.get_or_insert_with(random_load_output_message);
|
||||
// Don't start a new timer if one is already running
|
||||
if self.warping_timer_handle.is_some() {
|
||||
return;
|
||||
|
||||
@@ -879,7 +879,6 @@ impl View for AIBlock {
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
log::info!("[bedrock-debug] AIBlock::render() called");
|
||||
// When the AI block is hidden, we don't need to render anything.
|
||||
if self.is_hidden(app) {
|
||||
return ConstrainedBox::new(Empty::new().finish())
|
||||
|
||||
@@ -255,27 +255,6 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
| AIBlockOutputStatus::Failed { .. } => {
|
||||
if let Some(output) = status.output_to_render() {
|
||||
let output = output.get();
|
||||
let total_text_len: usize = output
|
||||
.messages
|
||||
.iter()
|
||||
.map(|m| match &m.message {
|
||||
AIAgentOutputMessageType::Text(t) => t
|
||||
.sections
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
AIAgentTextSection::PlainText { text } => text.text().len(),
|
||||
AIAgentTextSection::Code { code, .. } => code.len(),
|
||||
_ => 0,
|
||||
})
|
||||
.sum::<usize>(),
|
||||
_ => 0,
|
||||
})
|
||||
.sum();
|
||||
log::info!(
|
||||
"[bedrock-debug] render output: messages={}, total_text_len={}",
|
||||
output.messages.len(),
|
||||
total_text_len
|
||||
);
|
||||
let is_complete = matches!(status, AIBlockOutputStatus::Complete { .. });
|
||||
let is_output_for_static_prompt_suggestions =
|
||||
props.model.contains_static_prompt_suggestion_input(app);
|
||||
|
||||
@@ -3977,7 +3977,6 @@ impl BlocklistAIController {
|
||||
"[bedrock-debug] AfterStreamFinished: NO actions to queue, was_passive={}, is_any_unfinished={}",
|
||||
was_passive_request, is_any_exchange_unfinished
|
||||
);
|
||||
|
||||
// If this is a child conversation (has a parent) and the
|
||||
// stream ended with EndTurn and no actions, the child agent
|
||||
// is done. Mark it as Success so the StartAgentExecutor
|
||||
@@ -4002,17 +4001,34 @@ impl BlocklistAIController {
|
||||
});
|
||||
}
|
||||
|
||||
// Crosscheck Work experiment: if the main agent finished
|
||||
// with no actions (i.e., it produced a final response), and
|
||||
// this is not a child or passive conversation, trigger a
|
||||
// reviewer sub-agent to critique the output.
|
||||
if !is_child && !was_passive_request {
|
||||
// Crosscheck Work experiment: remember that the main agent produced a final
|
||||
// response. Start the reviewer only after stream cleanup below so fast reviewer
|
||||
// feedback cannot race the stale in-flight response-stream entry.
|
||||
let should_trigger_crosscheck = !is_child && !was_passive_request;
|
||||
|
||||
// Remove the completed stream before starting the reviewer. A fast reviewer can
|
||||
// otherwise return feedback while this stream is still considered in flight,
|
||||
// causing `send_request_input` to reject and silently drop the correction turn.
|
||||
if cancellation.is_none() {
|
||||
self.in_flight_response_streams.cleanup_stream(&stream_id);
|
||||
|
||||
// Now that the stream is cleaned up, re-check for pending
|
||||
// orchestration events that couldn't be drained earlier.
|
||||
self.handle_pending_events_ready(conversation_id, ctx);
|
||||
}
|
||||
|
||||
if should_trigger_crosscheck {
|
||||
self.maybe_trigger_crosscheck(conversation_id, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Cancelled streams will handle pending_response_stream updates synchronously.
|
||||
if cancellation.is_none() {
|
||||
// Cancelled streams handle pending-response-stream updates synchronously. The
|
||||
// no-action crosscheck path above also cleans up early before starting its reviewer.
|
||||
if cancellation.is_none()
|
||||
&& self
|
||||
.in_flight_response_streams
|
||||
.has_active_stream_for_conversation(conversation_id, ctx)
|
||||
{
|
||||
self.in_flight_response_streams.cleanup_stream(&stream_id);
|
||||
|
||||
// Now that the stream is cleaned up, re-check for pending
|
||||
|
||||
@@ -6439,6 +6439,19 @@ impl TerminalView {
|
||||
|
||||
self.maybe_send_agent_mode_desktop_notification(conversation_id, ctx);
|
||||
|
||||
// Once the complete agent run is terminal (including all tool-call follow-ups),
|
||||
// restore the input caret if focus is still within this terminal view. Do not steal
|
||||
// focus from another pane or window if the user moved on while the agent was busy.
|
||||
if matches!(
|
||||
new_status,
|
||||
ConversationStatus::Success
|
||||
| ConversationStatus::Error
|
||||
| ConversationStatus::Cancelled
|
||||
) && ctx.is_self_or_child_focused()
|
||||
{
|
||||
self.redetermine_global_focus(ctx);
|
||||
}
|
||||
|
||||
// Show AI credits modal for cloud-mode out-of-credits failures.
|
||||
if FeatureFlag::CloudMode.is_enabled()
|
||||
&& self.is_ambient_agent_session(ctx)
|
||||
|
||||
Reference in New Issue
Block a user