Fix orchestration and provider reliability
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user