Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -0,0 +1,394 @@
use std::{cell::RefCell, rc::Rc};
use anyhow::anyhow;
use chrono::{DateTime, Local, TimeDelta};
use futures::channel::oneshot;
use uuid::Uuid;
use warp_multi_agent_api::response_event;
use warpui::{Entity, ModelContext, SingletonEntity};
use crate::{
ai::agent::{
api::{self, generate_multi_agent_output, ConvertToAPITypeError},
conversation::AIConversationId,
AIIdentifiers, CancellationReason,
},
network::NetworkStatus,
report_error, send_telemetry_from_ctx,
server::server_api::ServerApiProvider,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ResponseStreamId(String);
impl ResponseStreamId {
pub fn for_shared_session(init_event: &response_event::StreamInit) -> Self {
// Make the stream ID unique per viewing by appending a local UUID
// This prevents collisions when replaying the same conversation multiple times
// (either on close-and-reopen or when viewing the same shared session from multiple terminals)
Self(format!("{}-{}", init_event.request_id, Uuid::new_v4()))
}
#[cfg(test)]
pub fn new_for_test() -> Self {
Self(Uuid::new_v4().to_string())
}
}
/// Model wrapping an agent API response stream.
///
/// Emits events when the output corresponding to the stream is updated, typically after receiving
/// each response chunk.
///
/// Handles retries internally - retries are only attempted if no ClientActions events have been
/// received yet, ensuring we don't retry after the AI has started executing actions.
pub struct ResponseStream {
id: ResponseStreamId,
params: api::RequestParams,
retry_count: usize,
start_time: DateTime<Local>,
time_to_latest_event: TimeDelta,
cancellation_tx: Option<oneshot::Sender<()>>,
/// Store the original error for telemetry when retries succeed
original_error: Option<String>,
/// Track whether we've received any client actions
/// If true, we cannot retry on subsequent errors since actions may have been executed
has_received_client_actions: bool,
/// AI identifiers for telemetry emission
ai_identifiers: AIIdentifiers,
/// Whether this request can attempt to resume the conversation on error.
/// This is true for all requests except those that are themselves the result of a resume
/// triggered by a previous error.
can_attempt_resume_on_error: bool,
/// Whether we should attempt to resume the conversation after the stream finishes.
///
/// This is set when we receive a retryable error after client actions have been received
/// and `can_attempt_resume_on_error` is true.
should_resume_conversation_after_stream_finished: bool,
/// Unique, internal id for the current request.
///
/// This ensures that the model never emits events for a request that was already cancelled (or
/// retried) and is still receiving lagging events.
///
/// Note this is unique compared to `id`; this is unique across retry requests while the response
/// stream id remains stable.
current_request_id: Option<Uuid>,
}
impl ResponseStream {
pub fn new(
params: api::RequestParams,
ai_identifiers: AIIdentifiers,
can_attempt_resume_on_error: bool,
ctx: &mut ModelContext<Self>,
) -> Self {
let server_api = ServerApiProvider::as_ref(ctx).get();
let (cancellation_tx, cancellation_rx) = oneshot::channel();
let start_time = Local::now();
let request_id = Uuid::new_v4();
let params_clone = params.clone();
let _ =
ctx.spawn(
async move {
generate_multi_agent_output(server_api, params_clone, cancellation_rx).await
},
move |me, stream, ctx| {
me.handle_response_stream_result(request_id, stream, ctx);
},
);
Self {
id: ResponseStreamId(Uuid::new_v4().to_string()),
params: params.clone(),
start_time,
time_to_latest_event: TimeDelta::seconds(0),
cancellation_tx: Some(cancellation_tx),
retry_count: 0,
original_error: None,
has_received_client_actions: false,
ai_identifiers,
can_attempt_resume_on_error,
should_resume_conversation_after_stream_finished: false,
current_request_id: Some(request_id),
}
}
pub fn id(&self) -> &ResponseStreamId {
&self.id
}
/// Returns true if we should attempt to resume the conversation after the stream finishes.
pub fn should_resume_conversation_after_stream_finished(&self) -> bool {
self.should_resume_conversation_after_stream_finished
}
/// Helper function to emit AgentModeError telemetry for error that is retryable (not user visible).
fn emit_retryable_agent_mode_error_telemetry(
&self,
error: String,
ctx: &mut ModelContext<Self>,
) {
send_telemetry_from_ctx!(
crate::TelemetryEvent::AgentModeError {
identifiers: self.ai_identifiers.clone(),
error,
is_user_visible: false,
will_attempt_to_resume: false,
},
ctx
);
}
fn retry(&mut self, ctx: &mut ModelContext<Self>) {
self.retry_count += 1;
self.has_received_client_actions = false; // Reset for the new attempt
let (cancellation_tx, cancellation_rx) = oneshot::channel();
if let Some(old_cancellation_tx) = self.cancellation_tx.take() {
let _ = old_cancellation_tx.send(());
}
self.cancellation_tx = Some(cancellation_tx);
let request_id = Uuid::new_v4();
self.current_request_id = Some(request_id);
let params = self.params.clone();
let server_api = ServerApiProvider::as_ref(ctx).get();
let _ = ctx.spawn(
async move { generate_multi_agent_output(server_api, params, cancellation_rx).await },
move |me, stream, ctx| {
me.handle_response_stream_result(request_id, stream, ctx);
},
);
}
/// Cancels the stream. The conversation_id is preserved in the emitted event for async handling.
pub(super) fn cancel(
&mut self,
reason: CancellationReason,
conversation_id: AIConversationId,
ctx: &mut ModelContext<Self>,
) {
self.current_request_id = None;
let Some(cancellation_tx) = self.cancellation_tx.take() else {
return;
};
let _ = cancellation_tx.send(());
ctx.emit(ResponseStreamEvent::AfterStreamFinished {
cancellation: Some(StreamCancellation {
reason,
conversation_id,
}),
});
}
fn handle_response_stream_result(
&mut self,
request_id: Uuid,
stream_result: Result<api::ResponseStream, ConvertToAPITypeError>,
ctx: &mut ModelContext<Self>,
) {
match stream_result {
Ok(stream) => {
ctx.spawn_stream_local(
stream,
move |me, event, ctx| {
me.handle_response_stream_event(request_id, event, ctx);
},
move |me, ctx| {
me.on_response_stream_complete(request_id, ctx);
},
);
}
Err(e) => {
log::error!("Failed to send request to multi-agent API: {e:?}");
self.on_response_stream_complete(request_id, ctx);
}
}
}
fn handle_response_stream_event(
&mut self,
request_id: Uuid,
event: api::Event,
ctx: &mut ModelContext<Self>,
) {
if self.current_request_id.is_none_or(|id| id != request_id) {
return;
}
self.time_to_latest_event = Local::now().signed_duration_since(self.start_time);
match &event {
Ok(response_event) => {
if let Some(event_type) = &response_event.r#type {
match event_type {
warp_multi_agent_api::response_event::Type::Init(init_event) => {
// Capture server_output_id from StreamInit event
self.ai_identifiers.server_output_id =
Some(crate::ai::agent::ServerOutputId::new(
init_event.request_id.clone(),
));
}
warp_multi_agent_api::response_event::Type::ClientActions(_) => {
// Mark that we've received client actions
self.has_received_client_actions = true;
}
warp_multi_agent_api::response_event::Type::Finished(finished_event) => {
// Emit retry success telemetry on successful completion
if matches!(
finished_event.reason,
Some(warp_multi_agent_api::response_event::stream_finished::Reason::Done(_)) | None
) {
// Emit retry success telemetry if this was a successful completion after retries
if self.retry_count > 0 {
if let Some(original_error) = &self.original_error {
send_telemetry_from_ctx!(
crate::TelemetryEvent::AgentModeRequestRetrySucceeded {
identifiers: self.ai_identifiers.clone(),
retry_count: self.retry_count,
original_error: original_error.clone(),
},
ctx
);
}
}
}
}
}
}
ctx.emit(ResponseStreamEvent::ReceivedEvent(Consumable::new(event)));
}
Err(e) => {
// Store original error if this is the first error
if self.retry_count == 0 {
self.original_error = Some(format!("{e:?}"));
}
// Only retry if:
// 1. We haven't received any client actions yet (this is the first event or only init events)
// 2. The error is retryable
// 3. We haven't exceeded max retries
// 4. We're online
const MAX_RETRIES: usize = 3;
let network_status = NetworkStatus::as_ref(ctx);
let is_online = network_status.is_online();
let is_retryable = e.is_retryable();
let should_retry = !self.has_received_client_actions
&& is_retryable
&& self.retry_count < MAX_RETRIES
&& is_online;
if should_retry {
log::warn!(
"MultiAgent request failed, retrying (attempt {}/{}) - Error: {e:?}",
self.retry_count + 1,
MAX_RETRIES
);
// Only emit error telemetry here if we're retrying.
// Final errors that aren't being retried are emitted elsewhere.
self.emit_retryable_agent_mode_error_telemetry(format!("{e:?}"), ctx);
self.retry(ctx);
// Don't emit the error event, we're retrying
// TODO: emit a separate event if controller needs to know about failures that are being retried
return;
}
// If we can't retry (because client actions were received) but the error is
// retryable and we're allowed to attempt a resume, signal that the controller
// should resume the conversation after the stream completes.
let should_attempt_resume = self.has_received_client_actions
&& is_retryable
&& self.can_attempt_resume_on_error;
if should_attempt_resume {
self.should_resume_conversation_after_stream_finished = true;
}
#[cfg(feature = "crash_reporting")]
sentry::with_scope(
|scope| {
scope.set_tag(
"has_received_client_actions",
self.has_received_client_actions,
);
scope.set_tag("error", format!("{e:?}"));
scope.set_tag("is_retryable", e.is_retryable());
scope.set_tag("is_online", is_online);
scope.set_tag("retry_count", self.retry_count);
},
|| {
report_error!(anyhow!(e.clone()).context(format!(
"MultiAgent request failed after {} retries",
self.retry_count
)));
},
);
#[cfg(not(feature = "crash_reporting"))]
{
report_error!(anyhow!(e.clone()).context(format!(
"MultiAgent request failed after {} retries",
self.retry_count
)));
}
ctx.emit(ResponseStreamEvent::ReceivedEvent(Consumable::new(event)));
}
}
}
fn on_response_stream_complete(&mut self, request_id: Uuid, ctx: &mut ModelContext<Self>) {
if self.current_request_id.is_none_or(|id| id != request_id) {
return;
}
ctx.emit(ResponseStreamEvent::AfterStreamFinished { cancellation: None });
self.cancellation_tx = None;
}
}
#[derive(Debug)]
pub struct Consumable<T> {
value: Rc<RefCell<Option<T>>>,
}
impl<T> Consumable<T> {
fn new(value: T) -> Self {
Consumable {
value: Rc::new(RefCell::new(Some(value))),
}
}
pub(super) fn consume(&self) -> Option<T> {
self.value.borrow_mut().take()
}
}
impl<T> Clone for Consumable<T> {
fn clone(&self) -> Self {
Consumable {
value: Rc::clone(&self.value),
}
}
}
/// Cancellation context preserved for async event handling.
/// Includes conversation_id because truncation can remove exchange mappings before the event is processed.
#[derive(Debug, Clone)]
pub struct StreamCancellation {
pub reason: CancellationReason,
pub conversation_id: AIConversationId,
}
#[derive(Debug, Clone)]
pub enum ResponseStreamEvent {
ReceivedEvent(Consumable<api::Event>),
AfterStreamFinished {
/// Some for cancellation (with context), None for natural completion (uses dynamic lookup).
cancellation: Option<StreamCancellation>,
},
}
impl Entity for ResponseStream {
type Event = ResponseStreamEvent;
}