zeta_prompt: Add V4 edit prediction API types (#59869)

# Objective

Part of EP-10
Closes EP-118

- Describe the objective or issue this PR addresses.
- If you're fixing a specific issue, use "Fixes #X" for each issue as
[described in the GitHub
docs](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).

## Solution

- Describe the solution used to achieve the objective above.

## Testing

- Did you test these changes? If so, how?
- Are there any parts that need more testing?
- How can other people (reviewers) test your changes? Is there anything
specific they need to know?
- If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

> This section is optional. If this PR does not include a visual change
or does not add a new user-facing feature, you can delete this section.

- Help others understand the result of this PR by showcasing your
awesome work!
- If this PR includes a visual change, consider adding a screenshot,
GIF, or video
- A before/after comparison is very useful for changes to existing
features!

While a showcase should aim to be brief and digestible, you can use a
toggleable section to save space on longer showcases:

<details>
  <summary>Click to view showcase</summary>

My super cool demos here

</details>

---

Release Notes:

- N/A or Added/Fixed/Improved ...

Co-authored-by: Nbarry <nbarry@zed.dev>
This commit is contained in:
Ben Kunkle 2026-06-25 10:07:41 -04:00 committed by GitHub
parent b6e1e27887
commit 99129bb29e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 691 additions and 273 deletions

View file

@ -1,5 +1,7 @@
#[cfg(feature = "predict-edits")]
pub mod predict_edits_v3;
#[cfg(feature = "predict-edits")]
pub mod predict_edits_v4;
use std::str::FromStr;
use std::sync::Arc;

View file

@ -31,7 +31,7 @@ pub struct RawCompletionRequest {
#[derive(Debug, Serialize, Deserialize)]
pub struct PredictEditsV3Request {
#[serde(flatten)]
pub input: zeta_prompt::ZetaPromptInput,
pub input: zeta_prompt::Zeta2PromptInput,
}
#[derive(Debug, Deserialize, Serialize)]

View file

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct PredictEditsV4Request {
#[serde(flatten)]
pub input: zeta_prompt::Zeta3PromptInput,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PredictEditsV4Response {
pub request_id: String,
pub patch: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_version: Option<String>,
}

View file

@ -59,7 +59,7 @@ use std::rc::Rc;
use text::{AnchorRangeExt, Edit};
use workspace::{AppState, Workspace};
use zeta_prompt::ContextSource;
use zeta_prompt::{ZetaFormat, ZetaPromptInput};
use zeta_prompt::{Zeta2PromptInput, ZetaFormat};
use std::mem;
use std::ops::Range;
@ -99,9 +99,8 @@ use crate::license_detection::LicenseDetectionWatcher;
use crate::mercury::Mercury;
pub use crate::metrics::{KeptRateResult, compute_kept_rate};
use crate::onboarding_modal::ZedPredictModal;
pub use crate::prediction::EditPrediction;
pub use crate::prediction::EditPredictionId;
use crate::prediction::EditPredictionResult;
pub use crate::prediction::{EditPrediction, EditPredictionId, EditPredictionInputs};
pub use language_model::ApiKeyState;
pub use telemetry_events::EditPredictionRating;
pub use zed_edit_prediction_delegate::ZedEditPredictionDelegate;
@ -2898,7 +2897,7 @@ impl EditPredictionStore {
}
pub(crate) async fn send_v3_request(
input: ZetaPromptInput,
input: Zeta2PromptInput,
preferred_experiment: Option<String>,
client: Arc<Client>,
llm_token: LlmApiToken,

View file

@ -41,7 +41,7 @@ use util::{
};
use uuid::Uuid;
use workspace::{AppState, CollaboratorId, MultiWorkspace};
use zeta_prompt::ZetaPromptInput;
use zeta_prompt::Zeta2PromptInput;
use crate::udiff::apply_diff_to_string;
use crate::{
@ -2699,7 +2699,7 @@ async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) {
buffer: buffer.clone(),
snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
id: EditPredictionId("the-id".into()),
inputs: ZetaPromptInput {
inputs: EditPredictionInputs::V2(Zeta2PromptInput {
events: Default::default(),
related_files: Default::default(),
active_buffer_diagnostics: vec![],
@ -2712,7 +2712,7 @@ async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) {
in_open_source_repo: false,
can_collect_data: false,
repo_url: None,
},
}),
model_version: None,
trigger: PredictEditsRequestTrigger::Other,
};

View file

@ -4,11 +4,10 @@ use std::{borrow::Cow, fmt::Write as _, mem, path::Path, sync::Arc};
use telemetry_events::EditPredictionRating;
pub use zeta_prompt::udiff::{
CURSOR_POSITION_MARKER, encode_cursor_in_patch, extract_cursor_from_patch,
CURSOR_POSITION_MARKER, INLINE_CURSOR_MARKER, encode_cursor_in_patch, extract_cursor_from_patch,
};
use crate::data_collection::format_cursor_excerpt;
pub const INLINE_CURSOR_MARKER: &str = "<|user_cursor|>";
/// Maximum cursor file size to capture (64KB).
/// Files larger than this will not have their content captured,
@ -494,8 +493,7 @@ impl ExampleSpec {
/// to the start of the hunk.
///
/// In the serialized representation of this example, the cursor position is represented
/// using a comment line in the diff, beginning with `#`, and containing a `[CURSOR_POSITION]`
/// marker with the same format as the [`Self::cursor_excerpt`].
/// using an inline `<|user_cursor|>` marker in an added diff line.
pub fn expected_patches_with_cursor_positions(&self) -> Vec<(String, Option<usize>)> {
self.expected_patches
.iter()
@ -784,8 +782,7 @@ mod tests {
+// prints a greeting
fn main() {
- println!("hi");
+ println!("hello, {}", );
# ^[CURSOR_POSITION]
+ println!("hello, {}", <|user_cursor|>);
let x = 42;
}
"#}
@ -814,8 +811,7 @@ mod tests {
+++ b/test.rs
@@ -1,2 +1,2 @@
-fn old() {}
+fn new_name() {}
# ^[CURSOR_POSITION]
+fn new_<|user_cursor|>name() {}
"#};
let cursor_offset = "fn new_name() {}".find("name").unwrap();
@ -826,7 +822,7 @@ mod tests {
assert_eq!(
encoded_once
.lines()
.filter(|line| line.contains(CURSOR_POSITION_MARKER))
.filter(|line| line.contains(INLINE_CURSOR_MARKER))
.count(),
1
);

View file

@ -1,5 +1,5 @@
use crate::{
EditPredictionId, EditPredictionModelInput, cursor_excerpt,
EditPredictionId, EditPredictionInputs, EditPredictionModelInput, cursor_excerpt,
open_ai_compatible::{self, load_open_ai_compatible_api_key_if_needed},
prediction::EditPredictionResult,
};
@ -10,7 +10,7 @@ use language::{
language_settings::all_language_settings,
};
use std::{path::Path, sync::Arc, time::Instant};
use zeta_prompt::{ZetaPromptInput, compute_editable_and_context_ranges};
use zeta_prompt::{Zeta2PromptInput, compute_editable_and_context_ranges};
const FIM_CONTEXT_TOKENS: usize = 512;
@ -19,7 +19,7 @@ struct FimRequestOutput {
edits: Vec<(std::ops::Range<Anchor>, Arc<str>)>,
editable_range: std::ops::Range<Anchor>,
snapshot: BufferSnapshot,
inputs: ZetaPromptInput,
inputs: Zeta2PromptInput,
buffer: Entity<Buffer>,
}
@ -78,7 +78,7 @@ pub fn request_prediction(
0,
);
let inputs = ZetaPromptInput {
let inputs = Zeta2PromptInput {
events,
related_files: Some(Vec::new()),
active_buffer_diagnostics: Vec::new(),
@ -154,7 +154,7 @@ pub fn request_prediction(
output.edits.into(),
None,
Some(output.editable_range),
output.inputs,
EditPredictionInputs::V2(output.inputs),
None,
trigger,
cx.background_executor().now() - request_start,

View file

@ -1,7 +1,7 @@
use crate::{
DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionModelInput,
EditPredictionStartedDebugEvent, EditPredictionStore, open_ai_response::text_from_response,
prediction::EditPredictionResult, zeta::compute_edits,
DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionInputs,
EditPredictionModelInput, EditPredictionStartedDebugEvent, EditPredictionStore,
open_ai_response::text_from_response, prediction::EditPredictionResult, zeta::compute_edits,
};
use anyhow::{Context as _, Result};
use cloud_llm_client::EditPredictionRejectReason;
@ -16,7 +16,7 @@ use language_model::{ApiKeyState, EnvVar, env_var};
use release_channel::AppVersion;
use serde::{Deserialize, Serialize};
use std::{mem, ops::Range, path::Path, sync::Arc};
use zeta_prompt::ZetaPromptInput;
use zeta_prompt::Zeta2PromptInput;
const MERCURY_API_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions";
@ -103,7 +103,7 @@ impl Mercury {
+ excerpt_ranges.editable_350.start)
..(excerpt_offset_range.start + excerpt_ranges.editable_350.end);
let inputs = zeta_prompt::ZetaPromptInput {
let inputs = zeta_prompt::Zeta2PromptInput {
events,
related_files: Some(related_files),
cursor_offset_in_excerpt: cursor_point.to_offset(&snapshot)
@ -255,7 +255,7 @@ impl Mercury {
edits.into(),
None,
Some(editable_range),
inputs,
EditPredictionInputs::V2(inputs),
None,
trigger,
cx.background_executor().now() - request_start,
@ -267,7 +267,7 @@ impl Mercury {
}
}
fn build_prompt(inputs: &ZetaPromptInput) -> String {
fn build_prompt(inputs: &Zeta2PromptInput) -> String {
const RECENTLY_VIEWED_SNIPPETS_START: &str = "<|recently_viewed_code_snippets|>\n";
const RECENTLY_VIEWED_SNIPPETS_END: &str = "<|/recently_viewed_code_snippets|>\n";
const RECENTLY_VIEWED_SNIPPET_START: &str = "<|recently_viewed_code_snippet|>\n";

View file

@ -4,7 +4,7 @@ use cloud_llm_client::{EditPredictionRejectReason, PredictEditsRequestTrigger};
use edit_prediction_types::{PredictedCursorPosition, interpolate_edits};
use gpui::{AsyncApp, Entity, SharedString};
use language::{Anchor, Buffer, BufferSnapshot, EditPreview, TextBufferSnapshot};
use zeta_prompt::ZetaPromptInput;
use zeta_prompt::{Zeta2PromptInput, Zeta3PromptInput};
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)]
pub struct EditPredictionId(pub SharedString);
@ -21,6 +21,13 @@ impl std::fmt::Display for EditPredictionId {
}
}
#[derive(Clone, serde::Serialize)]
#[serde(tag = "version", content = "input")]
pub enum EditPredictionInputs {
V2(Zeta2PromptInput),
V3(Zeta3PromptInput),
}
/// A prediction response that was returned from the provider, whether it was ultimately valid or not.
pub struct EditPredictionResult {
pub prediction: EditPrediction,
@ -36,7 +43,7 @@ impl EditPredictionResult {
edits: Arc<[(Range<Anchor>, Arc<str>)]>,
cursor_position: Option<PredictedCursorPosition>,
editable_range: Option<Range<Anchor>>,
inputs: ZetaPromptInput,
inputs: EditPredictionInputs,
model_version: Option<String>,
trigger: PredictEditsRequestTrigger,
e2e_latency: std::time::Duration,
@ -99,8 +106,8 @@ pub struct EditPrediction {
pub editable_range: Option<Range<Anchor>>,
pub snapshot: BufferSnapshot,
pub edit_preview: EditPreview,
pub inputs: EditPredictionInputs,
pub buffer: Entity<Buffer>,
pub inputs: zeta_prompt::ZetaPromptInput,
pub model_version: Option<String>,
pub trigger: PredictEditsRequestTrigger,
}
@ -134,7 +141,7 @@ mod tests {
use super::*;
use gpui::{App, Entity, TestAppContext, prelude::*};
use language::{Buffer, ToOffset as _};
use zeta_prompt::ZetaPromptInput;
use zeta_prompt::Zeta2PromptInput;
#[gpui::test]
async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) {
@ -157,7 +164,7 @@ mod tests {
edit_preview,
model_version: None,
trigger: PredictEditsRequestTrigger::Other,
inputs: ZetaPromptInput {
inputs: EditPredictionInputs::V2(Zeta2PromptInput {
events: vec![],
related_files: Some(vec![]),
active_buffer_diagnostics: vec![],
@ -170,7 +177,7 @@ mod tests {
in_open_source_repo: false,
can_collect_data: false,
repo_url: None,
},
}),
};
cx.update(|cx| {

View file

@ -1,8 +1,8 @@
use crate::{
CloudRequestTimeoutError, CurrentEditPrediction, DebugEvent, EditPredictionFinishedDebugEvent,
EditPredictionId, EditPredictionModelInput, EditPredictionStartedDebugEvent,
EditPredictionStore, PromptHistoryBoundary, ZedUpdateRequiredError,
buffer_path_with_id_fallback,
EditPredictionId, EditPredictionInputs, EditPredictionModelInput,
EditPredictionStartedDebugEvent, EditPredictionStore, PromptHistoryBoundary,
ZedUpdateRequiredError, buffer_path_with_id_fallback,
cursor_excerpt::{self, compute_cursor_excerpt, compute_syntax_ranges},
data_collection::CapturedPredictionContext,
prediction::EditPredictionResult,
@ -24,7 +24,7 @@ use workspace::workspace_error::{ErrorAction, ErrorSeverity, WorkspaceError};
use std::{ops::Range, path::Path, sync::Arc};
use zeta_prompt::{
ParsedOutput, ZetaFormat, ZetaPromptInput, excerpt_ranges_for_format, format_zeta_prompt,
ParsedOutput, Zeta2PromptInput, ZetaFormat, excerpt_ranges_for_format, format_zeta_prompt,
get_prefill, parse_zeta2_model_output, stop_tokens_for_format,
zeta1::{self, EDITABLE_REGION_END_MARKER},
};
@ -84,7 +84,7 @@ pub(crate) fn request_prediction_with_zeta(
let app_version = AppVersion::global(cx);
struct Prediction {
prompt_input: ZetaPromptInput,
prompt_input: Zeta2PromptInput,
buffer: Entity<Buffer>,
snapshot: BufferSnapshot,
edits: Vec<(Range<Anchor>, Arc<str>)>,
@ -396,7 +396,7 @@ pub(crate) fn request_prediction_with_zeta(
edits.into(),
cursor_position,
Some(editable_anchor_range),
inputs,
EditPredictionInputs::V2(inputs),
model_version,
trigger,
request_duration,
@ -602,7 +602,7 @@ pub fn zeta2_prompt_input(
is_open_source: bool,
can_collect_data: bool,
repo_url: Option<String>,
) -> (Range<usize>, zeta_prompt::ZetaPromptInput) {
) -> (Range<usize>, zeta_prompt::Zeta2PromptInput) {
let (excerpt_point_range, excerpt_offset_range, cursor_offset_in_excerpt) =
compute_cursor_excerpt(snapshot, cursor_offset);
@ -624,7 +624,7 @@ pub fn zeta2_prompt_input(
ACTIVE_BUFFER_DIAGNOSTIC_ADDITIONAL_CONTEXT_TOKEN_COUNT,
);
let prompt_input = zeta_prompt::ZetaPromptInput {
let prompt_input = zeta_prompt::Zeta2PromptInput {
cursor_path: excerpt_path,
cursor_excerpt,
cursor_offset_in_excerpt,

View file

@ -16,7 +16,7 @@ use std::{
io::Read,
path::{Path, PathBuf},
};
use zeta_prompt::ZetaPromptInput;
use zeta_prompt::Zeta2PromptInput;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Example {
@ -26,7 +26,7 @@ pub struct Example {
/// The full content of the file where an edit is being predicted, and the
/// actual cursor offset.
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_inputs: Option<ZetaPromptInput>,
pub prompt_inputs: Option<Zeta2PromptInput>,
/// The input and expected output from the edit prediction model.
#[serde(skip_serializing_if = "Option::is_none")]

View file

@ -10,14 +10,14 @@ use gpui::AsyncApp;
use std::ops::Range;
use std::sync::Arc;
use zeta_prompt::{
ZetaFormat, ZetaPromptInput, format_edit_history_within_budget, format_expected_output,
Zeta2PromptInput, ZetaFormat, format_edit_history_within_budget, format_expected_output,
format_zeta_prompt,
hashed_regions::{self, SnippetMarkers},
max_edit_event_count_for_format, resolve_cursor_region,
};
fn resolved_excerpt_ranges_for_format(
input: &zeta_prompt::ZetaPromptInput,
input: &zeta_prompt::Zeta2PromptInput,
format: ZetaFormat,
) -> (Range<usize>, Range<usize>) {
let (_, editable_range_in_context, context_range, _) = resolve_cursor_region(input, format);
@ -457,7 +457,7 @@ impl TeacherJumpsPrompt {
Ok((patch, actual_cursor))
}
fn format_edit_history(prompt_inputs: &ZetaPromptInput) -> String {
fn format_edit_history(prompt_inputs: &Zeta2PromptInput) -> String {
format_edit_history_within_budget(
&prompt_inputs.events,
"",
@ -473,7 +473,7 @@ impl TeacherJumpsPrompt {
/// is skipped: it renders in its own prompt section via
/// `format_cursor_excerpt`, and including it here would duplicate it.
fn format_context(
prompt_inputs: &ZetaPromptInput,
prompt_inputs: &Zeta2PromptInput,
marker_table: &[SnippetMarkers],
max_tokens: usize,
cursor_file_ix: usize,
@ -601,7 +601,7 @@ impl TeacherJumpsPrompt {
/// content appears in the prompt exactly once.
fn format_cursor_excerpt(
example: &Example,
prompt_inputs: &ZetaPromptInput,
prompt_inputs: &Zeta2PromptInput,
marker_table: &[SnippetMarkers],
cursor: &hashed_regions::RelatedFileCursor,
) -> Result<String> {
@ -841,7 +841,7 @@ mod tests {
human_feedback: Vec::new(),
rating: None,
},
prompt_inputs: Some(zeta_prompt::ZetaPromptInput {
prompt_inputs: Some(zeta_prompt::Zeta2PromptInput {
cursor_path: std::path::Path::new("src/main.rs").into(),
cursor_excerpt: cursor_excerpt.into(),
cursor_offset_in_excerpt: cursor_offset,
@ -1598,7 +1598,7 @@ mod tests {
.map(|index| format!("line{index:02}\n"))
.collect::<String>();
let cursor_offset = excerpt.find("line40").expect("cursor line exists");
let prompt_inputs = zeta_prompt::ZetaPromptInput {
let prompt_inputs = zeta_prompt::Zeta2PromptInput {
cursor_path: std::path::Path::new("src/main.rs").into(),
cursor_excerpt: excerpt.clone().into(),
cursor_offset_in_excerpt: cursor_offset,

View file

@ -15,7 +15,7 @@ use gpui::{AsyncApp, Entity};
use language::{Anchor, Buffer, LanguageNotFound, ToOffset};
use project::{Project, ProjectPath, buffer_store::BufferStoreEvent};
use std::{fs, path::PathBuf, sync::Arc};
use zeta_prompt::ZetaPromptInput;
use zeta_prompt::Zeta2PromptInput;
pub async fn run_load_project(
example: &mut Example,
@ -96,7 +96,7 @@ pub async fn run_load_project(
);
(
ZetaPromptInput {
Zeta2PromptInput {
cursor_path: example.spec.cursor_path.clone(),
cursor_excerpt,
cursor_offset_in_excerpt,

View file

@ -12,7 +12,7 @@ use std::sync::Arc;
use std::time::Duration;
use telemetry_events::EditPredictionRating;
use zeta_prompt::{ZetaFormat, ZetaPromptInput, excerpt_range_for_format};
use zeta_prompt::{Zeta2PromptInput, ZetaFormat, excerpt_range_for_format};
use crate::PredictionProvider;
use crate::example::{Example, ExamplePrediction, ExamplePrompt};
@ -1130,7 +1130,7 @@ fn rated_examples_from_response<'a>(
let request_id = get_string("request_id");
let inputs_json = get_json("inputs");
let inputs: Option<ZetaPromptInput> = match &inputs_json {
let inputs: Option<Zeta2PromptInput> = match &inputs_json {
Some(v) => match serde_json::from_value(v.clone()) {
Ok(parsed) => Some(parsed),
Err(e) => {
@ -1188,7 +1188,7 @@ fn build_rated_example(
request_id: Option<String>,
device_id: String,
time: String,
input: ZetaPromptInput,
input: Zeta2PromptInput,
output: String,
settled_editable_region: Option<String>,
rating: String,
@ -1296,7 +1296,7 @@ fn requested_examples_from_response<'a>(
let device_id = get_string("device_id");
let time = get_string("time");
let input_json = get_json("input");
let input: Option<ZetaPromptInput> =
let input: Option<Zeta2PromptInput> =
input_json.clone().and_then(|v| serde_json::from_value(v).ok());
let zed_version = get_string("zed_version");
@ -1376,7 +1376,7 @@ fn settled_examples_from_response<'a>(
let time = get_string("time");
let input_raw = get_value("input");
let input_json = parse_json_value(input_raw.as_ref());
let input: Option<ZetaPromptInput> = input_json
let input: Option<Zeta2PromptInput> = input_json
.as_ref()
.and_then(|parsed| serde_json::from_value(parsed.clone()).ok());
let requested_output = get_string("requested_output");
@ -1497,7 +1497,7 @@ fn captured_examples_from_response<'a>(
let time = get_string("time");
let input_raw = get_value("input");
let input_json = parse_json_value(input_raw.as_ref());
let input: Option<ZetaPromptInput> = input_json
let input: Option<Zeta2PromptInput> = input_json
.as_ref()
.and_then(|parsed| serde_json::from_value(parsed.clone()).ok());
let example_raw = get_value("example");
@ -1580,7 +1580,7 @@ fn build_settled_example(
request_id: String,
device_id: String,
time: String,
input: ZetaPromptInput,
input: Zeta2PromptInput,
requested_output: String,
settled_editable_region: String,
requested_format: ZetaFormat,
@ -1636,7 +1636,7 @@ fn build_captured_example(
request_id: String,
device_id: String,
time: String,
input: ZetaPromptInput,
input: Zeta2PromptInput,
mut example_spec: ExampleSpec,
settled_editable_region: String,
zed_version: Option<String>,
@ -1721,7 +1721,7 @@ fn rejected_examples_from_response<'a>(
let device_id = get_string("device_id");
let time = get_string("time");
let input_json = get_json("input");
let input: Option<ZetaPromptInput> =
let input: Option<Zeta2PromptInput> =
input_json.clone().and_then(|v| serde_json::from_value(v).ok());
let prompt = get_string("prompt");
let output = get_string("output");
@ -1768,7 +1768,7 @@ fn build_rejected_example(
request_id: String,
device_id: String,
time: String,
input: ZetaPromptInput,
input: Zeta2PromptInput,
prompt: Option<String>,
output: String,
settled_editable_region: Option<String>,
@ -1872,7 +1872,7 @@ fn accepted_examples_from_response<'a>(
let device_id = get_string("device_id");
let time = get_string("time");
let input_json = get_json("input");
let input: Option<ZetaPromptInput> =
let input: Option<Zeta2PromptInput> =
input_json.clone().and_then(|v| serde_json::from_value(v).ok());
let prompt = get_string("prompt");
let output = get_string("output");
@ -1913,7 +1913,7 @@ fn build_accepted_example(
request_id: String,
device_id: String,
time: String,
input: ZetaPromptInput,
input: Zeta2PromptInput,
prompt: Option<String>,
output: String,
settled_editable_region: Option<String>,
@ -1970,7 +1970,7 @@ fn build_example_from_snowflake(
request_id: String,
device_id: String,
time: String,
input: ZetaPromptInput,
input: Zeta2PromptInput,
tags: Vec<String>,
rejection: Option<RejectionInfo>,
zed_version: Option<String>,

View file

@ -202,7 +202,7 @@ pub fn run_context_coverage_scoring(
fn context_excerpts(
_example: &Example,
prompt_inputs: &zeta_prompt::ZetaPromptInput,
prompt_inputs: &zeta_prompt::Zeta2PromptInput,
retrieved_context_byte_limit: Option<usize>,
context_source_filter: Option<&[ContextSource]>,
) -> Vec<Excerpt> {
@ -836,7 +836,7 @@ mod tests {
use edit_prediction::example_spec::ExampleSpec;
use edit_prediction_metrics::PredictionScore;
use std::path::Path;
use zeta_prompt::{ExcerptRanges, RelatedExcerpt, ZetaPromptInput};
use zeta_prompt::{ExcerptRanges, RelatedExcerpt, Zeta2PromptInput};
#[test]
fn summary_includes_limited_filtered_retrieved_context_bytes_once_per_example() {
@ -889,7 +889,7 @@ mod tests {
human_feedback: Vec::new(),
rating: None,
},
prompt_inputs: Some(ZetaPromptInput {
prompt_inputs: Some(Zeta2PromptInput {
cursor_path: Path::new("project/src/main.rs").into(),
cursor_excerpt: "".into(),
cursor_offset_in_excerpt: 0,

View file

@ -797,10 +797,10 @@ mod tests {
use super::*;
use indoc::indoc;
use zeta_prompt::udiff::{apply_diff_to_string, unified_diff_with_context};
use zeta_prompt::{ExcerptRanges, ZetaPromptInput};
use zeta_prompt::{ExcerptRanges, Zeta2PromptInput};
fn compute_prediction_reversal_ratio(
prompt_inputs: &ZetaPromptInput,
prompt_inputs: &Zeta2PromptInput,
predicted_content: &str,
cursor_path: &Path,
) -> f32 {
@ -817,8 +817,8 @@ mod tests {
content: &str,
events: Vec<Arc<zeta_prompt::Event>>,
excerpt_start_row: Option<u32>,
) -> ZetaPromptInput {
ZetaPromptInput {
) -> Zeta2PromptInput {
Zeta2PromptInput {
cursor_path: Arc::from(Path::new("src/test.rs")),
cursor_excerpt: content.into(),
cursor_offset_in_excerpt: 0,

View file

@ -1,6 +1,8 @@
use buffer_diff::BufferDiff;
use cloud_llm_client::PredictEditsRequestTrigger;
use edit_prediction::{EditPrediction, EditPredictionRating, EditPredictionStore};
use edit_prediction::{
EditPrediction, EditPredictionInputs, EditPredictionRating, EditPredictionStore,
};
use editor::{Editor, Inlay, MultiBuffer};
use feature_flags::{FeatureFlag, PresenceFlag, register_feature_flag};
use gpui::{
@ -18,13 +20,14 @@ use project::{
};
use settings::Settings as _;
use std::rc::Rc;
use std::{fmt::Write, ops::Range, sync::Arc};
use std::{fmt::Write, ops::Range, path::Path, sync::Arc};
use theme_settings::ThemeSettings;
use ui::{
ContextMenu, DropdownMenu, KeyBinding, List, ListItem, ListItemSpacing, PopoverMenuHandle,
Tooltip, prelude::*,
};
use workspace::{ModalView, Workspace};
use zeta_prompt::{ContextSource, FilePosition, RelatedExcerpt, RelatedFile, Zeta3PromptInput};
actions!(
zeta,
@ -409,6 +412,145 @@ impl RatePredictionsModal {
Some(format!("{header}{diff_body}"))
}
fn write_formatted_inputs(formatted_inputs: &mut String, inputs: &EditPredictionInputs) {
match inputs {
EditPredictionInputs::V2(inputs) => {
Self::write_events(formatted_inputs, &inputs.events);
Self::write_related_files(
formatted_inputs,
inputs.related_files.as_deref().unwrap_or_default(),
);
Self::write_cursor_excerpt(
formatted_inputs,
inputs.cursor_path.as_ref(),
inputs.cursor_excerpt.as_ref(),
inputs.cursor_offset_in_excerpt,
);
}
EditPredictionInputs::V3(inputs) => {
Self::write_events(formatted_inputs, &inputs.events);
Self::write_related_files(formatted_inputs, &inputs.editable_context);
Self::write_zeta3_cursor_excerpt(formatted_inputs, inputs);
}
}
}
fn write_events(formatted_inputs: &mut String, events: &[Arc<zeta_prompt::Event>]) {
write!(formatted_inputs, "## Events\n\n").unwrap();
for event in events {
formatted_inputs.push_str("```diff\n");
zeta_prompt::write_event(formatted_inputs, event.as_ref());
formatted_inputs.push_str("```\n\n");
}
}
fn write_related_files(formatted_inputs: &mut String, included_files: &[RelatedFile]) {
write!(formatted_inputs, "## Related files\n\n").unwrap();
for included_file in included_files {
write!(formatted_inputs, "### {}\n\n", included_file.path.display()).unwrap();
for excerpt in included_file.excerpts.iter() {
write!(
formatted_inputs,
"```{}\n{}\n```\n",
included_file.path.display(),
excerpt.text
)
.unwrap();
}
}
}
fn write_zeta3_cursor_excerpt(formatted_inputs: &mut String, inputs: &Zeta3PromptInput) {
let current_excerpt = inputs
.editable_context
.iter()
.filter(|file| file.path == inputs.cursor_path)
.flat_map(|file| file.excerpts.iter())
.find_map(|excerpt| {
if excerpt.context_source != ContextSource::CurrentFile {
return None;
}
Some((
excerpt,
Self::offset_for_position_in_excerpt(excerpt, inputs.cursor_position)?,
))
});
if let Some((excerpt, cursor_offset)) = current_excerpt {
Self::write_cursor_excerpt(
formatted_inputs,
inputs.cursor_path.as_ref(),
excerpt.text.as_ref(),
cursor_offset,
);
} else {
write!(formatted_inputs, "## Cursor Excerpt\n\n").unwrap();
writeln!(
formatted_inputs,
"No current-file excerpt found for `{}` at row {}, column {}.",
inputs.cursor_path.display(),
inputs.cursor_position.row,
inputs.cursor_position.column
)
.unwrap();
}
}
fn write_cursor_excerpt(
formatted_inputs: &mut String,
cursor_path: &Path,
cursor_excerpt: &str,
cursor_offset: usize,
) {
write!(formatted_inputs, "## Cursor Excerpt\n\n").unwrap();
let mut cursor_offset = cursor_offset.min(cursor_excerpt.len());
while !cursor_excerpt.is_char_boundary(cursor_offset) {
cursor_offset = cursor_offset.saturating_sub(1);
}
writeln!(
formatted_inputs,
"```{}\n{}<CURSOR>{}\n```\n",
cursor_path.display(),
&cursor_excerpt[..cursor_offset],
&cursor_excerpt[cursor_offset..],
)
.unwrap();
}
fn offset_for_position_in_excerpt(
excerpt: &RelatedExcerpt,
position: FilePosition,
) -> Option<usize> {
if position.row < excerpt.row_range.start {
return None;
}
let relative_row = (position.row - excerpt.row_range.start) as usize;
let text = excerpt.text.as_ref();
let mut row_start = 0;
for row in 0..=relative_row {
if row == relative_row {
let row_end = text[row_start..]
.find('\n')
.map_or(text.len(), |offset| row_start + offset);
let row_text = &text[row_start..row_end];
let column =
row_text.floor_char_boundary((position.column as usize).min(row_text.len()));
return Some(row_start + column);
}
row_start += text[row_start..].find('\n')? + 1;
}
None
}
pub fn select_completion(
&mut self,
prediction: Option<EditPrediction>,
@ -524,63 +666,7 @@ impl RatePredictionsModal {
});
let mut formatted_inputs = String::new();
write!(&mut formatted_inputs, "## Events\n\n").unwrap();
for event in &prediction.inputs.events {
formatted_inputs.push_str("```diff\n");
zeta_prompt::write_event(&mut formatted_inputs, event.as_ref());
formatted_inputs.push_str("```\n\n");
}
write!(&mut formatted_inputs, "## Related files\n\n").unwrap();
for included_file in prediction
.inputs
.related_files
.as_deref()
.unwrap_or_default()
.iter()
{
write!(
&mut formatted_inputs,
"### {}\n\n",
included_file.path.display()
)
.unwrap();
for excerpt in included_file.excerpts.iter() {
write!(
&mut formatted_inputs,
"```{}\n{}\n```\n",
included_file.path.display(),
excerpt.text
)
.unwrap();
}
}
write!(&mut formatted_inputs, "## Cursor Excerpt\n\n").unwrap();
let mut cursor_offset = prediction
.inputs
.cursor_offset_in_excerpt
.min(prediction.inputs.cursor_excerpt.len());
while !prediction
.inputs
.cursor_excerpt
.is_char_boundary(cursor_offset)
{
cursor_offset = cursor_offset.saturating_sub(1);
}
writeln!(
&mut formatted_inputs,
"```{}\n{}<CURSOR>{}\n```\n",
prediction.inputs.cursor_path.display(),
&prediction.inputs.cursor_excerpt[..cursor_offset],
&prediction.inputs.cursor_excerpt[cursor_offset..],
)
.unwrap();
Self::write_formatted_inputs(&mut formatted_inputs, &prediction.inputs);
let current_editable_region = editable_range.as_ref().map(|range| {
prediction

View file

@ -13,7 +13,7 @@
//! weren't run through current-file retrieval can be normalized with
//! [`ensure_cursor_file_excerpt`] before rendering or parsing.
use crate::{ContextSource, RelatedExcerpt, RelatedFile, ZetaPromptInput, multi_region, udiff};
use crate::{ContextSource, RelatedExcerpt, RelatedFile, Zeta2PromptInput, multi_region, udiff};
use anyhow::{Context as _, Result, anyhow};
use std::{
borrow::Cow,
@ -50,11 +50,11 @@ pub struct SnippetMarkers {
///
/// The assignment is deterministic and independent of any later budget-based
/// truncation, so the same table can be rebuilt when parsing model output.
pub fn build_marker_table(input: &ZetaPromptInput) -> Vec<SnippetMarkers> {
pub fn build_marker_table(input: &Zeta2PromptInput) -> Vec<SnippetMarkers> {
build_marker_table_with_filter(input, |_| true)
}
pub fn build_editable_marker_table(input: &ZetaPromptInput) -> Vec<SnippetMarkers> {
pub fn build_editable_marker_table(input: &Zeta2PromptInput) -> Vec<SnippetMarkers> {
build_marker_table_with_filter(input, is_hash_region_editable_context_source)
}
@ -66,7 +66,7 @@ pub fn is_hash_region_editable_context_source(context_source: ContextSource) ->
}
fn build_marker_table_with_filter(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
include_context_source: impl Fn(ContextSource) -> bool,
) -> Vec<SnippetMarkers> {
let mut used_ids = HashSet::new();
@ -274,7 +274,7 @@ fn line_start_offset(text: &str, row: usize) -> Option<usize> {
Some(offset)
}
pub fn locate_cursor_in_related_files(input: &ZetaPromptInput) -> Option<RelatedFileCursor> {
pub fn locate_cursor_in_related_files(input: &Zeta2PromptInput) -> Option<RelatedFileCursor> {
let related_files = input.related_files.as_deref()?;
let excerpt_start_row = input.excerpt_start_row?;
let cursor_offset = input.cursor_offset_in_excerpt;
@ -316,7 +316,7 @@ pub fn locate_cursor_in_related_files(input: &ZetaPromptInput) -> Option<Related
///
/// All hashed-region context — including the current file — is addressed
/// through `related_files` (see module docs), so a prompt built from a
/// `ZetaPromptInput` whose `related_files` don't cover the cursor cannot be
/// `Zeta2PromptInput` whose `related_files` don't cover the cursor cannot be
/// rendered or parsed. Inputs produced by current-file context retrieval
/// (`ContextSource::CurrentFile`) are already covered and left untouched; this
/// normalizes the rest (e.g. raw settled-data samples, or any caller that
@ -333,7 +333,7 @@ pub fn locate_cursor_in_related_files(input: &ZetaPromptInput) -> Option<Related
/// the synthesized excerpt). Returns `false` only when coverage couldn't be
/// established — e.g. a missing `excerpt_start_row` or an empty
/// `cursor_excerpt` — in which case the input is left unchanged.
pub fn ensure_cursor_file_excerpt(input: &mut ZetaPromptInput) -> bool {
pub fn ensure_cursor_file_excerpt(input: &mut Zeta2PromptInput) -> bool {
if locate_cursor_in_related_files(input).is_some() {
return true;
}
@ -391,7 +391,7 @@ pub fn marker_table_for_excerpt(
}
fn merge_contiguous_snippets(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
marker_table: Vec<SnippetMarkers>,
) -> Result<Vec<ParseSnippet<'_>>> {
let related_files = input
@ -441,7 +441,7 @@ fn merge_contiguous_snippets(
}
fn snippet_path_and_start_row(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
snippet: &ParseSnippet<'_>,
) -> Result<(PathBuf, u32)> {
let related_files = input
@ -565,7 +565,7 @@ pub fn encode_from_old_and_new(
/// which emits exactly two tags per block and no intermediate tags. Any
/// unpaired trailing tag is ignored.
pub fn parse_output_as_patch(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
output: &str,
cursor_marker: &str,
) -> Result<String> {
@ -651,7 +651,7 @@ fn find_all_marker_tags(text: &str) -> Vec<(String, usize, usize)> {
/// every region that contains it; the returned [`HashRegionCursor`] reports the
/// first such position.
pub fn build_patch_from_spans(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
spans: &[(String, String, String)],
cursor_marker: &str,
) -> Result<(String, Option<HashRegionCursor>)> {
@ -731,7 +731,7 @@ pub fn build_patch_from_spans(
/// Apply resolved edits to their snippets and emit one diff section per edited
/// snippet, in the order snippets first appear in the edit sequence.
fn assemble_patch_from_edits(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
snippets: &[ParseSnippet<'_>],
edits: Vec<ParsedSpanEdit>,
) -> Result<(String, Option<HashRegionCursor>)> {
@ -887,7 +887,7 @@ fn detect_trailing_deletion(old_span: &str, new_span: &str) -> Option<String> {
/// If at least one hunk is reachable, the remaining hunks are still encoded
/// (partial edit). If no hunk is reachable, the output is `NO_EDITS`.
pub fn encode_patch_as_output(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
patch: &str,
cursor_offset: Option<usize>,
cursor_marker: &str,
@ -973,8 +973,8 @@ mod tests {
use std::path::PathBuf;
use std::sync::Arc;
fn make_input(cursor_excerpt: &str, related: &[(&str, &[&str])]) -> ZetaPromptInput {
ZetaPromptInput {
fn make_input(cursor_excerpt: &str, related: &[(&str, &[&str])]) -> Zeta2PromptInput {
Zeta2PromptInput {
cursor_path: PathBuf::from("src/main.rs").into(),
cursor_excerpt: cursor_excerpt.into(),
cursor_offset_in_excerpt: 0,

View file

@ -88,53 +88,37 @@ pub fn strip_diff_metadata(diff: &str) -> String {
result
}
/// Marker used to encode cursor position in patch comment lines.
pub const CURSOR_POSITION_MARKER: &str = "[CURSOR_POSITION]";
pub const INLINE_CURSOR_MARKER: &str = "<|user_cursor|>";
/// Extract cursor offset from a patch and return `(clean_patch, cursor_offset)`.
///
/// Cursor position is encoded as a comment line (starting with `#`) containing
/// `[CURSOR_POSITION]`. A `^` in the line indicates the cursor column; a `<`
/// indicates column 0. The offset is computed relative to addition (`+`) and
/// context (` `) lines accumulated so far in the hunk, which represent the
/// cursor position within the new text contributed by the hunk.
pub fn extract_cursor_from_patch(patch: &str) -> (String, Option<usize>) {
let mut clean_patch = String::new();
let mut cursor_offset: Option<usize> = None;
let mut cursor_offset = None;
let mut line_start_offset = 0usize;
let mut prev_line_start_offset = 0usize;
for line in patch.lines() {
let diff_line = DiffLine::parse(line);
if !clean_patch.is_empty() {
clean_patch.push('\n');
}
match &diff_line {
DiffLine::Garbage(content)
if content.starts_with('#') && content.contains(CURSOR_POSITION_MARKER) =>
{
let caret_column = if let Some(caret_pos) = content.find('^') {
caret_pos
} else if content.find('<').is_some() {
0
} else {
continue;
};
let cursor_column = caret_column.saturating_sub('#'.len_utf8());
cursor_offset = Some(prev_line_start_offset + cursor_column);
}
_ => {
if !clean_patch.is_empty() {
clean_patch.push('\n');
match DiffLine::parse(line) {
DiffLine::Addition(content) => {
let clean_content = content.replace(INLINE_CURSOR_MARKER, "");
if cursor_offset.is_none()
&& let Some(marker_offset) = content.find(INLINE_CURSOR_MARKER)
{
cursor_offset = Some(line_start_offset + marker_offset);
}
clean_patch.push('+');
clean_patch.push_str(&clean_content);
line_start_offset += clean_content.len() + 1;
}
DiffLine::Context(content) => {
clean_patch.push_str(line);
match diff_line {
DiffLine::Addition(content) | DiffLine::Context(content) => {
prev_line_start_offset = line_start_offset;
line_start_offset += content.len() + 1;
}
_ => {}
}
line_start_offset += content.len() + 1;
}
_ => clean_patch.push_str(line),
}
}
@ -356,40 +340,33 @@ pub fn encode_cursor_in_patch(patch: &str, cursor_offset: Option<usize>) -> Stri
let mut line_start_offset = 0usize;
for line in patch.lines() {
if matches!(
DiffLine::parse(line),
DiffLine::Garbage(content)
if content.starts_with('#') && content.contains(CURSOR_POSITION_MARKER)
) {
continue;
}
if !result.is_empty() {
result.push('\n');
}
result.push_str(line);
match DiffLine::parse(line) {
DiffLine::Addition(content) => {
let content = content.replace(INLINE_CURSOR_MARKER, "");
let line_end_offset = line_start_offset + content.len();
if cursor_offset >= line_start_offset && cursor_offset <= line_end_offset {
let cursor_column = cursor_offset - line_start_offset;
result.push('\n');
result.push('#');
for _ in 0..cursor_column {
result.push(' ');
}
write!(result, "^{}", CURSOR_POSITION_MARKER).unwrap();
result.push('+');
if cursor_offset >= line_start_offset
&& cursor_offset <= line_end_offset
&& let Some(before) = content.get(..cursor_offset - line_start_offset)
&& let Some(after) = content.get(cursor_offset - line_start_offset..)
{
result.push_str(before);
result.push_str(INLINE_CURSOR_MARKER);
result.push_str(after);
} else {
result.push_str(&content);
}
line_start_offset = line_end_offset + 1;
}
DiffLine::Context(content) => {
result.push_str(line);
line_start_offset += content.len() + 1;
}
_ => {}
_ => result.push_str(line),
}
}

View file

@ -51,7 +51,7 @@ pub fn clamp_text_to_token_count(text: &str, max_tokens: usize) -> &str {
}
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
pub struct ZetaPromptInput {
pub struct Zeta2PromptInput {
pub cursor_path: Arc<Path>,
pub cursor_excerpt: Arc<str>,
pub cursor_offset_in_excerpt: usize,
@ -78,6 +78,30 @@ pub struct ZetaPromptInput {
pub repo_url: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FilePosition {
pub row: u32,
pub column: u32,
}
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
pub struct Zeta3PromptInput {
pub cursor_path: Arc<Path>,
pub cursor_position: FilePosition,
pub events: Vec<Arc<Event>>,
pub editable_context: Vec<RelatedFile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub syntax_ranges: Vec<Range<usize>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_buffer_diagnostics: Vec<ActiveBufferDiagnostic>,
#[serde(default)]
pub in_open_source_repo: bool,
#[serde(default)]
pub can_collect_data: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repo_url: Option<String>,
}
#[derive(
Default,
Clone,
@ -275,7 +299,7 @@ pub enum ContextSource {
OracleSnippet,
}
pub fn prompt_input_contains_special_tokens(input: &ZetaPromptInput, format: ZetaFormat) -> bool {
pub fn prompt_input_contains_special_tokens(input: &Zeta2PromptInput, format: ZetaFormat) -> bool {
special_tokens_for_format(format).iter().any(|token| {
if let Some(line_token) = token.strip_suffix('\n') {
input.cursor_excerpt.lines().any(|line| line == line_token)
@ -285,8 +309,49 @@ pub fn prompt_input_contains_special_tokens(input: &ZetaPromptInput, format: Zet
})
}
pub fn format_zeta_prompt(input: &ZetaPromptInput, format: ZetaFormat) -> Option<String> {
let max_prompt_tokens = match format {
pub fn format_zeta_prompt(input: &Zeta2PromptInput, format: ZetaFormat) -> Option<String> {
format_prompt_with_budget_for_format(input, format, max_prompt_tokens_for_format(format))
}
pub fn format_zeta3_prompt(input: &Zeta3PromptInput, format: ZetaFormat) -> Option<String> {
match format {
ZetaFormat::V0318SeedMultiRegions => {}
_ => return None,
}
let (current_excerpt, cursor_offset_in_excerpt) = zeta3_current_file_excerpt(input)?;
let (context, editable_range, context_range, cursor_offset) = resolve_zeta3_cursor_region(
current_excerpt.text.as_ref(),
cursor_offset_in_excerpt,
&input.syntax_ranges,
format,
);
let relative_row_range =
offset_range_to_row_range(current_excerpt.text.as_ref(), context_range);
let cursor_row_range = current_excerpt.row_range.start + relative_row_range.start
..current_excerpt.row_range.start + relative_row_range.end;
let related_files = filter_redundant_excerpts(
zeta3_related_files(input, current_excerpt),
input.cursor_path.as_ref(),
cursor_row_range,
);
format_resolved_prompt_with_budget(
format,
input.cursor_path.as_ref(),
context,
&editable_range,
cursor_offset,
&input.events,
&related_files,
&input.active_buffer_diagnostics,
Some(input.cursor_position.row),
max_prompt_tokens_for_format(format),
)
}
fn max_prompt_tokens_for_format(format: ZetaFormat) -> usize {
match format {
ZetaFormat::V0112MiddleAtEnd
| ZetaFormat::V0113Ordered
| ZetaFormat::V0114180EditableRegion
@ -306,9 +371,88 @@ pub fn format_zeta_prompt(input: &ZetaPromptInput, format: ZetaFormat) -> Option
ZetaFormat::V0615HashRegions => 8000,
ZetaFormat::V0420Diagnostics => 8192,
ZetaFormat::V0327SingleFile => 16384,
};
}
}
format_prompt_with_budget_for_format(input, format, max_prompt_tokens)
fn zeta3_current_file_excerpt(input: &Zeta3PromptInput) -> Option<(&RelatedExcerpt, usize)> {
input
.editable_context
.iter()
.filter(|file| file.path == input.cursor_path)
.flat_map(|file| file.excerpts.iter())
.find_map(|excerpt| {
if excerpt.context_source != ContextSource::CurrentFile {
return None;
}
Some((
excerpt,
offset_for_position_in_excerpt(excerpt, input.cursor_position)?,
))
})
}
fn offset_for_position_in_excerpt(
excerpt: &RelatedExcerpt,
position: FilePosition,
) -> Option<usize> {
if position.row < excerpt.row_range.start {
return None;
}
let relative_row = (position.row - excerpt.row_range.start) as usize;
let text = excerpt.text.as_ref();
let mut row_start = 0;
for row in 0..=relative_row {
if row == relative_row {
let row_end = text[row_start..]
.find('\n')
.map_or(text.len(), |offset| row_start + offset);
let row_text = &text[row_start..row_end];
let column =
row_text.floor_char_boundary((position.column as usize).min(row_text.len()));
return Some(row_start + column);
}
row_start += text[row_start..].find('\n')? + 1;
}
None
}
fn zeta3_related_files(
input: &Zeta3PromptInput,
current_excerpt: &RelatedExcerpt,
) -> Vec<RelatedFile> {
input
.editable_context
.iter()
.filter_map(|file| {
let mut file = file.clone();
if file.path == input.cursor_path {
file.excerpts.retain(|excerpt| excerpt != current_excerpt);
}
(!file.excerpts.is_empty()).then_some(file)
})
.collect()
}
fn resolve_zeta3_cursor_region<'a>(
cursor_excerpt: &'a str,
cursor_offset: usize,
syntax_ranges: &[Range<usize>],
format: ZetaFormat,
) -> (&'a str, Range<usize>, Range<usize>, usize) {
let (editable_tokens, context_tokens) = token_limits_for_format(format);
let (editable_range, context_range) = compute_editable_and_context_ranges(
cursor_excerpt,
cursor_offset,
syntax_ranges,
editable_tokens,
context_tokens,
);
adjust_cursor_region(cursor_excerpt, cursor_offset, editable_range, context_range)
}
pub fn special_tokens_for_format(format: ZetaFormat) -> &'static [&'static str] {
@ -888,7 +1032,7 @@ fn assemble_single_file_fim_prompt(
}
fn format_hash_region_related_files_within_budget(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
marker_table: &[hashed_regions::SnippetMarkers],
cursor: &hashed_regions::RelatedFileCursor,
max_tokens: usize,
@ -1020,7 +1164,7 @@ fn format_hash_region_related_files_within_budget(
}
fn format_hash_regions_prompt_with_budget(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
max_tokens: usize,
) -> Option<String> {
let marker_table = hashed_regions::build_marker_table(input);
@ -1054,14 +1198,19 @@ fn format_hash_regions_prompt_with_budget(
}
pub fn format_prompt_with_budget_for_format(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
format: ZetaFormat,
max_tokens: usize,
) -> Option<String> {
if format == ZetaFormat::V0615HashRegions {
return format_hash_regions_prompt_with_budget(
input,
apply_prompt_budget_margin(max_tokens),
);
}
let (context, editable_range, context_range, cursor_offset) =
resolve_cursor_region(input, format);
let path = &*input.cursor_path;
let empty_files = Vec::new();
let input_related_files = input.related_files.as_deref().unwrap_or(&empty_files);
let filtered_related_files = if format == ZetaFormat::V0615HashRegions {
@ -1079,12 +1228,41 @@ pub fn format_prompt_with_budget_for_format(
} else {
input_related_files.to_vec()
};
let related_files = filtered_related_files.as_slice();
let cursor_buffer_row = input.excerpt_start_row.map(|excerpt_start_row| {
excerpt_start_row
+ input.cursor_excerpt[..context_range.start + cursor_offset]
.bytes()
.filter(|byte| *byte == b'\n')
.count() as u32
});
format_resolved_prompt_with_budget(
format,
input.cursor_path.as_ref(),
context,
&editable_range,
cursor_offset,
&input.events,
&filtered_related_files,
&input.active_buffer_diagnostics,
cursor_buffer_row,
max_tokens,
)
}
fn format_resolved_prompt_with_budget(
format: ZetaFormat,
path: &Path,
context: &str,
editable_range: &Range<usize>,
cursor_offset: usize,
events: &[Arc<Event>],
related_files: &[RelatedFile],
active_buffer_diagnostics: &[ActiveBufferDiagnostic],
cursor_buffer_row: Option<u32>,
max_tokens: usize,
) -> Option<String> {
let prompt = match format {
ZetaFormat::V0615HashRegions => {
format_hash_regions_prompt_with_budget(input, apply_prompt_budget_margin(max_tokens))?
}
ZetaFormat::V0211SeedCoder
| ZetaFormat::V0331SeedCoderModelPy
| ZetaFormat::V0304SeedNoEdits
@ -1100,27 +1278,19 @@ pub fn format_prompt_with_budget_for_format(
&mut cursor_section,
path,
context,
&editable_range,
editable_range,
cursor_offset,
);
let cursor_buffer_row = input.excerpt_start_row.map(|excerpt_start_row| {
excerpt_start_row
+ input.cursor_excerpt[..context_range.start + cursor_offset]
.bytes()
.filter(|byte| *byte == b'\n')
.count() as u32
});
let budget_with_margin = apply_prompt_budget_margin(max_tokens);
seed_coder::assemble_fim_prompt(
context,
&editable_range,
editable_range,
&cursor_section,
&input.events,
events,
related_files,
if format == ZetaFormat::V0420Diagnostics {
&input.active_buffer_diagnostics
active_buffer_diagnostics
} else {
&[]
},
@ -1136,15 +1306,15 @@ pub fn format_prompt_with_budget_for_format(
&mut cursor_section,
path,
context,
&editable_range,
editable_range,
cursor_offset,
);
qwen::assemble_fim_prompt(
context,
&editable_range,
editable_range,
&cursor_section,
&input.events,
events,
related_files,
apply_prompt_budget_margin(max_tokens),
)
@ -1156,15 +1326,15 @@ pub fn format_prompt_with_budget_for_format(
&mut cursor_section,
path,
context,
&editable_range,
editable_range,
cursor_offset,
);
assemble_single_file_fim_prompt(
context,
&editable_range,
editable_range,
&cursor_section,
&input.events,
events,
apply_prompt_budget_margin(max_tokens),
)
}
@ -1175,7 +1345,7 @@ pub fn format_prompt_with_budget_for_format(
&mut cursor_section,
path,
context,
&editable_range,
editable_range,
cursor_offset,
);
@ -1184,7 +1354,7 @@ pub fn format_prompt_with_budget_for_format(
remaining_budget = remaining_budget.saturating_sub(cursor_tokens);
let edit_history_section = format_edit_history_within_budget(
&input.events,
events,
"<|file_sep|>",
"edit history",
remaining_budget,
@ -1194,7 +1364,7 @@ pub fn format_prompt_with_budget_for_format(
remaining_budget = remaining_budget.saturating_sub(edit_history_tokens);
let related_files_section = format_related_files_within_budget(
&related_files,
related_files,
"<|file_sep|>",
"",
remaining_budget,
@ -1473,10 +1643,10 @@ pub fn encode_patch_as_output_for_format(
}
}
/// Given a `ZetaPromptInput`, a format, and a patch (with cursor already
/// Given a `Zeta2PromptInput`, a format, and a patch (with cursor already
/// extracted), produce the expected model output string for training.
pub fn format_expected_output(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
format: ZetaFormat,
patch: &str,
cursor_offset: Option<usize>,
@ -1659,7 +1829,7 @@ pub fn parsed_output_from_editable_region(
pub fn parse_zeta2_model_output(
output: &str,
format: ZetaFormat,
prompt_inputs: &ZetaPromptInput,
prompt_inputs: &Zeta2PromptInput,
) -> Result<ParsedOutput> {
let output = match output_end_marker_for_format(format) {
Some(marker) => output.strip_suffix(marker).unwrap_or(output),
@ -1737,7 +1907,7 @@ pub fn parse_zeta2_model_output(
pub fn parse_zeta2_model_output_as_patch(
output: &str,
format: ZetaFormat,
prompt_inputs: &ZetaPromptInput,
prompt_inputs: &Zeta2PromptInput,
) -> Result<String> {
if format == ZetaFormat::V0615HashRegions {
return hashed_regions::parse_output_as_patch(prompt_inputs, output, CURSOR_MARKER);
@ -1747,8 +1917,46 @@ pub fn parse_zeta2_model_output_as_patch(
parsed_output_to_patch(prompt_inputs, parsed)
}
pub fn parse_zeta3_model_output_as_patch(
output: &str,
format: ZetaFormat,
input: &Zeta3PromptInput,
) -> Result<String> {
match format {
ZetaFormat::V0318SeedMultiRegions => {}
_ => anyhow::bail!("unsupported Zeta3 output format: {format}"),
}
let output = match output_end_marker_for_format(format) {
Some(marker) => output.strip_suffix(marker).unwrap_or(output),
None => output,
};
let (current_excerpt, cursor_offset_in_excerpt) = zeta3_current_file_excerpt(input)
.ok_or_else(|| anyhow!("Zeta3 input is missing current-file editable context at cursor"))?;
let (context, editable_range_in_context, context_range, _) = resolve_zeta3_cursor_region(
current_excerpt.text.as_ref(),
cursor_offset_in_excerpt,
&input.syntax_ranges,
format,
);
let old_editable_region = &context[editable_range_in_context.clone()];
let range_in_excerpt = editable_range_in_context.start + context_range.start
..editable_range_in_context.end + context_range.start;
let parsed = parsed_output_from_editable_region(
range_in_excerpt,
multi_region::apply_marker_span_v0318(old_editable_region, output)?,
);
parsed_output_to_patch_for_excerpt(
input.cursor_path.as_ref(),
current_excerpt.text.as_ref(),
current_excerpt.row_range.start,
parsed,
)
}
pub fn cursor_position_from_parsed_output(
prompt_inputs: &ZetaPromptInput,
prompt_inputs: &Zeta2PromptInput,
parsed: &ParsedOutput,
) -> Option<CursorPosition> {
let cursor_offset = parsed.cursor_offset_in_new_editable_region?;
@ -1785,11 +1993,24 @@ pub fn cursor_position_from_parsed_output(
}
pub fn parsed_output_to_patch(
prompt_inputs: &ZetaPromptInput,
prompt_inputs: &Zeta2PromptInput,
parsed: ParsedOutput,
) -> Result<String> {
parsed_output_to_patch_for_excerpt(
prompt_inputs.cursor_path.as_ref(),
prompt_inputs.cursor_excerpt.as_ref(),
0,
parsed,
)
}
fn parsed_output_to_patch_for_excerpt(
path: &Path,
excerpt: &str,
excerpt_start_row: u32,
parsed: ParsedOutput,
) -> Result<String> {
let range_in_excerpt = parsed.range_in_excerpt;
let excerpt = prompt_inputs.cursor_excerpt.as_ref();
let old_text = excerpt[range_in_excerpt.clone()].to_string();
let mut new_text = parsed.new_editable_region;
@ -1802,7 +2023,8 @@ pub fn parsed_output_to_patch(
}
let editable_region_offset = range_in_excerpt.start;
let editable_region_start_line = excerpt[..editable_region_offset].matches('\n').count() as u32;
let editable_region_start_line =
excerpt_start_row + excerpt[..editable_region_offset].matches('\n').count() as u32;
let editable_region_lines = old_text_normalized.lines().count() as u32;
let diff = udiff::unified_diff_with_context(
@ -1813,11 +2035,7 @@ pub fn parsed_output_to_patch(
editable_region_lines,
);
let path = prompt_inputs
.cursor_path
.to_string_lossy()
.trim_start_matches('/')
.to_string();
let path = path.to_string_lossy().trim_start_matches('/').to_string();
let formatted_diff = format!("--- a/{path}\n+++ b/{path}\n{diff}");
Ok(udiff::encode_cursor_in_patch(
@ -1834,7 +2052,7 @@ pub fn excerpt_range_for_format(
}
pub fn resolve_cursor_region(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
format: ZetaFormat,
) -> (&str, Range<usize>, Range<usize>, usize) {
let (editable_range, context_range) = if format == ZetaFormat::V0327SingleFile {
@ -1859,11 +2077,25 @@ pub fn resolve_cursor_region(
excerpt_range_for_format(format, &input.excerpt_ranges)
};
adjust_cursor_region(
&input.cursor_excerpt,
input.cursor_offset_in_excerpt,
editable_range,
context_range,
)
}
fn adjust_cursor_region(
cursor_excerpt: &str,
cursor_offset: usize,
editable_range: Range<usize>,
context_range: Range<usize>,
) -> (&str, Range<usize>, Range<usize>, usize) {
let context_start = context_range.start;
let context_text = &input.cursor_excerpt[context_range.clone()];
let context_text = &cursor_excerpt[context_range.clone()];
let adjusted_editable =
(editable_range.start - context_start)..(editable_range.end - context_start);
let adjusted_cursor = input.cursor_offset_in_excerpt - context_start;
let adjusted_cursor = cursor_offset - context_start;
(
context_text,
@ -1873,7 +2105,7 @@ pub fn resolve_cursor_region(
)
}
pub fn get_prefill(input: &ZetaPromptInput, format: ZetaFormat) -> String {
pub fn get_prefill(input: &Zeta2PromptInput, format: ZetaFormat) -> String {
let (context, editable_range, _, _) = resolve_cursor_region(input, format);
get_prefill_for_format(format, context, &editable_range)
}
@ -5120,10 +5352,10 @@ pub mod zeta1 {
prompt
}
/// Formats a complete zeta1 prompt from a `ZetaPromptInput` using the given
/// Formats a complete zeta1 prompt from a `Zeta2PromptInput` using the given
/// editable and context byte-offset ranges within `cursor_excerpt`.
pub fn format_zeta1_from_input(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
editable_range: Range<usize>,
context_range: Range<usize>,
) -> String {
@ -5189,7 +5421,7 @@ pub mod zeta1 {
/// Formats the excerpt section of a zeta1 prompt using byte-offset ranges
/// within `cursor_excerpt`.
fn format_zeta1_excerpt(
input: &ZetaPromptInput,
input: &Zeta2PromptInput,
editable_range: Range<usize>,
context_range: Range<usize>,
) -> String {
@ -5298,9 +5530,9 @@ mod tests {
cursor_offset: usize,
events: Vec<Event>,
related_files: Vec<RelatedFile>,
) -> ZetaPromptInput {
) -> Zeta2PromptInput {
let context_range = 0..cursor_excerpt.len();
ZetaPromptInput {
Zeta2PromptInput {
cursor_path: Path::new("test.rs").into(),
cursor_excerpt: cursor_excerpt.into(),
cursor_offset_in_excerpt: cursor_offset,
@ -5329,8 +5561,8 @@ mod tests {
editable_range: Range<usize>,
context_range: Range<usize>,
cursor_offset: usize,
) -> ZetaPromptInput {
ZetaPromptInput {
) -> Zeta2PromptInput {
Zeta2PromptInput {
cursor_path: Path::new("test.rs").into(),
cursor_excerpt: excerpt.into(),
cursor_offset_in_excerpt: cursor_offset,
@ -5380,7 +5612,7 @@ mod tests {
}
}
fn format_with_budget(input: &ZetaPromptInput, max_tokens: usize) -> Option<String> {
fn format_with_budget(input: &Zeta2PromptInput, max_tokens: usize) -> Option<String> {
format_prompt_with_budget_for_format(input, ZetaFormat::V0114180EditableRegion, max_tokens)
}
@ -5811,13 +6043,13 @@ mod tests {
}
#[track_caller]
fn format_seed_coder(input: &ZetaPromptInput) -> String {
fn format_seed_coder(input: &Zeta2PromptInput) -> String {
format_prompt_with_budget_for_format(input, ZetaFormat::V0211SeedCoder, 10000)
.expect("seed coder prompt formatting should succeed")
}
#[track_caller]
fn format_seed_coder_with_budget(input: &ZetaPromptInput, max_tokens: usize) -> String {
fn format_seed_coder_with_budget(input: &Zeta2PromptInput, max_tokens: usize) -> String {
format_prompt_with_budget_for_format(input, ZetaFormat::V0211SeedCoder, max_tokens)
.expect("seed coder prompt formatting should succeed")
}
@ -6056,7 +6288,7 @@ mod tests {
fn test_v0615_formats_hashed_markers_for_rendered_related_context() {
let current_text = "fn main() {\n let value = 1;\n}\n";
let cursor_offset = current_text.find("let value").unwrap();
let input = ZetaPromptInput {
let input = Zeta2PromptInput {
cursor_path: Path::new("test.rs").into(),
cursor_excerpt: current_text.into(),
cursor_offset_in_excerpt: cursor_offset,
@ -6130,7 +6362,7 @@ mod tests {
fn test_v0615_parse_related_file_jump_as_patch() {
let current_text = "fn main() {\n helper();\n}\n";
let helper_text = "fn helper() {\n one();\n}\n";
let input = ZetaPromptInput {
let input = Zeta2PromptInput {
cursor_path: Path::new("test.rs").into(),
cursor_excerpt: current_text.into(),
cursor_offset_in_excerpt: current_text.find("helper").unwrap(),
@ -6190,7 +6422,7 @@ mod tests {
fn test_v0615_expected_output_round_trips_to_patch() {
let current_text = "fn main() {\n helper();\n}\n";
let helper_text = "fn helper() {\n one();\n}\n";
let input = ZetaPromptInput {
let input = Zeta2PromptInput {
cursor_path: Path::new("test.rs").into(),
cursor_excerpt: current_text.into(),
cursor_offset_in_excerpt: current_text.find("helper").unwrap(),
@ -6254,6 +6486,110 @@ mod tests {
);
}
#[test]
fn test_zeta3_prompt_matches_zeta2_seed_multi_region_prompt() {
let excerpt = "fn main() {\n helper();\n}\n";
let cursor_offset = excerpt.find("helper").expect("cursor text exists") + "help".len();
let cursor_row_start = excerpt.find(" helper").expect("cursor row exists");
let syntax_ranges = vec![0..excerpt.len()];
let related_file = make_related_file("related.rs", "fn helper() {}\n");
let mut zeta2_input = make_input(
excerpt,
0..excerpt.len(),
cursor_offset,
vec![make_event("test.rs", "-old\n+new\n")],
vec![related_file.clone()],
);
zeta2_input.excerpt_start_row = Some(10);
zeta2_input.excerpt_ranges =
compute_legacy_excerpt_ranges(excerpt, cursor_offset, &syntax_ranges);
zeta2_input.syntax_ranges = Some(syntax_ranges.clone());
let zeta3_input = Zeta3PromptInput {
cursor_path: Path::new("test.rs").into(),
cursor_position: FilePosition {
row: 11,
column: (cursor_offset - cursor_row_start) as u32,
},
events: vec![Arc::new(make_event("test.rs", "-old\n+new\n"))],
editable_context: vec![
RelatedFile {
path: Path::new("test.rs").into(),
max_row: 12,
excerpts: vec![RelatedExcerpt {
row_range: 10..12,
text: excerpt.into(),
order: 0,
context_source: ContextSource::CurrentFile,
}],
in_open_source_repo: false,
},
related_file,
],
syntax_ranges,
active_buffer_diagnostics: vec![],
in_open_source_repo: false,
can_collect_data: false,
repo_url: None,
};
assert_eq!(
format_zeta3_prompt(&zeta3_input, ZetaFormat::V0318SeedMultiRegions),
format_zeta_prompt(&zeta2_input, ZetaFormat::V0318SeedMultiRegions),
);
}
#[test]
fn test_zeta3_parse_seed_multi_region_output_as_patch() {
let prefix = (0..20)
.map(|index| format!("prefix {index}\n"))
.collect::<String>();
let excerpt = "fn main() {\n let value = 1;\n dbg!(value);\n}\n";
let new_excerpt = "fn main() {\n let value = 2;\n dbg!(value);\n}\n";
let cursor_offset = excerpt.find("value =").expect("cursor text exists");
let input = Zeta3PromptInput {
cursor_path: Path::new("test.rs").into(),
cursor_position: FilePosition { row: 21, column: 8 },
events: vec![],
editable_context: vec![RelatedFile {
path: Path::new("test.rs").into(),
max_row: 24,
excerpts: vec![RelatedExcerpt {
row_range: 20..24,
text: excerpt.into(),
order: 0,
context_source: ContextSource::CurrentFile,
}],
in_open_source_repo: false,
}],
syntax_ranges: vec![0..excerpt.len()],
active_buffer_diagnostics: vec![],
in_open_source_repo: false,
can_collect_data: false,
repo_url: None,
};
let output = multi_region::encode_from_old_and_new_v0318(
excerpt,
new_excerpt,
Some(cursor_offset),
CURSOR_MARKER,
multi_region::V0318_END_MARKER,
)
.unwrap();
let patch =
parse_zeta3_model_output_as_patch(&output, ZetaFormat::V0318SeedMultiRegions, &input)
.unwrap();
let full_text = format!("{prefix}{excerpt}");
let expected = format!("{prefix}{new_excerpt}");
assert!(patch.contains(CURSOR_MARKER));
assert_eq!(
udiff::apply_diff_to_string(&patch.replace(CURSOR_MARKER, ""), &full_text).unwrap(),
expected
);
}
#[test]
fn test_seed_coder_no_context() {
let input = make_input("before\nmiddle\nafter", 7..13, 10, vec![], vec![]);
@ -6391,7 +6727,7 @@ mod tests {
#[test]
fn test_format_zeta1_from_input_basic() {
let excerpt = "fn before() {}\nfn foo() {\n let x = 1;\n}\nfn after() {}\n";
let input = ZetaPromptInput {
let input = Zeta2PromptInput {
cursor_path: Path::new("src/main.rs").into(),
cursor_excerpt: excerpt.into(),
cursor_offset_in_excerpt: 30,
@ -6455,7 +6791,7 @@ mod tests {
#[test]
fn test_format_zeta1_from_input_no_start_of_file() {
let excerpt = "fn foo() {\n let x = 1;\n}\n";
let input = ZetaPromptInput {
let input = Zeta2PromptInput {
cursor_path: Path::new("src/main.rs").into(),
cursor_excerpt: excerpt.into(),
cursor_offset_in_excerpt: 15,
@ -6514,7 +6850,7 @@ mod tests {
let editable_range = 10..37;
let context_range = 0..excerpt.len();
let input = ZetaPromptInput {
let input = Zeta2PromptInput {
cursor_path: Path::new("test.rs").into(),
cursor_excerpt: excerpt.into(),
cursor_offset_in_excerpt: 25,