Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:56:37.590Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

retrieve_context.rs

592 lines · 22.0 KB · rust
1use crate::{
2    example::Example,
3    format_prompt::line_start_offset,
4    headless::EpAppState,
5    load_project::run_load_project,
6    progress::{ExampleProgress, InfoStyle, Step, StepProgress},
7};
8use anyhow::Context as _;
9use clap::ValueEnum;
10use collections::{HashMap, HashSet};
11use edit_prediction::{DebugEvent, EditPredictionStore, udiff::refresh_worktree_entries};
12use edit_prediction_context::OracleTarget;
13use futures::{FutureExt as _, StreamExt as _, channel::mpsc};
14use gpui::{AsyncApp, Entity};
15use language::Buffer;
16use project::Project;
17use std::{
18    path::{Path, PathBuf},
19    sync::Arc,
20    time::Duration,
21};
22use zeta_prompt::{ContextSource, udiff::DiffLine};
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
25pub enum ContextRetrievalType {
26    Lsp,
27    Editable,
28    CurrentFile,
29    EditHistory,
30    EditHistoryFile,
31    GitLog,
32    Bm25,
33    OracleFile,
34    OracleSnippet,
35    #[default]
36    All,
37    None,
38}
39
40impl std::fmt::Display for ContextRetrievalType {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            ContextRetrievalType::Lsp => write!(f, "lsp"),
44            ContextRetrievalType::Editable => write!(f, "editable"),
45            ContextRetrievalType::CurrentFile => write!(f, "current-file"),
46            ContextRetrievalType::EditHistory => write!(f, "edit-history"),
47            ContextRetrievalType::EditHistoryFile => write!(f, "edit-history-file"),
48            ContextRetrievalType::GitLog => write!(f, "git-log"),
49            ContextRetrievalType::Bm25 => write!(f, "bm25"),
50            ContextRetrievalType::OracleFile => write!(f, "oracle-file"),
51            ContextRetrievalType::OracleSnippet => write!(f, "oracle-snippet"),
52            ContextRetrievalType::All => write!(f, "all"),
53            ContextRetrievalType::None => write!(f, "none"),
54        }
55    }
56}
57
58impl ContextRetrievalType {
59    pub fn context_sources(self) -> Vec<ContextSource> {
60        match self {
61            ContextRetrievalType::Lsp => vec![ContextSource::Lsp],
62            ContextRetrievalType::Editable => editable_context_sources(),
63            ContextRetrievalType::CurrentFile => vec![ContextSource::CurrentFile],
64            ContextRetrievalType::EditHistory => vec![ContextSource::EditHistory],
65            ContextRetrievalType::EditHistoryFile => vec![ContextSource::EditHistoryFile],
66            ContextRetrievalType::GitLog => vec![ContextSource::GitLog],
67            ContextRetrievalType::Bm25 => vec![ContextSource::Bm25],
68            ContextRetrievalType::OracleFile => vec![ContextSource::OracleFile],
69            ContextRetrievalType::OracleSnippet => vec![ContextSource::OracleSnippet],
70            ContextRetrievalType::All => {
71                let mut sources = vec![ContextSource::Lsp];
72                sources.extend(editable_context_sources());
73                sources
74            }
75            ContextRetrievalType::None => Vec::new(),
76        }
77    }
78}
79
80pub fn context_sources_for_types(context_types: &[ContextRetrievalType]) -> Vec<ContextSource> {
81    let mut context_sources = Vec::new();
82    for context_type in context_types {
83        for context_source in context_type.context_sources() {
84            if !context_sources.contains(&context_source) {
85                context_sources.push(context_source);
86            }
87        }
88    }
89    context_sources
90}
91
92fn editable_context_sources() -> Vec<ContextSource> {
93    vec![
94        ContextSource::CursorExcerpt,
95        ContextSource::CurrentFile,
96        ContextSource::EditHistory,
97        ContextSource::EditHistoryFile,
98        ContextSource::GitLog,
99        ContextSource::Bm25,
100    ]
101}
102
103pub async fn run_context_retrieval(
104    example: &mut Example,
105    app_state: Arc<EpAppState>,
106    example_progress: &ExampleProgress,
107    context_types: Vec<ContextRetrievalType>,
108    force: bool,
109    mut cx: AsyncApp,
110) -> anyhow::Result<()> {
111    if (!force
112        && example
113            .prompt_inputs
114            .as_ref()
115            .is_some_and(|inputs| inputs.related_files.is_some()))
116        || example.spec.repository_url.is_empty()
117    {
118        return Ok(());
119    }
120
121    run_load_project(example, app_state.clone(), example_progress, cx.clone()).await?;
122
123    let step_progress: Arc<StepProgress> = example_progress.start(Step::Context).into();
124
125    let state = example.state.as_ref().unwrap();
126    let project = state.project.clone();
127
128    let ep_store = cx
129        .update(|cx| EditPredictionStore::try_global(cx))
130        .context("EditPredictionStore not initialized")?;
131
132    let mut context_files = Vec::new();
133    let context_sources = context_sources_for_types(&context_types);
134
135    if context_sources.contains(&ContextSource::Lsp) {
136        let _lsp_handle = project.update(&mut cx, |project, cx| {
137            project.register_buffer_with_language_servers(&state.buffer, cx)
138        });
139        wait_for_language_servers_to_start(&project, &state.buffer, &step_progress, &mut cx)
140            .await?;
141
142        let mut events = ep_store.update(&mut cx, |store, cx| {
143            store.register_buffer(&state.buffer, &project, cx);
144            store.refresh_context(&project, &state.buffer, state.cursor_position, cx);
145            store.debug_info(&project, cx)
146        });
147
148        while let Some(event) = events.next().await {
149            match event {
150                DebugEvent::ContextRetrievalFinished(_) => {
151                    break;
152                }
153                _ => {}
154            }
155        }
156
157        context_files
158            .extend(ep_store.update(&mut cx, |store, cx| store.context_for_project(&project, cx)));
159    }
160
161    let editable_context_sources = context_sources
162        .into_iter()
163        .filter(|context_source| *context_source != ContextSource::Lsp)
164        .collect::<Vec<_>>();
165    if !editable_context_sources.is_empty() {
166        let oracle_targets = if editable_context_sources.contains(&ContextSource::OracleFile)
167            || editable_context_sources.contains(&ContextSource::OracleSnippet)
168        {
169            let oracle_targets = oracle_targets_from_expected_patches(example);
170            let oracle_paths = oracle_targets
171                .iter()
172                .map(|target| target.path.clone())
173                .collect::<Vec<_>>();
174            refresh_paths(&project, &oracle_paths, &mut cx).await?;
175            oracle_targets
176        } else {
177            Vec::new()
178        };
179
180        let editable_context = ep_store
181            .update(&mut cx, |store, cx| {
182                store.collect_editable_context(
183                    project.clone(),
184                    state.buffer.clone(),
185                    state.cursor_position,
186                    oracle_targets,
187                    editable_context_sources,
188                    cx,
189                )
190            })
191            .await?;
192        merge_context_files(&mut context_files, editable_context);
193    }
194
195    let excerpt_count: usize = context_files.iter().map(|f| f.excerpts.len()).sum();
196    step_progress.set_info(format!("{} excerpts", excerpt_count), InfoStyle::Normal);
197
198    if let Some(prompt_inputs) = example.prompt_inputs.as_mut() {
199        prompt_inputs.related_files = Some(context_files);
200    }
201    Ok(())
202}
203
204fn merge_context_files(
205    context_files: &mut Vec<zeta_prompt::RelatedFile>,
206    new_files: Vec<zeta_prompt::RelatedFile>,
207) {
208    for mut new_file in new_files {
209        if let Some(existing_file) = context_files
210            .iter_mut()
211            .find(|existing_file| existing_file.path == new_file.path)
212        {
213            existing_file.max_row = existing_file.max_row.max(new_file.max_row);
214            existing_file.excerpts.append(&mut new_file.excerpts);
215            existing_file.in_open_source_repo =
216                existing_file.in_open_source_repo && new_file.in_open_source_repo;
217        } else {
218            context_files.push(new_file);
219        }
220    }
221    for file in context_files.iter_mut() {
222        coalesce_touching_excerpts(&mut file.excerpts);
223    }
224}
225
226/// Sort a file's excerpts by position and merge those whose row ranges touch
227/// or overlap. Touching excerpts render seamlessly in prompts (no `...`
228/// separator), so keeping them separate splits edit addressing across a
229/// boundary the model can't see and wastes adjacent marker tags (see
230/// `TeacherJumpsPrompt::parse`).
231///
232/// A merged excerpt keeps the minimum `order` (and that excerpt's context
233/// source), so the highest-priority content still survives budget-based
234/// selection downstream; the tradeoff is that lower-priority touching
235/// content is now selected together with it.
236fn coalesce_touching_excerpts(excerpts: &mut Vec<zeta_prompt::RelatedExcerpt>) {
237    excerpts.sort_by_key(|excerpt| (excerpt.row_range.start, excerpt.row_range.end));
238    let mut coalesced: Vec<zeta_prompt::RelatedExcerpt> = Vec::with_capacity(excerpts.len());
239    for excerpt in excerpts.drain(..) {
240        let Some(last) = coalesced.last_mut() else {
241            coalesced.push(excerpt);
242            continue;
243        };
244        if excerpt.row_range.start > last.row_range.end {
245            coalesced.push(excerpt);
246            continue;
247        }
248        if excerpt.row_range.end > last.row_range.end {
249            // Touching or overlapping. Shared rows come from the same buffer
250            // snapshot, so drop the duplicated prefix of the new excerpt and
251            // append the rest.
252            let mut overlap_rows = (last.row_range.end - excerpt.row_range.start) as usize;
253            if !last.text.ends_with('\n') && !last.text.is_empty() {
254                // An unterminated final line means `last` includes the
255                // content of its `row_range.end` row itself.
256                overlap_rows += 1;
257            }
258            let Some(tail_start) = line_start_offset(&excerpt.text, overlap_rows) else {
259                // The excerpt has fewer lines than its row range claims;
260                // leave it unmerged rather than corrupt the text.
261                coalesced.push(excerpt);
262                continue;
263            };
264            let mut text =
265                String::with_capacity(last.text.len() + excerpt.text.len() - tail_start + 1);
266            text.push_str(&last.text);
267            if !text.is_empty() && !text.ends_with('\n') {
268                text.push('\n');
269            }
270            text.push_str(&excerpt.text[tail_start..]);
271            last.text = text.into();
272            last.row_range.end = excerpt.row_range.end;
273        }
274        if excerpt.order < last.order {
275            last.order = excerpt.order;
276            last.context_source = excerpt.context_source;
277        }
278    }
279    *excerpts = coalesced;
280}
281
282fn oracle_targets_from_expected_patches(example: &Example) -> Vec<OracleTarget> {
283    let mut target_indices: HashMap<PathBuf, usize> = HashMap::default();
284    let mut targets: Vec<(PathBuf, Vec<std::ops::Range<u32>>)> = Vec::new();
285
286    let mut target_index = |path: &str, targets: &mut Vec<(PathBuf, Vec<std::ops::Range<u32>>)>| {
287        let path = Path::new(path).to_path_buf();
288        *target_indices.entry(path.clone()).or_insert_with(|| {
289            targets.push((path, Vec::new()));
290            targets.len() - 1
291        })
292    };
293
294    for patch in &example.spec.expected_patches {
295        // Index of the target whose old-side rows the current hunk headers
296        // refer to. Hunk old rows refer to the file's current state only for
297        // the first expected patch; later patches drift, but the snippet
298        // padding absorbs small offsets.
299        let mut current_target: Option<usize> = None;
300        for line in patch.lines() {
301            match DiffLine::parse(line) {
302                DiffLine::OldPath { path } => {
303                    current_target = (path.as_ref() != "/dev/null")
304                        .then(|| target_index(path.as_ref(), &mut targets));
305                }
306                DiffLine::NewPath { path } => {
307                    if path.as_ref() != "/dev/null" {
308                        let index = target_index(path.as_ref(), &mut targets);
309                        if current_target.is_none() {
310                            current_target = Some(index);
311                        }
312                    }
313                }
314                DiffLine::HunkHeader(Some(location)) => {
315                    if let Some(index) = current_target {
316                        let start = location.start_line_old;
317                        targets[index].1.push(start..start + location.count_old);
318                    }
319                }
320                _ => {}
321            }
322        }
323    }
324
325    targets
326        .into_iter()
327        .map(|(path, row_ranges)| OracleTarget {
328            path: path.into(),
329            row_ranges,
330        })
331        .collect()
332}
333
334async fn refresh_paths(
335    project: &Entity<Project>,
336    paths: &[Arc<Path>],
337    cx: &mut AsyncApp,
338) -> anyhow::Result<()> {
339    if paths.is_empty() {
340        return Ok(());
341    }
342
343    let Some(worktree) = project.read_with(cx, |project, cx| project.visible_worktrees(cx).next())
344    else {
345        return Ok(());
346    };
347
348    refresh_worktree_entries(&worktree, paths.iter().map(|path| path.as_ref()), cx).await
349}
350
351async fn wait_for_language_servers_to_start(
352    project: &Entity<Project>,
353    buffer: &Entity<Buffer>,
354    step_progress: &Arc<StepProgress>,
355    cx: &mut AsyncApp,
356) -> anyhow::Result<()> {
357    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
358
359    // Determine which servers exist for this buffer, and which are still starting.
360    let mut servers_pending_start = HashSet::default();
361    let mut servers_pending_diagnostics = HashSet::default();
362    buffer.update(cx, |buffer, cx| {
363        lsp_store.update(cx, |lsp_store, cx| {
364            let ids = lsp_store.language_servers_for_local_buffer(buffer, cx);
365            for &id in &ids {
366                match lsp_store.language_server_statuses.get(&id) {
367                    None => {
368                        servers_pending_start.insert(id);
369                        servers_pending_diagnostics.insert(id);
370                    }
371                    Some(status) if status.has_pending_diagnostic_updates => {
372                        servers_pending_diagnostics.insert(id);
373                    }
374                    Some(_) => {}
375                }
376            }
377        });
378    });
379
380    step_progress.set_substatus(format!(
381        "waiting for {} LSPs",
382        servers_pending_diagnostics.len()
383    ));
384
385    let timeout_duration = if servers_pending_start.is_empty() {
386        Duration::from_secs(30)
387    } else {
388        Duration::from_secs(60 * 5)
389    };
390    let timeout = cx.background_executor().timer(timeout_duration).shared();
391
392    let (mut started_tx, mut started_rx) = mpsc::channel(servers_pending_start.len().max(1));
393    let (mut diag_tx, mut diag_rx) = mpsc::channel(servers_pending_diagnostics.len().max(1));
394    let subscriptions = [cx.subscribe(&lsp_store, {
395        let step_progress = step_progress.clone();
396        move |lsp_store, event, cx| match event {
397            project::LspStoreEvent::LanguageServerAdded(id, name, _) => {
398                step_progress.set_substatus(format!("LSP started: {}", name));
399                started_tx.try_send(*id).ok();
400            }
401            project::LspStoreEvent::DiskBasedDiagnosticsFinished { language_server_id } => {
402                let name = lsp_store
403                    .read(cx)
404                    .language_server_adapter_for_id(*language_server_id)
405                    .unwrap()
406                    .name();
407                step_progress.set_substatus(format!("LSP idle: {}", name));
408                diag_tx.try_send(*language_server_id).ok();
409            }
410            project::LspStoreEvent::LanguageServerUpdate {
411                message:
412                    client::proto::update_language_server::Variant::WorkProgress(
413                        client::proto::LspWorkProgress {
414                            message: Some(message),
415                            ..
416                        },
417                    ),
418                ..
419            } => {
420                step_progress.set_substatus(message.clone());
421            }
422            _ => {}
423        }
424    })];
425
426    // Phase 1: wait for all servers to start.
427    while !servers_pending_start.is_empty() {
428        futures::select! {
429            id = started_rx.next() => {
430                if let Some(id) = id {
431                    servers_pending_start.remove(&id);
432                }
433            },
434            _ = timeout.clone().fuse() => {
435                return Err(anyhow::anyhow!("LSP wait timed out after {} minutes", timeout_duration.as_secs() / 60));
436            }
437        }
438    }
439
440    // Save the buffer so the server sees the current content and kicks off diagnostics.
441    project
442        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
443        .await?;
444
445    // Phase 2: wait for all servers to finish their diagnostic pass.
446    while !servers_pending_diagnostics.is_empty() {
447        futures::select! {
448            id = diag_rx.next() => {
449                if let Some(id) = id {
450                    servers_pending_diagnostics.remove(&id);
451                }
452            },
453            _ = timeout.clone().fuse() => {
454                return Err(anyhow::anyhow!("LSP wait timed out after {} minutes", timeout_duration.as_secs() / 60));
455            }
456        }
457    }
458
459    drop(subscriptions);
460    step_progress.clear_substatus();
461    Ok(())
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use zeta_prompt::{ContextSource, RelatedExcerpt, RelatedFile};
468
469    fn excerpt(
470        row_range: std::ops::Range<u32>,
471        text: &str,
472        order: usize,
473        context_source: ContextSource,
474    ) -> RelatedExcerpt {
475        RelatedExcerpt {
476            row_range,
477            text: Arc::from(text),
478            order,
479            context_source,
480        }
481    }
482
483    #[test]
484    fn test_coalesce_touching_excerpts() {
485        let mut excerpts = vec![
486            excerpt(2..4, "line2\nline3\n", 1, ContextSource::EditHistory),
487            excerpt(0..2, "line0\nline1\n", 3, ContextSource::Bm25),
488            excerpt(4..6, "line4\nline5\n", 2, ContextSource::OracleSnippet),
489            excerpt(10..12, "line10\nline11\n", 0, ContextSource::Bm25),
490        ];
491        coalesce_touching_excerpts(&mut excerpts);
492
493        assert_eq!(excerpts.len(), 2);
494        assert_eq!(excerpts[0].row_range, 0..6);
495        assert_eq!(
496            excerpts[0].text.as_ref(),
497            "line0\nline1\nline2\nline3\nline4\nline5\n"
498        );
499        // The merged excerpt keeps the highest priority (minimum order) and
500        // its context source.
501        assert_eq!(excerpts[0].order, 1);
502        assert_eq!(excerpts[0].context_source, ContextSource::EditHistory);
503        // The gapped excerpt stays separate.
504        assert_eq!(excerpts[1].row_range, 10..12);
505    }
506
507    #[test]
508    fn test_coalesce_overlapping_excerpts_drops_duplicated_rows() {
509        let mut excerpts = vec![
510            excerpt(0..3, "line0\nline1\nline2\n", 0, ContextSource::Bm25),
511            excerpt(
512                2..5,
513                "line2\nline3\nline4\n",
514                1,
515                ContextSource::OracleSnippet,
516            ),
517        ];
518        coalesce_touching_excerpts(&mut excerpts);
519
520        assert_eq!(excerpts.len(), 1);
521        assert_eq!(excerpts[0].row_range, 0..5);
522        assert_eq!(
523            excerpts[0].text.as_ref(),
524            "line0\nline1\nline2\nline3\nline4\n"
525        );
526        assert_eq!(excerpts[0].order, 0);
527        assert_eq!(excerpts[0].context_source, ContextSource::Bm25);
528    }
529
530    #[test]
531    fn test_coalesce_contained_excerpt_upgrades_order() {
532        let mut excerpts = vec![
533            excerpt(0..4, "line0\nline1\nline2\nline3\n", 5, ContextSource::Bm25),
534            excerpt(1..3, "line1\nline2\n", 2, ContextSource::OracleSnippet),
535        ];
536        coalesce_touching_excerpts(&mut excerpts);
537
538        assert_eq!(excerpts.len(), 1);
539        assert_eq!(excerpts[0].row_range, 0..4);
540        assert_eq!(excerpts[0].text.as_ref(), "line0\nline1\nline2\nline3\n");
541        assert_eq!(excerpts[0].order, 2);
542        assert_eq!(excerpts[0].context_source, ContextSource::OracleSnippet);
543    }
544
545    #[test]
546    fn test_coalesce_handles_unterminated_final_line() {
547        // An excerpt ending without a newline includes the content of its
548        // `row_range.end` row (e.g. git-log excerpts ending at EOF), so a
549        // touching excerpt's first row duplicates it.
550        let mut excerpts = vec![
551            excerpt(0..2, "line0\nline1\nline2", 0, ContextSource::GitLog),
552            excerpt(2..4, "line2\nline3\n", 1, ContextSource::Bm25),
553        ];
554        coalesce_touching_excerpts(&mut excerpts);
555
556        assert_eq!(excerpts.len(), 1);
557        assert_eq!(excerpts[0].row_range, 0..4);
558        assert_eq!(excerpts[0].text.as_ref(), "line0\nline1\nline2\nline3\n");
559    }
560
561    #[test]
562    fn test_merge_context_files_coalesces_across_sources() {
563        let path: Arc<Path> = Path::new("root/src/lib.rs").into();
564        let mut context_files = vec![RelatedFile {
565            path: path.clone(),
566            max_row: 100,
567            excerpts: vec![excerpt(
568                0..2,
569                "line0\nline1\n",
570                0,
571                ContextSource::CurrentFile,
572            )],
573            in_open_source_repo: false,
574        }];
575        let new_files = vec![RelatedFile {
576            path,
577            max_row: 100,
578            excerpts: vec![excerpt(2..4, "line2\nline3\n", 1, ContextSource::Bm25)],
579            in_open_source_repo: false,
580        }];
581        merge_context_files(&mut context_files, new_files);
582
583        assert_eq!(context_files.len(), 1);
584        assert_eq!(context_files[0].excerpts.len(), 1);
585        assert_eq!(context_files[0].excerpts[0].row_range, 0..4);
586        assert_eq!(
587            context_files[0].excerpts[0].text.as_ref(),
588            "line0\nline1\nline2\nline3\n"
589        );
590    }
591}
592
Served at tenant.openagents/omega Member data and write actions are omitted.