Skip to content

Commit 27d8ef6

Browse files
authored
Add AgentConversationEntry and friends. (#10167)
Adds a new AgentConversationEntry abstraction designed to replace ConversationOrTask. ConversationOrTask requires a bifurcation between local and cloud conversations that is prone to skew. Downstream of ConversationOrTask, navigation policy (e.g. should clicking this item open a cloud mode pane? or open a cloud transcript view?) and other behaviors are computed and cached. With cloud conversation continuation and local->cloud handoff, it makes less sense to do this bifurcation up front, and instead introduce a unified abstraction that broadly represents an AI conversation, whether its a local conversation started by the user, a cloud ambient agent conversation, or a conversation for which we only currently have server metadata (e.g. have not actually fetched transcript contents from storage). This refactor will make it easier to implement correct behavior for UI that depends on a conversation's provenance. This is the first PR in a 4 pr stack, introducing the abstraction itself - but does not hook it into anythinganything. There is 1 master tech spec and 4 more granular specs for each incremental PR.
1 parent 4be9ec1 commit 27d8ef6

8 files changed

Lines changed: 1060 additions & 26 deletions

app/src/ai/agent_conversations_model.rs

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#[allow(dead_code)]
2+
pub mod entry;
3+
4+
pub use entry::AgentConversationEntry;
5+
16
use crate::ai::active_agent_views_model::ActiveAgentViewsModel;
27
use crate::ai::agent::api::ServerConversationToken;
38
use crate::ai::agent::conversation::{AIConversationId, ConversationStatus};
@@ -273,7 +278,7 @@ impl AgentRunDisplayStatus {
273278
return Self::from_task_state(task);
274279
}
275280
let history_model = BlocklistAIHistoryModel::as_ref(app);
276-
AgentConversationsModel::conversation_id_shadowed_by_task(task, history_model)
281+
entry::conversation_id_shadowed_by_task(task, history_model)
277282
.and_then(|conversation_id| history_model.conversation(&conversation_id))
278283
.map(|conversation| Self::from_conversation_status(conversation.status()))
279284
.unwrap_or_else(|| Self::from_task_state(task))
@@ -1418,32 +1423,67 @@ impl AgentConversationsModel {
14181423
}
14191424
}
14201425

1421-
/// Returns the local conversation ID represented by the given task, if this task and a
1422-
/// conversation entry both point at the same underlying local run.
1423-
///
1424-
/// We first match using the orchestration agent ID (task ID / run ID under v2), and fall back
1425-
/// to the server conversation token for cases where the task only carries conversation identity
1426-
/// through `conversation_id`.
1427-
fn conversation_id_shadowed_by_task(
1428-
task: &AmbientAgentTask,
1429-
history_model: &BlocklistAIHistoryModel,
1430-
) -> Option<AIConversationId> {
1431-
history_model
1432-
.conversation_id_for_agent_id(&task.run_id().to_string())
1433-
.or_else(|| {
1434-
task.conversation_id().and_then(|conversation_id| {
1435-
history_model.find_conversation_id_by_server_token(
1436-
&ServerConversationToken::new(conversation_id.to_string()),
1437-
)
1438-
})
1439-
})
1440-
}
1441-
14421426
fn conversation_ids_shadowed_by_tasks(&self, app: &AppContext) -> HashSet<AIConversationId> {
14431427
let history_model = BlocklistAIHistoryModel::as_ref(app);
14441428
self.tasks
14451429
.values()
1446-
.filter_map(|task| Self::conversation_id_shadowed_by_task(task, history_model))
1430+
.filter_map(|task| entry::conversation_id_shadowed_by_task(task, history_model))
1431+
.collect()
1432+
}
1433+
1434+
/// Returns normalized, owned entries for agent management/navigation surfaces.
1435+
///
1436+
/// This projection keeps task, local conversation, and cloud metadata identity together while
1437+
/// leaving the current `ConversationOrTask` call sites unchanged.
1438+
#[allow(dead_code)]
1439+
pub fn get_entries(
1440+
&self,
1441+
filters: &AgentManagementFilters,
1442+
app: &AppContext,
1443+
) -> Vec<AgentConversationEntry> {
1444+
let history_model = BlocklistAIHistoryModel::as_ref(app);
1445+
let mut entries = Vec::new();
1446+
let mut attached_conversation_ids = HashSet::new();
1447+
let mut emitted_conversation_ids = HashSet::new();
1448+
1449+
for task in self.tasks.values() {
1450+
let entry = entry::entry_for_task(task, history_model, app);
1451+
if let Some(conversation_id) = entry.identity.local_conversation_id {
1452+
attached_conversation_ids.insert(conversation_id);
1453+
}
1454+
entries.push(entry);
1455+
}
1456+
1457+
for metadata in self.conversations.values() {
1458+
let conversation_id = metadata.nav_data.id;
1459+
if attached_conversation_ids.contains(&conversation_id) {
1460+
continue;
1461+
}
1462+
let entry = entry::entry_for_conversation(metadata, history_model, app);
1463+
emitted_conversation_ids.insert(conversation_id);
1464+
entries.push(entry);
1465+
}
1466+
1467+
for metadata in history_model.get_local_conversations_metadata() {
1468+
if attached_conversation_ids.contains(&metadata.id)
1469+
|| emitted_conversation_ids.contains(&metadata.id)
1470+
{
1471+
continue;
1472+
}
1473+
let nav_data =
1474+
ConversationNavigationData::from_historical_conversation_metadata(metadata);
1475+
entries.push(entry::entry_for_historical_metadata(
1476+
metadata,
1477+
nav_data,
1478+
history_model,
1479+
app,
1480+
));
1481+
}
1482+
1483+
entries
1484+
.into_iter()
1485+
.filter(|entry| entry.matches_filters(filters, app))
1486+
.sorted_by(|a, b| b.display.last_updated.cmp(&a.display.last_updated))
14471487
.collect()
14481488
}
14491489

0 commit comments

Comments
 (0)