Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 30 additions & 41 deletions app/src/ai/blocklist/block/view_impl/orchestration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ use crate::ai::blocklist::inline_action::inline_action_icons::{self, icon_size};
use crate::ai::blocklist::inline_action::requested_action::{
render_requested_action_row, render_requested_action_row_for_text,
};
use crate::ai::blocklist::orchestration_topology::{
orchestrator_agent_id_for_conversation, resolve_orchestration_participant,
OrchestrationParticipantKind,
};
use crate::ai::blocklist::BlocklistAIHistoryModel;
use crate::appearance::Appearance;
use crate::terminal::view::TerminalAction;
Expand All @@ -63,14 +67,6 @@ impl OrchestrationParticipant {
}
}

fn unknown_child() -> Self {
Self {
display_name: "Unknown agent".to_string(),
avatar: OrchestrationAvatar::agent("Unknown agent".to_string()),
conversation_id: None,
}
}

fn is_orchestrator(&self) -> bool {
matches!(&self.avatar, OrchestrationAvatar::Orchestrator)
}
Expand All @@ -90,21 +86,28 @@ fn participant_for_agent_id(
orchestrator_agent_id: Option<&str>,
app: &AppContext,
) -> OrchestrationParticipant {
if let Some(conversation_id) = conversation_id_for_agent_id(agent_id, app) {
if let Some(conversation) =
BlocklistAIHistoryModel::as_ref(app).conversation(&conversation_id)
{
return participant_for_conversation(
conversation,
orchestrator_agent_id,
Some(agent_id),
);
let participant = resolve_orchestration_participant(
BlocklistAIHistoryModel::as_ref(app),
agent_id,
orchestrator_agent_id,
);
let display_name = participant.kind.display_name().to_string();
let avatar = match &participant.kind {
OrchestrationParticipantKind::Orchestrator => OrchestrationAvatar::Orchestrator,
OrchestrationParticipantKind::Agent { .. } | OrchestrationParticipantKind::Unknown => {
OrchestrationAvatar::agent(display_name.clone())
}
};
OrchestrationParticipant {
display_name,
avatar,
conversation_id: match &participant.kind {
OrchestrationParticipantKind::Orchestrator | OrchestrationParticipantKind::Unknown => {
None
}
OrchestrationParticipantKind::Agent { .. } => participant.conversation_id,
},
}
if orchestrator_agent_id.is_some_and(|id| id == agent_id) {
return OrchestrationParticipant::orchestrator();
}
OrchestrationParticipant::unknown_child()
}

fn participant_for_conversation(
Expand All @@ -131,18 +134,6 @@ fn participant_for_conversation(
}
}

fn orchestrator_agent_id_for_conversation(
conversation: &AIConversation,
app: &AppContext,
) -> Option<String> {
match conversation.parent_conversation_id() {
Some(parent_id) => BlocklistAIHistoryModel::as_ref(app)
.conversation(&parent_id)
.and_then(|parent| parent.orchestration_agent_id()),
None => conversation.orchestration_agent_id(),
}
}

fn participant_for_current_conversation(
props: Props,
orchestrator_agent_id: Option<&str>,
Expand Down Expand Up @@ -343,10 +334,9 @@ pub(super) fn render_messages_received_from_agents(
if messages.is_empty() {
return Empty::new().finish();
}
let orchestrator_agent_id = props
.model
.conversation(app)
.and_then(|conversation| orchestrator_agent_id_for_conversation(conversation, app));
let orchestrator_agent_id = props.model.conversation(app).and_then(|conversation| {
orchestrator_agent_id_for_conversation(BlocklistAIHistoryModel::as_ref(app), conversation)
});
let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
for (index, msg) in messages.iter().enumerate() {
let sender =
Expand Down Expand Up @@ -422,10 +412,9 @@ pub(super) fn render_send_message(
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let status = props.action_model.as_ref(app).get_action_status(action_id);
let orchestrator_agent_id = props
.model
.conversation(app)
.and_then(|conversation| orchestrator_agent_id_for_conversation(conversation, app));
let orchestrator_agent_id = props.model.conversation(app).and_then(|conversation| {
orchestrator_agent_id_for_conversation(BlocklistAIHistoryModel::as_ref(app), conversation)
});
let recipient_participants =
participant_for_agent_ids(address, orchestrator_agent_id.as_deref(), app);
let recipients = participant_display_names(&recipient_participants);
Expand Down
85 changes: 85 additions & 0 deletions app/src/ai/blocklist/orchestration_topology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,91 @@ pub enum OrchestrationNavigationDirection {
Next,
}

/// Semantic role of a participant in an orchestration transcript.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OrchestrationParticipantKind {
Orchestrator,
Agent { name: String },
Unknown,
}

impl OrchestrationParticipantKind {
pub(super) fn display_name(&self) -> &str {
match self {
Self::Orchestrator => "Orchestrator",
Self::Agent { name } => name,
Self::Unknown => "Unknown agent",
}
}
}

/// Frontend-independent identity for an orchestration participant.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedOrchestrationParticipant {
pub kind: OrchestrationParticipantKind,
pub conversation_id: Option<AIConversationId>,
}

impl ResolvedOrchestrationParticipant {
fn orchestrator(conversation_id: Option<AIConversationId>) -> Self {
Self {
kind: OrchestrationParticipantKind::Orchestrator,
conversation_id,
}
}

fn unknown() -> Self {
Self {
kind: OrchestrationParticipantKind::Unknown,
conversation_id: None,
}
}
}

/// Returns the agent ID of the conversation that orchestrates `conversation`.
pub fn orchestrator_agent_id_for_conversation(
history: &BlocklistAIHistoryModel,
conversation: &AIConversation,
) -> Option<String> {
match history.resolved_parent_conversation_id_for_conversation(conversation) {
Some(parent_id) => history
.conversation(&parent_id)
.and_then(AIConversation::orchestration_agent_id)
.or_else(|| conversation.parent_agent_id().map(str::to_owned)),
None => conversation
.parent_agent_id()
.map(str::to_owned)
.or_else(|| conversation.orchestration_agent_id()),
}
}

/// Resolves a server-side agent ID to frontend-independent participant data.
pub fn resolve_orchestration_participant(
history: &BlocklistAIHistoryModel,
agent_id: &str,
orchestrator_agent_id: Option<&str>,
) -> ResolvedOrchestrationParticipant {
let conversation_id = history.conversation_id_for_agent_id(agent_id);
if orchestrator_agent_id == Some(agent_id) {
return ResolvedOrchestrationParticipant::orchestrator(conversation_id);
}
let Some(conversation_id) = conversation_id else {
return ResolvedOrchestrationParticipant::unknown();
};
let Some(conversation) = history.conversation(&conversation_id) else {
return ResolvedOrchestrationParticipant::unknown();
};
let name = conversation
.agent_name()
.filter(|name| !name.is_empty())
.unwrap_or("Agent")
.to_string();
ResolvedOrchestrationParticipant {
kind: OrchestrationParticipantKind::Agent { name },
conversation_id: Some(conversation_id),
}
}

const DONE_STATUS_KEY: u8 = 3;

fn pill_status_sort_key(status: Option<&ConversationStatus>) -> u8 {
Expand Down
80 changes: 80 additions & 0 deletions app/src/ai/blocklist/orchestration_topology_tests.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,89 @@
use uuid::Uuid;
use warpui::{App, EntityId, ModelHandle};

use super::*;
use crate::ai::agent::conversation::{AIConversationId, ConversationStatus};
use crate::ai::blocklist::BlocklistAIHistoryModel;
use crate::test_util::settings::initialize_history_persistence_for_tests;

#[test]
fn participant_resolution_uses_the_direct_parent_as_orchestrator() {
App::test((), |mut app| async move {
initialize_history_persistence_for_tests(&mut app);
let history_model = app.add_singleton_model(|_| BlocklistAIHistoryModel::new_for_test());
let surface_id = EntityId::new();
let root_run_id = Uuid::new_v4().to_string();
let child_run_id = Uuid::new_v4().to_string();
let grandchild_run_id = Uuid::new_v4().to_string();

let (root_id, child_id, grandchild_id) = history_model.update(&mut app, |history, ctx| {
let root_id = history.start_new_conversation(surface_id, false, false, false, ctx);
history.assign_run_id_for_conversation(
root_id,
root_run_id.clone(),
None,
surface_id,
ctx,
);
let child_id = history.start_new_child_conversation(
surface_id,
"child".to_string(),
root_id,
None,
ctx,
);
history.assign_run_id_for_conversation(
child_id,
child_run_id.clone(),
None,
surface_id,
ctx,
);
let grandchild_id = history.start_new_child_conversation(
surface_id,
"grandchild".to_string(),
child_id,
None,
ctx,
);
history.assign_run_id_for_conversation(
grandchild_id,
grandchild_run_id,
None,
surface_id,
ctx,
);
(root_id, child_id, grandchild_id)
});

history_model.read(&app, |history, _| {
let grandchild = history
.conversation(&grandchild_id)
.expect("grandchild conversation exists");
assert_eq!(
orchestrator_agent_id_for_conversation(history, grandchild),
Some(child_run_id.clone())
);
assert_eq!(
resolve_orchestration_participant(history, &child_run_id, Some(&child_run_id)),
ResolvedOrchestrationParticipant {
kind: OrchestrationParticipantKind::Orchestrator,
conversation_id: Some(child_id),
}
);
assert_eq!(
resolve_orchestration_participant(history, &root_run_id, Some(&child_run_id)),
ResolvedOrchestrationParticipant {
kind: OrchestrationParticipantKind::Agent {
name: "Agent".to_string(),
},
conversation_id: Some(root_id),
}
);
});
});
}

#[test]
fn pill_order_keys_prioritize_attention_then_in_progress_then_done() {
let blocked = ConversationStatus::Blocked {
Expand Down
2 changes: 1 addition & 1 deletion app/src/ai/execution_profiles/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub struct AIExecutionProfilesModel {

impl AIExecutionProfilesModel {
#[allow(unused_variables)]
pub fn new(launch_mode: &LaunchMode, ctx: &mut ModelContext<Self>) -> Self {
pub(crate) fn new(launch_mode: &LaunchMode, ctx: &mut ModelContext<Self>) -> Self {
cfg_if::cfg_if! {
if #[cfg(feature = "agent_mode_evals")] {
let default_profile_state = DefaultProfileState::Unsynced {
Expand Down
4 changes: 4 additions & 0 deletions app/src/tui_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ pub use crate::ai::blocklist::history_model::{
pub use crate::ai::blocklist::orchestration_event_streamer::{
register_agent_event_consumer, unregister_agent_event_consumer,
};
pub use crate::ai::blocklist::orchestration_topology::{
orchestrator_agent_id_for_conversation, resolve_orchestration_participant,
OrchestrationParticipantKind, ResolvedOrchestrationParticipant,
};
pub use crate::ai::blocklist::view_util::format_credits;
#[cfg(not(target_family = "wasm"))]
pub use crate::ai::blocklist::{
Expand Down
Loading
Loading