edit prediction: Report early-rejected predictions and fix cancel bug (#43585)

Many prediction requests end up being rejected early without ever being
set as the current prediction. Before this change, those cases weren’t
reported as rejections because the `request_prediction_with_*` functions
simply returned `Ok(None)`.

With this update, whenever we get a successful response from the
provider, we will return at least the `id`, allowing it to be properly
reported. The request now also includes a “reject reason,” since the
different variants carry distinct implications for prediction quality.

All of these scenarios are now covered by tests. While adding them, I
also found and fixed a bug where some cancelled predictions were
incorrectly being set as the current one.

Release Notes:

- N/A

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
This commit is contained in:
Agus Zubiaga
2025-11-26 20:15:05 +00:00
committed by GitHub
co-authored by MrSubidubi
parent 61a414df77
commit f89e5308e3
8 changed files with 845 additions and 182 deletions
@@ -200,12 +200,31 @@ pub struct RejectEditPredictionsBody {
pub rejections: Vec<EditPredictionRejection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EditPredictionRejection {
pub request_id: String,
#[serde(default)]
pub reason: EditPredictionRejectReason,
pub was_shown: bool,
}
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum EditPredictionRejectReason {
/// New requests were triggered before this one completed
Canceled,
/// No edits returned
Empty,
/// Edits returned, but none remained after interpolation
InterpolatedEmpty,
/// The new prediction was preferred over the current one
Replaced,
/// The current prediction was preferred over the new one
CurrentPreferred,
/// The current prediction was discarded
#[default]
Discarded,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CompletionMode {
+60 -35
View File
@@ -5,6 +5,7 @@ use std::{
time::{Duration, Instant},
};
use cloud_llm_client::EditPredictionRejectReason;
use gpui::{AsyncApp, Entity, SharedString};
use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, TextBufferSnapshot};
use serde::Serialize;
@@ -24,13 +25,71 @@ impl std::fmt::Display for EditPredictionId {
}
}
/// A prediction response that was returned from the provider, whether it was ultimately valid or not.
pub struct EditPredictionResult {
pub id: EditPredictionId,
pub prediction: Result<EditPrediction, EditPredictionRejectReason>,
}
impl EditPredictionResult {
pub async fn new(
id: EditPredictionId,
edited_buffer: &Entity<Buffer>,
edited_buffer_snapshot: &BufferSnapshot,
edits: Arc<[(Range<Anchor>, Arc<str>)]>,
buffer_snapshotted_at: Instant,
response_received_at: Instant,
inputs: EditPredictionInputs,
cx: &mut AsyncApp,
) -> Self {
if edits.is_empty() {
return Self {
id,
prediction: Err(EditPredictionRejectReason::Empty),
};
}
let Some((edits, snapshot, edit_preview_task)) = edited_buffer
.read_with(cx, |buffer, cx| {
let new_snapshot = buffer.snapshot();
let edits: Arc<[_]> =
interpolate_edits(&edited_buffer_snapshot, &new_snapshot, edits)?.into();
Some((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
})
.ok()
.flatten()
else {
return Self {
id,
prediction: Err(EditPredictionRejectReason::InterpolatedEmpty),
};
};
let edit_preview = edit_preview_task.await;
Self {
id: id.clone(),
prediction: Ok(EditPrediction {
id,
edits,
snapshot,
edit_preview,
inputs,
buffer: edited_buffer.clone(),
buffer_snapshotted_at,
response_received_at,
}),
}
}
}
#[derive(Clone)]
pub struct EditPrediction {
pub id: EditPredictionId,
pub edits: Arc<[(Range<Anchor>, Arc<str>)]>,
pub snapshot: BufferSnapshot,
pub edit_preview: EditPreview,
// We keep a reference to the buffer so that we do not need to reload it from disk when applying the prediction.
pub buffer: Entity<Buffer>,
pub buffer_snapshotted_at: Instant,
pub response_received_at: Instant,
@@ -46,40 +105,6 @@ pub struct EditPredictionInputs {
}
impl EditPrediction {
pub async fn new(
id: EditPredictionId,
edited_buffer: &Entity<Buffer>,
edited_buffer_snapshot: &BufferSnapshot,
edits: Arc<[(Range<Anchor>, Arc<str>)]>,
buffer_snapshotted_at: Instant,
response_received_at: Instant,
inputs: EditPredictionInputs,
cx: &mut AsyncApp,
) -> Option<Self> {
let (edits, snapshot, edit_preview_task) = edited_buffer
.read_with(cx, |buffer, cx| {
let new_snapshot = buffer.snapshot();
let edits: Arc<[_]> =
interpolate_edits(&edited_buffer_snapshot, &new_snapshot, edits)?.into();
Some((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
})
.ok()??;
let edit_preview = edit_preview_task.await;
Some(EditPrediction {
id,
edits,
snapshot,
edit_preview,
inputs,
buffer: edited_buffer.clone(),
buffer_snapshotted_at,
response_received_at,
})
}
pub fn interpolate(
&self,
new_snapshot: &TextBufferSnapshot,
+11 -2
View File
@@ -1,6 +1,7 @@
use std::{cmp, sync::Arc, time::Duration};
use client::{Client, UserStore};
use cloud_llm_client::EditPredictionRejectReason;
use edit_prediction::{DataCollectionState, Direction, EditPredictionProvider};
use gpui::{App, Entity, prelude::*};
use language::ToPoint as _;
@@ -132,7 +133,11 @@ impl EditPredictionProvider for ZetaEditPredictionProvider {
fn discard(&mut self, cx: &mut Context<Self>) {
self.zeta.update(cx, |zeta, cx| {
zeta.discard_current_prediction(&self.project, cx);
zeta.reject_current_prediction(
EditPredictionRejectReason::Discarded,
&self.project,
cx,
);
});
}
@@ -169,7 +174,11 @@ impl EditPredictionProvider for ZetaEditPredictionProvider {
let Some(edits) = prediction.interpolate(&snapshot) else {
self.zeta.update(cx, |zeta, cx| {
zeta.discard_current_prediction(&self.project, cx);
zeta.reject_current_prediction(
EditPredictionRejectReason::InterpolatedEmpty,
&self.project,
cx,
);
});
return None;
};
+5 -5
View File
@@ -18,7 +18,7 @@ use std::{
time::Instant,
};
use crate::{EditPrediction, EditPredictionId, EditPredictionInputs};
use crate::{EditPredictionId, EditPredictionInputs, prediction::EditPredictionResult};
const SWEEP_API_URL: &str = "https://autocomplete.sweep.dev/backend/next_edit_autocomplete";
@@ -45,7 +45,7 @@ impl SweepAi {
recent_paths: &VecDeque<ProjectPath>,
diagnostic_search_range: Range<Point>,
cx: &mut App,
) -> Task<Result<Option<EditPrediction>>> {
) -> Task<Result<Option<EditPredictionResult>>> {
let debug_info = self.debug_info.clone();
let Some(api_token) = self.api_token.clone() else {
return Task::ready(Ok(None));
@@ -242,8 +242,8 @@ impl SweepAi {
cx.spawn(async move |cx| {
let (id, edits, old_snapshot, response_received_at, inputs) = result.await?;
anyhow::Ok(
EditPrediction::new(
anyhow::Ok(Some(
EditPredictionResult::new(
EditPredictionId(id.into()),
&buffer,
&old_snapshot,
@@ -254,7 +254,7 @@ impl SweepAi {
cx,
)
.await,
)
))
})
}
}
+737 -131
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -4,7 +4,7 @@ use std::{fmt::Write, ops::Range, path::Path, sync::Arc, time::Instant};
use crate::{
EditPredictionId, ZedUpdateRequiredError, Zeta,
prediction::{EditPrediction, EditPredictionInputs},
prediction::{EditPredictionInputs, EditPredictionResult},
};
use anyhow::{Context as _, Result};
use cloud_llm_client::{
@@ -36,7 +36,7 @@ pub(crate) fn request_prediction_with_zeta1(
position: language::Anchor,
events: Vec<Arc<Event>>,
cx: &mut Context<Zeta>,
) -> Task<Result<Option<EditPrediction>>> {
) -> Task<Result<Option<EditPredictionResult>>> {
let buffer = buffer.clone();
let buffer_snapshotted_at = Instant::now();
let client = zeta.client.clone();
@@ -216,7 +216,7 @@ pub(crate) fn request_prediction_with_zeta1(
);
}
edit_prediction
edit_prediction.map(Some)
})
}
@@ -229,7 +229,7 @@ fn process_completion_response(
buffer_snapshotted_at: Instant,
received_response_at: Instant,
cx: &AsyncApp,
) -> Task<Result<Option<EditPrediction>>> {
) -> Task<Result<EditPredictionResult>> {
let snapshot = snapshot.clone();
let request_id = prediction_response.request_id;
let output_excerpt = prediction_response.output_excerpt;
@@ -246,8 +246,9 @@ fn process_completion_response(
.await?
.into();
Ok(EditPrediction::new(
EditPredictionId(request_id.into()),
let id = EditPredictionId(request_id.into());
Ok(EditPredictionResult::new(
id,
&buffer,
&snapshot,
edits,
+1 -1
View File
@@ -538,7 +538,7 @@ async fn run_edit_prediction(
let prediction_task = zeta.update(cx, |zeta, cx| {
zeta.request_prediction(&project, buffer, cursor, cx)
});
prediction_task.await.unwrap().unwrap()
prediction_task.await.unwrap().unwrap().prediction.unwrap()
}
async fn make_test_zeta(
+4 -1
View File
@@ -235,7 +235,10 @@ pub async fn perform_predict(
let mut result = Arc::into_inner(result).unwrap().into_inner().unwrap();
result.diff = prediction
.and_then(|prediction| prediction.edit_preview.as_unified_diff(&prediction.edits))
.and_then(|prediction| {
let prediction = prediction.prediction.ok()?;
prediction.edit_preview.as_unified_diff(&prediction.edits)
})
.unwrap_or_default();
anyhow::Ok(result)