Fix orchestration and provider reliability

This commit is contained in:
Ryan Ward
2026-08-20 15:13:19 -05:00
parent c585961149
commit 1336f00dfb
17 changed files with 418 additions and 58 deletions
@@ -18,6 +18,7 @@ use crate::ai::agent_conversations_model::entry::AgentConversationEntryId;
use crate::ai::agent_conversations_model::{
AgentConversationNavigationSubject, AgentConversationsModel,
};
use crate::ai::blocklist::orchestration_topology::descendant_conversation_ids_in_spawn_order;
use crate::ai::blocklist::BlocklistAIHistoryModel;
use crate::terminal::view::TerminalAction;
use crate::ui_components::blended_colors;
@@ -54,6 +55,57 @@ pub(crate) fn conversation_id_for_agent_id(
})
}
/// Resolve an agent identifier within the active orchestration tree first.
/// Agent ids are server-facing identifiers and can be reused by separate
/// orchestrators, so the global index is only a fallback for legacy output.
pub(crate) fn conversation_id_for_agent_id_in_orchestration(
agent_id: &str,
orchestrator_id: AIConversationId,
app: &AppContext,
) -> Option<AIConversationId> {
let canonical_id = canonical_agent_id(agent_id);
let history = BlocklistAIHistoryModel::as_ref(app);
let matches = |conversation: &AIConversation| {
conversation
.orchestration_agent_id()
.as_deref()
.is_some_and(|id| id == canonical_id)
|| conversation.run_id().is_some_and(|id| id == canonical_id)
|| conversation
.server_conversation_token()
.is_some_and(|token| token.as_str() == canonical_id)
};
std::iter::once(orchestrator_id)
.chain(descendant_conversation_ids_in_spawn_order(
history,
orchestrator_id,
))
.find(|conversation_id| {
history
.conversation(conversation_id)
.is_some_and(|conversation| matches(conversation))
})
.or_else(|| conversation_id_for_agent_id(canonical_id, app))
}
/// Resolve an agent id relative to the conversation currently shown by a
/// terminal view. Inline orchestration cards must use this instead of the
/// global agent-id index because separate orchestrators can have overlapping
/// server-facing child identifiers.
pub(crate) fn conversation_id_for_agent_id_in_terminal_view(
agent_id: &str,
terminal_view_id: EntityId,
app: &AppContext,
) -> Option<AIConversationId> {
let history = BlocklistAIHistoryModel::as_ref(app);
let active_conversation = history.active_conversation(terminal_view_id)?;
let orchestrator_id = active_conversation
.parent_conversation_id()
.unwrap_or_else(|| active_conversation.id());
conversation_id_for_agent_id_in_orchestration(agent_id, orchestrator_id, app)
}
/// True if the conversation is open in some other visible pane. Hidden
/// child-agent panes are excluded so unopened children don't look
/// "already open".
@@ -114,8 +166,13 @@ pub(crate) fn dispatch_focus_or_open_child_agent_pane(
let self_pane_group_id =
pane_group_id_containing_terminal_view(self_terminal_view_id, app);
if Some(owner_pane_group_id) == self_pane_group_id {
// Same pane group: swap to the child pane in place.
ctx.dispatch_typed_action(TerminalAction::RevealChildAgent { conversation_id });
} else {
// Different pane group: focus the exact canonical owner.
// The conversation id was resolved from the source
// orchestration tree, so this cannot select a same-named
// child belonging to another orchestrator.
ctx.dispatch_typed_action(WorkspaceAction::FocusTerminalViewInWorkspace {
terminal_view_id: owner_view_id,
});
+2 -1
View File
@@ -4756,11 +4756,12 @@ impl AIBlock {
) {
ctx.subscribe_to_model(action_model, |me, action_model, event, ctx| {
let action_id = event.action_id();
let is_finished_action = matches!(event, BlocklistAIActionEvent::FinishedAction { .. });
if event
.conversation_id()
.is_some_and(|conversation_id| conversation_id != me.client_ids.conversation_id)
|| me.is_finished()
|| (me.is_finished() && !is_finished_action)
|| !me.requested_action_ids.contains(action_id)
{
// Technically, this subscription should be unregistered after `is_finished` is
+4 -1
View File
@@ -209,7 +209,10 @@ impl BlocklistAIStatusBar {
.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() {
if is_active_conversation
&& !new_status.is_in_progress()
&& !new_status.is_transient_error()
{
me.stop_warping_timer();
}
ctx.notify();
@@ -337,6 +337,14 @@ pub fn render_warping_indicator<V: View>(
let mut should_render_waiting_icon = false;
let mut non_shimmering_text = None;
if let Some(status_message) = props
.model
.conversation(app)
.and_then(|conversation| conversation.status_error_message())
.filter(|message| message.starts_with("Retrying LLM request"))
{
non_shimmering_text = Some(format!("{status_message}"));
}
let message = if let Some(summarization_type) = summarization_type {
// Choose the appropriate message based on summarization type
let base_message = match summarization_type {
@@ -22,8 +22,8 @@ use crate::ai::agent::{
use crate::ai::blocklist::action_model::AIActionStatus;
use crate::ai::blocklist::agent_view::orchestration_avatar::OrchestrationAvatar;
use crate::ai::blocklist::agent_view::orchestration_conversation_links::{
conversation_id_for_agent_id, conversation_navigation_card_with_icon,
dispatch_focus_or_open_child_agent_pane,
conversation_id_for_agent_id, conversation_id_for_agent_id_in_orchestration,
conversation_navigation_card_with_icon, dispatch_focus_or_open_child_agent_pane,
};
use crate::ai::blocklist::block::model::AIBlockModelHelper;
use crate::ai::blocklist::block::{
@@ -106,6 +106,28 @@ fn participant_for_agent_id(
OrchestrationParticipant::unknown_child()
}
fn participant_for_agent_id_in_orchestration(
agent_id: &str,
orchestrator_agent_id: Option<&str>,
orchestrator_conversation_id: AIConversationId,
app: &AppContext,
) -> OrchestrationParticipant {
if let Some(conversation_id) =
conversation_id_for_agent_id_in_orchestration(agent_id, orchestrator_conversation_id, app)
{
if let Some(conversation) =
BlocklistAIHistoryModel::as_ref(app).conversation(&conversation_id)
{
return participant_for_conversation(
conversation,
orchestrator_agent_id,
Some(agent_id),
);
}
}
participant_for_agent_id(agent_id, orchestrator_agent_id, app)
}
fn participant_for_conversation(
conversation: &AIConversation,
orchestrator_agent_id: Option<&str>,
@@ -346,15 +368,32 @@ pub(super) fn render_messages_received_from_agents(
.model
.conversation(app)
.and_then(|conversation| orchestrator_agent_id_for_conversation(conversation, app));
let orchestrator_conversation_id = props.model.conversation(app).map(|conversation| {
conversation
.parent_conversation_id()
.unwrap_or_else(|| conversation.id())
});
let Some(orchestrator_conversation_id) = orchestrator_conversation_id else {
return Empty::new().finish();
};
let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
for (index, msg) in messages.iter().enumerate() {
let sender =
participant_for_agent_id(&msg.sender_agent_id, orchestrator_agent_id.as_deref(), app);
let sender = participant_for_agent_id_in_orchestration(
&msg.sender_agent_id,
orchestrator_agent_id.as_deref(),
orchestrator_conversation_id,
app,
);
let recipients = msg
.addresses
.iter()
.map(|agent_id| {
participant_for_agent_id(agent_id, orchestrator_agent_id.as_deref(), app)
participant_for_agent_id_in_orchestration(
agent_id,
orchestrator_agent_id.as_deref(),
orchestrator_conversation_id,
app,
)
})
.collect::<Vec<_>>();
let row_message_id = received_message_collapsible_id(&msg.message_id);
@@ -580,7 +619,19 @@ pub(super) fn render_start_agent(
);
return Empty::new().finish();
};
let child_conversation_card_data = child_conversation_card_data_for_result(result, app);
let orchestrator_conversation_id = props.model.conversation(app).map(|conversation| {
conversation
.parent_conversation_id()
.unwrap_or_else(|| conversation.id())
});
let child_conversation_card_data =
orchestrator_conversation_id.and_then(|orchestrator_id| {
child_conversation_card_data_for_result_in_orchestration(
result,
orchestrator_id,
app,
)
});
let (label_fragments, status_icon) = match result {
StartAgentResult::Success { .. } => (
vec![
@@ -841,6 +892,31 @@ fn child_conversation_card_data_for_result(
}
}
fn child_conversation_card_data_for_result_in_orchestration(
result: &StartAgentResult,
orchestrator_id: AIConversationId,
app: &AppContext,
) -> Option<ChildConversationCardData> {
match result {
StartAgentResult::Success { agent_id, .. } => {
let conversation_id =
conversation_id_for_agent_id_in_orchestration(agent_id, orchestrator_id, app)?;
let conversation =
BlocklistAIHistoryModel::as_ref(app).conversation(&conversation_id)?;
let agent_name = conversation.agent_name().unwrap_or("Agent").to_string();
let status = conversation.status().clone();
let title = available_conversation_title_for_id(conversation_id, app)?;
Some(ChildConversationCardData {
conversation_id,
agent_name,
title,
status,
})
}
StartAgentResult::Error { .. } | StartAgentResult::Cancelled { .. } => None,
}
}
fn available_conversation_title_for_id(
conversation_id: AIConversationId,
app: &AppContext,
+51 -1
View File
@@ -6189,6 +6189,35 @@ impl BlocklistAIController {
}
#[cfg(not(target_family = "wasm"))]
if let Some(lifecycle) = lifecycle.as_ref() {
if lifecycle.phase == ProviderLlmLifecyclePhase::Requested {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
history.update_conversation_status(
self.terminal_surface_id,
conversation_id,
ConversationStatus::InProgress,
ctx,
);
});
}
if lifecycle.phase == ProviderLlmLifecyclePhase::RetryScheduled {
let retry_message = format!(
"Retrying LLM request ({}/3): {}",
lifecycle.retry_attempt,
lifecycle
.error
.as_deref()
.unwrap_or("temporary provider error")
);
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
history.update_conversation_status_with_error(
self.terminal_surface_id,
conversation_id,
ConversationStatus::TransientError,
Some(RenderableAIError::other(retry_message, false)),
ctx,
);
});
}
remote_logging::log_model_event(
ctx,
provider_llm_lifecycle_remote_log_record(
@@ -6199,7 +6228,28 @@ impl BlocklistAIController {
);
}
#[cfg(target_family = "wasm")]
let _ = lifecycle;
if let Some(lifecycle) = lifecycle.as_ref() {
if lifecycle.phase == ProviderLlmLifecyclePhase::Requested {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
history.update_conversation_status(
self.terminal_surface_id,
conversation_id,
ConversationStatus::InProgress,
ctx,
);
});
}
if lifecycle.phase == ProviderLlmLifecyclePhase::RetryScheduled {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
history.update_conversation_status(
self.terminal_surface_id,
conversation_id,
ConversationStatus::TransientError,
ctx,
);
});
}
}
let _ = acknowledgement.send(result);
}
ProviderDriveMessage::Checkpoint {
@@ -30,7 +30,7 @@ use crate::ai::blocklist::action_model::{
RunAgentsExecutorEvent, RunAgentsSpawningSnapshot,
};
use crate::ai::blocklist::agent_view::orchestration_conversation_links::{
conversation_id_for_agent_id, conversation_navigation_card_with_icon,
conversation_id_for_agent_id_in_terminal_view, conversation_navigation_card_with_icon,
dispatch_focus_or_open_child_agent_pane,
};
use crate::ai::blocklist::agent_view::orchestration_pill_bar::render_static_agent_pill;
@@ -1784,7 +1784,7 @@ fn render_run_agents_child_row(
let outcome_conversation_id = outcome.and_then(|outcome| match &outcome.kind {
RunAgentsAgentOutcomeKind::Launched { agent_id }
| RunAgentsAgentOutcomeKind::Completed { agent_id, .. } => {
conversation_id_for_agent_id(agent_id, app)
conversation_id_for_agent_id_in_terminal_view(agent_id, terminal_view_id, app)
}
RunAgentsAgentOutcomeKind::Failed { .. } => None,
});