edit_file: Let agent specify locations of edit chunks (#32628)

These changes help the agent edit files when `<old_text>` matches more
than one location.

First, the agent can specify an optional `<old_text line=XX>` parameter.
When this is provided and multiple matches exist, we use this hint to
identify the best match.

Second, when there is ambiguity in matches, we now return the agent a
more helpful message listing the line numbers of all possible matches.

Together, these changes should reduce the number of misplaced edits and
agent confusion.

I have ensured the LLM Worker works with these prompt changes.


Release Notes:

- Agent: Improved locating edits
This commit is contained in:
Oleksiy Syvokon 2025-06-14 09:59:30 +03:00 committed by GitHub
parent e8d495806f
commit 5d293ae8ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 286 additions and 63 deletions

View file

@ -286,7 +286,13 @@ impl EditAgent {
_ => {
let ranges = resolved_old_text
.into_iter()
.map(|text| text.range)
.map(|text| {
let start_line =
(snapshot.offset_to_point(text.range.start).row + 1) as usize;
let end_line =
(snapshot.offset_to_point(text.range.end).row + 1) as usize;
start_line..end_line
})
.collect();
output_events
.unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))
@ -429,25 +435,25 @@ impl EditAgent {
let task = cx.background_spawn(async move {
let mut matcher = StreamingFuzzyMatcher::new(snapshot);
while let Some(edit_event) = edit_events.next().await {
let EditParserEvent::OldTextChunk { chunk, done } = edit_event? else {
let EditParserEvent::OldTextChunk {
chunk,
done,
line_hint,
} = edit_event?
else {
break;
};
old_range_tx.send(matcher.push(&chunk))?;
old_range_tx.send(matcher.push(&chunk, line_hint))?;
if done {
break;
}
}
let matches = matcher.finish();
let best_match = matcher.select_best_match();
let old_range = if matches.len() == 1 {
matches.first()
} else {
// No matches or multiple ambiguous matches
None
};
old_range_tx.send(old_range.cloned())?;
old_range_tx.send(best_match.clone())?;
let indent = LineIndent::from_iter(
matcher
@ -456,10 +462,18 @@ impl EditAgent {
.unwrap_or(&String::new())
.chars(),
);
let resolved_old_texts = matches
.into_iter()
.map(|range| ResolvedOldText { range, indent })
.collect::<Vec<_>>();
let resolved_old_texts = if let Some(best_match) = best_match {
vec![ResolvedOldText {
range: best_match,
indent,
}]
} else {
matches
.into_iter()
.map(|range| ResolvedOldText { range, indent })
.collect::<Vec<_>>()
};
Ok((edit_events, resolved_old_texts))
});
@ -1374,10 +1388,12 @@ mod tests {
&agent,
indoc! {"
<old_text>
return 42;
return 42;
}
</old_text>
<new_text>
return 100;
return 100;
}
</new_text>
"},
&mut rng,
@ -1407,7 +1423,7 @@ mod tests {
// And AmbiguousEditRange even should be emitted
let events = drain_events(&mut events);
let ambiguous_ranges = vec![17..31, 52..66, 87..101];
let ambiguous_ranges = vec![2..3, 6..7, 10..11];
assert!(
events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),
"Should emit AmbiguousEditRange for non-unique text"