Route Bedrock through Rig and improve agent observability

This commit is contained in:
2026-08-22 12:02:45 -05:00
parent 1f1d0737a9
commit f291cfe803
28 changed files with 519 additions and 938 deletions
+5
View File
@@ -1018,6 +1018,10 @@ impl BlocklistAIStatusBar {
.as_ref(app)
.provider_retry_status(conversation.id())
.map(|status| status.label());
let model_progress_text = self
.controller
.as_ref(app)
.provider_tool_call_progress_label(conversation.id());
let secondary_element = if fallback_warping_text.is_some() {
Some(render_fallback_explanation(model.as_ref(), app))
} else {
@@ -1077,6 +1081,7 @@ impl BlocklistAIStatusBar {
force_refresh_button,
default_warping_text,
retry_status_text,
model_progress_text,
secondary_element,
last_snapshot_at,
warping_start_time: self.warping_start_time,
@@ -219,6 +219,7 @@ pub struct WarpingProps<'a, V> {
pub terminal_model: &'a TerminalModel,
pub default_warping_text: String,
pub retry_status_text: Option<String>,
pub model_progress_text: Option<String>,
pub secondary_element: Option<Box<dyn Element>>,
/// When an LRC subagent has sent at least one snapshot, the timestamp of the most recent snapshot.
pub last_snapshot_at: Option<instant::Instant>,
@@ -479,6 +480,13 @@ pub fn render_warping_indicator<V: View>(
}
};
if let Some(model_progress_text) = props.model_progress_text.as_deref() {
non_shimmering_text = Some(match non_shimmering_text {
Some(text) if !text.is_empty() => format!("{text} · {model_progress_text}"),
Some(_) | None => format!(" · {model_progress_text}"),
});
}
if let Some(retry_status_text) = props.retry_status_text.as_deref() {
non_shimmering_text = Some(match non_shimmering_text {
Some(text) if !text.is_empty() => format!("{text} {retry_status_text}"),
+106 -11
View File
@@ -266,6 +266,7 @@ async fn collect_progressive_summary(
}
AgentEvent::TurnStarted { .. }
| AgentEvent::KeepAlive
| AgentEvent::ToolCallProgress { .. }
| AgentEvent::ReasoningDelta { .. }
| AgentEvent::ReasoningCompleted { .. }
| AgentEvent::RuntimeActivityUpdated { .. }
@@ -848,6 +849,60 @@ impl ProviderRetryStatus {
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ProviderToolCallProgressStatus {
call_id: String,
name: Option<String>,
arguments_bytes: u64,
}
impl ProviderToolCallProgressStatus {
fn label(&self) -> String {
let activity = match self.name.as_deref() {
Some("apply_file_diffs") => "Preparing file edit",
Some(name) if name.starts_with("mcp__") => "Preparing MCP tool call",
Some(_) | None => "Preparing tool call",
};
if self.arguments_bytes == 0 {
return format!("{activity}");
}
let kibibytes = self.arguments_bytes.saturating_add(1023) / 1024;
format!("{activity}… ({kibibytes} KB received)")
}
}
enum ProviderToolCallProgressUpdate {
Unchanged,
Set(ProviderToolCallProgressStatus),
Clear,
}
fn provider_tool_call_progress_update(
projection: &ProviderRunProjection,
) -> ProviderToolCallProgressUpdate {
match projection {
ProviderRunProjection::ModelEvent {
event:
AgentEvent::ToolCallProgress {
call_id,
name,
arguments_bytes,
},
..
} => ProviderToolCallProgressUpdate::Set(ProviderToolCallProgressStatus {
call_id: call_id.clone(),
name: name.clone(),
arguments_bytes: *arguments_bytes,
}),
ProviderRunProjection::ModelTurnRequested { .. }
| ProviderRunProjection::ModelTurnFinished { .. }
| ProviderRunProjection::ModelRetry { .. }
| ProviderRunProjection::ToolBatchReady { .. } => ProviderToolCallProgressUpdate::Clear,
ProviderRunProjection::ModelTurnStarted { .. }
| ProviderRunProjection::ModelEvent { .. } => ProviderToolCallProgressUpdate::Unchanged,
}
}
struct ActiveProviderRunSlot {
stream_id: ResponseStreamId,
response_stream: ModelHandle<ResponseStream>,
@@ -867,6 +922,7 @@ struct ActiveProviderRunSlot {
pending_command_completion: Option<PendingProviderCommandCompletion>,
monitor_prose_continuations: usize,
retry_status: Option<ProviderRetryStatus>,
tool_call_progress: Option<ProviderToolCallProgressStatus>,
}
struct QueuedProviderRun {
@@ -1906,6 +1962,7 @@ enum ProviderDriveMessage {
Projection {
lifecycle: Option<ProviderLlmLifecycle>,
latest_usage: Option<Usage>,
tool_call_progress: ProviderToolCallProgressUpdate,
events: Vec<warp_multi_agent_api::ResponseEvent>,
acknowledgement: oneshot::Sender<Result<(), String>>,
},
@@ -2098,6 +2155,16 @@ impl BlocklistAIController {
.and_then(|slot| slot.retry_status)
}
pub(crate) fn provider_tool_call_progress_label(
&self,
conversation_id: AIConversationId,
) -> Option<String> {
self.active_provider_runs
.get(&conversation_id)
.and_then(|slot| slot.tool_call_progress.as_ref())
.map(ProviderToolCallProgressStatus::label)
}
fn has_unresolved_ask_user_question(
&self,
conversation_id: AIConversationId,
@@ -5137,6 +5204,7 @@ impl BlocklistAIController {
pending_command_completion: None,
monitor_prose_continuations: 0,
retry_status: None,
tool_call_progress: None,
};
match self.active_provider_runs.entry(conversation_data.id) {
Entry::Occupied(_) => {
@@ -5759,6 +5827,7 @@ impl BlocklistAIController {
pending_command_completion,
monitor_prose_continuations,
retry_status: None,
tool_call_progress: None,
},
);
if let Err(error) =
@@ -5963,6 +6032,7 @@ impl BlocklistAIController {
pending_command_completion: None,
monitor_prose_continuations: 0,
retry_status: None,
tool_call_progress: None,
},
base_provider_config,
cli_provider_config,
@@ -6316,6 +6386,8 @@ impl BlocklistAIController {
turn_control,
|projection| {
let lifecycle = provider_llm_lifecycle(&projection);
let tool_call_progress =
provider_tool_call_progress_update(&projection);
let latest_usage = match &projection {
ProviderRunProjection::ModelEvent {
event: AgentEvent::UsageUpdated { usage },
@@ -6337,6 +6409,7 @@ impl BlocklistAIController {
.send(ProviderDriveMessage::Projection {
lifecycle,
latest_usage,
tool_call_progress,
events,
acknowledgement,
})
@@ -6396,6 +6469,7 @@ impl BlocklistAIController {
ProviderDriveMessage::Projection {
lifecycle,
latest_usage,
tool_call_progress,
events,
acknowledgement,
} => {
@@ -6421,8 +6495,29 @@ impl BlocklistAIController {
break;
}
}
let progress_changed = {
let slot = self
.active_provider_runs
.get_mut(&conversation_id)
.expect("provider projection retained its active run slot");
match tool_call_progress {
ProviderToolCallProgressUpdate::Unchanged => false,
ProviderToolCallProgressUpdate::Set(progress) => {
let changed = slot
.tool_call_progress
.as_ref()
.is_none_or(|current| current.label() != progress.label());
slot.tool_call_progress = Some(progress);
changed
}
ProviderToolCallProgressUpdate::Clear => {
slot.tool_call_progress.take().is_some()
}
}
};
let mut should_refresh_status = progress_changed;
if let Some(lifecycle) = lifecycle.as_ref() {
let should_refresh_status = {
should_refresh_status |= {
let slot = self
.active_provider_runs
.get_mut(&conversation_id)
@@ -6451,16 +6546,6 @@ impl BlocklistAIController {
}
}
};
if should_refresh_status {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
history.update_conversation_status(
self.terminal_surface_id,
conversation_id,
ConversationStatus::InProgress,
ctx,
);
});
}
#[cfg(not(target_family = "wasm"))]
remote_logging::log_model_event(
ctx,
@@ -6471,6 +6556,16 @@ impl BlocklistAIController {
),
);
}
if should_refresh_status {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
history.update_conversation_status(
self.terminal_surface_id,
conversation_id,
ConversationStatus::InProgress,
ctx,
);
});
}
let _ = acknowledgement.send(result);
}
ProviderDriveMessage::Checkpoint {
+6
View File
@@ -468,6 +468,7 @@ fn queued_provider_follow_up_persists_while_active_slot_is_unprepared() {
pending_command_completion: None,
monitor_prose_continuations: 0,
retry_status: None,
tool_call_progress: None,
},
);
controller
@@ -494,6 +495,7 @@ fn queued_provider_follow_up_persists_while_active_slot_is_unprepared() {
pending_command_completion: None,
monitor_prose_continuations: 0,
retry_status: None,
tool_call_progress: None,
},
base_provider_config: crate::ai::provider::ProviderConfig::None,
cli_provider_config: crate::ai::provider::ProviderConfig::None,
@@ -1044,6 +1046,7 @@ fn cancelling_provider_startup_keeps_slot_and_durable_cancellation() {
pending_command_completion: None,
monitor_prose_continuations: 0,
retry_status: None,
tool_call_progress: None,
},
);
@@ -1143,6 +1146,7 @@ fn same_conversation_follow_up_waits_for_cancelled_provider_generation_cleanup()
pending_command_completion: None,
monitor_prose_continuations: 0,
retry_status: None,
tool_call_progress: None,
},
);
controller
@@ -1175,6 +1179,7 @@ fn same_conversation_follow_up_waits_for_cancelled_provider_generation_cleanup()
pending_command_completion: None,
monitor_prose_continuations: 0,
retry_status: None,
tool_call_progress: None,
},
base_provider_config: crate::ai::provider::ProviderConfig::None,
cli_provider_config: crate::ai::provider::ProviderConfig::None,
@@ -1285,6 +1290,7 @@ fn non_follow_up_provider_cancellation_does_not_admit_an_overlapping_generation(
pending_command_completion: None,
monitor_prose_continuations: 0,
retry_status: None,
tool_call_progress: None,
},
);
@@ -341,9 +341,9 @@ pub struct RequestedCommandView {
autoexecute_readonly_commands_speedbump_checkbox_handle: MouseStateHandle,
manage_autonomy_settings_link_handle: MouseStateHandle,
// Selection support for MCP tool call detail text
mcp_content_selection_handle: SelectionHandle,
mcp_content_selected_text: Arc<std::sync::RwLock<Option<String>>>,
// Selection support for scrollable command output and MCP tool-call detail text.
content_selection_handle: SelectionHandle,
content_selected_text: Arc<std::sync::RwLock<Option<String>>>,
// Structured request data and per-tree expansion state for JSON tree rendering.
// `mcp_request` is populated from the stream as soon as the tool name
@@ -598,8 +598,8 @@ impl RequestedCommandView {
position_id_prefix,
terminal_model,
ai_block_view_id,
mcp_content_selection_handle: SelectionHandle::default(),
mcp_content_selected_text: Arc::new(std::sync::RwLock::new(None)),
content_selection_handle: SelectionHandle::default(),
content_selected_text: Arc::new(std::sync::RwLock::new(None)),
mcp_request: None,
mcp_request_tree_state: Default::default(),
mcp_response_tree_state: Default::default(),
@@ -1092,10 +1092,10 @@ impl RequestedCommandView {
/// Returns the currently selected text.
pub fn selected_text(&self, ctx: &AppContext) -> Option<String> {
// Check MCP content selection first, then fall back to editor selection.
if let Ok(mcp_selection) = self.mcp_content_selected_text.read() {
if mcp_selection.is_some() {
return mcp_selection.clone();
// Check rendered pane content first, then fall back to editor selection.
if let Ok(content_selection) = self.content_selected_text.read() {
if content_selection.is_some() {
return content_selection.clone();
}
}
self.editor
@@ -1104,11 +1104,11 @@ impl RequestedCommandView {
}
pub fn clear_selection(&mut self, ctx: &mut ViewContext<Self>) {
// Clear MCP content selection if it exists, else fall back to editor selection.
self.mcp_content_selection_handle.clear();
if let Ok(mut mcp_selection) = self.mcp_content_selected_text.write() {
*mcp_selection = None;
} else if let Some(editor) = &self.editor {
self.content_selection_handle.clear();
if let Ok(mut content_selection) = self.content_selected_text.write() {
*content_selection = None;
}
if let Some(editor) = &self.editor {
editor.update(ctx, |editor, ctx| {
editor.clear_selection(ctx);
});
@@ -1630,12 +1630,12 @@ impl View for RequestedCommandView {
.with_selectable(true)
.finish();
let mcp_selected_text = self.mcp_content_selected_text.clone();
let content_selected_text = self.content_selected_text.clone();
let selectable_text = SelectableArea::new(
self.mcp_content_selection_handle.clone(),
self.content_selection_handle.clone(),
#[allow(clippy::unwrap_used)]
move |selection_args, _, _| {
*mcp_selected_text.write().unwrap() = selection_args.selection;
*content_selected_text.write().unwrap() = selection_args.selection;
},
text_element,
)
@@ -1661,10 +1661,23 @@ impl View for RequestedCommandView {
app,
)
.finish();
let content_selected_text = self.content_selected_text.clone();
let selectable_output = SelectableArea::new(
self.content_selection_handle.clone(),
#[allow(clippy::unwrap_used)]
move |selection_args, _, _| {
*content_selected_text.write().unwrap() = selection_args.selection;
},
output_text,
)
.on_selection_updated(|ctx, _| {
ctx.dispatch_typed_action(RequestedCommandViewAction::SelectText);
})
.finish();
let scrollable = NewScrollable::vertical(
SingleAxisConfig::Clipped {
handle: self.output_scroll_state.clone(),
child: output_text,
child: selectable_output,
},
Fill::None,
Fill::None,