agent: Snapshot context in user message instead of recreating it (#27967)

This makes context essentially work the same way as `read-file`,
increasing the likelihood of cache hits.

Just like with `read-file`, we'll notify the model when the user makes
an edit to one of the tracked files. In the future, we want to send a
diff instead of just a list of files, but that's an orthogonal change.


Release Notes:
- agent: Improved caching of files in context

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Agus Zubiaga 2025-04-03 15:52:28 -03:00 committed by GitHub
parent 0c82541f0a
commit 315f1bf168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 551 additions and 136 deletions

View file

@ -146,11 +146,11 @@ pub struct ContextSymbolId {
pub range: Range<Anchor>,
}
pub fn attach_context_to_message<'a>(
message: &mut LanguageModelRequestMessage,
/// Formats a collection of contexts into a string representation
pub fn format_context_as_string<'a>(
contexts: impl Iterator<Item = &'a AssistantContext>,
cx: &App,
) {
) -> Option<String> {
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut symbol_context = Vec::new();
@ -167,64 +167,78 @@ pub fn attach_context_to_message<'a>(
}
}
let mut context_chunks = Vec::new();
if file_context.is_empty()
&& directory_context.is_empty()
&& symbol_context.is_empty()
&& fetch_context.is_empty()
&& thread_context.is_empty()
{
return None;
}
let mut result = String::new();
result.push_str("\n<context>\n\
The following items were attached by the user. You don't need to use other tools to read them.\n\n");
if !file_context.is_empty() {
context_chunks.push("<files>\n");
result.push_str("<files>\n");
for context in file_context {
context_chunks.push(&context.context_buffer.text);
result.push_str(&context.context_buffer.text);
}
context_chunks.push("\n</files>\n");
result.push_str("</files>\n");
}
if !directory_context.is_empty() {
context_chunks.push("<directories>\n");
result.push_str("<directories>\n");
for context in directory_context {
for context_buffer in &context.context_buffers {
context_chunks.push(&context_buffer.text);
result.push_str(&context_buffer.text);
}
}
context_chunks.push("\n</directories>\n");
result.push_str("</directories>\n");
}
if !symbol_context.is_empty() {
context_chunks.push("<symbols>\n");
result.push_str("<symbols>\n");
for context in symbol_context {
context_chunks.push(&context.context_symbol.text);
result.push_str(&context.context_symbol.text);
result.push('\n');
}
context_chunks.push("\n</symbols>\n");
result.push_str("</symbols>\n");
}
if !fetch_context.is_empty() {
context_chunks.push("<fetched_urls>\n");
result.push_str("<fetched_urls>\n");
for context in &fetch_context {
context_chunks.push(&context.url);
context_chunks.push(&context.text);
result.push_str(&context.url);
result.push('\n');
result.push_str(&context.text);
result.push('\n');
}
context_chunks.push("\n</fetched_urls>\n");
result.push_str("</fetched_urls>\n");
}
// Need to own the SharedString for summary so that it can be referenced.
let mut thread_context_chunks = Vec::new();
if !thread_context.is_empty() {
context_chunks.push("<conversation_threads>\n");
result.push_str("<conversation_threads>\n");
for context in &thread_context {
thread_context_chunks.push(context.summary(cx));
thread_context_chunks.push(context.text.clone());
result.push_str(&context.summary(cx));
result.push('\n');
result.push_str(&context.text);
result.push('\n');
}
context_chunks.push("\n</conversation_threads>\n");
result.push_str("</conversation_threads>\n");
}
for chunk in &thread_context_chunks {
context_chunks.push(chunk);
}
result.push_str("</context>\n");
Some(result)
}
if !context_chunks.is_empty() {
message.content.push(
"\n<context>\n\
The following items were attached by the user. You don't need to use other tools to read them.\n\n".into(),
);
message.content.push(context_chunks.join("\n").into());
message.content.push("\n</context>\n".into());
pub fn attach_context_to_message<'a>(
message: &mut LanguageModelRequestMessage,
contexts: impl Iterator<Item = &'a AssistantContext>,
cx: &App,
) {
if let Some(context_string) = format_context_as_string(contexts, cx) {
message.content.push(context_string.into());
}
}