197 lines
7.8 KiB
Rust
197 lines
7.8 KiB
Rust
use crate::ai::agent::conversation::AIConversation;
|
|
use crate::ai::agent::task::TaskId;
|
|
use crate::ai::agent::AIAgentExchange;
|
|
use crate::ai::agent::MessageId;
|
|
use api::client_action as api_client_action;
|
|
use api::response_event as api_response_event;
|
|
use api::response_event::stream_finished as stream_finished_event;
|
|
use std::collections::HashMap;
|
|
use warp_multi_agent_api::{self as api, ResponseEvent};
|
|
|
|
// Reconstructs all response events from conversations for use in session sharing.
|
|
// These messages are used to replay conversations as if they were happening live.
|
|
pub fn reconstruct_response_events_from_conversations(
|
|
conversations: &[AIConversation],
|
|
) -> Vec<ResponseEvent> {
|
|
let mut events = vec![];
|
|
|
|
// Build a map of message_id -> (task_id, message, conversation) for quick lookup
|
|
let mut message_map: HashMap<MessageId, (&TaskId, &api::Message, &AIConversation)> =
|
|
HashMap::new();
|
|
for conversation in conversations {
|
|
for task in conversation.all_tasks() {
|
|
for message in task.messages() {
|
|
message_map.insert(
|
|
MessageId::new(message.id.clone()),
|
|
(task.id(), message, conversation),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect all exchanges from all conversations and sort by start time
|
|
let mut all_exchanges: Vec<(&AIConversation, &AIAgentExchange)> = conversations
|
|
.iter()
|
|
.flat_map(|conv| {
|
|
conv.all_exchanges()
|
|
.into_iter()
|
|
.map(move |exchange| (conv, exchange))
|
|
})
|
|
.collect();
|
|
all_exchanges.sort_by_key(|(_, exchange)| exchange.start_time);
|
|
|
|
// Track which conversations have had their tasks created.
|
|
// We need to send CreateTask on the first exchange to upgrade local task IDs
|
|
// to server task IDs (required for AddMessagesToTask to find the correct task).
|
|
let mut initialized_conversations = std::collections::HashSet::new();
|
|
|
|
// For each exchange (in chronological order), emit events
|
|
for (conversation, exchange) in all_exchanges {
|
|
// Collect messages for this exchange in chronological order
|
|
let mut exchange_messages: Vec<(&TaskId, &api::Message)> = exchange
|
|
.added_message_ids
|
|
.iter()
|
|
.filter_map(|msg_id| {
|
|
message_map
|
|
.get(msg_id)
|
|
.map(|(task_id, msg, _)| (*task_id, *msg))
|
|
})
|
|
.collect();
|
|
|
|
if exchange_messages.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Sort by timestamp to ensure chronological order
|
|
exchange_messages.sort_by_key(|(_, message)| {
|
|
message.timestamp.as_ref().map(|ts| (ts.seconds, ts.nanos))
|
|
});
|
|
|
|
// Use the server conversation token if it's available.
|
|
// Otherwise, fall back to the id that this conversation was forked from.
|
|
// This ensures viewers can properly group historical exchanges together.
|
|
let token = conversation
|
|
.server_conversation_token()
|
|
.or_else(|| conversation.forked_from_server_conversation_token())
|
|
.map(|t| t.as_str().to_string())
|
|
.unwrap_or_default();
|
|
let request_id = exchange_messages
|
|
.first()
|
|
.map(|(_, msg)| msg.request_id.clone())
|
|
.unwrap_or_default();
|
|
|
|
// Start this exchange
|
|
events.push(ResponseEvent {
|
|
r#type: Some(api_response_event::Type::Init(
|
|
api_response_event::StreamInit {
|
|
request_id,
|
|
conversation_id: token.clone(),
|
|
// Shared session replays don't need a run_id; the empty
|
|
// string is filtered to None by initialize_output_for_response_stream.
|
|
run_id: String::new(),
|
|
},
|
|
)),
|
|
});
|
|
|
|
// On the first exchange of each conversation, send CreateTask events to upgrade
|
|
// local task IDs to server task IDs. We construct a task with empty messages
|
|
// because the messages will be added via AddMessagesToTask below - including them
|
|
// in CreateTask would cause duplicate content in the exchange.
|
|
let conversation_id = conversation.id();
|
|
let is_first_exchange = !initialized_conversations.contains(&conversation_id);
|
|
if is_first_exchange {
|
|
initialized_conversations.insert(conversation_id);
|
|
for task in conversation.all_tasks() {
|
|
if let Some(task_source) = task.source() {
|
|
let task_without_messages = api::Task {
|
|
id: task_source.id.clone(),
|
|
description: task_source.description.clone(),
|
|
dependencies: task_source.dependencies.clone(),
|
|
messages: vec![], // Empty - messages added via AddMessagesToTask
|
|
summary: task_source.summary.clone(),
|
|
server_data: task_source.server_data.clone(),
|
|
};
|
|
events.push(wrap_action_in_event(api_client_action::Action::CreateTask(
|
|
api_client_action::CreateTask {
|
|
task: Some(task_without_messages),
|
|
},
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send all messages for this exchange
|
|
for (task_id, message) in exchange_messages {
|
|
events.push(wrap_action_in_event(
|
|
api_client_action::Action::AddMessagesToTask(
|
|
api_client_action::AddMessagesToTask {
|
|
task_id: task_id.to_string(),
|
|
messages: vec![message.clone()],
|
|
},
|
|
),
|
|
));
|
|
}
|
|
|
|
// Finish this exchange
|
|
events.push(create_finished_event_from_conversation(conversation));
|
|
}
|
|
|
|
events
|
|
}
|
|
|
|
/// Wrap a ClientAction in a ResponseEvent.
|
|
fn wrap_action_in_event(action: api_client_action::Action) -> ResponseEvent {
|
|
ResponseEvent {
|
|
r#type: Some(api_response_event::Type::ClientActions(
|
|
api_response_event::ClientActions {
|
|
actions: vec![api::ClientAction {
|
|
action: Some(action),
|
|
}],
|
|
},
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Creates a StreamFinished event from a conversation.
|
|
fn create_finished_event_from_conversation(conversation: &AIConversation) -> ResponseEvent {
|
|
// Build conversation usage metadata from the conversation's metadata
|
|
let usage_metadata = Some(
|
|
api_response_event::stream_finished::ConversationUsageMetadata {
|
|
context_window_usage: conversation.context_window_usage(),
|
|
credits_spent: conversation.credits_spent(),
|
|
summarized: conversation.was_summarized(),
|
|
#[allow(deprecated)]
|
|
token_usage: conversation
|
|
.token_usage()
|
|
.iter()
|
|
.map(|u| u.to_proto_combined())
|
|
.collect(),
|
|
tool_usage_metadata: Some(conversation.tool_usage_metadata().into()),
|
|
warp_token_usage: conversation
|
|
.token_usage()
|
|
.iter()
|
|
.filter_map(|u| u.to_proto_warp_usage())
|
|
.collect(),
|
|
byok_token_usage: conversation
|
|
.token_usage()
|
|
.iter()
|
|
.filter_map(|u| u.to_proto_byok_usage())
|
|
.collect(),
|
|
},
|
|
);
|
|
|
|
ResponseEvent {
|
|
r#type: Some(api_response_event::Type::Finished(
|
|
api_response_event::StreamFinished {
|
|
reason: Some(stream_finished_event::Reason::Done(
|
|
stream_finished_event::Done {},
|
|
)),
|
|
conversation_usage_metadata: usage_metadata,
|
|
token_usage: vec![],
|
|
should_refresh_model_config: false,
|
|
request_cost: None,
|
|
},
|
|
)),
|
|
}
|
|
}
|